summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
Diffstat (limited to 'includes')
-rw-r--r--includes/Action.php119
-rw-r--r--includes/AjaxDispatcher.php10
-rw-r--r--includes/AjaxResponse.php6
-rw-r--r--includes/ArrayUtils.php69
-rw-r--r--includes/Article.php413
-rw-r--r--includes/AuthPlugin.php29
-rw-r--r--includes/AutoLoader.php314
-rw-r--r--includes/Autopromote.php17
-rw-r--r--includes/Block.php396
-rw-r--r--includes/CacheHelper.php2
-rw-r--r--includes/CallableUpdate.php30
-rw-r--r--includes/Category.php95
-rw-r--r--includes/CategoryPage.php12
-rw-r--r--includes/CategoryViewer.php145
-rw-r--r--includes/Categoryfinder.php39
-rw-r--r--includes/Cdb.php8
-rw-r--r--includes/Cdb_PHP.php38
-rw-r--r--includes/ChangeTags.php106
-rw-r--r--includes/ChangesFeed.php59
-rw-r--r--includes/ChangesList.php1270
-rw-r--r--includes/Collation.php301
-rw-r--r--includes/ConfEditor.php67
-rw-r--r--includes/Cookie.php38
-rw-r--r--includes/DataUpdate.php14
-rw-r--r--includes/DefaultSettings.php2461
-rw-r--r--includes/DeferredUpdates.php18
-rw-r--r--includes/Defines.php67
-rw-r--r--includes/DeprecatedGlobal.php2
-rw-r--r--includes/EditPage.php1384
-rw-r--r--includes/Exception.php356
-rw-r--r--includes/Export.php146
-rw-r--r--includes/ExternalEdit.php132
-rw-r--r--includes/ExternalStore.php172
-rw-r--r--includes/ExternalStoreDB.php185
-rw-r--r--includes/ExternalUser.php309
-rw-r--r--includes/FakeTitle.php6
-rw-r--r--includes/Fallback.php55
-rw-r--r--includes/Feed.php52
-rw-r--r--includes/FeedUtils.php115
-rw-r--r--includes/FileDeleteForm.php50
-rw-r--r--includes/ForkController.php8
-rw-r--r--includes/FormOptions.php175
-rw-r--r--includes/GitInfo.php55
-rw-r--r--includes/GlobalFunctions.php1063
-rw-r--r--includes/HTMLForm.php781
-rw-r--r--includes/HashRing.php142
-rw-r--r--includes/HistoryBlob.php86
-rw-r--r--includes/Hooks.php294
-rw-r--r--includes/Html.php375
-rw-r--r--includes/HtmlFormatter.php356
-rw-r--r--includes/HttpFunctions.php169
-rw-r--r--includes/IP.php74
-rw-r--r--includes/ImageGallery.php406
-rw-r--r--includes/ImagePage.php237
-rw-r--r--includes/ImageQueryPage.php25
-rw-r--r--includes/Import.php271
-rw-r--r--includes/Init.php150
-rw-r--r--includes/Licenses.php11
-rw-r--r--includes/LinkFilter.php44
-rw-r--r--includes/Linker.php551
-rw-r--r--includes/LinksUpdate.php235
-rw-r--r--includes/MWCryptRand.php (renamed from includes/CryptRand.php)22
-rw-r--r--includes/MWFunction.php34
-rw-r--r--includes/MagicWord.php51
-rw-r--r--includes/MappedIterator.php114
-rw-r--r--includes/Message.php367
-rw-r--r--includes/MessageBlobStore.php33
-rw-r--r--includes/Metadata.php69
-rw-r--r--includes/MimeMagic.php329
-rw-r--r--includes/Namespace.php69
-rw-r--r--includes/OutputHandler.php90
-rw-r--r--includes/OutputPage.php804
-rw-r--r--includes/PHPVersionError.php43
-rw-r--r--includes/PageQueryPage.php5
-rw-r--r--includes/Pager.php199
-rw-r--r--includes/PathRouter.php20
-rw-r--r--includes/PoolCounter.php231
-rw-r--r--includes/Preferences.php596
-rw-r--r--includes/PrefixSearch.php50
-rw-r--r--includes/ProtectionForm.php156
-rw-r--r--includes/ProxyTools.php44
-rw-r--r--includes/QueryPage.php178
-rw-r--r--includes/Revision.php714
-rw-r--r--includes/RevisionList.php9
-rw-r--r--includes/Sanitizer.php510
-rw-r--r--includes/ScopedCallback.php73
-rw-r--r--includes/SeleniumWebSettings.php222
-rw-r--r--includes/Setup.php179
-rw-r--r--includes/SiteConfiguration.php215
-rw-r--r--includes/SiteStats.php148
-rw-r--r--includes/Skin.php276
-rw-r--r--includes/SkinLegacy.php882
-rw-r--r--includes/SkinTemplate.php446
-rw-r--r--includes/SpecialPage.php307
-rw-r--r--includes/SpecialPageFactory.php66
-rw-r--r--includes/SqlDataUpdate.php16
-rw-r--r--includes/SquidPurgeClient.php47
-rw-r--r--includes/StatCounter.php150
-rw-r--r--includes/Status.php149
-rw-r--r--includes/StreamFile.php20
-rw-r--r--includes/StringUtils.php176
-rw-r--r--includes/StubObject.php48
-rw-r--r--includes/Timestamp.php293
-rw-r--r--includes/Title.php1333
-rw-r--r--includes/TitleArray.php4
-rw-r--r--includes/UIDGenerator.php337
-rw-r--r--includes/User.php1952
-rw-r--r--includes/UserArray.php2
-rw-r--r--includes/UserMailer.php290
-rw-r--r--includes/UserRightsProxy.php24
-rw-r--r--includes/ViewCountUpdate.php17
-rw-r--r--includes/WatchedItem.php140
-rw-r--r--includes/WebRequest.php304
-rw-r--r--includes/WebResponse.php131
-rw-r--r--includes/WebStart.php67
-rw-r--r--includes/Wiki.php161
-rw-r--r--includes/WikiError.php4
-rw-r--r--includes/WikiFilePage.php13
-rw-r--r--includes/WikiMap.php46
-rw-r--r--includes/WikiPage.php1769
-rw-r--r--includes/Xml.php387
-rw-r--r--includes/XmlTypeCheck.php115
-rw-r--r--includes/ZhClient.php16
-rw-r--r--includes/ZhConversion.php792
-rw-r--r--includes/ZipDirectoryReader.php42
-rw-r--r--includes/actions/CachedAction.php36
-rw-r--r--includes/actions/CreditsAction.php28
-rw-r--r--includes/actions/DeleteAction.php11
-rw-r--r--includes/actions/EditAction.php35
-rw-r--r--includes/actions/HistoryAction.php126
-rw-r--r--includes/actions/InfoAction.php373
-rw-r--r--includes/actions/MarkpatrolledAction.php5
-rw-r--r--includes/actions/ProtectAction.php20
-rw-r--r--includes/actions/PurgeAction.php19
-rw-r--r--includes/actions/RawAction.php60
-rw-r--r--includes/actions/RenderAction.php11
-rw-r--r--includes/actions/RevertAction.php6
-rw-r--r--includes/actions/RevisiondeleteAction.php5
-rw-r--r--includes/actions/RollbackAction.php35
-rw-r--r--includes/actions/ViewAction.php11
-rw-r--r--includes/actions/WatchAction.php72
-rw-r--r--includes/api/ApiBase.php487
-rw-r--r--includes/api/ApiBlock.php44
-rw-r--r--includes/api/ApiComparePages.php26
-rw-r--r--includes/api/ApiCreateAccount.php296
-rw-r--r--includes/api/ApiDelete.php28
-rw-r--r--includes/api/ApiDisabled.php8
-rw-r--r--includes/api/ApiEditPage.php232
-rw-r--r--includes/api/ApiEmailUser.php10
-rw-r--r--includes/api/ApiExpandTemplates.php14
-rw-r--r--includes/api/ApiFeedContributions.php39
-rw-r--r--includes/api/ApiFeedWatchlist.php106
-rw-r--r--includes/api/ApiFileRevert.php16
-rw-r--r--includes/api/ApiFormatBase.php56
-rw-r--r--includes/api/ApiFormatDbg.php8
-rw-r--r--includes/api/ApiFormatDump.php8
-rw-r--r--includes/api/ApiFormatJson.php30
-rw-r--r--includes/api/ApiFormatNone.php (renamed from includes/HttpFunctions.old.php)30
-rw-r--r--includes/api/ApiFormatPhp.php8
-rw-r--r--includes/api/ApiFormatRaw.php4
-rw-r--r--includes/api/ApiFormatTxt.php8
-rw-r--r--includes/api/ApiFormatWddx.php83
-rw-r--r--includes/api/ApiFormatXml.php141
-rw-r--r--includes/api/ApiFormatYaml.php6
-rw-r--r--includes/api/ApiHelp.php91
-rw-r--r--includes/api/ApiImageRotate.php230
-rw-r--r--includes/api/ApiImport.php20
-rw-r--r--includes/api/ApiLogin.php17
-rw-r--r--includes/api/ApiLogout.php8
-rw-r--r--includes/api/ApiMain.php303
-rw-r--r--includes/api/ApiModuleManager.php171
-rw-r--r--includes/api/ApiMove.php29
-rw-r--r--includes/api/ApiOpenSearch.php29
-rw-r--r--includes/api/ApiOptions.php72
-rw-r--r--includes/api/ApiPageSet.php570
-rw-r--r--includes/api/ApiParamInfo.php120
-rw-r--r--includes/api/ApiParse.php273
-rw-r--r--includes/api/ApiPatrol.php54
-rw-r--r--includes/api/ApiProtect.php13
-rw-r--r--includes/api/ApiPurge.php147
-rw-r--r--includes/api/ApiQuery.php653
-rw-r--r--includes/api/ApiQueryAllCategories.php20
-rw-r--r--includes/api/ApiQueryAllImages.php67
-rw-r--r--includes/api/ApiQueryAllLinks.php182
-rw-r--r--includes/api/ApiQueryAllMessages.php23
-rw-r--r--includes/api/ApiQueryAllPages.php27
-rw-r--r--includes/api/ApiQueryAllUsers.php20
-rw-r--r--includes/api/ApiQueryBacklinks.php50
-rw-r--r--includes/api/ApiQueryBase.php142
-rw-r--r--includes/api/ApiQueryBlocks.php77
-rw-r--r--includes/api/ApiQueryCategories.php16
-rw-r--r--includes/api/ApiQueryCategoryInfo.php5
-rw-r--r--includes/api/ApiQueryCategoryMembers.php23
-rw-r--r--includes/api/ApiQueryDeletedrevs.php32
-rw-r--r--includes/api/ApiQueryDisabled.php8
-rw-r--r--includes/api/ApiQueryDuplicateFiles.php42
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php22
-rw-r--r--includes/api/ApiQueryExternalLinks.php18
-rw-r--r--includes/api/ApiQueryFileRepoInfo.php115
-rw-r--r--includes/api/ApiQueryFilearchive.php39
-rw-r--r--includes/api/ApiQueryIWBacklinks.php10
-rw-r--r--includes/api/ApiQueryIWLinks.php19
-rw-r--r--includes/api/ApiQueryImageInfo.php186
-rw-r--r--includes/api/ApiQueryImages.php17
-rw-r--r--includes/api/ApiQueryInfo.php169
-rw-r--r--includes/api/ApiQueryLangBacklinks.php15
-rw-r--r--includes/api/ApiQueryLangLinks.php29
-rw-r--r--includes/api/ApiQueryLinks.php17
-rw-r--r--includes/api/ApiQueryLogEvents.php83
-rw-r--r--includes/api/ApiQueryORM.php264
-rw-r--r--includes/api/ApiQueryPagePropNames.php116
-rw-r--r--includes/api/ApiQueryPageProps.php17
-rw-r--r--includes/api/ApiQueryPagesWithProp.php189
-rw-r--r--includes/api/ApiQueryProtectedTitles.php11
-rw-r--r--includes/api/ApiQueryQueryPage.php23
-rw-r--r--includes/api/ApiQueryRandom.php6
-rw-r--r--includes/api/ApiQueryRecentChanges.php106
-rw-r--r--includes/api/ApiQueryRevisions.php199
-rw-r--r--includes/api/ApiQuerySearch.php47
-rw-r--r--includes/api/ApiQuerySiteinfo.php119
-rw-r--r--includes/api/ApiQueryStashImageInfo.php7
-rw-r--r--includes/api/ApiQueryTags.php9
-rw-r--r--includes/api/ApiQueryUserContributions.php34
-rw-r--r--includes/api/ApiQueryUserInfo.php30
-rw-r--r--includes/api/ApiQueryUsers.php80
-rw-r--r--includes/api/ApiQueryWatchlist.php113
-rw-r--r--includes/api/ApiQueryWatchlistRaw.php10
-rw-r--r--includes/api/ApiResult.php99
-rw-r--r--includes/api/ApiRollback.php10
-rw-r--r--includes/api/ApiRsd.php14
-rw-r--r--includes/api/ApiSetNotificationTimestamp.php69
-rw-r--r--includes/api/ApiTokens.php75
-rw-r--r--includes/api/ApiUnblock.php23
-rw-r--r--includes/api/ApiUndelete.php18
-rw-r--r--includes/api/ApiUpload.php301
-rw-r--r--includes/api/ApiUserrights.php15
-rw-r--r--includes/api/ApiWatch.php36
-rw-r--r--includes/cache/BacklinkCache.php (renamed from includes/BacklinkCache.php)195
-rw-r--r--includes/cache/CacheDependency.php12
-rw-r--r--includes/cache/FileCacheBase.php10
-rw-r--r--includes/cache/GenderCache.php22
-rw-r--r--includes/cache/HTMLCacheUpdate.php227
-rw-r--r--includes/cache/HTMLFileCache.php11
-rw-r--r--includes/cache/LinkBatch.php6
-rw-r--r--includes/cache/LinkCache.php98
-rw-r--r--includes/cache/LocalisationCache.php (renamed from includes/LocalisationCache.php)138
-rw-r--r--includes/cache/MessageCache.php866
-rw-r--r--includes/cache/ProcessCacheLRU.php19
-rw-r--r--includes/cache/ResourceFileCache.php2
-rw-r--r--includes/cache/SquidUpdate.php208
-rw-r--r--includes/cache/UserCache.php25
-rw-r--r--includes/changes/ChangesList.php552
-rw-r--r--includes/changes/EnhancedChangesList.php661
-rw-r--r--includes/changes/OldChangesList.php130
-rw-r--r--includes/changes/RCCacheEntry.php35
-rw-r--r--includes/changes/RecentChange.php (renamed from includes/RecentChange.php)429
-rw-r--r--includes/clientpool/RedisConnectionPool.php356
-rw-r--r--includes/content/AbstractContent.php444
-rw-r--r--includes/content/Content.php508
-rw-r--r--includes/content/ContentHandler.php1115
-rw-r--r--includes/content/CssContent.php65
-rw-r--r--includes/content/CssContentHandler.php67
-rw-r--r--includes/content/JavaScriptContent.php66
-rw-r--r--includes/content/JavaScriptContentHandler.php67
-rw-r--r--includes/content/MessageContent.php158
-rw-r--r--includes/content/TextContent.php286
-rw-r--r--includes/content/TextContentHandler.php115
-rw-r--r--includes/content/WikitextContent.php323
-rw-r--r--includes/content/WikitextContentHandler.php113
-rw-r--r--includes/context/ContextSource.php24
-rw-r--r--includes/context/DerivativeContext.php47
-rw-r--r--includes/context/IContextSource.php17
-rw-r--r--includes/context/RequestContext.php148
-rw-r--r--includes/dao/DBAccessBase.php92
-rw-r--r--includes/dao/IDBAccessObject.php4
-rw-r--r--includes/db/ChronologyProtector.php106
-rw-r--r--includes/db/CloneDatabase.php41
-rw-r--r--includes/db/Database.php1130
-rw-r--r--includes/db/DatabaseError.php220
-rw-r--r--includes/db/DatabaseIbm_db2.php1721
-rw-r--r--includes/db/DatabaseMssql.php291
-rw-r--r--includes/db/DatabaseMysql.php934
-rw-r--r--includes/db/DatabaseMysqlBase.php1161
-rw-r--r--includes/db/DatabaseMysqli.php194
-rw-r--r--includes/db/DatabaseOracle.php164
-rw-r--r--includes/db/DatabasePostgres.php278
-rw-r--r--includes/db/DatabaseSqlite.php94
-rw-r--r--includes/db/DatabaseUtility.php16
-rw-r--r--includes/db/IORMRow.php29
-rw-r--r--includes/db/IORMTable.php91
-rw-r--r--includes/db/LBFactory.php108
-rw-r--r--includes/db/LBFactory_Multi.php17
-rw-r--r--includes/db/LBFactory_Single.php2
-rw-r--r--includes/db/LoadBalancer.php252
-rw-r--r--includes/db/LoadMonitor.php34
-rw-r--r--includes/db/ORMIterator.php4
-rw-r--r--includes/db/ORMResult.php2
-rw-r--r--includes/db/ORMRow.php215
-rw-r--r--includes/db/ORMTable.php506
-rw-r--r--includes/debug/Debug.php75
-rw-r--r--includes/diff/DairikiDiff.php117
-rw-r--r--includes/diff/DifferenceEngine.php364
-rw-r--r--includes/diff/WikiDiff3.php5
-rw-r--r--includes/extauth/Hardcoded.php84
-rw-r--r--includes/extauth/MediaWiki.php168
-rw-r--r--includes/extauth/vB.php146
-rw-r--r--includes/externalstore/ExternalStore.php227
-rw-r--r--includes/externalstore/ExternalStoreDB.php283
-rw-r--r--includes/externalstore/ExternalStoreHttp.php (renamed from includes/ExternalStoreHttp.php)20
-rw-r--r--includes/externalstore/ExternalStoreMedium.php79
-rw-r--r--includes/externalstore/ExternalStoreMwstore.php95
-rw-r--r--includes/filebackend/FSFile.php48
-rw-r--r--includes/filebackend/FSFileBackend.php430
-rw-r--r--includes/filebackend/FileBackend.php475
-rw-r--r--includes/filebackend/FileBackendGroup.php36
-rw-r--r--includes/filebackend/FileBackendMultiWrite.php250
-rw-r--r--includes/filebackend/FileBackendStore.php978
-rw-r--r--includes/filebackend/FileOp.php453
-rw-r--r--includes/filebackend/FileOpBatch.php68
-rw-r--r--includes/filebackend/README208
-rw-r--r--includes/filebackend/SwiftFileBackend.php687
-rw-r--r--includes/filebackend/TempFSFile.php19
-rw-r--r--includes/filebackend/filejournal/DBFileJournal.php42
-rw-r--r--includes/filebackend/filejournal/FileJournal.php65
-rw-r--r--includes/filebackend/lockmanager/DBLockManager.php214
-rw-r--r--includes/filebackend/lockmanager/FSLockManager.php42
-rw-r--r--includes/filebackend/lockmanager/LSLockManager.php8
-rw-r--r--includes/filebackend/lockmanager/LockManager.php385
-rw-r--r--includes/filebackend/lockmanager/LockManagerGroup.php58
-rw-r--r--includes/filebackend/lockmanager/MemcLockManager.php131
-rw-r--r--includes/filebackend/lockmanager/QuorumLockManager.php246
-rw-r--r--includes/filebackend/lockmanager/RedisLockManager.php288
-rw-r--r--includes/filebackend/lockmanager/ScopedLock.php104
-rw-r--r--includes/filerepo/FSRepo.php20
-rw-r--r--includes/filerepo/FileRepo.php325
-rw-r--r--includes/filerepo/ForeignAPIRepo.php244
-rw-r--r--includes/filerepo/ForeignDBRepo.php7
-rw-r--r--includes/filerepo/ForeignDBViaLBRepo.php2
-rw-r--r--includes/filerepo/LocalRepo.php77
-rw-r--r--includes/filerepo/README23
-rw-r--r--includes/filerepo/RepoGroup.php36
-rw-r--r--includes/filerepo/file/ArchivedFile.php171
-rw-r--r--includes/filerepo/file/File.php239
-rw-r--r--includes/filerepo/file/ForeignAPIFile.php57
-rw-r--r--includes/filerepo/file/ForeignDBFile.php20
-rw-r--r--includes/filerepo/file/LocalFile.php505
-rw-r--r--includes/filerepo/file/OldLocalFile.php68
-rw-r--r--includes/filerepo/file/UnregisteredLocalFile.php22
-rw-r--r--includes/gallery/ImageGalleryBase.php331
-rw-r--r--includes/gallery/NolinesImageGallery.php38
-rw-r--r--includes/gallery/PackedImageGallery.php105
-rw-r--r--includes/gallery/PackedOverlayImageGallery.php60
-rw-r--r--includes/gallery/TraditionalImageGallery.php328
-rw-r--r--includes/installer/CliInstaller.php15
-rw-r--r--includes/installer/DatabaseInstaller.php87
-rw-r--r--includes/installer/DatabaseUpdater.php495
-rw-r--r--includes/installer/Ibm_db2Installer.php270
-rw-r--r--includes/installer/Ibm_db2Updater.php91
-rw-r--r--includes/installer/InstallDocFormatter.php16
-rw-r--r--includes/installer/Installer.i18n.php4944
-rw-r--r--includes/installer/Installer.php386
-rw-r--r--includes/installer/LocalSettingsGenerator.php112
-rw-r--r--includes/installer/MysqlInstaller.php152
-rw-r--r--includes/installer/MysqlUpdater.php568
-rw-r--r--includes/installer/OracleInstaller.php70
-rw-r--r--includes/installer/OracleUpdater.php76
-rw-r--r--includes/installer/PhpBugTests.php2
-rw-r--r--includes/installer/PostgresInstaller.php83
-rw-r--r--includes/installer/PostgresUpdater.php627
-rw-r--r--includes/installer/SqliteInstaller.php44
-rw-r--r--includes/installer/SqliteUpdater.php121
-rw-r--r--includes/installer/WebInstaller.php245
-rw-r--r--includes/installer/WebInstallerOutput.php118
-rw-r--r--includes/installer/WebInstallerPage.php309
-rw-r--r--includes/interwiki/Interwiki.php96
-rw-r--r--includes/job/Job.php450
-rw-r--r--includes/job/JobQueue.php706
-rw-r--r--includes/job/JobQueueDB.php816
-rw-r--r--includes/job/JobQueueFederated.php473
-rw-r--r--includes/job/JobQueueGroup.php427
-rw-r--r--includes/job/JobQueueRedis.php856
-rw-r--r--includes/job/README81
-rw-r--r--includes/job/RefreshLinksJob.php202
-rw-r--r--includes/job/aggregator/JobQueueAggregator.php156
-rw-r--r--includes/job/aggregator/JobQueueAggregatorMemc.php124
-rw-r--r--includes/job/aggregator/JobQueueAggregatorRedis.php193
-rw-r--r--includes/job/jobs/AssembleUploadChunksJob.php127
-rw-r--r--includes/job/jobs/DoubleRedirectJob.php (renamed from includes/job/DoubleRedirectJob.php)92
-rw-r--r--includes/job/jobs/DuplicateJob.php59
-rw-r--r--includes/job/jobs/EmaillingJob.php (renamed from includes/job/EmaillingJob.php)5
-rw-r--r--includes/job/jobs/EnotifNotifyJob.php (renamed from includes/job/EnotifNotifyJob.php)5
-rw-r--r--includes/job/jobs/HTMLCacheUpdateJob.php263
-rw-r--r--includes/job/jobs/NullJob.php76
-rw-r--r--includes/job/jobs/PublishStashedFileJob.php140
-rw-r--r--includes/job/jobs/RefreshLinksJob.php222
-rw-r--r--includes/job/jobs/UploadFromUrlJob.php (renamed from includes/job/UploadFromUrlJob.php)11
-rw-r--r--includes/json/FormatJson.php220
-rw-r--r--includes/json/Services_JSON.php882
-rw-r--r--includes/libs/CSSJanus.php87
-rw-r--r--includes/libs/CSSMin.php61
-rw-r--r--includes/libs/GenericArrayObject.php15
-rw-r--r--includes/libs/HttpStatus.php2
-rw-r--r--includes/libs/IEContentAnalyzer.php13
-rw-r--r--includes/libs/IEUrlExtension.php14
-rw-r--r--includes/libs/JavaScriptMinifier.php20
-rw-r--r--includes/libs/jsminplus.php6
-rw-r--r--includes/libs/lessc.inc.php3742
-rw-r--r--includes/limit.sh107
-rw-r--r--includes/logging/DeleteLogFormatter.php196
-rw-r--r--includes/logging/LogEntry.php125
-rw-r--r--includes/logging/LogEventsList.php146
-rw-r--r--includes/logging/LogFormatter.php446
-rw-r--r--includes/logging/LogPage.php137
-rw-r--r--includes/logging/LogPager.php105
-rw-r--r--includes/logging/MoveLogFormatter.php82
-rw-r--r--includes/logging/NewUsersLogFormatter.php65
-rw-r--r--includes/logging/PatrolLog.php7
-rw-r--r--includes/logging/PatrolLogFormatter.php63
-rw-r--r--includes/logging/RightsLogFormatter.php112
-rw-r--r--includes/media/BMP.php8
-rw-r--r--includes/media/Bitmap.php123
-rw-r--r--includes/media/BitmapMetadataHandler.php78
-rw-r--r--includes/media/DjVu.php29
-rw-r--r--includes/media/DjVuImage.php69
-rw-r--r--includes/media/Exif.php243
-rw-r--r--includes/media/ExifBitmap.php23
-rw-r--r--includes/media/FormatMetadata.php193
-rw-r--r--includes/media/GIF.php30
-rw-r--r--includes/media/GIFMetadataExtractor.php90
-rw-r--r--includes/media/IPTC.php144
-rw-r--r--includes/media/ImageHandler.php28
-rw-r--r--includes/media/Jpeg.php37
-rw-r--r--includes/media/JpegMetadataExtractor.php79
-rw-r--r--includes/media/MediaHandler.php265
-rw-r--r--includes/media/MediaTransformOutput.php88
-rw-r--r--includes/media/PNG.php33
-rw-r--r--includes/media/PNGMetadataExtractor.php34
-rw-r--r--includes/media/SVG.php155
-rw-r--r--includes/media/SVGMetadataExtractor.php95
-rw-r--r--includes/media/Tiff.php9
-rw-r--r--includes/media/XCF.php10
-rw-r--r--includes/media/XMP.php681
-rw-r--r--includes/media/XMPInfo.php67
-rw-r--r--includes/media/XMPValidate.php205
-rw-r--r--includes/mime.info4
-rw-r--r--includes/mime.types7
-rw-r--r--includes/mobile/DeviceDetection.php459
-rw-r--r--includes/normal/Makefile2
-rw-r--r--includes/normal/README10
-rw-r--r--includes/normal/RandomTest.php8
-rw-r--r--includes/normal/Utf8CaseGenerate.php6
-rw-r--r--includes/normal/Utf8Test.php7
-rw-r--r--includes/normal/UtfNormal.php36
-rw-r--r--includes/normal/UtfNormalBench.php12
-rw-r--r--includes/normal/UtfNormalDefines.php4
-rw-r--r--includes/normal/UtfNormalGenerate.php4
-rw-r--r--includes/normal/UtfNormalMemStress.php10
-rw-r--r--includes/normal/UtfNormalTest.php16
-rw-r--r--includes/normal/UtfNormalTest2.php6
-rw-r--r--includes/normal/UtfNormalUtil.php14
-rw-r--r--includes/objectcache/APCBagOStuff.php43
-rw-r--r--includes/objectcache/BagOStuff.php131
-rw-r--r--includes/objectcache/DBABagOStuff.php63
-rw-r--r--includes/objectcache/EhcacheBagOStuff.php72
-rw-r--r--includes/objectcache/EmptyBagOStuff.php25
-rw-r--r--includes/objectcache/HashBagOStuff.php28
-rw-r--r--includes/objectcache/MemcachedBagOStuff.php29
-rw-r--r--includes/objectcache/MemcachedClient.php211
-rw-r--r--includes/objectcache/MemcachedPeclBagOStuff.php41
-rw-r--r--includes/objectcache/MemcachedPhpBagOStuff.php3
-rw-r--r--includes/objectcache/MultiWriteBagOStuff.php31
-rw-r--r--includes/objectcache/ObjectCache.php22
-rw-r--r--includes/objectcache/ObjectCacheSessionHandler.php10
-rw-r--r--includes/objectcache/RedisBagOStuff.php225
-rw-r--r--includes/objectcache/SqlBagOStuff.php522
-rw-r--r--includes/objectcache/WinCacheBagOStuff.php47
-rw-r--r--includes/objectcache/XCacheBagOStuff.php40
-rw-r--r--includes/parser/CacheTime.php4
-rw-r--r--includes/parser/CoreLinkFunctions.php92
-rw-r--r--includes/parser/CoreParserFunctions.php267
-rw-r--r--includes/parser/CoreTagHooks.php3
-rw-r--r--includes/parser/DateFormatter.php76
-rw-r--r--includes/parser/LinkHolderArray.php294
-rw-r--r--includes/parser/Parser.php1544
-rw-r--r--includes/parser/ParserCache.php25
-rw-r--r--includes/parser/ParserOptions.php107
-rw-r--r--includes/parser/ParserOutput.php315
-rw-r--r--includes/parser/Parser_DiffTest.php2
-rw-r--r--includes/parser/Parser_LinkHooks.php324
-rw-r--r--includes/parser/Preprocessor.php7
-rw-r--r--includes/parser/Preprocessor_DOM.php172
-rw-r--r--includes/parser/Preprocessor_Hash.php135
-rw-r--r--includes/parser/Preprocessor_HipHop.hphp2013
-rw-r--r--includes/parser/StripState.php7
-rw-r--r--includes/parser/Tidy.php69
-rw-r--r--includes/profiler/Profiler.php376
-rw-r--r--includes/profiler/ProfilerSimple.php31
-rw-r--r--includes/profiler/ProfilerSimpleText.php22
-rw-r--r--includes/profiler/ProfilerSimpleTrace.php24
-rw-r--r--includes/profiler/ProfilerSimpleUDP.php12
-rw-r--r--includes/profiler/ProfilerStub.php2
-rw-r--r--includes/rcfeed/IRCColourfulRCFeedFormatter.php99
-rw-r--r--includes/rcfeed/JSONRCFeedFormatter.php90
-rw-r--r--includes/rcfeed/RCFeedEngine.php12
-rw-r--r--includes/rcfeed/RCFeedFormatter.php13
-rw-r--r--includes/rcfeed/RedisPubSubFeedEngine.php41
-rw-r--r--includes/rcfeed/UDPRCFeedEngine.php10
-rw-r--r--includes/resourceloader/ResourceLoader.php282
-rw-r--r--includes/resourceloader/ResourceLoaderContext.php20
-rw-r--r--includes/resourceloader/ResourceLoaderFileModule.php315
-rw-r--r--includes/resourceloader/ResourceLoaderLESSFunctions.php67
-rw-r--r--includes/resourceloader/ResourceLoaderLanguageDataModule.php63
-rw-r--r--includes/resourceloader/ResourceLoaderModule.php156
-rw-r--r--includes/resourceloader/ResourceLoaderNoscriptModule.php2
-rw-r--r--includes/resourceloader/ResourceLoaderSiteModule.php25
-rw-r--r--includes/resourceloader/ResourceLoaderStartUpModule.php63
-rw-r--r--includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php69
-rw-r--r--includes/resourceloader/ResourceLoaderUserGroupsModule.php15
-rw-r--r--includes/resourceloader/ResourceLoaderUserModule.php21
-rw-r--r--includes/resourceloader/ResourceLoaderUserOptionsModule.php4
-rw-r--r--includes/resourceloader/ResourceLoaderUserTokensModule.php12
-rw-r--r--includes/resourceloader/ResourceLoaderWikiModule.php36
-rw-r--r--includes/revisiondelete/RevisionDelete.php108
-rw-r--r--includes/revisiondelete/RevisionDeleteAbstracts.php57
-rw-r--r--includes/revisiondelete/RevisionDeleteUser.php2
-rw-r--r--includes/revisiondelete/RevisionDeleter.php153
-rw-r--r--includes/search/SearchEngine.php244
-rw-r--r--includes/search/SearchIBM_DB2.php234
-rw-r--r--includes/search/SearchMssql.php25
-rw-r--r--includes/search/SearchMySQL.php74
-rw-r--r--includes/search/SearchOracle.php170
-rw-r--r--includes/search/SearchPostgres.php83
-rw-r--r--includes/search/SearchSqlite.php52
-rw-r--r--includes/search/SearchUpdate.php132
-rw-r--r--includes/site/MediaWikiSite.php352
-rw-r--r--includes/site/Site.php702
-rw-r--r--includes/site/SiteList.php300
-rw-r--r--includes/site/SiteSQLStore.php507
-rw-r--r--includes/site/SiteStore.php85
-rw-r--r--includes/specials/SpecialActiveusers.php87
-rw-r--r--includes/specials/SpecialAllmessages.php171
-rw-r--r--includes/specials/SpecialAllpages.php252
-rw-r--r--includes/specials/SpecialAncientpages.php30
-rw-r--r--includes/specials/SpecialBlankpage.php3
-rw-r--r--includes/specials/SpecialBlock.php147
-rw-r--r--includes/specials/SpecialBlockList.php74
-rw-r--r--includes/specials/SpecialBlockme.php62
-rw-r--r--includes/specials/SpecialBooksources.php81
-rw-r--r--includes/specials/SpecialBrokenRedirects.php75
-rw-r--r--includes/specials/SpecialCachedPage.php5
-rw-r--r--includes/specials/SpecialCategories.php46
-rw-r--r--includes/specials/SpecialChangeEmail.php79
-rw-r--r--includes/specials/SpecialChangePassword.php187
-rw-r--r--includes/specials/SpecialComparePages.php38
-rw-r--r--includes/specials/SpecialConfirmemail.php109
-rw-r--r--includes/specials/SpecialContributions.php435
-rw-r--r--includes/specials/SpecialDeadendpages.php29
-rw-r--r--includes/specials/SpecialDeletedContributions.php192
-rw-r--r--includes/specials/SpecialDisambiguations.php161
-rw-r--r--includes/specials/SpecialDoubleRedirects.php108
-rw-r--r--includes/specials/SpecialEditWatchlist.php215
-rw-r--r--includes/specials/SpecialEmailuser.php95
-rw-r--r--includes/specials/SpecialExport.php160
-rw-r--r--includes/specials/SpecialFewestrevisions.php58
-rw-r--r--includes/specials/SpecialFileDuplicateSearch.php71
-rw-r--r--includes/specials/SpecialFilepath.php62
-rw-r--r--includes/specials/SpecialImport.php259
-rw-r--r--includes/specials/SpecialJavaScriptTest.php75
-rw-r--r--includes/specials/SpecialLinkSearch.php110
-rw-r--r--includes/specials/SpecialListfiles.php311
-rw-r--r--includes/specials/SpecialListgrouprights.php69
-rw-r--r--includes/specials/SpecialListredirects.php54
-rw-r--r--includes/specials/SpecialListusers.php151
-rw-r--r--includes/specials/SpecialLockdb.php4
-rw-r--r--includes/specials/SpecialLog.php83
-rw-r--r--includes/specials/SpecialLonelypages.php57
-rw-r--r--includes/specials/SpecialLongpages.php5
-rw-r--r--includes/specials/SpecialMIMEsearch.php114
-rw-r--r--includes/specials/SpecialMergeHistory.php161
-rw-r--r--includes/specials/SpecialMostcategories.php61
-rw-r--r--includes/specials/SpecialMostimages.php31
-rw-r--r--includes/specials/SpecialMostinterwikis.php45
-rw-r--r--includes/specials/SpecialMostlinked.php84
-rw-r--r--includes/specials/SpecialMostlinkedcategories.php44
-rw-r--r--includes/specials/SpecialMostlinkedtemplates.php44
-rw-r--r--includes/specials/SpecialMostrevisions.php4
-rw-r--r--includes/specials/SpecialMovepage.php314
-rw-r--r--includes/specials/SpecialNewimages.php57
-rw-r--r--includes/specials/SpecialNewpages.php195
-rw-r--r--includes/specials/SpecialPagesWithProp.php156
-rw-r--r--includes/specials/SpecialPasswordReset.php102
-rw-r--r--includes/specials/SpecialPopularpages.php42
-rw-r--r--includes/specials/SpecialPreferences.php30
-rw-r--r--includes/specials/SpecialPrefixindex.php167
-rw-r--r--includes/specials/SpecialProtectedpages.php189
-rw-r--r--includes/specials/SpecialProtectedtitles.php92
-rw-r--r--includes/specials/SpecialRandomInCategory.php291
-rw-r--r--includes/specials/SpecialRandompage.php41
-rw-r--r--includes/specials/SpecialRandomredirect.php3
-rw-r--r--includes/specials/SpecialRecentchanges.php470
-rw-r--r--includes/specials/SpecialRecentchangeslinked.php101
-rw-r--r--includes/specials/SpecialRedirect.php235
-rw-r--r--includes/specials/SpecialResetTokens.php145
-rw-r--r--includes/specials/SpecialRevisiondelete.php199
-rw-r--r--includes/specials/SpecialSearch.php299
-rw-r--r--includes/specials/SpecialShortpages.php22
-rw-r--r--includes/specials/SpecialSpecialpages.php34
-rw-r--r--includes/specials/SpecialStatistics.php90
-rw-r--r--includes/specials/SpecialTags.php73
-rw-r--r--includes/specials/SpecialUnblock.php39
-rw-r--r--includes/specials/SpecialUncategorizedcategories.php13
-rw-r--r--includes/specials/SpecialUncategorizedimages.php5
-rw-r--r--includes/specials/SpecialUncategorizedpages.php24
-rw-r--r--includes/specials/SpecialUndelete.php970
-rw-r--r--includes/specials/SpecialUnlockdb.php4
-rw-r--r--includes/specials/SpecialUnusedcategories.php23
-rw-r--r--includes/specials/SpecialUnusedimages.php19
-rw-r--r--includes/specials/SpecialUnusedtemplates.php34
-rw-r--r--includes/specials/SpecialUnwatchedpages.php33
-rw-r--r--includes/specials/SpecialUpload.php216
-rw-r--r--includes/specials/SpecialUploadStash.php63
-rw-r--r--includes/specials/SpecialUserlogin.php609
-rw-r--r--includes/specials/SpecialUserlogout.php9
-rw-r--r--includes/specials/SpecialUserrights.php264
-rw-r--r--includes/specials/SpecialVersion.php308
-rw-r--r--includes/specials/SpecialWantedcategories.php22
-rw-r--r--includes/specials/SpecialWantedfiles.php22
-rw-r--r--includes/specials/SpecialWantedpages.php11
-rw-r--r--includes/specials/SpecialWantedtemplates.php18
-rw-r--r--includes/specials/SpecialWatchlist.php292
-rw-r--r--includes/specials/SpecialWhatlinkshere.php85
-rw-r--r--includes/specials/SpecialWithoutinterwiki.php32
-rw-r--r--includes/templates/NoLocalSettings.php16
-rw-r--r--includes/templates/Usercreate.php441
-rw-r--r--includes/templates/Userlogin.php280
-rw-r--r--includes/tidy.conf5
-rw-r--r--includes/upload/UploadBase.php400
-rw-r--r--includes/upload/UploadFromChunks.php72
-rw-r--r--includes/upload/UploadFromFile.php14
-rw-r--r--includes/upload/UploadFromStash.php23
-rw-r--r--includes/upload/UploadFromUrl.php71
-rw-r--r--includes/upload/UploadStash.php101
-rw-r--r--includes/zhtable/Makefile336
-rw-r--r--includes/zhtable/Makefile.py391
-rw-r--r--includes/zhtable/README33
-rw-r--r--includes/zhtable/printutf8.c99
-rw-r--r--includes/zhtable/simp2trad.manual372
-rw-r--r--includes/zhtable/simp2trad_noconvert.manual139
-rw-r--r--includes/zhtable/simp2trad_supp_set.manual2
-rw-r--r--includes/zhtable/simpphrases.manual2239
-rw-r--r--includes/zhtable/simpphrases_exclude.manual21
-rw-r--r--includes/zhtable/toCN.manual275
-rw-r--r--includes/zhtable/toHK.manual2300
-rw-r--r--includes/zhtable/toSG.manual21
-rw-r--r--includes/zhtable/toSimp.manual165
-rw-r--r--includes/zhtable/toTW.manual411
-rw-r--r--includes/zhtable/toTrad.manual186
-rw-r--r--includes/zhtable/trad2simp.manual150
-rw-r--r--includes/zhtable/trad2simp_noconvert.manual5
-rw-r--r--includes/zhtable/trad2simp_supp_set.manual3
-rw-r--r--includes/zhtable/trad2simp_supp_unset.manual0
-rw-r--r--includes/zhtable/tradphrases.manual4310
-rw-r--r--includes/zhtable/tradphrases_exclude.manual330
663 files changed, 73116 insertions, 52713 deletions
diff --git a/includes/Action.php b/includes/Action.php
index 51922251..4b6e4468 100644
--- a/includes/Action.php
+++ b/includes/Action.php
@@ -32,13 +32,13 @@
*
* Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input
* format (protect, delete, move, etc), and the just-do-something format (watch, rollback,
- * patrol, etc). The FormAction and FormlessAction classes respresent these two groups.
+ * patrol, etc). The FormAction and FormlessAction classes represent these two groups.
*/
abstract class Action {
/**
* Page on which we're performing the action
- * @var Page $page
+ * @var WikiPage|Article|ImagePage|CategoryPage|Page $page
*/
protected $page;
@@ -59,9 +59,9 @@ abstract class Action {
* the action is disabled, or null if it's not recognised
* @param $action String
* @param $overrides Array
- * @return bool|null|string
+ * @return bool|null|string|callable
*/
- private final static function getClass( $action, array $overrides ) {
+ final private static function getClass( $action, array $overrides ) {
global $wgActions;
$action = strtolower( $action );
@@ -88,13 +88,19 @@ abstract class Action {
* @return Action|bool|null false if the action is disabled, null
* if it is not recognised
*/
- public final static function factory( $action, Page $page, IContextSource $context = null ) {
- $class = self::getClass( $action, $page->getActionOverrides() );
- if ( $class ) {
- $obj = new $class( $page, $context );
+ final public static function factory( $action, Page $page, IContextSource $context = null ) {
+ $classOrCallable = self::getClass( $action, $page->getActionOverrides() );
+
+ if ( is_string( $classOrCallable ) ) {
+ $obj = new $classOrCallable( $page, $context );
return $obj;
}
- return $class;
+
+ if ( is_callable( $classOrCallable ) ) {
+ return call_user_func_array( $classOrCallable, array( $page, $context ) );
+ }
+
+ return $classOrCallable;
}
/**
@@ -106,7 +112,7 @@ abstract class Action {
* @param $context IContextSource
* @return string: action name
*/
- public final static function getActionName( IContextSource $context ) {
+ final public static function getActionName( IContextSource $context ) {
global $wgActions;
$request = $context->getRequest();
@@ -136,7 +142,7 @@ abstract class Action {
return 'view';
}
- $action = Action::factory( $actionName, $context->getWikiPage() );
+ $action = Action::factory( $actionName, $context->getWikiPage(), $context );
if ( $action instanceof Action ) {
return $action->getName();
}
@@ -147,10 +153,10 @@ abstract class Action {
/**
* Check if a given action is recognised, even if it's disabled
*
- * @param $name String: name of an action
+ * @param string $name name of an action
* @return Bool
*/
- public final static function exists( $name ) {
+ final public static function exists( $name ) {
return self::getClass( $name, array() ) !== null;
}
@@ -158,11 +164,17 @@ abstract class Action {
* Get the IContextSource in use here
* @return IContextSource
*/
- public final function getContext() {
+ final public function getContext() {
if ( $this->context instanceof IContextSource ) {
return $this->context;
+ } else if ( $this->page instanceof Article ) {
+ // NOTE: $this->page can be a WikiPage, which does not have a context.
+ wfDebug( __METHOD__ . ': no context known, falling back to Article\'s context.' );
+ return $this->page->getContext();
}
- return $this->page->getContext();
+
+ wfWarn( __METHOD__ . ': no context known, falling back to RequestContext::getMain().' );
+ return RequestContext::getMain();
}
/**
@@ -170,7 +182,7 @@ abstract class Action {
*
* @return WebRequest
*/
- public final function getRequest() {
+ final public function getRequest() {
return $this->getContext()->getRequest();
}
@@ -179,7 +191,7 @@ abstract class Action {
*
* @return OutputPage
*/
- public final function getOutput() {
+ final public function getOutput() {
return $this->getContext()->getOutput();
}
@@ -188,7 +200,7 @@ abstract class Action {
*
* @return User
*/
- public final function getUser() {
+ final public function getUser() {
return $this->getContext()->getUser();
}
@@ -197,7 +209,7 @@ abstract class Action {
*
* @return Skin
*/
- public final function getSkin() {
+ final public function getSkin() {
return $this->getContext()->getSkin();
}
@@ -206,17 +218,17 @@ abstract class Action {
*
* @return Language
*/
- public final function getLanguage() {
+ final public function getLanguage() {
return $this->getContext()->getLanguage();
}
/**
* Shortcut to get the user Language being used for this instance
*
- * @deprecated 1.19 Use getLanguage instead
+ * @deprecated since 1.19 Use getLanguage instead
* @return Language
*/
- public final function getLang() {
+ final public function getLang() {
wfDeprecated( __METHOD__, '1.19' );
return $this->getLanguage();
}
@@ -225,7 +237,7 @@ abstract class Action {
* Shortcut to get the Title object from the page
* @return Title
*/
- public final function getTitle() {
+ final public function getTitle() {
return $this->page->getTitle();
}
@@ -235,18 +247,26 @@ abstract class Action {
*
* @return Message object
*/
- public final function msg() {
+ final public function msg() {
$params = func_get_args();
return call_user_func_array( array( $this->getContext(), 'msg' ), $params );
}
/**
- * Protected constructor: use Action::factory( $action, $page ) to actually build
- * these things in the real world
+ * Constructor.
+ *
+ * Only public since 1.21
+ *
* @param $page Page
* @param $context IContextSource
*/
- protected function __construct( Page $page, IContextSource $context = null ) {
+ public function __construct( Page $page, IContextSource $context = null ) {
+ if ( $context === null ) {
+ wfWarn( __METHOD__ . ' called without providing a Context object.' );
+ // NOTE: We could try to initialize $context using $page->getContext(),
+ // if $page is an Article. That however seems to not work seamlessly.
+ }
+
$this->page = $page;
$this->context = $context;
}
@@ -255,7 +275,7 @@ abstract class Action {
* Return the name of the action this object responds to
* @return String lowercase
*/
- public abstract function getName();
+ abstract public function getName();
/**
* Get the permission required to perform this action. Often, but not always,
@@ -272,7 +292,7 @@ abstract class Action {
* must throw subclasses of ErrorPageError
*
* @param $user User: the user to check, or null to use the context user
- * @throws ErrorPageError
+ * @throws UserBlockedError|ReadOnlyError|PermissionsError
* @return bool True on success
*/
protected function checkCanExecute( User $user ) {
@@ -350,13 +370,13 @@ abstract class Action {
* $this->getOutput(), etc.
* @throws ErrorPageError
*/
- public abstract function show();
+ abstract public function show();
/**
* Execute the action in a silent fashion: do not display anything or release any errors.
* @return Bool whether execution was successful
*/
- public abstract function execute();
+ abstract public function execute();
}
/**
@@ -368,27 +388,32 @@ abstract class FormAction extends Action {
* Get an HTMLForm descriptor array
* @return Array
*/
- protected abstract function getFormFields();
+ abstract protected function getFormFields();
/**
* Add pre- or post-text to the form
* @return String HTML which will be sent to $form->addPreText()
*/
- protected function preText() { return ''; }
+ protected function preText() {
+ return '';
+ }
/**
* @return string
*/
- protected function postText() { return ''; }
+ protected function postText() {
+ return '';
+ }
/**
* Play with the HTMLForm if you need to more substantially
* @param $form HTMLForm
*/
- protected function alterForm( HTMLForm $form ) {}
+ protected function alterForm( HTMLForm $form ) {
+ }
/**
- * Get the HTMLForm to control behaviour
+ * Get the HTMLForm to control behavior
* @return HTMLForm|null
*/
protected function getForm() {
@@ -406,7 +431,7 @@ abstract class FormAction extends Action {
$this->getRequest()->getQueryValues(),
array( 'action' => null, 'title' => null )
);
- $form->addHiddenField( 'redirectparams', wfArrayToCGI( $params ) );
+ $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
$form->addPreText( $this->preText() );
$form->addPostText( $this->postText() );
@@ -425,21 +450,21 @@ abstract class FormAction extends Action {
* @param $data Array
* @return Bool|Array true for success, false for didn't-try, array of errors on failure
*/
- public abstract function onSubmit( $data );
+ abstract public function onSubmit( $data );
/**
* Do something exciting on successful processing of the form. This might be to show
* a confirmation message (watch, rollback, etc) or to redirect somewhere else (edit,
* protect, etc).
*/
- public abstract function onSuccess();
+ abstract public function onSuccess();
/**
* The basic pattern for actions is to display some sort of HTMLForm UI, maybe with
* some stuff underneath (history etc); to do some processing on submission of that
* form (delete, protect, etc) and to do something exciting on 'success', be that
* display something new or redirect to somewhere. Some actions have more exotic
- * behaviour, but that's what subclassing is for :D
+ * behavior, but that's what subclassing is for :D
*/
public function show() {
$this->setHeaders();
@@ -455,15 +480,16 @@ abstract class FormAction extends Action {
/**
* @see Action::execute()
- * @throws ErrorPageError
+ *
* @param $data array|null
* @param $captureErrors bool
+ * @throws ErrorPageError|Exception
* @return bool
*/
public function execute( array $data = null, $captureErrors = true ) {
try {
// Set a new context so output doesn't leak.
- $this->context = clone $this->page->getContext();
+ $this->context = clone $this->getContext();
// This will throw exceptions if there's a problem
$this->checkCanExecute( $this->getUser() );
@@ -507,7 +533,7 @@ abstract class FormlessAction extends Action {
* @return String|null will be added to the HTMLForm if present, or just added to the
* output if not. Return null to not add anything
*/
- public abstract function onView();
+ abstract public function onView();
/**
* We don't want an HTMLForm
@@ -544,14 +570,15 @@ abstract class FormlessAction extends Action {
/**
* Execute the action silently, not giving any output. Since these actions don't have
* forms, they probably won't have any data, but some (eg rollback) may do
- * @param $data Array values that would normally be in the GET request
- * @param $captureErrors Bool whether to catch exceptions and just return false
+ * @param array $data values that would normally be in the GET request
+ * @param bool $captureErrors whether to catch exceptions and just return false
+ * @throws ErrorPageError|Exception
* @return Bool whether execution was successful
*/
public function execute( array $data = null, $captureErrors = true ) {
try {
// Set a new context so output doesn't leak.
- $this->context = clone $this->page->getContext();
+ $this->context = clone $this->getContext();
if ( is_array( $data ) ) {
$this->context->setRequest( new FauxRequest( $data, false ) );
}
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php
index b00cf309..c9ca1283 100644
--- a/includes/AjaxDispatcher.php
+++ b/includes/AjaxDispatcher.php
@@ -63,7 +63,7 @@ class AjaxDispatcher {
$this->mode = "post";
}
- switch( $this->mode ) {
+ switch ( $this->mode ) {
case 'get':
$this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : '';
if ( ! empty( $_GET["rsargs"] ) ) {
@@ -111,15 +111,13 @@ class AjaxDispatcher {
wfHttpError(
400,
'Bad Request',
- "unknown function " . (string) $this->func_name
+ "unknown function " . $this->func_name
);
- } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true )
- && !$wgUser->isAllowed( 'read' ) )
- {
+ } elseif ( !User::isEveryoneAllowed( 'read' ) && !$wgUser->isAllowed( 'read' ) ) {
wfHttpError(
403,
'Forbidden',
- 'You must log in to view pages.' );
+ 'You are not allowed to view pages.' );
} else {
wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" );
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
index 6bf94ccb..d5536529 100644
--- a/includes/AjaxResponse.php
+++ b/includes/AjaxResponse.php
@@ -172,7 +172,7 @@ class AjaxResponse {
# tell the client to use a cached copy, without a way to purge it.
if ( $wgUseSquid ) {
- # Expect explicite purge of the proxy cache, but require end user agents
+ # Expect explicit 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
@@ -204,13 +204,13 @@ class AjaxResponse {
/**
* checkLastModified tells the client to use the client-cached response if
- * possible. If sucessful, the AjaxResponse is disabled so that
+ * possible. If successful, the AjaxResponse is disabled so that
* any future call to AjaxResponse::printText() have no effect.
*
* @param $timestamp string
* @return bool Returns true if the response code was set to 304 Not Modified.
*/
- function checkLastModified ( $timestamp ) {
+ function checkLastModified( $timestamp ) {
global $wgCachePages, $wgCacheEpoch, $wgUser;
$fname = 'AjaxResponse::checkLastModified';
diff --git a/includes/ArrayUtils.php b/includes/ArrayUtils.php
new file mode 100644
index 00000000..985271f7
--- /dev/null
+++ b/includes/ArrayUtils.php
@@ -0,0 +1,69 @@
+<?php
+
+class ArrayUtils {
+ /**
+ * Sort the given array in a pseudo-random order which depends only on the
+ * given key and each element value. This is typically used for load
+ * balancing between servers each with a local cache.
+ *
+ * Keys are preserved. The input array is modified in place.
+ *
+ * Note: Benchmarking on PHP 5.3 and 5.4 indicates that for small
+ * strings, md5() is only 10% slower than hash('joaat',...) etc.,
+ * since the function call overhead dominates. So there's not much
+ * justification for breaking compatibility with installations
+ * compiled with ./configure --disable-hash.
+ *
+ * @param $array The array to sort
+ * @param $key The string key
+ * @param $separator A separator used to delimit the array elements and the
+ * key. This can be chosen to provide backwards compatibility with
+ * various consistent hash implementations that existed before this
+ * function was introduced.
+ */
+ public static function consistentHashSort( &$array, $key, $separator = "\000" ) {
+ $hashes = array();
+ foreach ( $array as $elt ) {
+ $hashes[$elt] = md5( $elt . $separator . $key );
+ }
+ uasort( $array, function ( $a, $b ) use ( $hashes ) {
+ return strcmp( $hashes[$a], $hashes[$b] );
+ } );
+ }
+
+ /**
+ * Given an array of non-normalised probabilities, this function will select
+ * an element and return the appropriate key
+ *
+ * @param $weights array
+ *
+ * @return bool|int|string
+ */
+ public static function pickRandom( $weights ) {
+ if ( !is_array( $weights ) || count( $weights ) == 0 ) {
+ return false;
+ }
+
+ $sum = array_sum( $weights );
+ if ( $sum == 0 ) {
+ # No loads on any of them
+ # In previous versions, this triggered an unweighted random selection,
+ # but this feature has been removed as of April 2006 to allow for strict
+ # separation of query groups.
+ return false;
+ }
+ $max = mt_getrandmax();
+ $rand = mt_rand( 0, $max ) / $max * $sum;
+
+ $sum = 0;
+ foreach ( $weights as $i => $w ) {
+ $sum += $w;
+ # Do not return keys if they have 0 weight.
+ # Note that the "all 0 weight" case is handed above
+ if ( $w > 0 && $sum >= $rand ) {
+ break;
+ }
+ }
+ return $i;
+ }
+}
diff --git a/includes/Article.php b/includes/Article.php
index 9ab4b6ba..0b18221a 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -25,7 +25,7 @@
*
* This maintains WikiPage functions for backwards compatibility.
*
- * @todo move and rewrite code to an Action class
+ * @todo Move and rewrite code to an Action class
*
* See design.txt for an overview.
* Note: edit user interface and cache support functions have been
@@ -33,7 +33,7 @@
*
* @internal documentation reviewed 15 Mar 2010
*/
-class Article extends Page {
+class Article implements Page {
/**@{{
* @private
*/
@@ -57,10 +57,17 @@ class Article extends Page {
public $mParserOptions;
/**
- * Content of the revision we are working on
+ * Text of the revision we are working on
* @var string $mContent
*/
- var $mContent; // !<
+ var $mContent; // !< #BC cruft
+
+ /**
+ * Content of the revision we are working on
+ * @var Content
+ * @since 1.21
+ */
+ var $mContentObject; // !<
/**
* Is the content ($mContent) already loaded?
@@ -127,7 +134,7 @@ class Article extends Page {
/**
* Constructor from a page id
- * @param $id Int article ID to load
+ * @param int $id article ID to load
* @return Article|null
*/
public static function newFromID( $id ) {
@@ -153,7 +160,7 @@ class Article extends Page {
$page = null;
wfRunHooks( 'ArticleFromTitle', array( &$title, &$page ) );
if ( !$page ) {
- switch( $title->getNamespace() ) {
+ switch ( $title->getNamespace() ) {
case NS_FILE:
$page = new ImagePage( $title );
break;
@@ -231,9 +238,32 @@ class Article extends Page {
* This function has side effects! Do not use this function if you
* only want the real revision text if any.
*
+ * @deprecated in 1.21; use WikiPage::getContent() instead
+ *
* @return string Return the text of this revision
*/
public function getContent() {
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+ $content = $this->getContentObject();
+ return ContentHandler::getContentText( $content );
+ }
+
+ /**
+ * Returns a Content object representing the pages effective display content,
+ * not necessarily the revision's content!
+ *
+ * Note that getContent/loadContent do not follow redirects anymore.
+ * If you need to fetch redirectable content easily, try
+ * the shortcut in WikiPage::getRedirectTarget()
+ *
+ * This function has side effects! Do not use this function if you
+ * only want the real revision text if any.
+ *
+ * @return Content Return the content of this revision
+ *
+ * @since 1.21
+ */
+ protected function getContentObject() {
wfProfileIn( __METHOD__ );
if ( $this->mPage->getID() === 0 ) {
@@ -244,19 +274,19 @@ class Article extends Page {
if ( $text === false ) {
$text = '';
}
+
+ $content = ContentHandler::makeContent( $text, $this->getTitle() );
} else {
$message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
- $text = wfMessage( $message )->text();
+ $content = new MessageContent( $message, null, 'parsemag' );
}
- wfProfileOut( __METHOD__ );
-
- return $text;
} else {
- $this->fetchContent();
- wfProfileOut( __METHOD__ );
-
- return $this->mContent;
+ $this->fetchContentObject();
+ $content = $this->mContentObject;
}
+
+ wfProfileOut( __METHOD__ );
+ return $content;
}
/**
@@ -336,22 +366,61 @@ class Article extends Page {
* Get text of an article from database
* Does *NOT* follow redirects.
*
+ * @protected
+ * @note this is really internal functionality that should really NOT be used by other functions. For accessing
+ * article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code
+ * uses this method to retrieve page text from the database, so the function has to remain public for now.
+ *
* @return mixed string containing article contents, or false if null
+ * @deprecated in 1.21, use WikiPage::getContent() instead
*/
- function fetchContent() {
- if ( $this->mContentLoaded ) {
+ function fetchContent() { #BC cruft!
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ if ( $this->mContentLoaded && $this->mContent ) {
return $this->mContent;
}
wfProfileIn( __METHOD__ );
+ $content = $this->fetchContentObject();
+
+ // @todo Get rid of mContent everywhere!
+ $this->mContent = ContentHandler::getContentText( $content );
+ ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
+
+ wfProfileOut( __METHOD__ );
+
+ return $this->mContent;
+ }
+
+ /**
+ * Get text content object
+ * Does *NOT* follow redirects.
+ * TODO: when is this null?
+ *
+ * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
+ *
+ * @return Content|null|boolean false
+ *
+ * @since 1.21
+ */
+ protected function fetchContentObject() {
+ if ( $this->mContentLoaded ) {
+ return $this->mContentObject;
+ }
+
+ wfProfileIn( __METHOD__ );
+
$this->mContentLoaded = true;
+ $this->mContent = null;
$oldid = $this->getOldID();
# Pre-fill content with error message so that if something
# fails we'll have something telling us what we intended.
- $this->mContent = wfMessage( 'missing-revision', $oldid )->plain();
+ //XXX: this isn't page content but a UI message. horrible.
+ $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() );
if ( $oldid ) {
# $this->mRevision might already be fetched by getOldIDFromRequest()
@@ -371,6 +440,7 @@ class Article extends Page {
}
$this->mRevision = $this->mPage->getRevision();
+
if ( !$this->mRevision ) {
wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
wfProfileOut( __METHOD__ );
@@ -380,14 +450,14 @@ class Article extends Page {
// @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
// We should instead work with the Revision object when we need it...
- $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
+ $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed
$this->mRevIdFetched = $this->mRevision->getId();
- wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
+ wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
wfProfileOut( __METHOD__ );
- return $this->mContent;
+ return $this->mContentObject;
}
/**
@@ -420,7 +490,7 @@ class Article extends Page {
* @return Revision|null
*/
public function getRevisionFetched() {
- $this->fetchContent();
+ $this->fetchContentObject();
return $this->mRevision;
}
@@ -443,7 +513,7 @@ class Article extends Page {
* page of the given title.
*/
public function view() {
- global $wgParser, $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
+ global $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
wfProfileIn( __METHOD__ );
@@ -540,7 +610,7 @@ class Article extends Page {
$this->mParserOutput = false;
while ( !$outputDone && ++$pass ) {
- switch( $pass ) {
+ switch ( $pass ) {
case 1:
wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) );
break;
@@ -580,7 +650,7 @@ class Article extends Page {
break;
case 3:
# This will set $this->mRevision if needed
- $this->fetchContent();
+ $this->fetchContentObject();
# Are we looking at an old revision
if ( $oldid && $this->mRevision ) {
@@ -604,18 +674,25 @@ class Article extends Page {
wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
$this->showCssOrJsPage();
$outputDone = true;
- } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
+ } elseif ( !wfRunHooks( 'ArticleContentViewCustom',
+ array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
+
+ # Allow extensions do their own custom view for certain pages
+ $outputDone = true;
+ } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom',
+ array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
+
# Allow extensions do their own custom view for certain pages
$outputDone = true;
} else {
- $text = $this->getContent();
- $rt = Title::newFromRedirectArray( $text );
+ $content = $this->getContentObject();
+ $rt = $content ? $content->getRedirectChain() : null;
if ( $rt ) {
wfDebug( __METHOD__ . ": showing redirect=no page\n" );
# Viewing a redirect page (e.g. with parameter redirect=no)
$outputPage->addHTML( $this->viewRedirect( $rt ) );
# Parse just to get categories, displaytitle, etc.
- $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
+ $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false );
$outputPage->addParserOutputNoText( $this->mParserOutput );
$outputDone = true;
}
@@ -625,8 +702,8 @@ class Article extends Page {
# Run the parse, protected by a pool counter
wfDebug( __METHOD__ . ": doing uncached parse\n" );
- $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
- $this->getRevIdFetched(), $useParserCache, $this->getContent() );
+ $poolArticleView = new PoolWorkArticleView( $this->getPage(), $parserOptions,
+ $this->getRevIdFetched(), $useParserCache, $this->getContentObject() );
if ( !$poolArticleView->execute() ) {
$error = $poolArticleView->getError();
@@ -690,6 +767,8 @@ class Article extends Page {
$this->showViewFooter();
$this->mPage->doViewUpdates( $user );
+ $outputPage->addModules( 'mediawiki.action.view.postEdit' );
+
wfProfileOut( __METHOD__ );
}
@@ -708,6 +787,8 @@ class Article extends Page {
/**
* Show a diff page according to current request variables. For use within
* Article::view() only, other callers should use the DifferenceEngine class.
+ *
+ * @todo Make protected
*/
public function showDiffPage() {
$request = $this->getContext()->getRequest();
@@ -719,7 +800,17 @@ class Article extends Page {
$unhide = $request->getInt( 'unhide' ) == 1;
$oldid = $this->getOldID();
- $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+ $rev = $this->getRevisionFetched();
+
+ if ( !$rev ) {
+ $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) );
+ $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 );
+ return;
+ }
+
+ $contentHandler = $rev->getContentHandler();
+ $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+
// DifferenceEngine directly fetched the revision:
$this->mRevIdFetched = $de->mNewid;
$de->showDiffPage( $diffOnly );
@@ -736,66 +827,77 @@ class Article extends Page {
*
* This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
* page views.
+ *
+ * @param bool $showCacheHint whether to show a message telling the user to clear the browser cache (default: true).
*/
- protected function showCssOrJsPage() {
- $dir = $this->getContext()->getLanguage()->getDir();
- $lang = $this->getContext()->getLanguage()->getCode();
-
+ protected function showCssOrJsPage( $showCacheHint = true ) {
$outputPage = $this->getContext()->getOutput();
- $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
- 'clearyourcache' );
- // Give hooks a chance to customise the output
- if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
- // Wrap the whole lot in a <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
- $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
- $outputPage->addHTML( "\n</pre>\n" );
+ if ( $showCacheHint ) {
+ $dir = $this->getContext()->getLanguage()->getDir();
+ $lang = $this->getContext()->getLanguage()->getCode();
+
+ $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
+ 'clearyourcache' );
+ }
+
+ $this->fetchContentObject();
+
+ if ( $this->mContentObject ) {
+ // Give hooks a chance to customise the output
+ if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mContentObject, $this->getTitle(), $outputPage ) ) ) {
+ $po = $this->mContentObject->getParserOutput( $this->getTitle() );
+ $outputPage->addHTML( $po->getText() );
+ }
}
}
/**
* Get the robot policy to be used for the current view
- * @param $action String the action= GET parameter
- * @param $pOutput ParserOutput
+ * @param string $action the action= GET parameter
+ * @param $pOutput ParserOutput|null
* @return Array the policy that should be set
* TODO: actions other than 'view'
*/
- public function getRobotPolicy( $action, $pOutput ) {
+ public function getRobotPolicy( $action, $pOutput = null ) {
global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy;
$ns = $this->getTitle()->getNamespace();
- if ( $ns == NS_USER || $ns == NS_USER_TALK ) {
- # Don't index user and user talk pages for blocked users (bug 11443)
- if ( !$this->getTitle()->isSubpage() ) {
- if ( Block::newFromTarget( null, $this->getTitle()->getText() ) instanceof Block ) {
- return array(
- 'index' => 'noindex',
- 'follow' => 'nofollow'
- );
- }
+ # Don't index user and user talk pages for blocked users (bug 11443)
+ if ( ( $ns == NS_USER || $ns == NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) {
+ $specificTarget = null;
+ $vagueTarget = null;
+ $titleText = $this->getTitle()->getText();
+ if ( IP::isValid( $titleText ) ) {
+ $vagueTarget = $titleText;
+ } else {
+ $specificTarget = $titleText;
+ }
+ if ( Block::newFromTarget( $specificTarget, $vagueTarget ) instanceof Block ) {
+ return array(
+ 'index' => 'noindex',
+ 'follow' => 'nofollow'
+ );
}
}
if ( $this->mPage->getID() === 0 || $this->getOldID() ) {
# Non-articles (special pages etc), and old revisions
return array(
- 'index' => 'noindex',
+ 'index' => 'noindex',
'follow' => 'nofollow'
);
} elseif ( $this->getContext()->getOutput()->isPrintable() ) {
# Discourage indexing of printable versions, but encourage following
return array(
- 'index' => 'noindex',
+ 'index' => 'noindex',
'follow' => 'follow'
);
} elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) {
# For ?curid=x urls, disallow indexing
return array(
- 'index' => 'noindex',
+ 'index' => 'noindex',
'follow' => 'follow'
);
}
@@ -887,14 +989,13 @@ class Article extends Page {
// Set the fragment if one was specified in the redirect
if ( strval( $this->getTitle()->getFragment() ) != '' ) {
- $fragment = Xml::escapeJsString( $this->getTitle()->getFragmentForURL() );
- $outputPage->addInlineScript( "redirectToFragment(\"$fragment\");" );
+ $outputPage->addInlineScript( Xml::encodeJsCall(
+ 'redirectToFragment', array( $this->getTitle()->getFragmentForURL() )
+ ) );
}
// Add a <link rel="canonical"> tag
- $outputPage->addLink( array( 'rel' => 'canonical',
- 'href' => $this->getTitle()->getLocalURL() )
- );
+ $outputPage->setCanonicalUrl( $this->getTitle()->getLocalURL() );
// Tell the output object that the user arrived at this article through a redirect
$outputPage->setRedirectedFrom( $this->mRedirectedFrom );
@@ -936,11 +1037,10 @@ class Article extends Page {
$this->getContext()->getOutput()->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.
- $this->showPatrolFooter();
+ // Show a footer allowing the user to patrol the shown revision or page if possible
+ $patrolFooterShown = $this->showPatrolFooter();
- wfRunHooks( 'ArticleViewFooter', array( $this ) );
+ wfRunHooks( 'ArticleViewFooter', array( $this, $patrolFooterShown ) );
}
@@ -948,19 +1048,94 @@ class Article extends Page {
* 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.
+ * Side effect: When the patrol link is build, this method will call
+ * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
+ *
+ * @return bool
*/
public function showPatrolFooter() {
- $request = $this->getContext()->getRequest();
+ global $wgUseNPPatrol, $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
+
$outputPage = $this->getContext()->getOutput();
$user = $this->getContext()->getUser();
- $rcid = $request->getVal( 'rcid' );
+ $cache = wfGetMainCache();
+ $rc = false;
- if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol', $user ) ) {
- return;
+ if ( !$this->getTitle()->quickUserCan( 'patrol', $user ) || !( $wgUseRCPatrol || $wgUseNPPatrol ) ) {
+ // Patrolling is disabled or the user isn't allowed to
+ return false;
}
+ wfProfileIn( __METHOD__ );
+
+ // New page patrol: Get the timestamp of the oldest revison which
+ // the revision table holds for the given page. Then we look
+ // whether it's within the RC lifespan and if it is, we try
+ // to get the recentchanges row belonging to that entry
+ // (with rc_new = 1).
+
+ // Check for cached results
+ if ( $cache->get( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ) ) ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ if ( $this->mRevision && !RecentChange::isInRCLifespan( $this->mRevision->getTimestamp(), 21600 ) ) {
+ // The current revision is already older than what could be in the RC table
+ // 6h tolerance because the RC might not be cleaned out regularly
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $oldestRevisionTimestamp = $dbr->selectField(
+ 'revision',
+ 'MIN( rev_timestamp )',
+ array( 'rev_page' => $this->getTitle()->getArticleID() ),
+ __METHOD__
+ );
+
+ if ( $oldestRevisionTimestamp && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 ) ) {
+ // 6h tolerance because the RC might not be cleaned out regularly
+ $rc = RecentChange::newFromConds(
+ array(
+ 'rc_new' => 1,
+ 'rc_timestamp' => $oldestRevisionTimestamp,
+ 'rc_namespace' => $this->getTitle()->getNamespace(),
+ 'rc_cur_id' => $this->getTitle()->getArticleID(),
+ 'rc_patrolled' => 0
+ ),
+ __METHOD__,
+ array( 'USE INDEX' => 'new_name_timestamp' )
+ );
+ }
+
+ if ( !$rc ) {
+ // No RC entry around
+
+ // Cache the information we gathered above in case we can't patrol
+ // Don't cache in case we can patrol as this could change
+ $cache->set( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ), '1' );
+
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ if ( $rc->getPerformer()->getName() == $user->getName() ) {
+ // Don't show a patrol link for own creations. If the user could
+ // patrol them, they already would be patrolled
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $rcid = $rc->getAttribute( 'rc_id' );
+
$token = $user->getEditToken( $rcid );
+
$outputPage->preventClickjacking();
+ if ( $wgEnableAPI && $wgEnableWriteAPI && $user->isAllowed( 'writeapi' ) ) {
+ $outputPage->addModules( 'mediawiki.page.patrol.ajax' );
+ }
$link = Linker::linkKnown(
$this->getTitle(),
@@ -978,6 +1153,9 @@ class Article extends Page {
wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
'</div>'
);
+
+ wfProfileOut( __METHOD__ );
+ return true;
}
/**
@@ -987,6 +1165,8 @@ class Article extends Page {
public function showMissingArticle() {
global $wgSend404Code;
$outputPage = $this->getContext()->getOutput();
+ // Whether the page is a root user page of an existing user (but not a subpage)
+ $validUserPage = false;
# Show info in user (talk) namespace. Does the user exist? Is he blocked?
if ( $this->getTitle()->getNamespace() == NS_USER || $this->getTitle()->getNamespace() == NS_USER_TALK ) {
@@ -995,7 +1175,7 @@ class Article extends Page {
$user = User::newFromName( $rootPart, false /* allow IP users*/ );
$ip = User::isIP( $rootPart );
- if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist
+ if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
$outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) );
} elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
@@ -1013,6 +1193,9 @@ class Article extends Page {
)
)
);
+ $validUserPage = !$this->getTitle()->isSubpage();
+ } else {
+ $validUserPage = !$this->getTitle()->isSubpage();
}
}
@@ -1020,18 +1203,25 @@ class Article extends Page {
# Show delete and move logs
LogEventsList::showLogExtract( $outputPage, array( 'delete', 'move' ), $this->getTitle(), '',
- array( 'lim' => 10,
+ array( 'lim' => 10,
'conds' => array( "log_action != 'revision'" ),
'showIfEmpty' => false,
'msgKey' => array( 'moveddeleted-notice' ) )
);
- if ( !$this->mPage->hasViewableContent() && $wgSend404Code ) {
+ if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) {
// If there's no backing content, send a 404 Not Found
// for better machine handling of broken links.
$this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" );
}
+ if ( $validUserPage ) {
+ // Also apply the robot policy for nonexisting user pages (as those aren't served as 404)
+ $policy = $this->getRobotPolicy( 'view' );
+ $outputPage->setIndexPolicy( $policy['index'] );
+ $outputPage->setFollowPolicy( $policy['follow'] );
+ }
+
$hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) );
if ( ! $hookResult ) {
@@ -1081,7 +1271,7 @@ class Article extends Page {
} elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
# Give explanation and add a link to view the revision...
$oldid = intval( $this->getOldID() );
- $link = $this->getTitle()->getFullUrl( "oldid={$oldid}&unhide=1" );
+ $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
$msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
$outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
@@ -1104,7 +1294,7 @@ class Article extends Page {
* Revision as of \<date\>; view current revision
* \<- Previous version | Next Version -\>
*
- * @param $oldid int: revision ID of this article revision
+ * @param int $oldid revision ID of this article revision
*/
public function setOldSubtitle( $oldid = 0 ) {
if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
@@ -1166,7 +1356,7 @@ class Article extends Page {
'oldid' => $oldid
) + $extraParams
);
- $prev = $this->getTitle()->getPreviousRevisionID( $oldid ) ;
+ $prev = $this->getTitle()->getPreviousRevisionID( $oldid );
$prevlink = $prev
? Linker::linkKnown(
$this->getTitle(),
@@ -1363,13 +1553,7 @@ class Article extends Page {
$this->doDelete( $reason, $suppress );
- if ( $user->isLoggedIn() && $request->getCheck( 'wpWatch' ) != $user->isWatched( $title ) ) {
- if ( $request->getCheck( 'wpWatch' ) ) {
- WatchAction::doWatch( $title, $user );
- } else {
- WatchAction::doUnwatch( $title, $user );
- }
- }
+ WatchAction::doWatchOrUnwatch( $request->getCheck( 'wpWatch' ), $title, $user );
return;
}
@@ -1377,7 +1561,13 @@ class Article extends Page {
// Generate deletion reason
$hasHistory = false;
if ( !$reason ) {
- $reason = $this->generateReason( $hasHistory );
+ try {
+ $reason = $this->generateReason( $hasHistory );
+ } catch ( MWException $e ) {
+ # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
+ wfDebug( "Error while building auto delete summary: $e" );
+ $reason = '';
+ }
}
// If the page has a history, insert a warning
@@ -1406,7 +1596,7 @@ class Article extends Page {
/**
* Output deletion confirmation dialog
* @todo FIXME: Move to another file?
- * @param $reason String: prefilled reason
+ * @param string $reason prefilled reason
*/
public function confirmDelete( $reason ) {
wfDebug( "Article::confirmDelete\n" );
@@ -1614,9 +1804,11 @@ class Article extends Page {
*
* @param $oldid mixed integer Revision ID or null
* @param $user User The relevant user
- * @return ParserOutput or false if the given revsion ID is not found
+ * @return ParserOutput or false if the given revision ID is not found
*/
public function getParserOutput( $oldid = null, User $user = null ) {
+ //XXX: bypasses mParserOptions and thus setParserOptions()
+
if ( $user === null ) {
$parserOptions = $this->getParserOptions();
} else {
@@ -1627,6 +1819,21 @@ class Article extends Page {
}
/**
+ * Override the ParserOptions used to render the primary article wikitext.
+ *
+ * @param ParserOptions $options
+ * @throws MWException if the parser options where already initialized.
+ */
+ public function setParserOptions( ParserOptions $options ) {
+ if ( $this->mParserOptions ) {
+ throw new MWException( "can't change parser options after they have already been set" );
+ }
+
+ // clone, so if $options is modified later, it doesn't confuse the parser cache.
+ $this->mParserOptions = clone $options;
+ }
+
+ /**
* Get parser options suitable for rendering the primary article wikitext
* @return ParserOptions
*/
@@ -1757,15 +1964,16 @@ class Article extends Page {
*
* @deprecated in 1.18; call OutputPage::redirect() directly
* @param $noRedir Boolean: add redirect=no
- * @param $sectionAnchor String: section to redirect to, including "#"
- * @param $extraQuery String: extra query params
+ * @param string $sectionAnchor section to redirect to, including "#"
+ * @param string $extraQuery extra query params
*/
public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
wfDeprecated( __METHOD__, '1.18' );
if ( $noRedir ) {
$query = 'redirect=no';
- if ( $extraQuery )
+ if ( $extraQuery ) {
$query .= "&$extraQuery";
+ }
} else {
$query = $extraQuery;
}
@@ -1777,7 +1985,7 @@ class Article extends Page {
* Use PHP's magic __get handler to handle accessing of
* raw WikiPage fields for backwards compatibility.
*
- * @param $fname String Field name
+ * @param string $fname Field name
*/
public function __get( $fname ) {
if ( property_exists( $this->mPage, $fname ) ) {
@@ -1791,7 +1999,7 @@ class Article extends Page {
* Use PHP's magic __set handler to handle setting of
* raw WikiPage fields for backwards compatibility.
*
- * @param $fname String Field name
+ * @param string $fname Field name
* @param $fvalue mixed New value
*/
public function __set( $fname, $fvalue ) {
@@ -1810,8 +2018,8 @@ class Article extends Page {
* Use PHP's magic __call handler to transform instance calls to
* WikiPage functions for backwards compatibility.
*
- * @param $fname String Name of called method
- * @param $args Array Arguments to the method
+ * @param string $fname Name of called method
+ * @param array $args Arguments to the method
* @return mixed
*/
public function __call( $fname, $args ) {
@@ -1844,7 +2052,13 @@ class Article extends Page {
* @return bool
*/
public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) {
- return $this->mPage->updateRestrictions( $limit, $reason, $cascade, $expiry );
+ return $this->mPage->doUpdateRestrictions(
+ $limit,
+ $expiry,
+ $cascade,
+ $reason,
+ $this->getContext()->getUser()
+ );
}
/**
@@ -1891,7 +2105,9 @@ class Article extends Page {
* @return mixed
*/
public function generateReason( &$hasHistory ) {
- return $this->mPage->getAutoDeleteReason( $hasHistory );
+ $title = $this->mPage->getTitle();
+ $handler = ContentHandler::getForTitle( $title );
+ return $handler->getAutoDeleteReason( $title, $hasHistory );
}
// ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** //
@@ -1929,6 +2145,7 @@ class Article extends Page {
* @param $newtext
* @param $flags
* @return string
+ * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
*/
public static function getAutosummary( $oldtext, $newtext, $flags ) {
return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index 2e42439c..84cf3d5e 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -46,7 +46,7 @@ class AuthPlugin {
* you might need to munge it (for instance, for lowercase initial
* letters).
*
- * @param $username String: username.
+ * @param string $username username.
* @return bool
*/
public function userExists( $username ) {
@@ -60,8 +60,8 @@ class AuthPlugin {
* you might need to munge it (for instance, for lowercase initial
* letters).
*
- * @param $username String: username.
- * @param $password String: user password.
+ * @param string $username username.
+ * @param string $password user password.
* @return bool
*/
public function authenticate( $username, $password ) {
@@ -73,7 +73,7 @@ class AuthPlugin {
* Modify options in the login template.
*
* @param $template UserLoginTemplate object.
- * @param $type String 'signup' or 'login'. Added in 1.16.
+ * @param string $type 'signup' or 'login'. Added in 1.16.
*/
public function modifyUITemplate( &$template, &$type ) {
# Override this!
@@ -83,7 +83,7 @@ class AuthPlugin {
/**
* Set the domain this plugin is supposed to use when authenticating.
*
- * @param $domain String: authentication domain.
+ * @param string $domain authentication domain.
*/
public function setDomain( $domain ) {
$this->domain = $domain;
@@ -105,7 +105,7 @@ class AuthPlugin {
/**
* Check to see if the specific domain is a valid domain.
*
- * @param $domain String: authentication domain.
+ * @param string $domain authentication domain.
* @return bool
*/
public function validDomain( $domain ) {
@@ -194,7 +194,7 @@ class AuthPlugin {
* Return true if successful.
*
* @param $user User object.
- * @param $password String: password.
+ * @param string $password password.
* @return bool
*/
public function setPassword( $user, $password ) {
@@ -213,6 +213,19 @@ class AuthPlugin {
}
/**
+ * Update user groups in the external authentication database.
+ * Return true if successful.
+ *
+ * @param $user User object.
+ * @param $addgroups Groups to add.
+ * @param $delgroups Groups to remove.
+ * @return Boolean
+ */
+ public function updateExternalDBGroups( $user, $addgroups, $delgroups = array() ) {
+ return true;
+ }
+
+ /**
* Check to see if external accounts can be created.
* Return true if external accounts can be created.
* @return Boolean
@@ -251,7 +264,7 @@ class AuthPlugin {
* Check if a user should authenticate locally if the global authentication fails.
* If either this or strict() returns true, local authentication is not used.
*
- * @param $username String: username.
+ * @param string $username username.
* @return Boolean
*/
public function strictUserAuth( $username ) {
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index a8b22027..0706fe3f 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -33,12 +33,12 @@ $wgAutoloadLocalClasses = array(
'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
'AjaxResponse' => 'includes/AjaxResponse.php',
'AlphabeticPager' => 'includes/Pager.php',
+ 'ArrayUtils' => 'includes/ArrayUtils.php',
'Article' => 'includes/Article.php',
'AtomFeed' => 'includes/Feed.php',
'AuthPlugin' => 'includes/AuthPlugin.php',
'AuthPluginUser' => 'includes/AuthPlugin.php',
'Autopromote' => 'includes/Autopromote.php',
- 'BacklinkCache' => 'includes/BacklinkCache.php',
'BadTitleError' => 'includes/Exception.php',
'BaseTemplate' => 'includes/SkinTemplate.php',
'Block' => 'includes/Block.php',
@@ -55,7 +55,6 @@ $wgAutoloadLocalClasses = array(
'CdbWriter_DBA' => 'includes/Cdb.php',
'CdbWriter_PHP' => 'includes/Cdb_PHP.php',
'ChangesFeed' => 'includes/ChangesFeed.php',
- 'ChangesList' => 'includes/ChangesList.php',
'ChangeTags' => 'includes/ChangeTags.php',
'ChannelFeed' => 'includes/Feed.php',
'Collation' => 'includes/Collation.php',
@@ -65,14 +64,12 @@ $wgAutoloadLocalClasses = array(
'ConfEditorToken' => 'includes/ConfEditor.php',
'Cookie' => 'includes/Cookie.php',
'CookieJar' => 'includes/Cookie.php',
- 'MWCryptRand' => 'includes/CryptRand.php',
'CurlHttpRequest' => 'includes/HttpFunctions.php',
'DeferrableUpdate' => 'includes/DeferredUpdates.php',
'DeferredUpdates' => 'includes/DeferredUpdates.php',
+ 'MWCallableUpdate' => 'includes/CallableUpdate.php',
'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php',
'DerivativeRequest' => 'includes/WebRequest.php',
- 'DeviceDetection' => 'includes/mobile/DeviceDetection.php',
- 'DeviceProperties' => 'includes/mobile/DeviceDetection.php',
'DiffHistoryBlob' => 'includes/HistoryBlob.php',
'DoubleReplacer' => 'includes/StringUtils.php',
'DummyLinker' => 'includes/Linker.php',
@@ -89,14 +86,8 @@ $wgAutoloadLocalClasses = array(
'DumpPipeOutput' => 'includes/Export.php',
'EditPage' => 'includes/EditPage.php',
'EmailNotification' => 'includes/UserMailer.php',
- 'EnhancedChangesList' => 'includes/ChangesList.php',
'ErrorPageError' => 'includes/Exception.php',
'ExplodeIterator' => 'includes/StringUtils.php',
- 'ExternalEdit' => 'includes/ExternalEdit.php',
- 'ExternalStore' => 'includes/ExternalStore.php',
- 'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
- 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
- 'ExternalUser' => 'includes/ExternalUser.php',
'FakeTitle' => 'includes/FakeTitle.php',
'Fallback' => 'includes/Fallback.php',
'FatalError' => 'includes/Exception.php',
@@ -111,21 +102,27 @@ $wgAutoloadLocalClasses = array(
'FormOptions' => 'includes/FormOptions.php',
'FormSpecialPage' => 'includes/SpecialPage.php',
'GitInfo' => 'includes/GitInfo.php',
+ 'HashRing' => 'includes/HashRing.php',
'HashtableReplacer' => 'includes/StringUtils.php',
'HistoryBlob' => 'includes/HistoryBlob.php',
'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
'HistoryBlobStub' => 'includes/HistoryBlob.php',
'Hooks' => 'includes/Hooks.php',
'Html' => 'includes/Html.php',
+ 'HtmlFormatter' => 'includes/HtmlFormatter.php',
'HTMLApiField' => 'includes/HTMLForm.php',
+ 'HTMLButtonField' => 'includes/HTMLForm.php',
'HTMLCheckField' => 'includes/HTMLForm.php',
+ 'HTMLCheckMatrix' => 'includes/HTMLForm.php',
'HTMLEditTools' => 'includes/HTMLForm.php',
'HTMLFloatField' => 'includes/HTMLForm.php',
'HTMLForm' => 'includes/HTMLForm.php',
'HTMLFormField' => 'includes/HTMLForm.php',
+ 'HTMLFormFieldRequiredOptionsException' => 'includes/HTMLForm.php',
'HTMLHiddenField' => 'includes/HTMLForm.php',
'HTMLInfoField' => 'includes/HTMLForm.php',
'HTMLIntField' => 'includes/HTMLForm.php',
+ 'HTMLNestedFilterable' => 'includes/HTMLForm.php',
'HTMLMultiSelectField' => 'includes/HTMLForm.php',
'HTMLRadioField' => 'includes/HTMLForm.php',
'HTMLSelectAndOtherField' => 'includes/HTMLForm.php',
@@ -136,13 +133,9 @@ $wgAutoloadLocalClasses = array(
'HTMLTextField' => 'includes/HTMLForm.php',
'Http' => 'includes/HttpFunctions.php',
'HttpError' => 'includes/Exception.php',
- 'HttpRequest' => 'includes/HttpFunctions.old.php',
'ICacheHelper' => 'includes/CacheHelper.php',
'IcuCollation' => 'includes/Collation.php',
- 'IDeviceProperties' => 'includes/mobile/DeviceDetection.php',
- 'IDeviceDetector' => 'includes/mobile/DeviceDetection.php',
'IdentityCollation' => 'includes/Collation.php',
- 'ImageGallery' => 'includes/ImageGallery.php',
'ImageHistoryList' => 'includes/ImagePage.php',
'ImageHistoryPseudoPager' => 'includes/ImagePage.php',
'ImagePage' => 'includes/ImagePage.php',
@@ -153,27 +146,29 @@ $wgAutoloadLocalClasses = array(
'IndexPager' => 'includes/Pager.php',
'Interwiki' => 'includes/interwiki/Interwiki.php',
'IP' => 'includes/IP.php',
- 'LCStore_Accel' => 'includes/LocalisationCache.php',
- 'LCStore_CDB' => 'includes/LocalisationCache.php',
- 'LCStore_DB' => 'includes/LocalisationCache.php',
- 'LCStore_Null' => 'includes/LocalisationCache.php',
- 'LegacyTemplate' => 'includes/SkinLegacy.php',
+ 'LCStore' => 'includes/cache/LocalisationCache.php',
+ 'LCStore_Accel' => 'includes/cache/LocalisationCache.php',
+ 'LCStore_CDB' => 'includes/cache/LocalisationCache.php',
+ 'LCStore_DB' => 'includes/cache/LocalisationCache.php',
+ 'LCStore_Null' => 'includes/cache/LocalisationCache.php',
'License' => 'includes/Licenses.php',
'Licenses' => 'includes/Licenses.php',
'Linker' => 'includes/Linker.php',
'LinkFilter' => 'includes/LinkFilter.php',
'LinksUpdate' => 'includes/LinksUpdate.php',
'LinksDeletionUpdate' => 'includes/LinksUpdate.php',
- 'LocalisationCache' => 'includes/LocalisationCache.php',
- 'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php',
+ 'LocalisationCache' => 'includes/cache/LocalisationCache.php',
+ 'LocalisationCache_BulkLoad' => 'includes/cache/LocalisationCache.php',
'MagicWord' => 'includes/MagicWord.php',
'MagicWordArray' => 'includes/MagicWord.php',
'MailAddress' => 'includes/UserMailer.php',
+ 'MappedIterator' => 'includes/MappedIterator.php',
'MediaWiki' => 'includes/Wiki.php',
'MediaWiki_I18N' => 'includes/SkinTemplate.php',
'Message' => 'includes/Message.php',
'MessageBlobStore' => 'includes/MessageBlobStore.php',
'MimeMagic' => 'includes/MimeMagic.php',
+ 'MWCryptRand' => 'includes/MWCryptRand.php',
'MWException' => 'includes/Exception.php',
'MWExceptionHandler' => 'includes/Exception.php',
'MWFunction' => 'includes/MWFunction.php',
@@ -181,9 +176,8 @@ $wgAutoloadLocalClasses = array(
'MWHttpRequest' => 'includes/HttpFunctions.php',
'MWInit' => 'includes/Init.php',
'MWNamespace' => 'includes/Namespace.php',
- 'OldChangesList' => 'includes/ChangesList.php',
'OutputPage' => 'includes/OutputPage.php',
- 'Page' => 'includes/WikiPage.php',
+ 'Page' => 'includes/WikiPage.php',
'PageQueryPage' => 'includes/PageQueryPage.php',
'Pager' => 'includes/Pager.php',
'PasswordError' => 'includes/User.php',
@@ -194,6 +188,7 @@ $wgAutoloadLocalClasses = array(
'PoolCounter' => 'includes/PoolCounter.php',
'PoolCounter_Stub' => 'includes/PoolCounter.php',
'PoolCounterWork' => 'includes/PoolCounter.php',
+ 'PoolCounterWorkViaCallback' => 'includes/PoolCounter.php',
'PoolWorkArticleView' => 'includes/WikiPage.php',
'Preferences' => 'includes/Preferences.php',
'PreferencesForm' => 'includes/Preferences.php',
@@ -201,10 +196,10 @@ $wgAutoloadLocalClasses = array(
'ProtectionForm' => 'includes/ProtectionForm.php',
'QueryPage' => 'includes/QueryPage.php',
'QuickTemplate' => 'includes/SkinTemplate.php',
- 'RCCacheEntry' => 'includes/ChangesList.php',
+ 'RawMessage' => 'includes/Message.php',
'RdfMetaData' => 'includes/Metadata.php',
'ReadOnlyError' => 'includes/Exception.php',
- 'RecentChange' => 'includes/RecentChange.php',
+ 'RedirectSpecialArticle' => 'includes/SpecialPage.php',
'RedirectSpecialPage' => 'includes/SpecialPage.php',
'RegexlikeReplacer' => 'includes/StringUtils.php',
'ReplacementArray' => 'includes/StringUtils.php',
@@ -219,13 +214,13 @@ $wgAutoloadLocalClasses = array(
'Sanitizer' => 'includes/Sanitizer.php',
'DataUpdate' => 'includes/DataUpdate.php',
'SqlDataUpdate' => 'includes/SqlDataUpdate.php',
+ 'ScopedCallback' => 'includes/ScopedCallback.php',
'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
'SiteConfiguration' => 'includes/SiteConfiguration.php',
'SiteStats' => 'includes/SiteStats.php',
'SiteStatsInit' => 'includes/SiteStats.php',
'SiteStatsUpdate' => 'includes/SiteStats.php',
'Skin' => 'includes/Skin.php',
- 'SkinLegacy' => 'includes/SkinLegacy.php',
'SkinTemplate' => 'includes/SkinTemplate.php',
'SpecialCreateAccount' => 'includes/SpecialPage.php',
'SpecialListAdmins' => 'includes/SpecialPage.php',
@@ -234,11 +229,13 @@ $wgAutoloadLocalClasses = array(
'SpecialMypage' => 'includes/SpecialPage.php',
'SpecialMytalk' => 'includes/SpecialPage.php',
'SpecialMyuploads' => 'includes/SpecialPage.php',
+ 'SpecialAllMyUploads' => 'includes/SpecialPage.php',
'SpecialPage' => 'includes/SpecialPage.php',
'SpecialPageFactory' => 'includes/SpecialPageFactory.php',
'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
'SquidPurgeClient' => 'includes/SquidPurgeClient.php',
'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php',
+ 'StatCounter' => 'includes/StatCounter.php',
'Status' => 'includes/Status.php',
'StreamFile' => 'includes/StreamFile.php',
'StringUtils' => 'includes/StringUtils.php',
@@ -247,10 +244,12 @@ $wgAutoloadLocalClasses = array(
'StubUserLang' => 'includes/StubObject.php',
'TablePager' => 'includes/Pager.php',
'MWTimestamp' => 'includes/Timestamp.php',
+ 'TimestampException' => 'includes/Timestamp.php',
'Title' => 'includes/Title.php',
'TitleArray' => 'includes/TitleArray.php',
'TitleArrayFromResult' => 'includes/TitleArray.php',
'ThrottledError' => 'includes/Exception.php',
+ 'UIDGenerator' => 'includes/UIDGenerator.php',
'UnlistedSpecialPage' => 'includes/SpecialPage.php',
'UploadSourceAdapter' => 'includes/Import.php',
'UppercaseCollation' => 'includes/Collation.php',
@@ -272,9 +271,9 @@ $wgAutoloadLocalClasses = array(
'WikiError' => 'includes/WikiError.php',
'WikiErrorMsg' => 'includes/WikiError.php',
'WikiExporter' => 'includes/Export.php',
- 'WikiFilePage' => 'includes/WikiFilePage.php',
+ 'WikiFilePage' => 'includes/WikiFilePage.php',
'WikiImporter' => 'includes/Import.php',
- 'WikiPage' => 'includes/WikiPage.php',
+ 'WikiPage' => 'includes/WikiPage.php',
'WikiRevision' => 'includes/Import.php',
'WikiMap' => 'includes/WikiMap.php',
'WikiReference' => 'includes/WikiMap.php',
@@ -289,6 +288,21 @@ $wgAutoloadLocalClasses = array(
'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
+ # content handler
+ 'AbstractContent' => 'includes/content/AbstractContent.php',
+ 'ContentHandler' => 'includes/content/ContentHandler.php',
+ 'Content' => 'includes/content/Content.php',
+ 'CssContentHandler' => 'includes/content/CssContentHandler.php',
+ 'CssContent' => 'includes/content/CssContent.php',
+ 'JavaScriptContentHandler' => 'includes/content/JavaScriptContentHandler.php',
+ 'JavaScriptContent' => 'includes/content/JavaScriptContent.php',
+ 'MessageContent' => 'includes/content/MessageContent.php',
+ 'MWContentSerializationException' => 'includes/content/ContentHandler.php',
+ 'TextContentHandler' => 'includes/content/TextContentHandler.php',
+ 'TextContent' => 'includes/content/TextContent.php',
+ 'WikitextContentHandler' => 'includes/content/WikitextContentHandler.php',
+ 'WikitextContent' => 'includes/content/WikitextContent.php',
+
# includes/actions
'CachedAction' => 'includes/actions/CachedAction.php',
'CreditsAction' => 'includes/actions/CreditsAction.php',
@@ -318,6 +332,7 @@ $wgAutoloadLocalClasses = array(
'ApiBase' => 'includes/api/ApiBase.php',
'ApiBlock' => 'includes/api/ApiBlock.php',
'ApiComparePages' => 'includes/api/ApiComparePages.php',
+ 'ApiCreateAccount' => 'includes/api/ApiCreateAccount.php',
'ApiDelete' => 'includes/api/ApiDelete.php',
'ApiDisabled' => 'includes/api/ApiDisabled.php',
'ApiEditPage' => 'includes/api/ApiEditPage.php',
@@ -331,6 +346,7 @@ $wgAutoloadLocalClasses = array(
'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
+ 'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
@@ -339,11 +355,13 @@ $wgAutoloadLocalClasses = array(
'ApiFormatXmlRsd' => 'includes/api/ApiRsd.php',
'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php',
'ApiHelp' => 'includes/api/ApiHelp.php',
+ 'ApiImageRotate' => 'includes/api/ApiImageRotate.php',
'ApiImport' => 'includes/api/ApiImport.php',
'ApiImportReporter' => 'includes/api/ApiImport.php',
'ApiLogin' => 'includes/api/ApiLogin.php',
'ApiLogout' => 'includes/api/ApiLogout.php',
'ApiMain' => 'includes/api/ApiMain.php',
+ 'ApiModuleManager' => 'includes/api/ApiModuleManager.php',
'ApiMove' => 'includes/api/ApiMove.php',
'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
'ApiOptions' => 'includes/api/ApiOptions.php',
@@ -383,11 +401,15 @@ $wgAutoloadLocalClasses = array(
'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php',
+ 'ApiQueryORM' => 'includes/api/ApiQueryORM.php',
'ApiQueryPageProps' => 'includes/api/ApiQueryPageProps.php',
+ 'ApiQueryPagesWithProp' => 'includes/api/ApiQueryPagesWithProp.php',
+ 'ApiQueryPagePropNames' => 'includes/api/ApiQueryPagePropNames.php',
'ApiQueryProtectedTitles' => 'includes/api/ApiQueryProtectedTitles.php',
'ApiQueryQueryPage' => 'includes/api/ApiQueryQueryPage.php',
'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php',
'ApiQueryRecentChanges' => 'includes/api/ApiQueryRecentChanges.php',
+ 'ApiQueryFileRepoInfo' => 'includes/api/ApiQueryFileRepoInfo.php',
'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
@@ -410,6 +432,7 @@ $wgAutoloadLocalClasses = array(
'UsageException' => 'includes/api/ApiMain.php',
# includes/cache
+ 'BacklinkCache' => 'includes/cache/BacklinkCache.php',
'CacheDependency' => 'includes/cache/CacheDependency.php',
'ConstantDependency' => 'includes/cache/CacheDependency.php',
'DependencyWrapper' => 'includes/cache/CacheDependency.php',
@@ -418,7 +441,6 @@ $wgAutoloadLocalClasses = array(
'GenderCache' => 'includes/cache/GenderCache.php',
'GlobalDependency' => 'includes/cache/CacheDependency.php',
'HTMLCacheUpdate' => 'includes/cache/HTMLCacheUpdate.php',
- 'HTMLCacheUpdateJob' => 'includes/cache/HTMLCacheUpdate.php',
'HTMLFileCache' => 'includes/cache/HTMLFileCache.php',
'LinkBatch' => 'includes/cache/LinkBatch.php',
'LinkCache' => 'includes/cache/LinkCache.php',
@@ -430,6 +452,17 @@ $wgAutoloadLocalClasses = array(
'TitleDependency' => 'includes/cache/CacheDependency.php',
'TitleListDependency' => 'includes/cache/CacheDependency.php',
+ # includes/changes
+ 'ChangesList' => 'includes/changes/ChangesList.php',
+ 'EnhancedChangesList' => 'includes/changes/EnhancedChangesList.php',
+ 'OldChangesList' => 'includes/changes/OldChangesList.php',
+ 'RCCacheEntry' => 'includes/changes/RCCacheEntry.php',
+ 'RecentChange' => 'includes/changes/RecentChange.php',
+
+ # includes/clientpool
+ 'RedisConnectionPool' => 'includes/clientpool/RedisConnectionPool.php',
+ 'RedisConnRef' => 'includes/clientpool/RedisConnectionPool.php',
+
# includes/context
'ContextSource' => 'includes/context/ContextSource.php',
'DerivativeContext' => 'includes/context/DerivativeContext.php',
@@ -438,16 +471,17 @@ $wgAutoloadLocalClasses = array(
# includes/dao
'IDBAccessObject' => 'includes/dao/IDBAccessObject.php',
+ 'DBAccessBase' => 'includes/dao/DBAccessBase.php',
# includes/db
'Blob' => 'includes/db/DatabaseUtility.php',
- 'ChronologyProtector' => 'includes/db/LBFactory.php',
+ 'ChronologyProtector' => 'includes/db/ChronologyProtector.php',
'CloneDatabase' => 'includes/db/CloneDatabase.php',
- 'Database' => 'includes/db/DatabaseMysql.php',
'DatabaseBase' => 'includes/db/Database.php',
- 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php',
'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
'DatabaseMysql' => 'includes/db/DatabaseMysql.php',
+ 'DatabaseMysqlBase' => 'includes/db/DatabaseMysqlBase.php',
+ 'DatabaseMysqli' => 'includes/db/DatabaseMysqli.php',
'DatabaseOracle' => 'includes/db/DatabaseOracle.php',
'DatabasePostgres' => 'includes/db/DatabasePostgres.php',
'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
@@ -455,8 +489,10 @@ $wgAutoloadLocalClasses = array(
'DatabaseType' => 'includes/db/Database.php',
'DBAccessError' => 'includes/db/LBFactory.php',
'DBConnectionError' => 'includes/db/DatabaseError.php',
+ 'DBConnRef' => 'includes/db/LoadBalancer.php',
'DBError' => 'includes/db/DatabaseError.php',
'DBObject' => 'includes/db/DatabaseUtility.php',
+ 'IDatabase' => 'includes/db/Database.php',
'IORMRow' => 'includes/db/IORMRow.php',
'IORMTable' => 'includes/db/IORMTable.php',
'DBMasterPos' => 'includes/db/DatabaseUtility.php',
@@ -464,10 +500,6 @@ $wgAutoloadLocalClasses = array(
'DBUnexpectedError' => 'includes/db/DatabaseError.php',
'FakeResultWrapper' => 'includes/db/DatabaseUtility.php',
'Field' => 'includes/db/DatabaseUtility.php',
- 'IBM_DB2Blob' => 'includes/db/DatabaseIbm_db2.php',
- 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php',
- 'IBM_DB2Helper' => 'includes/db/DatabaseIbm_db2.php',
- 'IBM_DB2Result' => 'includes/db/DatabaseIbm_db2.php',
'LBFactory' => 'includes/db/LBFactory.php',
'LBFactory_Fake' => 'includes/db/LBFactory.php',
'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
@@ -481,8 +513,8 @@ $wgAutoloadLocalClasses = array(
'LoadMonitor_Null' => 'includes/db/LoadMonitor.php',
'MssqlField' => 'includes/db/DatabaseMssql.php',
'MssqlResult' => 'includes/db/DatabaseMssql.php',
- 'MySQLField' => 'includes/db/DatabaseMysql.php',
- 'MySQLMasterPos' => 'includes/db/DatabaseMysql.php',
+ 'MySQLField' => 'includes/db/DatabaseMysqlBase.php',
+ 'MySQLMasterPos' => 'includes/db/DatabaseMysqlBase.php',
'ORAField' => 'includes/db/DatabaseOracle.php',
'ORAResult' => 'includes/db/DatabaseOracle.php',
'ORMIterator' => 'includes/db/ORMIterator.php',
@@ -517,14 +549,17 @@ $wgAutoloadLocalClasses = array(
'WikiDiff3' => 'includes/diff/WikiDiff3.php',
'WordLevelDiff' => 'includes/diff/DairikiDiff.php',
- # includes/extauth
- 'ExternalUser_Hardcoded' => 'includes/extauth/Hardcoded.php',
- 'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php',
- 'ExternalUser_vB' => 'includes/extauth/vB.php',
+ # includes/externalstore
+ 'ExternalStore' => 'includes/externalstore/ExternalStore.php',
+ 'ExternalStoreDB' => 'includes/externalstore/ExternalStoreDB.php',
+ 'ExternalStoreHttp' => 'includes/externalstore/ExternalStoreHttp.php',
+ 'ExternalStoreMedium' => 'includes/externalstore/ExternalStoreMedium.php',
+ 'ExternalStoreMwstore' => 'includes/externalstore/ExternalStoreMwstore.php',
# includes/filebackend
'FileBackendGroup' => 'includes/filebackend/FileBackendGroup.php',
'FileBackend' => 'includes/filebackend/FileBackend.php',
+ 'FileBackendError' => 'includes/filebackend/FileBackend.php',
'FileBackendStore' => 'includes/filebackend/FileBackendStore.php',
'FileBackendStoreShardListIterator' => 'includes/filebackend/FileBackendStore.php',
'FileBackendStoreShardDirIterator' => 'includes/filebackend/FileBackendStore.php',
@@ -548,13 +583,15 @@ $wgAutoloadLocalClasses = array(
'NullFileJournal' => 'includes/filebackend/filejournal/FileJournal.php',
'LockManagerGroup' => 'includes/filebackend/lockmanager/LockManagerGroup.php',
'LockManager' => 'includes/filebackend/lockmanager/LockManager.php',
- 'ScopedLock' => 'includes/filebackend/lockmanager/LockManager.php',
+ 'ScopedLock' => 'includes/filebackend/lockmanager/ScopedLock.php',
'FSLockManager' => 'includes/filebackend/lockmanager/FSLockManager.php',
'DBLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php',
'LSLockManager' => 'includes/filebackend/lockmanager/LSLockManager.php',
'MemcLockManager' => 'includes/filebackend/lockmanager/MemcLockManager.php',
- 'QuorumLockManager' => 'includes/filebackend/lockmanager/LockManager.php',
- 'MySqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php',
+ 'QuorumLockManager' => 'includes/filebackend/lockmanager/QuorumLockManager.php',
+ 'MySqlLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php',
+ 'PostgreSqlLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php',
+ 'RedisLockManager' => 'includes/filebackend/lockmanager/RedisLockManager.php',
'NullLockManager' => 'includes/filebackend/lockmanager/LockManager.php',
'FileOp' => 'includes/filebackend/FileOp.php',
'FileOpBatch' => 'includes/filebackend/FileOpBatch.php',
@@ -562,8 +599,8 @@ $wgAutoloadLocalClasses = array(
'CopyFileOp' => 'includes/filebackend/FileOp.php',
'MoveFileOp' => 'includes/filebackend/FileOp.php',
'DeleteFileOp' => 'includes/filebackend/FileOp.php',
- 'ConcatenateFileOp' => 'includes/filebackend/FileOp.php',
'CreateFileOp' => 'includes/filebackend/FileOp.php',
+ 'DescribeFileOp' => 'includes/filebackend/FileOp.php',
'NullFileOp' => 'includes/filebackend/FileOp.php',
# includes/filerepo
@@ -594,11 +631,8 @@ $wgAutoloadLocalClasses = array(
'CliInstaller' => 'includes/installer/CliInstaller.php',
'DatabaseInstaller' => 'includes/installer/DatabaseInstaller.php',
'DatabaseUpdater' => 'includes/installer/DatabaseUpdater.php',
- 'Ibm_db2Installer' => 'includes/installer/Ibm_db2Installer.php',
- 'Ibm_db2Updater' => 'includes/installer/Ibm_db2Updater.php',
'InstallDocFormatter' => 'includes/installer/InstallDocFormatter.php',
'Installer' => 'includes/installer/Installer.php',
- 'LBFactory_InstallerFake' => 'includes/installer/Installer.php',
'LocalSettingsGenerator' => 'includes/installer/LocalSettingsGenerator.php',
'MysqlInstaller' => 'includes/installer/MysqlInstaller.php',
'MysqlUpdater' => 'includes/installer/MysqlUpdater.php',
@@ -631,18 +665,33 @@ $wgAutoloadLocalClasses = array(
'WebInstallerPage' => 'includes/installer/WebInstallerPage.php',
# includes/job
- 'DoubleRedirectJob' => 'includes/job/DoubleRedirectJob.php',
- 'EmaillingJob' => 'includes/job/EmaillingJob.php',
- 'EnotifNotifyJob' => 'includes/job/EnotifNotifyJob.php',
'Job' => 'includes/job/Job.php',
- 'RefreshLinksJob' => 'includes/job/RefreshLinksJob.php',
- 'RefreshLinksJob2' => 'includes/job/RefreshLinksJob.php',
- 'UploadFromUrlJob' => 'includes/job/UploadFromUrlJob.php',
+ 'JobQueue' => 'includes/job/JobQueue.php',
+ 'JobQueueAggregator' => 'includes/job/aggregator/JobQueueAggregator.php',
+ 'JobQueueAggregatorMemc' => 'includes/job/aggregator/JobQueueAggregatorMemc.php',
+ 'JobQueueAggregatorRedis' => 'includes/job/aggregator/JobQueueAggregatorRedis.php',
+ 'JobQueueDB' => 'includes/job/JobQueueDB.php',
+ 'JobQueueConnectionError' => 'includes/job/JobQueue.php',
+ 'JobQueueError' => 'includes/job/JobQueue.php',
+ 'JobQueueGroup' => 'includes/job/JobQueueGroup.php',
+ 'JobQueueFederated' => 'includes/job/JobQueueFederated.php',
+ 'JobQueueRedis' => 'includes/job/JobQueueRedis.php',
+
+ # includes/job/jobs
+ 'DoubleRedirectJob' => 'includes/job/jobs/DoubleRedirectJob.php',
+ 'DuplicateJob' => 'includes/job/jobs/DuplicateJob.php',
+ 'EmaillingJob' => 'includes/job/jobs/EmaillingJob.php',
+ 'EnotifNotifyJob' => 'includes/job/jobs/EnotifNotifyJob.php',
+ 'HTMLCacheUpdateJob' => 'includes/job/jobs/HTMLCacheUpdateJob.php',
+ 'NullJob' => 'includes/job/jobs/NullJob.php',
+ 'RefreshLinksJob' => 'includes/job/jobs/RefreshLinksJob.php',
+ 'RefreshLinksJob2' => 'includes/job/jobs/RefreshLinksJob.php',
+ 'UploadFromUrlJob' => 'includes/job/jobs/UploadFromUrlJob.php',
+ 'AssembleUploadChunksJob' => 'includes/job/jobs/AssembleUploadChunksJob.php',
+ 'PublishStashedFileJob' => 'includes/job/jobs/PublishStashedFileJob.php',
# includes/json
'FormatJson' => 'includes/json/FormatJson.php',
- 'Services_JSON' => 'includes/json/Services_JSON.php',
- 'Services_JSON_Error' => 'includes/json/Services_JSON.php',
# includes/libs
'CSSJanus' => 'includes/libs/CSSJanus.php',
@@ -660,9 +709,16 @@ $wgAutoloadLocalClasses = array(
'JSToken' => 'includes/libs/jsminplus.php',
'JSTokenizer' => 'includes/libs/jsminplus.php',
+ # includes/libs/lessphp
+ 'lessc' => 'includes/libs/lessc.inc.php',
+ 'lessc_parser' => 'includes/libs/lessc.inc.php',
+ 'lessc_formatter_classic' => 'includes/libs/lessc.inc.php',
+ 'lessc_formatter_compressed' => 'includes/libs/lessc.inc.php',
+ 'lessc_formatter_lessjs' => 'includes/libs/lessc.inc.php',
+
# includes/logging
'DatabaseLogEntry' => 'includes/logging/LogEntry.php',
- 'DeleteLogFormatter' => 'includes/logging/LogFormatter.php',
+ 'DeleteLogFormatter' => 'includes/logging/DeleteLogFormatter.php',
'LegacyLogFormatter' => 'includes/logging/LogFormatter.php',
'LogEntry' => 'includes/logging/LogEntry.php',
'LogEventsList' => 'includes/logging/LogEventsList.php',
@@ -671,11 +727,22 @@ $wgAutoloadLocalClasses = array(
'LogPage' => 'includes/logging/LogPage.php',
'LogPager' => 'includes/logging/LogPager.php',
'ManualLogEntry' => 'includes/logging/LogEntry.php',
- 'MoveLogFormatter' => 'includes/logging/LogFormatter.php',
- 'NewUsersLogFormatter' => 'includes/logging/LogFormatter.php',
+ 'MoveLogFormatter' => 'includes/logging/MoveLogFormatter.php',
+ 'NewUsersLogFormatter' => 'includes/logging/NewUsersLogFormatter.php',
'PatrolLog' => 'includes/logging/PatrolLog.php',
- 'PatrolLogFormatter' => 'includes/logging/LogFormatter.php',
+ 'PatrolLogFormatter' => 'includes/logging/PatrolLogFormatter.php',
'RCDatabaseLogEntry' => 'includes/logging/LogEntry.php',
+ 'RightsLogFormatter' => 'includes/logging/RightsLogFormatter.php',
+
+ # Image gallery
+
+ 'ImageGallery' => 'includes/gallery/TraditionalImageGallery.php',
+ 'ImageGalleryBase' => 'includes/gallery/ImageGalleryBase.php',
+ 'NolinesImageGallery' => 'includes/gallery/NolinesImageGallery.php',
+ 'TraditionalImageGallery' => 'includes/gallery/TraditionalImageGallery.php',
+ 'PackedImageGallery' => 'includes/gallery/PackedImageGallery.php',
+ 'PackedHoverImageGallery' => 'includes/gallery/PackedOverlayImageGallery.php',
+ 'PackedOverlayImageGallery' => 'includes/gallery/PackedOverlayImageGallery.php',
# includes/media
'BitmapHandler' => 'includes/media/Bitmap.php',
@@ -737,55 +804,40 @@ $wgAutoloadLocalClasses = array(
# includes/parser
'CacheTime' => 'includes/parser/CacheTime.php',
- '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/Parser_LinkHooks.php',
'MWTidy' => 'includes/parser/Tidy.php',
'MWTidyWrapper' => 'includes/parser/Tidy.php',
'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPCustomFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDAccum_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPDPart' => 'includes/parser/Preprocessor_DOM.php',
'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDPart_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPDStack' => 'includes/parser/Preprocessor_DOM.php',
'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php',
'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDStackElement_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDStack_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPFrame' => 'includes/parser/Preprocessor.php',
'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPNode' => 'includes/parser/Preprocessor.php',
'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php',
'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php',
'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php',
'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php',
'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php',
- 'PPNode_HipHop_Array' => 'includes/parser/Preprocessor_HipHop.hphp',
- 'PPNode_HipHop_Attr' => 'includes/parser/Preprocessor_HipHop.hphp',
- 'PPNode_HipHop_Text' => 'includes/parser/Preprocessor_HipHop.hphp',
- 'PPNode_HipHop_Tree' => 'includes/parser/Preprocessor_HipHop.hphp',
'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPTemplateFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'Parser' => 'includes/parser/Parser.php',
'ParserCache' => 'includes/parser/ParserCache.php',
'ParserOptions' => 'includes/parser/ParserOptions.php',
'ParserOutput' => 'includes/parser/ParserOutput.php',
'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php',
- 'Parser_LinkHooks' => 'includes/parser/Parser_LinkHooks.php',
'Preprocessor' => 'includes/parser/Preprocessor.php',
'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php',
'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'Preprocessor_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
'StripState' => 'includes/parser/StripState.php',
# includes/profiler
@@ -795,12 +847,22 @@ $wgAutoloadLocalClasses = array(
'ProfilerSimpleTrace' => 'includes/profiler/ProfilerSimpleTrace.php',
'ProfilerSimpleUDP' => 'includes/profiler/ProfilerSimpleUDP.php',
'ProfilerStub' => 'includes/profiler/ProfilerStub.php',
+ 'ProfileSection' => 'includes/profiler/Profiler.php',
+
+ # includes/rcfeed
+ 'RCFeedEngine' => 'includes/rcfeed/RCFeedEngine.php',
+ 'RedisPubSubFeedEngine' => 'includes/rcfeed/RedisPubSubFeedEngine.php',
+ 'UDPRCFeedEngine' => 'includes/rcfeed/UDPRCFeedEngine.php',
+ 'RCFeedFormatter' => 'includes/rcfeed/RCFeedFormatter.php',
+ 'IRCColourfulRCFeedFormatter' => 'includes/rcfeed/IRCColourfulRCFeedFormatter.php',
+ 'JSONRCFeedFormatter' => 'includes/rcfeed/JSONRCFeedFormatter.php',
# includes/resourceloader
'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php',
'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php',
'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePageModule' => 'includes/resourceloader/ResourceLoaderFilePageModule.php',
+ 'ResourceLoaderLESSFunctions' => 'includes/resourceloader/ResourceLoaderLESSFunctions.php',
'ResourceLoaderModule' => 'includes/resourceloader/ResourceLoaderModule.php',
'ResourceLoaderNoscriptModule' => 'includes/resourceloader/ResourceLoaderNoscriptModule.php',
'ResourceLoaderSiteModule' => 'includes/resourceloader/ResourceLoaderSiteModule.php',
@@ -827,7 +889,6 @@ $wgAutoloadLocalClasses = array(
'RevDel_LogList' => 'includes/revisiondelete/RevisionDelete.php',
'RevDel_RevisionItem' => 'includes/revisiondelete/RevisionDelete.php',
'RevDel_RevisionList' => 'includes/revisiondelete/RevisionDelete.php',
- 'RevisionDelete' => 'includes/revisiondelete/RevisionDelete.php',
'RevisionDeleter' => 'includes/revisiondelete/RevisionDeleter.php',
'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php',
@@ -839,7 +900,6 @@ $wgAutoloadLocalClasses = array(
'SearchEngine' => 'includes/search/SearchEngine.php',
'SearchEngineDummy' => 'includes/search/SearchEngine.php',
'SearchHighlighter' => 'includes/search/SearchEngine.php',
- 'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php',
'SearchMssql' => 'includes/search/SearchMssql.php',
'SearchMySQL' => 'includes/search/SearchMySQL.php',
'SearchNearMatchResultSet' => 'includes/search/SearchEngine.php',
@@ -850,10 +910,19 @@ $wgAutoloadLocalClasses = array(
'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/site
+ 'MediaWikiSite' => 'includes/site/MediaWikiSite.php',
+ 'Site' => 'includes/site/Site.php',
+ 'SiteObject' => 'includes/site/Site.php',
+ 'SiteArray' => 'includes/site/SiteList.php',
+ 'SiteList' => 'includes/site/SiteList.php',
+ 'SiteSQLStore' => 'includes/site/SiteSQLStore.php',
+ 'Sites' => 'includes/site/SiteSQLStore.php',
+ 'SiteStore' => 'includes/site/SiteStore.php',
+
# includes/specials
'ActiveUsersPager' => 'includes/specials/SpecialActiveusers.php',
'AllmessagesTablePager' => 'includes/specials/SpecialAllmessages.php',
@@ -862,12 +931,9 @@ $wgAutoloadLocalClasses = array(
'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php',
'CategoryPager' => 'includes/specials/SpecialCategories.php',
'ContribsPager' => 'includes/specials/SpecialContributions.php',
- 'DBLockForm' => 'includes/specials/SpecialLockdb.php',
- 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php',
'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php',
'DeletedContribsPager' => 'includes/specials/SpecialDeletedContributions.php',
'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php',
- 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php',
'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php',
'EditWatchlistCheckboxSeriesField' => 'includes/specials/SpecialEditWatchlist.php',
'EditWatchlistNormalHTMLForm' => 'includes/specials/SpecialEditWatchlist.php',
@@ -908,7 +974,6 @@ $wgAutoloadLocalClasses = array(
'SpecialBlankpage' => 'includes/specials/SpecialBlankpage.php',
'SpecialBlock' => 'includes/specials/SpecialBlock.php',
'SpecialBlockList' => 'includes/specials/SpecialBlockList.php',
- 'SpecialBlockme' => 'includes/specials/SpecialBlockme.php',
'SpecialBookSources' => 'includes/specials/SpecialBooksources.php',
'SpecialCachedPage' => 'includes/specials/SpecialCachedPage.php',
'SpecialCategories' => 'includes/specials/SpecialCategories.php',
@@ -928,18 +993,21 @@ $wgAutoloadLocalClasses = array(
'SpecialLockdb' => 'includes/specials/SpecialLockdb.php',
'SpecialLog' => 'includes/specials/SpecialLog.php',
'SpecialMergeHistory' => 'includes/specials/SpecialMergeHistory.php',
- 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php',
'SpecialNewFiles' => 'includes/specials/SpecialNewimages.php',
'SpecialNewpages' => 'includes/specials/SpecialNewpages.php',
'SpecialPasswordReset' => 'includes/specials/SpecialPasswordReset.php',
+ 'SpecialPagesWithProp' => 'includes/specials/SpecialPagesWithProp.php',
'SpecialPermanentLink' => 'includes/SpecialPage.php',
'SpecialPreferences' => 'includes/specials/SpecialPreferences.php',
'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php',
'SpecialProtectedpages' => 'includes/specials/SpecialProtectedpages.php',
'SpecialProtectedtitles' => 'includes/specials/SpecialProtectedtitles.php',
+ 'SpecialRandomInCategory' => 'includes/specials/SpecialRandomInCategory.php',
'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php',
'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php',
'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php',
+ 'SpecialRedirect' => 'includes/specials/SpecialRedirect.php',
+ 'SpecialResetTokens' => 'includes/specials/SpecialResetTokens.php',
'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php',
'SpecialSearch' => 'includes/specials/SpecialSearch.php',
'SpecialSpecialpages' => 'includes/specials/SpecialSpecialpages.php',
@@ -989,7 +1057,7 @@ $wgAutoloadLocalClasses = array(
'UploadFromUrl' => 'includes/upload/UploadFromUrl.php',
'UploadStash' => 'includes/upload/UploadStash.php',
'UploadStashBadPathException' => 'includes/upload/UploadStash.php',
- 'UploadStashBadVersionException' => 'includes/upload/UploadStash.php',
+ 'UploadStashException' => 'includes/upload/UploadStash.php',
'UploadStashFile' => 'includes/upload/UploadStash.php',
'UploadStashFileException' => 'includes/upload/UploadStash.php',
'UploadStashFileNotFoundException' => 'includes/upload/UploadStash.php',
@@ -1004,20 +1072,29 @@ $wgAutoloadLocalClasses = array(
'FakeConverter' => 'languages/Language.php',
'Language' => 'languages/Language.php',
'LanguageConverter' => 'languages/LanguageConverter.php',
+ 'CLDRPluralRuleConverter' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+ 'CLDRPluralRuleConverter_Expression' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+ 'CLDRPluralRuleConverter_Fragment' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+ 'CLDRPluralRuleConverter_Operator' => 'languages/utils/CLDRPluralRuleEvaluator.php',
'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+ 'CLDRPluralRuleEvaluator_Range' => 'languages/utils/CLDRPluralRuleEvaluator.php',
'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleEvaluator.php',
# maintenance
+ 'BackupDumper' => 'maintenance/backup.inc',
'ConvertLinks' => 'maintenance/convertLinks.php',
'DeleteArchivedFilesImplementation' => 'maintenance/deleteArchivedFiles.inc',
'DeleteArchivedRevisionsImplementation' => 'maintenance/deleteArchivedRevisions.inc',
'DeleteDefaultMessages' => 'maintenance/deleteDefaultMessages.php',
+ 'DumpDBZip2Output' => 'maintenance/backup.inc',
+ 'ExportProgressFilter' => 'maintenance/backup.inc',
'FakeMaintenance' => 'maintenance/Maintenance.php',
+ 'FixExtLinksProtocolRelative' => 'maintenance/fixExtLinksProtocolRelative.php',
'LoggedUpdateMaintenance' => 'maintenance/Maintenance.php',
'Maintenance' => 'maintenance/Maintenance.php',
- 'FixExtLinksProtocolRelative' => 'maintenance/fixExtLinksProtocolRelative.php',
'PopulateCategory' => 'maintenance/populateCategory.php',
'PopulateImageSha1' => 'maintenance/populateImageSha1.php',
+ 'PopulateFilearchiveSha1' => 'maintenance/populateFilearchiveSha1.php',
'PopulateLogSearch' => 'maintenance/populateLogSearch.php',
'PopulateLogUsertext' => 'maintenance/populateLogUsertext.php',
'PopulateParentId' => 'maintenance/populateParentId.php',
@@ -1040,55 +1117,21 @@ $wgAutoloadLocalClasses = array(
'wikiStatsOutput' => 'maintenance/language/StatOutputs.php',
# maintenance/term
- 'AnsiTermColorer' => 'maintenance/term/MWTerm.php',
+ 'AnsiTermColorer' => 'maintenance/term/MWTerm.php',
'DummyTermColorer' => 'maintenance/term/MWTerm.php',
# mw-config
'InstallerOverrides' => 'mw-config/overrides.php',
'MyLocalSettingsGenerator' => 'mw-config/overrides.php',
- # tests
- 'DbTestPreviewer' => 'tests/testHelpers.inc',
- 'DbTestRecorder' => 'tests/testHelpers.inc',
- 'DelayedParserTest' => 'tests/testHelpers.inc',
- 'TestFileIterator' => 'tests/testHelpers.inc',
- 'TestRecorder' => 'tests/testHelpers.inc',
-
- # tests/phpunit/includes
- 'GenericArrayObjectTest' => 'tests/phpunit/includes/libs/GenericArrayObjectTest.php',
-
- # tests/phpunit/includes/db
- 'ORMRowTest' => 'tests/phpunit/includes/db/ORMRowTest.php',
-
- # tests/parser
- 'ParserTest' => 'tests/parser/parserTest.inc',
- 'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php',
-
- # tests/selenium
- 'Selenium' => 'tests/selenium/Selenium.php',
- 'SeleniumLoader' => 'tests/selenium/SeleniumLoader.php',
- 'SeleniumTestCase' => 'tests/selenium/SeleniumTestCase.php',
- 'SeleniumTestConsoleLogger' => 'tests/selenium/SeleniumTestConsoleLogger.php',
- 'SeleniumTestHTMLLogger' => 'tests/selenium/SeleniumTestHTMLLogger.php',
- 'SeleniumTestListener' => 'tests/selenium/SeleniumTestListener.php',
- 'SeleniumTestSuite' => 'tests/selenium/SeleniumTestSuite.php',
- 'SeleniumConfig' => 'tests/selenium/SeleniumConfig.php',
-
# skins
'CologneBlueTemplate' => 'skins/CologneBlue.php',
'ModernTemplate' => 'skins/Modern.php',
'MonoBookTemplate' => 'skins/MonoBook.php',
- 'NostalgiaTemplate' => 'skins/Nostalgia.php',
- 'SkinChick' => 'skins/Chick.php',
'SkinCologneBlue' => 'skins/CologneBlue.php',
'SkinModern' => 'skins/Modern.php',
'SkinMonoBook' => 'skins/MonoBook.php',
- 'SkinMySkin' => 'skins/MySkin.php',
- 'SkinNostalgia' => 'skins/Nostalgia.php',
- 'SkinSimple' => 'skins/Simple.php',
- 'SkinStandard' => 'skins/Standard.php',
'SkinVector' => 'skins/Vector.php',
- 'StandardTemplate' => 'skins/Standard.php',
'VectorTemplate' => 'skins/Vector.php',
);
@@ -1096,7 +1139,7 @@ class AutoLoader {
/**
* autoload - take a class name and attempt to load it
*
- * @param $className String: name of class we're looking for.
+ * @param string $className name of class we're looking for.
* @return bool Returning false is important on failure as
* it allows Zend to try and look in other registered autoloaders
* as well.
@@ -1104,12 +1147,13 @@ class AutoLoader {
static function autoload( $className ) {
global $wgAutoloadClasses, $wgAutoloadLocalClasses;
- // Workaround for PHP bug <https://bugs.php.net/bug.php?id=49143> (5.3.2. is broken, it's fixed in 5.3.6).
- // Strip leading backslashes from class names. When namespaces are used, leading backslashes are used to indicate
- // the top-level namespace, e.g. \foo\Bar. When used like this in the code, the leading backslash isn't passed to
- // the auto-loader ($className would be 'foo\Bar'). However, if a class is accessed using a string instead of a
- // class literal (e.g. $class = '\foo\Bar'; new $class()), then some versions of PHP do not strip the leading
- // backlash in this case, causing autoloading to fail.
+ // Workaround for PHP bug <https://bugs.php.net/bug.php?id=49143> (5.3.2. is broken, it's
+ // fixed in 5.3.6). Strip leading backslashes from class names. When namespaces are used,
+ // leading backslashes are used to indicate the top-level namespace, e.g. \foo\Bar. When
+ // used like this in the code, the leading backslash isn't passed to the auto-loader
+ // ($className would be 'foo\Bar'). However, if a class is accessed using a string instead
+ // of a class literal (e.g. $class = '\foo\Bar'; new $class()), then some versions of PHP
+ // do not strip the leading backlash in this case, causing autoloading to fail.
$className = ltrim( $className, '\\' );
if ( isset( $wgAutoloadLocalClasses[$className] ) ) {
@@ -1144,7 +1188,7 @@ class AutoLoader {
$filename = "$IP/$filename";
}
- require( $filename );
+ require $filename;
return true;
}
@@ -1162,12 +1206,4 @@ class AutoLoader {
}
}
-if ( function_exists( 'spl_autoload_register' ) ) {
- spl_autoload_register( array( 'AutoLoader', 'autoload' ) );
-} else {
- function __autoload( $class ) {
- AutoLoader::autoload( $class );
- }
-
- ini_set( 'unserialize_callback_func', '__autoload' );
-}
+spl_autoload_register( array( 'AutoLoader', 'autoload' ) );
diff --git a/includes/Autopromote.php b/includes/Autopromote.php
index 9c77855d..170d7abf 100644
--- a/includes/Autopromote.php
+++ b/includes/Autopromote.php
@@ -54,7 +54,7 @@ class Autopromote {
* Does not return groups the user already belongs to or has once belonged.
*
* @param $user User The user to get the groups for
- * @param $event String key in $wgAutopromoteOnce (each one has groups/criteria)
+ * @param string $event key in $wgAutopromoteOnce (each one has groups/criteria)
*
* @return array Groups the user should be promoted to.
*
@@ -126,7 +126,8 @@ class Autopromote {
return false;
} elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes)
if ( count( $cond ) > 3 ) {
- wfWarn( 'recCheckCondition() given XOR ("^") condition on three or more conditions. Check your $wgAutopromote and $wgAutopromoteOnce settings.' );
+ wfWarn( 'recCheckCondition() given XOR ("^") condition on three or more conditions.' .
+ ' Check your $wgAutopromote and $wgAutopromoteOnce settings.' );
}
return self::recCheckCondition( $cond[1], $user )
xor self::recCheckCondition( $cond[2], $user );
@@ -140,8 +141,8 @@ class Autopromote {
return true;
}
}
- # If we got here, the array presumably does not contain other condi-
- # tions; it's not recursive. Pass it off to self::checkCondition.
+ // If we got here, the array presumably does not contain other conditions;
+ // it's not recursive. Pass it off to self::checkCondition.
if ( !is_array( $cond ) ) {
$cond = array( $cond );
}
@@ -152,11 +153,11 @@ class Autopromote {
/**
* As recCheckCondition, but *not* recursive. The only valid conditions
* are those whose first element is APCOND_EMAILCONFIRMED/APCOND_EDITCOUNT/
- * APCOND_AGE. Other types will throw an exception if no extension evalu-
- * ates them.
+ * APCOND_AGE. Other types will throw an exception if no extension evaluates them.
*
- * @param $cond Array: A condition, which must not contain other conditions
+ * @param array $cond A condition, which must not contain other conditions
* @param $user User The user to check the condition against
+ * @throws MWException
* @return bool Whether the condition is true for the user
*/
private static function checkCondition( $cond, User $user ) {
@@ -165,7 +166,7 @@ class Autopromote {
return false;
}
- switch( $cond[0] ) {
+ switch ( $cond[0] ) {
case APCOND_EMAILCONFIRMED:
if ( Sanitizer::validateEmail( $user->getEmail() ) ) {
if ( $wgEmailAuthentication ) {
diff --git a/includes/Block.php b/includes/Block.php
index 732699dc..34b89e73 100644
--- a/includes/Block.php
+++ b/includes/Block.php
@@ -65,11 +65,11 @@ class Block {
$timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0,
$hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byText = '' )
{
- if( $timestamp === 0 ){
+ if ( $timestamp === 0 ) {
$timestamp = wfTimestampNow();
}
- if( count( func_get_args() ) > 0 ){
+ if ( count( func_get_args() ) > 0 ) {
# Soon... :D
# wfDeprecated( __METHOD__ . " with arguments" );
}
@@ -106,7 +106,7 @@ class Block {
* 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 string $address IP address of user/anon
* @param $user Integer: user id of user
* @return Block Object
* @deprecated since 1.18
@@ -164,7 +164,7 @@ class Block {
/**
* Check if two blocks are effectively equal. Doesn't check irrelevant things like
- * the blocking user or the block timestamp, only things which affect the blocked user *
+ * the blocking user or the block timestamp, only things which affect the blocked user
*
* @param $block Block
*
@@ -199,23 +199,23 @@ class Block {
/**
* Get a block from the DB, with either the given address or the given username
*
- * @param $address string The IP address of the user, or blank to skip IP blocks
- * @param $user int The user ID, or zero for anonymous users
+ * @param string $address The IP address of the user, or blank to skip IP blocks
+ * @param int $user The user ID, or zero for anonymous users
* @return Boolean: the user is blocked from editing
* @deprecated since 1.18
*/
public function load( $address = '', $user = 0 ) {
wfDeprecated( __METHOD__, '1.18' );
- if( $user ){
+ if ( $user ) {
$username = User::whoIs( $user );
$block = self::newFromTarget( $username, $address );
} else {
$block = self::newFromTarget( null, $address );
}
- if( $block instanceof Block ){
+ if ( $block instanceof Block ) {
# This is mildly evil, but hey, it's B/C :D
- foreach( $block as $variable => $value ){
+ foreach ( $block as $variable => $value ) {
$this->$variable = $value;
}
return true;
@@ -227,16 +227,17 @@ class Block {
/**
* Load a block from the database which affects the already-set $this->target:
* 1) A block directly on the given user or IP
- * 2) A rangeblock encompasing the given IP (smallest first)
+ * 2) A rangeblock encompassing the given IP (smallest first)
* 3) An autoblock on the given IP
* @param $vagueTarget User|String also search for blocks affecting this target. Doesn't
* make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups.
+ * @throws MWException
* @return Bool whether a relevant block was found
*/
protected function newLoad( $vagueTarget = null ) {
$db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_SLAVE );
- if( $this->type !== null ){
+ if ( $this->type !== null ) {
$conds = array(
'ipb_address' => array( (string)$this->target ),
);
@@ -246,11 +247,11 @@ class Block {
# Be aware that the != '' check is explicit, since empty values will be
# passed by some callers (bug 29116)
- if( $vagueTarget != ''){
+ if ( $vagueTarget != '' ) {
list( $target, $type ) = self::parseTarget( $vagueTarget );
- switch( $type ) {
+ switch ( $type ) {
case self::TYPE_USER:
- # Slightly wierd, but who are we to argue?
+ # Slightly weird, but who are we to argue?
$conds['ipb_address'][] = (string)$target;
break;
@@ -284,20 +285,20 @@ class Block {
# This is begging for $this = $bestBlock, but that's not allowed in PHP :(
$bestBlockPreventsEdit = null;
- foreach( $res as $row ){
+ foreach ( $res as $row ) {
$block = self::newFromRow( $row );
# Don't use expired blocks
- if( $block->deleteIfExpired() ){
+ if ( $block->deleteIfExpired() ) {
continue;
}
# Don't use anon only blocks on users
- if( $this->type == self::TYPE_USER && !$block->isHardblock() ){
+ if ( $this->type == self::TYPE_USER && !$block->isHardblock() ) {
continue;
}
- if( $block->getType() == self::TYPE_RANGE ){
+ if ( $block->getType() == self::TYPE_RANGE ) {
# This is the number of bits that are allowed to vary in the block, give
# or take some floating point errors
$end = wfBaseconvert( $block->getRangeEnd(), 16, 10 );
@@ -306,20 +307,20 @@ class Block {
# This has the nice property that a /32 block is ranked equally with a
# single-IP block, which is exactly what it is...
- $score = self::TYPE_RANGE - 1 + ( $size / 128 );
+ $score = self::TYPE_RANGE - 1 + ( $size / 128 );
} else {
$score = $block->getType();
}
- if( $score < $bestBlockScore ){
+ if ( $score < $bestBlockScore ) {
$bestBlockScore = $score;
$bestRow = $row;
$bestBlockPreventsEdit = $block->prevents( 'edit' );
}
}
- if( $bestRow !== null ){
+ if ( $bestRow !== null ) {
$this->initFromRow( $bestRow );
$this->prevents( 'edit', $bestBlockPreventsEdit );
return true;
@@ -329,9 +330,9 @@ class Block {
}
/**
- * Get a set of SQL conditions which will select rangeblocks encompasing a given range
- * @param $start String Hexadecimal IP representation
- * @param $end String Hexadecimal IP represenation, or null to use $start = $end
+ * Get a set of SQL conditions which will select rangeblocks encompassing a given range
+ * @param string $start Hexadecimal IP representation
+ * @param string $end Hexadecimal IP representation, or null to use $start = $end
* @return String
*/
public static function getRangeCond( $start, $end = null ) {
@@ -370,9 +371,9 @@ class Block {
protected static function getIpFragment( $hex ) {
global $wgBlockCIDRLimit;
if ( substr( $hex, 0, 3 ) == 'v6-' ) {
- return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) );
+ return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) );
} else {
- return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) );
+ return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) );
}
}
@@ -417,7 +418,7 @@ class Block {
* @param $row ResultWrapper row from the ipblocks table
* @return Block
*/
- public static function newFromRow( $row ){
+ public static function newFromRow( $row ) {
$block = new Block;
$block->initFromRow( $row );
return $block;
@@ -426,6 +427,7 @@ class Block {
/**
* Delete the row from the IP blocks table.
*
+ * @throws MWException
* @return Boolean
*/
public function delete() {
@@ -463,7 +465,7 @@ class Block {
Block::purgeExpired();
$row = $this->getDatabaseArray();
- $row['ipb_id'] = $dbw->nextSequenceValue("ipblocks_ipb_id_seq");
+ $row['ipb_id'] = $dbw->nextSequenceValue( "ipblocks_ipb_id_seq" );
$dbw->insert(
'ipblocks',
@@ -486,7 +488,7 @@ class Block {
* Update a block in the DB with new parameters.
* The ID field needs to be loaded first.
*
- * @return Int number of affected rows, which should probably be 1 or something's
+ * @return Int number of affected rows, which should probably be 1 or something has
* gone slightly awry
*/
public function update() {
@@ -508,8 +510,8 @@ class Block {
* @param $db DatabaseBase
* @return Array
*/
- protected function getDatabaseArray( $db = null ){
- if( !$db ){
+ protected function getDatabaseArray( $db = null ) {
+ if ( !$db ) {
$db = wfGetDB( DB_SLAVE );
}
$expiry = $db->encodeExpiry( $this->mExpiry );
@@ -534,10 +536,10 @@ class Block {
'ipb_expiry' => $expiry,
'ipb_range_start' => $this->getRangeStart(),
'ipb_range_end' => $this->getRangeEnd(),
- 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite
+ 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite
'ipb_block_email' => $this->prevents( 'sendemail' ),
'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ),
- 'ipb_parent_block_id' => $this->mParentBlockId
+ 'ipb_parent_block_id' => $this->mParentBlockId
);
return $a;
@@ -570,10 +572,17 @@ class Block {
* blocked by this Block. This will use the recentchanges table.
*
* @param Block $block
- * @param Array &$blockIds
+ * @param array &$blockIds
* @return Array: block IDs of retroactive autoblocks made
*/
protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) {
+ global $wgPutIPinRC;
+
+ // No IPs are in recentchanges table, so nothing to select
+ if ( !$wgPutIPinRC ) {
+ return;
+ }
+
$dbr = wfGetDB( DB_SLAVE );
$options = array( 'ORDER BY' => 'rc_timestamp DESC' );
@@ -583,16 +592,18 @@ class Block {
$options['LIMIT'] = 1;
$res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds,
- __METHOD__ , $options );
+ __METHOD__, $options );
- if ( !$dbr->numRows( $res ) ) {
+ if ( !$res->numRows() ) {
# No results, don't autoblock anything
wfDebug( "No IP found to retroactively autoblock\n" );
} else {
foreach ( $res as $row ) {
if ( $row->rc_ip ) {
$id = $block->doAutoblock( $row->rc_ip );
- if ( $id ) $blockIds[] = $id;
+ if ( $id ) {
+ $blockIds[] = $id;
+ }
}
}
}
@@ -602,7 +613,7 @@ class Block {
* Checks whether a given IP is on the autoblock whitelist.
* TODO: this probably belongs somewhere else, but not sure where...
*
- * @param $ip String: The IP to check
+ * @param string $ip The IP to check
* @return Boolean
*/
public static function isWhitelistedFromAutoblocks( $ip ) {
@@ -645,7 +656,7 @@ class Block {
/**
* Autoblocks the given IP, referring to this Block.
*
- * @param $autoblockIP String: the IP to autoblock.
+ * @param string $autoblockIP the IP to autoblock.
* @return mixed: block ID if an autoblock was inserted, false if not.
*/
public function doAutoblock( $autoblockIP ) {
@@ -672,7 +683,7 @@ class Block {
if ( $ipblock ) {
# Check if the block is an autoblock and would exceed the user block
# if renewed. If so, do nothing, otherwise prolong the block time...
- if ( $ipblock->mAuto && // @TODO: why not compare $ipblock->mExpiry?
+ if ( $ipblock->mAuto && // @todo Why not compare $ipblock->mExpiry?
$this->mExpiry > Block::getAutoblockExpiry( $ipblock->mTimestamp )
) {
# Reset block timestamp to now and its expiry to
@@ -687,7 +698,7 @@ class Block {
wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" );
$autoblock->setTarget( $autoblockIP );
$autoblock->setBlocker( $this->getBlocker() );
- $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )->inContentLanguage()->text();
+ $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )->inContentLanguage()->plain();
$timestamp = wfTimestampNow();
$autoblock->mTimestamp = $timestamp;
$autoblock->mAuto = 1;
@@ -780,10 +791,11 @@ class Block {
/**
* Get the IP address at the start of the range in Hex form
+ * @throws MWException
* @return String IP in Hex form
*/
public function getRangeStart() {
- switch( $this->type ) {
+ switch ( $this->type ) {
case self::TYPE_USER:
return '';
case self::TYPE_IP:
@@ -791,16 +803,18 @@ class Block {
case self::TYPE_RANGE:
list( $start, /*...*/ ) = IP::parseRange( $this->target );
return $start;
- default: throw new MWException( "Block with invalid type" );
+ default:
+ throw new MWException( "Block with invalid type" );
}
}
/**
- * Get the IP address at the start of the range in Hex form
+ * Get the IP address at the end of the range in Hex form
+ * @throws MWException
* @return String IP in Hex form
*/
public function getRangeEnd() {
- switch( $this->type ) {
+ switch ( $this->type ) {
case self::TYPE_USER:
return '';
case self::TYPE_IP:
@@ -808,7 +822,8 @@ class Block {
case self::TYPE_RANGE:
list( /*...*/, $end ) = IP::parseRange( $this->target );
return $end;
- default: throw new MWException( "Block with invalid type" );
+ default:
+ throw new MWException( "Block with invalid type" );
}
}
@@ -896,7 +911,7 @@ class Block {
* @return Bool
*/
public function prevents( $action, $x = null ) {
- switch( $action ) {
+ switch ( $action ) {
case 'edit':
# For now... <evil laugh>
return true;
@@ -934,7 +949,7 @@ class Block {
/**
* Encode expiry for DB
*
- * @param $expiry String: timestamp for expiry, or
+ * @param string $expiry timestamp for expiry, or
* @param $db DatabaseBase object
* @return String
* @deprecated since 1.18; use $dbw->encodeExpiry() instead
@@ -947,8 +962,8 @@ class Block {
/**
* Decode expiry which has come from the DB
*
- * @param $expiry String: Database expiry format
- * @param $timestampType Int Requested timestamp format
+ * @param string $expiry Database expiry format
+ * @param int $timestampType Requested timestamp format
* @return String
* @deprecated since 1.18; use $wgLang->formatExpiry() instead
*/
@@ -971,9 +986,9 @@ class Block {
}
/**
- * Gets rid of uneeded numbers in quad-dotted/octet IP strings
+ * Gets rid of unneeded numbers in quad-dotted/octet IP strings
* For example, 127.111.113.151/24 -> 127.111.113.0/24
- * @param $range String: IP address to normalize
+ * @param string $range IP address to normalize
* @return string
* @deprecated since 1.18, call IP::sanitizeRange() directly
*/
@@ -986,9 +1001,16 @@ class Block {
* Purge expired blocks from the ipblocks table
*/
public static function purgeExpired() {
+ if ( wfReadOnly() ) {
+ return;
+ }
+
+ $method = __METHOD__;
$dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'ipblocks',
- array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ );
+ $dbw->onTransactionIdle( function() use ( $dbw, $method ) {
+ $dbw->delete( 'ipblocks',
+ array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), $method );
+ } );
}
/**
@@ -1005,7 +1027,7 @@ class Block {
/**
* Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute
* ("24 May 2034"), into an absolute timestamp we can put into the database.
- * @param $expiry String: whatever was typed into the form
+ * @param string $expiry whatever was typed into the form
* @return String: timestamp or "infinity" string for th DB implementation
* @deprecated since 1.18 moved to SpecialBlock::parseExpiryInput()
*/
@@ -1029,7 +1051,7 @@ class Block {
* @param $vagueTarget String|User|Int as above, but we will search for *any* block which
* affects that target (so for an IP address, get ranges containing that IP; and also
* get any relevant autoblocks). Leave empty or blank to skip IP-based lookups.
- * @param $fromMaster Bool whether to use the DB_MASTER database
+ * @param bool $fromMaster whether to use the DB_MASTER database
* @return Block|null (null if no relevant block could be found). The target and type
* of the returned Block will refer to the actual block which was found, which might
* not be the same as the target you gave if you used $vagueTarget!
@@ -1037,47 +1059,234 @@ class Block {
public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) {
list( $target, $type ) = self::parseTarget( $specificTarget );
- if( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ){
+ if ( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ) {
return Block::newFromID( $target );
- } elseif( $target === null && $vagueTarget == '' ){
+ } elseif ( $target === null && $vagueTarget == '' ) {
# We're not going to find anything useful here
# Be aware that the == '' check is explicit, since empty values will be
# passed by some callers (bug 29116)
return null;
- } elseif( in_array( $type, array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE ) ) ) {
+ } elseif ( in_array( $type, array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ) ) ) {
$block = new Block();
$block->fromMaster( $fromMaster );
- if( $type !== null ){
+ if ( $type !== null ) {
$block->setTarget( $target );
}
- if( $block->newLoad( $vagueTarget ) ){
+ if ( $block->newLoad( $vagueTarget ) ) {
return $block;
}
}
return null;
}
+
/**
- * From an existing Block, get the target and the type of target. Note that it is
- * always safe to treat the target as a string; for User objects this will return
- * User::__toString() which in turn gives User::getName().
+ * Get all blocks that match any IP from an array of IP addresses
*
- * @param $target String|Int|User
- * @return array( User|String, Block::TYPE_ constant )
+ * @param Array $ipChain list of IPs (strings), usually retrieved from the
+ * X-Forwarded-For header of the request
+ * @param Bool $isAnon Exclude anonymous-only blocks if false
+ * @param Bool $fromMaster Whether to query the master or slave database
+ * @return Array of Blocks
+ * @since 1.22
+ */
+ public static function getBlocksForIPList( array $ipChain, $isAnon, $fromMaster = false ) {
+ if ( !count( $ipChain ) ) {
+ return array();
+ }
+
+ wfProfileIn( __METHOD__ );
+ $conds = array();
+ foreach ( array_unique( $ipChain ) as $ipaddr ) {
+ # Discard invalid IP addresses. Since XFF can be spoofed and we do not
+ # necessarily trust the header given to us, make sure that we are only
+ # checking for blocks on well-formatted IP addresses (IPv4 and IPv6).
+ # Do not treat private IP spaces as special as it may be desirable for wikis
+ # to block those IP ranges in order to stop misbehaving proxies that spoof XFF.
+ if ( !IP::isValid( $ipaddr ) ) {
+ continue;
+ }
+ # Don't check trusted IPs (includes local squids which will be in every request)
+ if ( wfIsTrustedProxy( $ipaddr ) ) {
+ continue;
+ }
+ # Check both the original IP (to check against single blocks), as well as build
+ # the clause to check for rangeblocks for the given IP.
+ $conds['ipb_address'][] = $ipaddr;
+ $conds[] = self::getRangeCond( IP::toHex( $ipaddr ) );
+ }
+
+ if ( !count( $conds ) ) {
+ wfProfileOut( __METHOD__ );
+ return array();
+ }
+
+ if ( $fromMaster ) {
+ $db = wfGetDB( DB_MASTER );
+ } else {
+ $db = wfGetDB( DB_SLAVE );
+ }
+ $conds = $db->makeList( $conds, LIST_OR );
+ if ( !$isAnon ) {
+ $conds = array( $conds, 'ipb_anon_only' => 0 );
+ }
+ $selectFields = array_merge(
+ array( 'ipb_range_start', 'ipb_range_end' ),
+ Block::selectFields()
+ );
+ $rows = $db->select( 'ipblocks',
+ $selectFields,
+ $conds,
+ __METHOD__
+ );
+
+ $blocks = array();
+ foreach ( $rows as $row ) {
+ $block = self::newFromRow( $row );
+ if ( !$block->deleteIfExpired() ) {
+ $blocks[] = $block;
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $blocks;
+ }
+
+ /**
+ * From a list of multiple blocks, find the most exact and strongest Block.
+ * The logic for finding the "best" block is:
+ * - Blocks that match the block's target IP are preferred over ones in a range
+ * - Hardblocks are chosen over softblocks that prevent account creation
+ * - Softblocks that prevent account creation are chosen over other softblocks
+ * - Other softblocks are chosen over autoblocks
+ * - If there are multiple exact or range blocks at the same level, the one chosen
+ * is random
+
+ * @param Array $ipChain list of IPs (strings). This is used to determine how "close"
+ * a block is to the server, and if a block matches exactly, or is in a range.
+ * The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2,
+ * local-squid, ...)
+ * @param Array $block Array of blocks
+ * @return Block|null the "best" block from the list
+ */
+ public static function chooseBlock( array $blocks, array $ipChain ) {
+ if ( !count( $blocks ) ) {
+ return null;
+ } elseif ( count( $blocks ) == 1 ) {
+ return $blocks[0];
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ // Sort hard blocks before soft ones and secondarily sort blocks
+ // that disable account creation before those that don't.
+ usort( $blocks, function( Block $a, Block $b ) {
+ $aWeight = (int)$a->isHardblock() . (int)$a->prevents( 'createaccount' );
+ $bWeight = (int)$b->isHardblock() . (int)$b->prevents( 'createaccount' );
+ return strcmp( $bWeight, $aWeight ); // highest weight first
+ } );
+
+ $blocksListExact = array(
+ 'hard' => false,
+ 'disable_create' => false,
+ 'other' => false,
+ 'auto' => false
+ );
+ $blocksListRange = array(
+ 'hard' => false,
+ 'disable_create' => false,
+ 'other' => false,
+ 'auto' => false
+ );
+ $ipChain = array_reverse( $ipChain );
+
+ foreach ( $blocks as $block ) {
+ // Stop searching if we have already have a "better" block. This
+ // is why the order of the blocks matters
+ if ( !$block->isHardblock() && $blocksListExact['hard'] ) {
+ break;
+ } elseif ( !$block->prevents( 'createaccount' ) && $blocksListExact['disable_create'] ) {
+ break;
+ }
+
+ foreach ( $ipChain as $checkip ) {
+ $checkipHex = IP::toHex( $checkip );
+ if ( (string)$block->getTarget() === $checkip ) {
+ if ( $block->isHardblock() ) {
+ $blocksListExact['hard'] = $blocksListExact['hard'] ?: $block;
+ } elseif ( $block->prevents( 'createaccount' ) ) {
+ $blocksListExact['disable_create'] = $blocksListExact['disable_create'] ?: $block;
+ } elseif ( $block->mAuto ) {
+ $blocksListExact['auto'] = $blocksListExact['auto'] ?: $block;
+ } else {
+ $blocksListExact['other'] = $blocksListExact['other'] ?: $block;
+ }
+ // We found closest exact match in the ip list, so go to the next Block
+ break;
+ } elseif ( array_filter( $blocksListExact ) == array()
+ && $block->getRangeStart() <= $checkipHex
+ && $block->getRangeEnd() >= $checkipHex
+ ) {
+ if ( $block->isHardblock() ) {
+ $blocksListRange['hard'] = $blocksListRange['hard'] ?: $block;
+ } elseif ( $block->prevents( 'createaccount' ) ) {
+ $blocksListRange['disable_create'] = $blocksListRange['disable_create'] ?: $block;
+ } elseif ( $block->mAuto ) {
+ $blocksListRange['auto'] = $blocksListRange['auto'] ?: $block;
+ } else {
+ $blocksListRange['other'] = $blocksListRange['other'] ?: $block;
+ }
+ break;
+ }
+ }
+ }
+
+ if ( array_filter( $blocksListExact ) == array() ) {
+ $blocksList = &$blocksListRange;
+ } else {
+ $blocksList = &$blocksListExact;
+ }
+
+ $chosenBlock = null;
+ if ( $blocksList['hard'] ) {
+ $chosenBlock = $blocksList['hard'];
+ } elseif ( $blocksList['disable_create'] ) {
+ $chosenBlock = $blocksList['disable_create'];
+ } elseif ( $blocksList['other'] ) {
+ $chosenBlock = $blocksList['other'];
+ } elseif ( $blocksList['auto'] ) {
+ $chosenBlock = $blocksList['auto'];
+ } else {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Proxy block found, but couldn't be classified." );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $chosenBlock;
+ }
+
+ /**
+ * From an existing Block, get the target and the type of target.
+ * Note that, except for null, it is always safe to treat the target
+ * as a string; for User objects this will return User::__toString()
+ * which in turn gives User::getName().
+ *
+ * @param $target String|Int|User|null
+ * @return array( User|String|null, Block::TYPE_ constant|null )
*/
public static function parseTarget( $target ) {
# We may have been through this before
- if( $target instanceof User ){
- if( IP::isValid( $target->getName() ) ){
+ if ( $target instanceof User ) {
+ if ( IP::isValid( $target->getName() ) ) {
return array( $target, self::TYPE_IP );
} else {
return array( $target, self::TYPE_USER );
}
- } elseif( $target === null ){
+ } elseif ( $target === null ) {
return array( null, null );
}
@@ -1098,7 +1307,7 @@ class Block {
# Consider the possibility that this is not a username at all
# but actually an old subpage (bug #29797)
- if( strpos( $target, '/' ) !== false ){
+ if ( strpos( $target, '/' ) !== false ) {
# An old subpage, drill down to the user behind it
$parts = explode( '/', $target );
$target = $parts[0];
@@ -1165,7 +1374,7 @@ class Block {
* Set the target for this block, and update $this->type accordingly
* @param $target Mixed
*/
- public function setTarget( $target ){
+ public function setTarget( $target ) {
list( $this->target, $this->type ) = self::parseTarget( $target );
}
@@ -1173,15 +1382,54 @@ class Block {
* Get the user who implemented this block
* @return User|string Local User object or string for a foreign user
*/
- public function getBlocker(){
+ public function getBlocker() {
return $this->blocker;
}
/**
* Set the user who implemented (or will implement) this block
- * @param $user User|string Local User object or username string for foriegn users
+ * @param $user User|string Local User object or username string for foreign users
*/
- public function setBlocker( $user ){
+ public function setBlocker( $user ) {
$this->blocker = $user;
}
+
+ /**
+ * Get the key and parameters for the corresponding error message.
+ *
+ * @since 1.22
+ * @param IContextSource $context
+ * @return array
+ */
+ public function getPermissionsError( IContextSource $context ) {
+ $blocker = $this->getBlocker();
+ if ( $blocker instanceof User ) { // local user
+ $blockerUserpage = $blocker->getUserPage();
+ $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]";
+ } else { // foreign user
+ $link = $blocker;
+ }
+
+ $reason = $this->mReason;
+ if ( $reason == '' ) {
+ $reason = $context->msg( 'blockednoreason' )->text();
+ }
+
+ /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
+ * This could be a username, an IP range, or a single IP. */
+ $intended = $this->getTarget();
+
+ $lang = $context->getLanguage();
+ return array(
+ $this->mAuto ? 'autoblockedtext' : 'blockedtext',
+ $link,
+ $reason,
+ $context->getRequest()->getIP(),
+ $this->getByName(),
+ $this->getId(),
+ $lang->formatExpiry( $this->mExpiry ),
+ (string)$intended,
+ $lang->timeanddate( wfTimestamp( TS_MW, $this->mTimestamp ), true ),
+ );
+ }
}
diff --git a/includes/CacheHelper.php b/includes/CacheHelper.php
index ac46fc42..f0ae5a31 100644
--- a/includes/CacheHelper.php
+++ b/includes/CacheHelper.php
@@ -18,7 +18,7 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
diff --git a/includes/CallableUpdate.php b/includes/CallableUpdate.php
new file mode 100644
index 00000000..6eb55413
--- /dev/null
+++ b/includes/CallableUpdate.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Deferrable Update for closure/callback
+ */
+class MWCallableUpdate implements DeferrableUpdate {
+
+ /**
+ * @var closure/callabck
+ */
+ private $callback;
+
+ /**
+ * @param callable $callback
+ */
+ public function __construct( $callback ) {
+ if ( !is_callable( $callback ) ) {
+ throw new MWException( 'Not a valid callback/closure!' );
+ }
+ $this->callback = $callback;
+ }
+
+ /**
+ * Run the update
+ */
+ public function doUpdate() {
+ call_user_func( $this->callback );
+ }
+
+}
diff --git a/includes/Category.php b/includes/Category.php
index b7b12e8a..126b8fee 100644
--- a/includes/Category.php
+++ b/includes/Category.php
@@ -40,10 +40,12 @@ 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.
+ * @throws MWException
* @return bool True on success, false on failure.
*/
protected function initialize() {
@@ -57,6 +59,9 @@ class Category {
# Already initialized
return true;
}
+
+ wfProfileIn( __METHOD__ );
+
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow(
'category',
@@ -65,15 +70,17 @@ class Category {
__METHOD__
);
+ wfProfileOut( __METHOD__ );
+
if ( !$row ) {
# Okay, there were no contents. Nothing to initialize.
if ( $this->mTitle ) {
# If there is a title object but no record in the category table, treat this as an empty category
- $this->mID = false;
- $this->mName = $this->mTitle->getDBkey();
- $this->mPages = 0;
+ $this->mID = false;
+ $this->mName = $this->mTitle->getDBkey();
+ $this->mPages = 0;
$this->mSubcats = 0;
- $this->mFiles = 0;
+ $this->mFiles = 0;
return true;
} else {
@@ -81,11 +88,11 @@ class Category {
}
}
- $this->mID = $row->cat_id;
- $this->mName = $row->cat_title;
- $this->mPages = $row->cat_pages;
+ $this->mID = $row->cat_id;
+ $this->mName = $row->cat_title;
+ $this->mPages = $row->cat_pages;
$this->mSubcats = $row->cat_subcats;
- $this->mFiles = $row->cat_files;
+ $this->mFiles = $row->cat_files;
# (bug 13683) If the count is negative, then 1) it's obviously wrong
# and should not be kept, and 2) we *probably* don't have to scan many
@@ -100,7 +107,7 @@ class Category {
/**
* Factory function.
*
- * @param $name Array: A category name (no "Category:" prefix). It need
+ * @param array $name A category name (no "Category:" prefix). It need
* not be normalized, with spaces replaced by underscores.
* @return mixed Category, or false on a totally invalid name
*/
@@ -161,7 +168,7 @@ class Category {
# 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
+ # was ever added to the category. This case should be treated link an empty
# category, if possible.
if ( $row->cat_title === null ) {
@@ -173,41 +180,53 @@ class Category {
$cat->mName = $title->getDBkey(); # if we have a title object, fetch the category name from there
}
- $cat->mID = false;
+ $cat->mID = false;
$cat->mSubcats = 0;
- $cat->mPages = 0;
- $cat->mFiles = 0;
+ $cat->mPages = 0;
+ $cat->mFiles = 0;
} else {
- $cat->mName = $row->cat_title;
- $cat->mID = $row->cat_id;
+ $cat->mName = $row->cat_title;
+ $cat->mID = $row->cat_id;
$cat->mSubcats = $row->cat_subcats;
- $cat->mPages = $row->cat_pages;
- $cat->mFiles = $row->cat_files;
+ $cat->mPages = $row->cat_pages;
+ $cat->mFiles = $row->cat_files;
}
return $cat;
}
/** @return mixed DB key name, or false on failure */
- public function getName() { return $this->getX( 'mName' ); }
+ public function getName() {
+ return $this->getX( 'mName' );
+ }
/** @return mixed Category ID, or false on failure */
- public function getID() { return $this->getX( 'mID' ); }
+ 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' ); }
+ public function getPageCount() {
+ return $this->getX( 'mPages' );
+ }
/** @return mixed Number of subcategories, or false on failure */
- public function getSubcatCount() { return $this->getX( 'mSubcats' ); }
+ public function getSubcatCount() {
+ return $this->getX( 'mSubcats' );
+ }
/** @return mixed Number of member files, or false on failure */
- public function getFileCount() { return $this->getX( 'mFiles' ); }
+ public function getFileCount() {
+ return $this->getX( 'mFiles' );
+ }
/**
* @return Title|bool Title for this category, or false on failure.
*/
public function getTitle() {
- if ( $this->mTitle ) return $this->mTitle;
+ if ( $this->mTitle ) {
+ return $this->mTitle;
+ }
if ( !$this->initialize() ) {
return false;
@@ -225,20 +244,22 @@ class Category {
* @return TitleArray object for category members.
*/
public function getMembers( $limit = false, $offset = '' ) {
+ wfProfileIn( __METHOD__ );
+
$dbr = wfGetDB( DB_SLAVE );
$conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' );
$options = array( 'ORDER BY' => 'cl_sortkey' );
if ( $limit ) {
- $options[ 'LIMIT' ] = $limit;
+ $options['LIMIT'] = $limit;
}
if ( $offset !== '' ) {
$conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
}
- return TitleArray::newFromResult(
+ $result = TitleArray::newFromResult(
$dbr->select(
array( 'page', 'categorylinks' ),
array( 'page_id', 'page_namespace', 'page_title', 'page_len',
@@ -248,6 +269,10 @@ class Category {
$options
)
);
+
+ wfProfileOut( __METHOD__ );
+
+ return $result;
}
/**
@@ -258,7 +283,7 @@ class Category {
if ( !$this->initialize() ) {
return false;
}
- return $this-> { $key } ;
+ return $this->{$key};
}
/**
@@ -278,8 +303,10 @@ class Category {
}
}
+ wfProfileIn( __METHOD__ );
+
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin( __METHOD__ );
+ $dbw->begin( __METHOD__ );
# Insert the row if it doesn't exist yet (e.g., this is being run via
# update.php from a pre-1.16 schema). TODO: This will cause lots and
@@ -302,12 +329,12 @@ class Category {
$result = $dbw->selectRow(
array( 'categorylinks', 'page' ),
array( 'pages' => 'COUNT(*)',
- 'subcats' => "COUNT($cond1)",
- 'files' => "COUNT($cond2)"
+ 'subcats' => "COUNT($cond1)",
+ 'files' => "COUNT($cond2)"
),
array( 'cl_to' => $this->mName, 'page_id = cl_from' ),
__METHOD__,
- 'LOCK IN SHARE MODE'
+ array( 'LOCK IN SHARE MODE' )
);
$ret = $dbw->update(
'category',
@@ -321,10 +348,12 @@ class Category {
);
$dbw->commit( __METHOD__ );
+ wfProfileOut( __METHOD__ );
+
# Now we should update our local counts.
- $this->mPages = $result->pages;
+ $this->mPages = $result->pages;
$this->mSubcats = $result->subcats;
- $this->mFiles = $result->files;
+ $this->mFiles = $result->files;
return $ret;
}
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index 32e270e8..ba71aa01 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -40,7 +40,7 @@ class CategoryPage extends Article {
/**
* Constructor from a page id
- * @param $id Int article ID to load
+ * @param int $id article ID to load
* @return CategoryPage|null
*/
public static function newFromID( $id ) {
@@ -56,7 +56,7 @@ class CategoryPage extends Article {
$diffOnly = $request->getBool( 'diffonly',
$this->getContext()->getUser()->getOption( 'diffonly' ) );
- if ( isset( $diff ) && $diffOnly ) {
+ if ( $diff !== null && $diffOnly ) {
parent::view();
return;
}
@@ -106,7 +106,13 @@ class CategoryPage extends Article {
unset( $reqArray["from"] );
unset( $reqArray["to"] );
- $viewer = new $this->mCategoryViewerClass( $this->getContext()->getTitle(), $this->getContext(), $from, $until, $reqArray );
+ $viewer = new $this->mCategoryViewerClass(
+ $this->getContext()->getTitle(),
+ $this->getContext(),
+ $from,
+ $until,
+ $reqArray
+ );
$this->getContext()->getOutput()->addHTML( $viewer->getHTML() );
}
}
diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php
index 3bb2bc9b..55d9c1e5 100644
--- a/includes/CategoryViewer.php
+++ b/includes/CategoryViewer.php
@@ -71,11 +71,12 @@ class CategoryViewer extends ContextSource {
* @since 1.19 $context is a second, required parameter
* @param $title Title
* @param $context IContextSource
- * @param $from String
- * @param $until String
+ * @param array $from An array with keys page, subcat,
+ * and file for offset of results of each section (since 1.17)
+ * @param array $until An array with 3 keys for until of each section (since 1.17)
* @param $query Array
*/
- function __construct( $title, IContextSource $context, $from = '', $until = '', $query = array() ) {
+ function __construct( $title, IContextSource $context, $from = array(), $until = array(), $query = array() ) {
global $wgCategoryPagingLimit;
$this->title = $title;
$this->setContext( $context );
@@ -140,8 +141,17 @@ class CategoryViewer extends ContextSource {
$this->children = array();
$this->children_start_char = array();
if ( $this->showGallery ) {
- $this->gallery = new ImageGallery();
+ // Note that null for mode is taken to mean use default.
+ $mode = $this->getRequest()->getVal( 'gallerymode', null );
+ try {
+ $this->gallery = ImageGalleryBase::factory( $mode );
+ } catch ( MWException $e ) {
+ // User specified something invalid, fallback to default.
+ $this->gallery = ImageGalleryBase::factory();
+ }
+
$this->gallery->setHideBadImages();
+ $this->gallery->setContext( $this->getContext() );
} else {
$this->imgsNoGallery = array();
$this->imgsNoGallery_start_char = array();
@@ -173,7 +183,7 @@ class CategoryViewer extends ContextSource {
/**
* Add a subcategory to the internal lists, using a title object
- * @deprecated since 1.17 kept for compatibility, please use addSubcategoryObject instead
+ * @deprecated since 1.17 kept for compatibility, use addSubcategoryObject instead
*/
function addSubcategory( Title $title, $sortkey, $pageLength ) {
wfDeprecated( __METHOD__, '1.17' );
@@ -181,14 +191,14 @@ class CategoryViewer extends ContextSource {
}
/**
- * Get the character to be used for sorting subcategories.
- * If there's a link from Category:A to Category:B, the sortkey of the resulting
- * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
- * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
- * else use sortkey...
- *
- * @param Title $title
- * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
+ * Get the character to be used for sorting subcategories.
+ * If there's a link from Category:A to Category:B, the sortkey of the resulting
+ * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
+ * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
+ * else use sortkey...
+ *
+ * @param Title $title
+ * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
* @return string
*/
function getSubcategorySortChar( $title, $sortkey ) {
@@ -248,7 +258,7 @@ class CategoryViewer extends ContextSource {
$link = Linker::link( $title );
if ( $isRedirect ) {
// This seems kind of pointless given 'mw-redirect' class,
- // but keeping for back-compatiability with user css.
+ // but keeping for back-compatibility with user css.
$link = '<span class="redirect-in-category">' . $link . '</span>';
}
$this->articles[] = $link;
@@ -259,15 +269,15 @@ class CategoryViewer extends ContextSource {
function finaliseCategoryState() {
if ( $this->flip['subcat'] ) {
- $this->children = array_reverse( $this->children );
+ $this->children = array_reverse( $this->children );
$this->children_start_char = array_reverse( $this->children_start_char );
}
if ( $this->flip['page'] ) {
- $this->articles = array_reverse( $this->articles );
+ $this->articles = array_reverse( $this->articles );
$this->articles_start_char = array_reverse( $this->articles_start_char );
}
if ( !$this->showGallery && $this->flip['file'] ) {
- $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
+ $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
$this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
}
}
@@ -287,10 +297,10 @@ class CategoryViewer extends ContextSource {
# the collation in the database differs from the one
# set in $wgCategoryCollation, pagination might go totally haywire.
$extraConds = array( 'cl_type' => $type );
- if ( $this->from[$type] !== null ) {
+ if ( isset( $this->from[$type] ) && $this->from[$type] !== null ) {
$extraConds[] = 'cl_sortkey >= '
. $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
- } elseif ( $this->until[$type] !== null ) {
+ } elseif ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
$extraConds[] = 'cl_sortkey < '
. $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
$this->flip[$type] = true;
@@ -302,7 +312,7 @@ class CategoryViewer extends ContextSource {
'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title',
'cat_subcats', 'cat_pages', 'cat_files',
'cl_sortkey_prefix', 'cl_collation' ),
- array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ),
+ array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ),
__METHOD__,
array(
'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
@@ -310,8 +320,11 @@ class CategoryViewer extends ContextSource {
'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
),
array(
- 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
- 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY )
+ 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
+ 'category' => array( 'LEFT JOIN', array(
+ 'cat_title = page_title',
+ 'page_namespace' => NS_CATEGORY
+ ))
)
);
@@ -388,7 +401,7 @@ class CategoryViewer extends ContextSource {
$r = '';
# @todo FIXME: Here and in the other two sections: we don't need to bother
- # with this rigamarole if the entire category contents fit on one page
+ # with this rigmarole if the entire category contents fit on one page
# and have already been retrieved. We can just use $rescnt in that
# case and save a query and some logic.
$dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
@@ -437,13 +450,13 @@ class CategoryViewer extends ContextSource {
* Get the paging links for a section (subcats/pages/files), to go at the top and bottom
* of the output.
*
- * @param $type String: 'page', 'subcat', or 'file'
+ * @param string $type 'page', 'subcat', or 'file'
* @return String: HTML output, possibly empty if there are no other pages
*/
private function getSectionPagingLinks( $type ) {
- if ( $this->until[$type] !== null ) {
+ if ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
return $this->pagingLinks( $this->nextPage[$type], $this->until[$type], $type );
- } elseif ( $this->nextPage[$type] !== null || $this->from[$type] !== null ) {
+ } elseif ( $this->nextPage[$type] !== null || ( isset( $this->from[$type] ) && $this->from[$type] !== null ) ) {
return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
} else {
return '';
@@ -469,7 +482,7 @@ class CategoryViewer extends ContextSource {
*/
function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
$list = '';
- if ( count ( $articles ) > $cutoff ) {
+ if ( count( $articles ) > $cutoff ) {
$list = self::columnList( $articles, $articles_start_char );
} elseif ( count( $articles ) > 0 ) {
// for short lists of articles in categories.
@@ -478,7 +491,7 @@ class CategoryViewer extends ContextSource {
$pageLang = $this->title->getPageLanguage();
$attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
- 'class' => 'mw-content-'.$pageLang->getDir() );
+ 'class' => 'mw-content-' . $pageLang->getDir() );
$list = Html::rawElement( 'div', $attribs, $list );
return $list;
@@ -522,7 +535,10 @@ class CategoryViewer extends ContextSource {
$first = true;
foreach ( $colContents as $char => $articles ) {
- $ret .= '<h3>' . htmlspecialchars( $char );
+ # Change space to non-breaking space to keep headers aligned
+ $h3char = $char === ' ' ? '&#160;' : htmlspecialchars( $char );
+
+ $ret .= '<h3>' . $h3char;
if ( $first && $char === $prevchar ) {
# We're continuing a previous chunk at the top of a new
# column, so add " cont." after the letter.
@@ -569,9 +585,9 @@ class CategoryViewer extends ContextSource {
/**
* Create paging links, as a helper method to getSectionPagingLinks().
*
- * @param $first String The 'until' parameter for the generated URL
- * @param $last String The 'from' parameter for the genererated URL
- * @param $type String A prefix for parameters, 'page' or 'subcat' or
+ * @param string $first The 'until' parameter for the generated URL
+ * @param string $last The 'from' parameter for the generated URL
+ * @param string $type A prefix for parameters, 'page' or 'subcat' or
* 'file'
* @return String HTML
*/
@@ -604,7 +620,7 @@ class CategoryViewer extends ContextSource {
);
}
- return $this->msg('categoryviewer-pagedlinks')->rawParams($prevLink, $nextLink)->escaped();
+ return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
}
/**
@@ -612,7 +628,8 @@ class CategoryViewer extends ContextSource {
* corresponds to the correct segment of the category.
*
* @param Title $title: The title (usually $this->title)
- * @param String $section: Which section
+ * @param string $section: Which section
+ * @throws MWException
* @return Title
*/
private function addFragmentToTitle( $title, $section ) {
@@ -634,33 +651,29 @@ class CategoryViewer extends ContextSource {
return Title::makeTitle( $title->getNamespace(),
$title->getDBkey(), $fragment );
}
+
/**
* What to do if the category table conflicts with the number of results
* returned? This function says what. Each type is considered independently
* of the other types.
*
- * Note for grepping: uses the messages category-article-count,
- * category-article-count-limited, category-subcat-count,
- * category-subcat-count-limited, category-file-count,
- * category-file-count-limited.
- *
- * @param $rescnt Int: The number of items returned by our database query.
- * @param $dbcnt Int: The number of items according to the category table.
- * @param $type String: 'subcat', 'article', or 'file'
- * @return String: A message giving the number of items, to output to HTML.
+ * @param int $rescnt The number of items returned by our database query.
+ * @param int $dbcnt The number of items according to the category table.
+ * @param string $type 'subcat', 'article', or 'file'
+ * @return string: A message giving the number of items, to output to HTML.
*/
private function getCountMessage( $rescnt, $dbcnt, $type ) {
- # There are three cases:
- # 1) The category table figure seems sane. It might be wrong, but
- # we can't do anything about it if we don't recalculate it on ev-
- # ery category view.
- # 2) The category table figure isn't sane, like it's smaller than the
- # number of actual results, *but* the number of results is less
- # than $this->limit and there's no offset. In this case we still
- # know the right figure.
- # 3) We have no idea.
-
- # Check if there's a "from" or "until" for anything
+ // There are three cases:
+ // 1) The category table figure seems sane. It might be wrong, but
+ // we can't do anything about it if we don't recalculate it on ev-
+ // ery category view.
+ // 2) The category table figure isn't sane, like it's smaller than the
+ // number of actual results, *but* the number of results is less
+ // than $this->limit and there's no offset. In this case we still
+ // know the right figure.
+ // 3) We have no idea.
+
+ // Check if there's a "from" or "until" for anything
// This is a little ugly, but we seem to use different names
// for the paging types then for the messages.
@@ -671,25 +684,31 @@ class CategoryViewer extends ContextSource {
}
$fromOrUntil = false;
- if ( $this->from[$pagingType] !== null || $this->until[$pagingType] !== null ) {
+ if ( ( isset( $this->from[$pagingType] ) && $this->from[$pagingType] !== null ) ||
+ ( isset( $this->until[$pagingType] ) && $this->until[$pagingType] !== null )
+ ) {
$fromOrUntil = true;
}
- if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil )
- && $dbcnt > $rescnt ) ) {
- # Case 1: seems sane.
+ if ( $dbcnt == $rescnt ||
+ ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
+ ) {
+ // Case 1: seems sane.
$totalcnt = $dbcnt;
} elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
- # Case 2: not sane, but salvageable. Use the number of results.
- # Since there are fewer than 200, we can also take this opportunity
- # to refresh the incorrect category table entry -- which should be
- # quick due to the small number of entries.
+ // Case 2: not sane, but salvageable. Use the number of results.
+ // Since there are fewer than 200, we can also take this opportunity
+ // to refresh the incorrect category table entry -- which should be
+ // quick due to the small number of entries.
$totalcnt = $rescnt;
$this->cat->refreshCounts();
} else {
- # Case 3: hopeless. Don't give a total count at all.
+ // Case 3: hopeless. Don't give a total count at all.
+ // Messages: category-subcat-count-limited, category-article-count-limited,
+ // category-file-count-limited
return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
}
+ // Messages: category-subcat-count, category-article-count, category-file-count
return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
}
}
diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
index e2b6a0ca..6ef224b6 100644
--- a/includes/Categoryfinder.php
+++ b/includes/Categoryfinder.php
@@ -28,17 +28,17 @@
*
* Example use :
* <code>
- * # Determines whether the article with the page_id 12345 is in both
- * # "Category 1" and "Category 2" or their subcategories, respectively
+ * # Determines whether the article with the page_id 12345 is in both
+ * # "Category 1" and "Category 2" or their subcategories, respectively
*
- * $cf = new Categoryfinder;
- * $cf->seed(
- * array( 12345 ),
- * array( 'Category 1', 'Category 2' ),
- * 'AND'
- * );
- * $a = $cf->run();
- * print implode( ',' , $a );
+ * $cf = new Categoryfinder;
+ * $cf->seed(
+ * array( 12345 ),
+ * array( 'Category 1', 'Category 2' ),
+ * 'AND'
+ * );
+ * $a = $cf->run();
+ * print implode( ',' , $a );
* </code>
*
*/
@@ -66,7 +66,7 @@ class Categoryfinder {
* Initializes the instance. Do this prior to calling run().
* @param $article_ids Array of article IDs
* @param $categories FIXME
- * @param $mode String: FIXME, default 'AND'.
+ * @param string $mode FIXME, default 'AND'.
* @todo FIXME: $categories/$mode
*/
function seed( $article_ids, $categories, $mode = 'AND' ) {
@@ -111,9 +111,9 @@ class Categoryfinder {
/**
* This functions recurses through the parent representation, trying to match the conditions
- * @param $id int The article/category to check
- * @param $conds array The array of categories to match
- * @param $path array used to check for recursion loops
+ * @param int $id The article/category to check
+ * @param array $conds The array of categories to match
+ * @param array $path used to check for recursion loops
* @return bool Does this match the conditions?
*/
function check( $id, &$conds, $path = array() ) {
@@ -124,7 +124,7 @@ class Categoryfinder {
$path[] = $id;
- # Shortcut (runtime paranoia): No contitions=all matched
+ # Shortcut (runtime paranoia): No conditions=all matched
if ( count( $conds ) == 0 ) {
return true;
}
@@ -135,7 +135,7 @@ class Categoryfinder {
# iterate through the parents
foreach ( $this->parents[$id] as $p ) {
- $pname = $p->cl_to ;
+ $pname = $p->cl_to;
# Is this a condition?
if ( isset( $conds[$pname] ) ) {
@@ -172,6 +172,8 @@ class Categoryfinder {
* Scans a "parent layer" of the articles/categories in $this->next
*/
function scan_next_layer() {
+ wfProfileIn( __METHOD__ );
+
# Find all parents of the article currently in $this->next
$layer = array();
$res = $this->dbr->select(
@@ -209,7 +211,7 @@ class Categoryfinder {
$res = $this->dbr->select(
/* FROM */ 'page',
/* SELECT */ array( 'page_id', 'page_title' ),
- /* WHERE */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ),
+ /* WHERE */ array( 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ),
__METHOD__ . '-2'
);
foreach ( $res as $o ) {
@@ -225,6 +227,7 @@ class Categoryfinder {
foreach ( $layer as $v ) {
$this->deadend[$v] = $v;
}
- }
+ wfProfileOut( __METHOD__ );
+ }
}
diff --git a/includes/Cdb.php b/includes/Cdb.php
index ae2e5b18..81c0afe1 100644
--- a/includes/Cdb.php
+++ b/includes/Cdb.php
@@ -133,8 +133,9 @@ class CdbReader_DBA {
}
function close() {
- if( isset($this->handle) )
+ if ( isset( $this->handle ) ) {
dba_close( $this->handle );
+ }
unset( $this->handle );
}
@@ -143,7 +144,6 @@ class CdbReader_DBA {
}
}
-
/**
* Writer class which uses the DBA extension
*/
@@ -164,8 +164,9 @@ class CdbWriter_DBA {
}
function close() {
- if( isset($this->handle) )
+ if ( isset( $this->handle ) ) {
dba_close( $this->handle );
+ }
if ( wfIsWindows() ) {
unlink( $this->realFileName );
}
@@ -181,4 +182,3 @@ class CdbWriter_DBA {
}
}
}
-
diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php
index 02be65f3..a38b9a86 100644
--- a/includes/Cdb_PHP.php
+++ b/includes/Cdb_PHP.php
@@ -73,10 +73,10 @@ class CdbFunctions {
public static function hash( $s ) {
$h = 5381;
for ( $i = 0; $i < strlen( $s ); $i++ ) {
- $h5 = ($h << 5) & 0xffffffff;
+ $h5 = ( $h << 5 ) & 0xffffffff;
// Do a 32-bit sum
// Inlined here for speed
- $sum = ($h & 0x3fffffff) + ($h5 & 0x3fffffff);
+ $sum = ( $h & 0x3fffffff ) + ( $h5 & 0x3fffffff );
$h =
(
( $sum & 0x40000000 ? 1 : 0 )
@@ -126,6 +126,7 @@ class CdbReader_PHP extends CdbReader {
/**
* @param $fileName string
+ * @throws MWException
*/
function __construct( $fileName ) {
$this->fileName = $fileName;
@@ -137,7 +138,7 @@ class CdbReader_PHP extends CdbReader {
}
function close() {
- if( isset( $this->handle ) ) {
+ if ( isset( $this->handle ) ) {
fclose( $this->handle );
}
unset( $this->handle );
@@ -179,7 +180,7 @@ class CdbReader_PHP extends CdbReader {
protected function read( $length, $pos ) {
if ( fseek( $this->handle, $pos ) == -1 ) {
// This can easily happen if the internal pointers are incorrect
- throw new MWException(
+ throw new MWException(
'Seek failed, file "' . $this->fileName . '" may be corrupted.' );
}
@@ -198,12 +199,13 @@ class CdbReader_PHP extends CdbReader {
/**
* Unpack an unsigned integer and throw an exception if it needs more than 31 bits
* @param $s
- * @return
+ * @throws MWException
+ * @return mixed
*/
protected function unpack31( $s ) {
$data = unpack( 'V', $s );
if ( $data[1] > 0x7fffffff ) {
- throw new MWException(
+ throw new MWException(
'Error in CDB file "' . $this->fileName . '", integer too big.' );
}
return $data[1];
@@ -330,10 +332,10 @@ class CdbWriter_PHP extends CdbWriter {
*/
public function close() {
$this->finish();
- if( isset($this->handle) ) {
+ if ( isset( $this->handle ) ) {
fclose( $this->handle );
}
- if ( wfIsWindows() && file_exists($this->realFileName) ) {
+ if ( wfIsWindows() && file_exists( $this->realFileName ) ) {
unlink( $this->realFileName );
}
if ( !rename( $this->tmpFileName, $this->realFileName ) ) {
@@ -349,7 +351,7 @@ class CdbWriter_PHP extends CdbWriter {
protected function write( $buf ) {
$len = fwrite( $this->handle, $buf );
if ( $len !== strlen( $buf ) ) {
- $this->throwException( 'Error writing to CDB file "'.$this->tmpFileName.'".' );
+ $this->throwException( 'Error writing to CDB file "' . $this->tmpFileName . '".' );
}
}
@@ -361,7 +363,7 @@ class CdbWriter_PHP extends CdbWriter {
$newpos = $this->pos + $len;
if ( $newpos > 0x7fffffff ) {
$this->throwException(
- 'A value in the CDB file "'.$this->tmpFileName.'" is too large.' );
+ 'A value in the CDB file "' . $this->tmpFileName . '" is too large.' );
}
$this->pos = $newpos;
}
@@ -390,10 +392,10 @@ class CdbWriter_PHP extends CdbWriter {
*/
protected function addbegin( $keylen, $datalen ) {
if ( $keylen > 0x7fffffff ) {
- $this->throwException( 'Key length too long in file "'.$this->tmpFileName.'".' );
+ $this->throwException( 'Key length too long in file "' . $this->tmpFileName . '".' );
}
if ( $datalen > 0x7fffffff ) {
- $this->throwException( 'Data length too long in file "'.$this->tmpFileName.'".' );
+ $this->throwException( 'Data length too long in file "' . $this->tmpFileName . '".' );
}
$buf = pack( 'VV', $keylen, $datalen );
$this->write( $buf );
@@ -409,7 +411,7 @@ class CdbWriter_PHP extends CdbWriter {
// 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'] ];
+ ++ $counts[255 & $item['h']];
}
// Fill in $starts with the *end* indexes
@@ -448,9 +450,11 @@ class CdbWriter_PHP extends CdbWriter {
$hp = $packedTables[$starts[$i] + $u];
$where = CdbFunctions::unsignedMod(
CdbFunctions::unsignedShiftRight( $hp['h'], 8 ), $len );
- while ( $hashtable[$where]['p'] )
- if ( ++$where == $len )
+ while ( $hashtable[$where]['p'] ) {
+ if ( ++$where == $len ) {
$where = 0;
+ }
+ }
$hashtable[$where] = $hp;
}
@@ -468,14 +472,14 @@ class CdbWriter_PHP extends CdbWriter {
// Write the pointer array at the start of the file
rewind( $this->handle );
if ( ftell( $this->handle ) != 0 ) {
- $this->throwException( 'Error rewinding to start of file "'.$this->tmpFileName.'".' );
+ $this->throwException( 'Error rewinding to start of file "' . $this->tmpFileName . '".' );
}
$this->write( $final );
}
/**
* Clean up the temp file and throw an exception
- *
+ *
* @param $msg string
* @throws MWException
*/
diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php
index 0ebc926f..3fc27f9a 100644
--- a/includes/ChangeTags.php
+++ b/includes/ChangeTags.php
@@ -25,8 +25,8 @@ class ChangeTags {
/**
* Creates HTML for the given tags
*
- * @param $tags String: Comma-separated list of tags
- * @param $page String: A label for the type of action which is being displayed,
+ * @param string $tags Comma-separated list of tags
+ * @param string $page A label for the type of action which is being displayed,
* for example: 'history', 'contributions' or 'newpages'
*
* @return Array with two items: (html, classes)
@@ -34,17 +34,18 @@ class ChangeTags {
* - classes: Array of strings: CSS classes used in the generated html, one class for each tag
*
*/
- static function formatSummaryRow( $tags, $page ) {
+ public static function formatSummaryRow( $tags, $page ) {
global $wgLang;
- if( !$tags )
+ if ( !$tags ) {
return array( '', array() );
+ }
$classes = array();
$tags = explode( ',', $tags );
$displayTags = array();
- foreach( $tags as $tag ) {
+ foreach ( $tags as $tag ) {
$displayTags[] = Xml::tags(
'span',
array( 'class' => 'mw-tag-marker ' .
@@ -53,7 +54,10 @@ class ChangeTags {
);
$classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
}
- $markers = wfMessage( 'parentheses' )->rawParams( $wgLang->commaList( $displayTags ) )->text();
+ $markers = wfMessage( 'tag-list-wrapper' )
+ ->numParams( count( $displayTags ) )
+ ->rawParams( $wgLang->commaList( $displayTags ) )
+ ->parse();
$markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
return array( $markers, $classes );
@@ -62,12 +66,12 @@ class ChangeTags {
/**
* Get a short description for a tag
*
- * @param $tag String: tag
+ * @param string $tag tag
*
* @return String: Short description of the tag from "mediawiki:tag-$tag" if this message exists,
* html-escaped version of $tag otherwise
*/
- static function tagDescription( $tag ) {
+ public static function tagDescription( $tag ) {
$msg = wfMessage( "tag-$tag" );
return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag );
}
@@ -75,38 +79,40 @@ class ChangeTags {
/**
* Add tags to a change given its rc_id, rev_id and/or log_id
*
- * @param $tags String|Array: Tags to add to the change
+ * @param string|array $tags Tags to add to the change
* @param $rc_id int: rc_id of the change to add the tags to
* @param $rev_id int: rev_id of the change to add the tags to
* @param $log_id int: log_id of the change to add the tags to
- * @param $params String: params to put in the ct_params field of tabel 'change_tag'
+ * @param string $params params to put in the ct_params field of table 'change_tag'
*
+ * @throws MWException
* @return bool: false if no changes are made, otherwise true
*
* @exception MWException when $rc_id, $rev_id and $log_id are all null
*/
- static function addTags( $tags, $rc_id = null, $rev_id = null, $log_id = null, $params = null ) {
+ public 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 ) {
- throw new MWException( "At least one of: RCID, revision ID, and log ID MUST be specified when adding a tag to a change!" );
+ 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__ );
@@ -137,7 +143,7 @@ class ChangeTags {
// Insert the tags rows.
$tagsRows = array();
- foreach( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
+ foreach ( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
$tagsRows[] = array_filter(
array(
'ct_tag' => $tag,
@@ -159,28 +165,27 @@ class ChangeTags {
* Handles selecting tags, and filtering.
* Needs $tables to be set up properly, so we can figure out which join conditions to use.
*
- * @param $tables String|Array: Tabel names, see DatabaseBase::select
- * @param $fields String|Array: Fields used in query, see DatabaseBase::select
- * @param $conds String|Array: conditions used in query, see DatabaseBase::select
+ * @param string|array $tables Table names, see DatabaseBase::select
+ * @param string|array $fields Fields used in query, see DatabaseBase::select
+ * @param string|array $conds conditions used in query, see DatabaseBase::select
* @param $join_conds Array: join conditions, see DatabaseBase::select
- * @param $options Array: options, see Database::select
- * @param $filter_tag String: tag to select on
- *
- * @exception MWException when unable to determine appropriate JOIN condition for tagging
+ * @param array $options options, see Database::select
+ * @param bool|string $filter_tag Tag to select on
*
+ * @throws MWException When unable to determine appropriate JOIN condition for tagging
*/
- static function modifyDisplayQuery( &$tables, &$fields, &$conds,
+ public 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.
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 ) ) {
$join_cond = 'rev_id';
@@ -193,14 +198,12 @@ class ChangeTags {
$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.
- global $wgOldChangeTagsIndex;
- $index = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
- $options['USE INDEX'] = array( 'change_tag' => $index );
+ $options['USE INDEX'] = array( 'change_tag' => 'change_tag_tag_id' );
unset( $options['FORCE INDEX'] );
$tables[] = 'change_tag';
$join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" );
@@ -211,7 +214,7 @@ class ChangeTags {
/**
* Build a text box to select a change tag
*
- * @param $selected String: tag to select by default
+ * @param string $selected tag to select by default
* @param $fullForm Boolean:
* - if false, then it returns an array of (label, form).
* - if true, it returns an entire form around the selector.
@@ -221,11 +224,12 @@ class ChangeTags {
* - if $fullForm is false: Array with
* - if $fullForm is true: String, html fragment
*/
- public static function buildTagFilterSelector( $selected='', $fullForm = false, Title $title = null ) {
+ public static function buildTagFilterSelector( $selected = '', $fullForm = false, Title $title = null ) {
global $wgUseTagFilter;
- if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) )
+ if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) {
return $fullForm ? '' : array();
+ }
$data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMessage( 'tag-filter' )->parse() ),
Xml::input( 'tagfilter', 20, $selected, array( 'class' => 'mw-tagfilter-input' ) ) );
@@ -251,7 +255,7 @@ class ChangeTags {
*
* @return Array of strings: tags
*/
- static function listDefinedTags() {
+ public static function listDefinedTags() {
// Caching...
global $wgMemc;
$key = wfMemcKey( 'valid-tags' );
@@ -277,4 +281,34 @@ class ChangeTags {
$wgMemc->set( $key, $emptyTags, 300 );
return $emptyTags;
}
+
+ /**
+ * Returns a map of any tags used on the wiki to number of edits
+ * tagged with them, ordered descending by the hitcount.
+ *
+ * @return array Array of string => int
+ */
+ public static function tagUsageStatistics() {
+ $out = array();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select(
+ 'change_tag',
+ array( 'ct_tag', 'hitcount' => 'count(*)' ),
+ array(),
+ __METHOD__,
+ array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' )
+ );
+
+ foreach ( $res as $row ) {
+ $out[$row->ct_tag] = $row->hitcount;
+ }
+ foreach ( self::listDefinedTags() as $tag ) {
+ if ( !isset( $out[$tag] ) ) {
+ $out[$tag] = 0;
+ }
+ }
+
+ return $out;
+ }
}
diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php
index ee4c2d64..0736c507 100644
--- a/includes/ChangesFeed.php
+++ b/includes/ChangesFeed.php
@@ -31,8 +31,8 @@ class ChangesFeed {
/**
* Constructor
*
- * @param $format String: feed's format (either 'rss' or 'atom')
- * @param $type String: type of feed (for cache keys)
+ * @param string $format feed's format (either 'rss' or 'atom')
+ * @param string $type type of feed (for cache keys)
*/
public function __construct( $format, $type ) {
$this->format = $format;
@@ -42,9 +42,9 @@ class ChangesFeed {
/**
* Get a ChannelFeed subclass object to use
*
- * @param $title String: feed's title
- * @param $description String: feed's description
- * @param $url String: url of origin page
+ * @param string $title feed's title
+ * @param string $description feed's description
+ * @param string $url url of origin page
* @return ChannelFeed subclass or false on failure
*/
public function getFeedObject( $title, $description, $url ) {
@@ -54,7 +54,7 @@ class ChangesFeed {
return false;
}
- if( !array_key_exists( $this->format, $wgFeedClasses ) ) {
+ if ( !array_key_exists( $this->format, $wgFeedClasses ) ) {
// falling back to atom
$this->format = 'atom';
}
@@ -92,7 +92,7 @@ class ChangesFeed {
* gets it quick too.
*/
$cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key );
- if( is_string( $cachedFeed ) ) {
+ if ( is_string( $cachedFeed ) ) {
wfDebug( "RC: Outputting cached feed\n" );
$feed->httpHeaders();
echo $cachedFeed;
@@ -110,9 +110,9 @@ class ChangesFeed {
/**
* 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
+ * @param string $feed feed's content
+ * @param string $timekey memcached key of the last modification
+ * @param string $key memcached key of the content
*/
public function saveToCache( $feed, $timekey, $key ) {
global $messageMemc;
@@ -125,8 +125,8 @@ class ChangesFeed {
* 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
+ * @param string $timekey memcached key of the last modification
+ * @param string $key memcached key of the content
* @return string|bool feed's content on cache hit or false on cache miss
*/
public function loadFromCache( $lastmod, $timekey, $key ) {
@@ -134,8 +134,8 @@ class ChangesFeed {
$feedLastmod = $messageMemc->get( $timekey );
- if( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
- /**
+ if ( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
+ /**
* If the cached feed was rendered very recently, we may
* go ahead and use it even if there have been edits made
* since it was rendered. This keeps a swarm of requests
@@ -146,7 +146,7 @@ class ChangesFeed {
$feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod );
$lastmodUnix = wfTimestamp( TS_UNIX, $lastmod );
- if( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix) {
+ if ( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix ) {
wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
if ( $feedLastmodUnix < $lastmodUnix ) {
$wgOut->setLastModified( $feedLastmod ); // bug 21916
@@ -172,30 +172,32 @@ class ChangesFeed {
# Merge adjacent edits by one user
$sorted = array();
$n = 0;
- foreach( $rows as $obj ) {
- if( $n > 0 &&
+ foreach ( $rows as $obj ) {
+ if ( $n > 0 &&
$obj->rc_type == RC_EDIT &&
$obj->rc_namespace >= 0 &&
- $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id &&
- $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) {
- $sorted[$n-1]->rc_last_oldid = $obj->rc_last_oldid;
+ $obj->rc_cur_id == $sorted[$n - 1]->rc_cur_id &&
+ $obj->rc_user_text == $sorted[$n - 1]->rc_user_text ) {
+ $sorted[$n - 1]->rc_last_oldid = $obj->rc_last_oldid;
} else {
$sorted[$n] = $obj;
$n++;
}
}
- foreach( $sorted as $obj ) {
+ foreach ( $sorted as $obj ) {
$title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
- $talkpage = MWNamespace::canTalk( $obj->rc_namespace ) ? $title->getTalkPage()->getFullUrl() : '';
+ $talkpage = MWNamespace::canTalk( $obj->rc_namespace ) ? $title->getTalkPage()->getFullURL() : '';
// Skip items with deleted content (avoids partially complete/inconsistent output)
- if( $obj->rc_deleted ) continue;
+ if ( $obj->rc_deleted ) {
+ continue;
+ }
if ( $obj->rc_this_oldid ) {
- $url = $title->getFullURL(
- 'diff=' . $obj->rc_this_oldid .
- '&oldid=' . $obj->rc_last_oldid
- );
+ $url = $title->getFullURL( array(
+ 'diff' => $obj->rc_this_oldid,
+ 'oldid' => $obj->rc_last_oldid,
+ ) );
} else {
// log entry or something like that.
$url = $title->getFullURL();
@@ -206,7 +208,8 @@ class ChangesFeed {
FeedUtils::formatDiff( $obj ),
$url,
$obj->rc_timestamp,
- ( $obj->rc_deleted & Revision::DELETED_USER ) ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text,
+ ( $obj->rc_deleted & Revision::DELETED_USER )
+ ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text,
$talkpage
);
$feed->outItem( $item );
diff --git a/includes/ChangesList.php b/includes/ChangesList.php
deleted file mode 100644
index 84677124..00000000
--- a/includes/ChangesList.php
+++ /dev/null
@@ -1,1270 +0,0 @@
-<?php
-/**
- * Classes to show lists of changes.
- *
- * These can be:
- * - watchlist
- * - related changes
- * - recent changes
- *
- * 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
- */
-
-/**
- * @todo document
- */
-class RCCacheEntry extends RecentChange {
- var $secureName, $link;
- var $curlink , $difflink, $lastlink, $usertalklink, $versionlink;
- var $userlink, $timestamp, $watched;
-
- /**
- * @param $rc RecentChange
- * @return RCCacheEntry
- */
- static function newFromParent( $rc ) {
- $rc2 = new RCCacheEntry;
- $rc2->mAttribs = $rc->mAttribs;
- $rc2->mExtra = $rc->mExtra;
- return $rc2;
- }
-}
-
-/**
- * Base class for all changes lists
- */
-class ChangesList extends ContextSource {
-
- /**
- * @var Skin
- */
- public $skin;
-
- protected $watchlist = false;
-
- protected $message;
-
- /**
- * Changeslist contructor
- *
- * @param $obj Skin or IContextSource
- */
- public function __construct( $obj ) {
- if ( $obj instanceof IContextSource ) {
- $this->setContext( $obj );
- $this->skin = $obj->getSkin();
- } else {
- $this->setContext( $obj->getContext() );
- $this->skin = $obj;
- }
- $this->preCacheMessages();
- }
-
- /**
- * Fetch an appropriate changes list class for the main context
- * This first argument used to be an User object.
- *
- * @deprecated in 1.18; use newFromContext() instead
- * @param $unused string|User Unused
- * @return ChangesList|EnhancedChangesList|OldChangesList derivative
- */
- public static function newFromUser( $unused ) {
- wfDeprecated( __METHOD__, '1.18' );
- return self::newFromContext( RequestContext::getMain() );
- }
-
- /**
- * Fetch an appropriate changes list class for the specified context
- * Some users might want to use an enhanced list format, for instance
- *
- * @param $context IContextSource to use
- * @return ChangesList|EnhancedChangesList|OldChangesList derivative
- */
- public static function newFromContext( IContextSource $context ) {
- $user = $context->getUser();
- $sk = $context->getSkin();
- $list = null;
- if( wfRunHooks( 'FetchChangesList', array( $user, &$sk, &$list ) ) ) {
- $new = $context->getRequest()->getBool( 'enhanced', $user->getOption( 'usenewrc' ) );
- return $new ? new EnhancedChangesList( $context ) : new OldChangesList( $context );
- } else {
- 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
- * they are called often, we call them once and save them in $this->message
- */
- private function preCacheMessages() {
- if( !isset( $this->message ) ) {
- foreach ( explode( ' ', 'cur diff hist last blocklink history ' .
- 'semicolon-separator pipe-separator' ) as $msg ) {
- $this->message[$msg] = $this->msg( $msg )->escaped();
- }
- }
- }
-
- /**
- * Returns the appropriate flags for new page, minor change and patrolling
- * @param $flags Array Associative array of 'flag' => Bool
- * @param $nothing String to use for empty space
- * @return String
- */
- protected function recentChangesFlags( $flags, $nothing = '&#160;' ) {
- $f = '';
- foreach( array( 'newpage', 'minor', 'bot', 'unpatrolled' ) as $flag ){
- $f .= isset( $flags[$flag] ) && $flags[$flag]
- ? self::flag( $flag )
- : $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 $flag String: 'newpage', 'unpatrolled', 'minor', or 'bot'
- * @return String: Raw HTML
- */
- public static function flag( $flag ) {
- static $messages = null;
- if ( is_null( $messages ) ) {
- $messages = array(
- 'newpage' => array( 'newpageletter', 'recentchanges-label-newpage' ),
- 'minoredit' => array( 'minoreditletter', 'recentchanges-label-minor' ),
- 'botedit' => array( 'boteditletter', 'recentchanges-label-bot' ),
- 'unpatrolled' => array( 'unpatrolledletter', 'recentchanges-label-unpatrolled' ),
- );
- foreach( $messages as &$value ) {
- $value[0] = wfMessage( $value[0] )->escaped();
- $value[1] = wfMessage( $value[1] )->escaped();
- }
- }
-
- # Inconsistent naming, bleh
- $map = array(
- 'newpage' => 'newpage',
- 'minor' => 'minoredit',
- 'bot' => 'botedit',
- 'unpatrolled' => 'unpatrolled',
- 'minoredit' => 'minoredit',
- 'botedit' => 'botedit',
- );
- $flag = $map[$flag];
-
- return "<abbr class='$flag' title='" . $messages[$flag][1] . "'>" . $messages[$flag][0] . '</abbr>';
- }
-
- /**
- * Returns text for the start of the tabular part of RC
- * @return String
- */
- public function beginRecentChangesList() {
- $this->rc_cache = array();
- $this->rcMoveIndex = 0;
- $this->rcCacheIndex = 0;
- $this->lastdate = '';
- $this->rclistOpen = false;
- $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' );
- return '';
- }
-
- /**
- * Show formatted char difference
- * @param $old Integer: bytes
- * @param $new Integer: bytes
- * @param $context IContextSource context to use
- * @return String
- */
- public static function showCharacterDifference( $old, $new, IContextSource $context = null ) {
- global $wgRCChangedSizeThreshold, $wgMiserMode;
-
- if ( !$context ) {
- $context = RequestContext::getMain();
- }
-
- $new = (int)$new;
- $old = (int)$old;
- $szdiff = $new - $old;
-
- $lang = $context->getLanguage();
- $code = $lang->getCode();
- static $fastCharDiff = array();
- if ( !isset($fastCharDiff[$code]) ) {
- $fastCharDiff[$code] = $wgMiserMode || $context->msg( 'rc-change-size' )->plain() === '$1';
- }
-
- $formattedSize = $lang->formatNum( $szdiff );
-
- if ( !$fastCharDiff[$code] ) {
- $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
- }
-
- if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) {
- $tag = 'strong';
- } else {
- $tag = 'span';
- }
-
- if ( $szdiff === 0 ) {
- $formattedSizeClass = 'mw-plusminus-null';
- }
- if ( $szdiff > 0 ) {
- $formattedSize = '+' . $formattedSize;
- $formattedSizeClass = 'mw-plusminus-pos';
- }
- if ( $szdiff < 0 ) {
- $formattedSizeClass = 'mw-plusminus-neg';
- }
-
- $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
-
- return Html::element( $tag,
- array( 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ),
- $context->msg( 'parentheses', $formattedSize )->plain() ) . $lang->getDirMark();
- }
-
- /**
- * Format the character difference of one or several changes.
- *
- * @param $old RecentChange
- * @param $new RecentChange last change to use, if not provided, $old will be used
- * @return string HTML fragment
- */
- public function formatCharacterDifference( RecentChange $old, RecentChange $new = null ) {
- $oldlen = $old->mAttribs['rc_old_len'];
-
- if ( $new ) {
- $newlen = $new->mAttribs['rc_new_len'];
- } else {
- $newlen = $old->mAttribs['rc_new_len'];
- }
-
- if( $oldlen === null || $newlen === null ) {
- return '';
- }
-
- return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() );
- }
-
- /**
- * Returns text for the end of RC
- * @return String
- */
- public function endRecentChangesList() {
- if( $this->rclistOpen ) {
- return "</ul>\n";
- } else {
- return '';
- }
- }
-
- public function insertDateHeader( &$s, $rc_timestamp ) {
- # Make date header if necessary
- $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
- if( $date != $this->lastdate ) {
- if( $this->lastdate != '' ) {
- $s .= "</ul>\n";
- }
- $s .= Xml::element( 'h4', null, $date ) . "\n<ul class=\"special\">";
- $this->lastdate = $date;
- $this->rclistOpen = true;
- }
- }
-
- public function insertLog( &$s, $title, $logtype ) {
- $page = new LogPage( $logtype );
- $logname = $page->getName()->escaped();
- $s .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $title, $logname ) )->escaped();
- }
-
- /**
- * @param $s
- * @param $rc RecentChange
- * @param $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'];
- } elseif ( !self::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) {
- $diffLink = $this->message['diff'];
- } else {
- $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 = Linker::linkKnown(
- $rc->getTitle(),
- $this->message['diff'],
- array( 'tabindex' => $rc->counter ),
- $query
- );
- }
- $diffhist = $diffLink . $this->message['pipe-separator'];
- # History link
- $diffhist .= Linker::linkKnown(
- $rc->getTitle(),
- $this->message['hist'],
- array(),
- array(
- 'curid' => $rc->mAttribs['rc_cur_id'],
- 'action' => 'history'
- )
- );
- $s .= $this->msg( 'parentheses' )->rawParams( $diffhist )->escaped() . ' <span class="mw-changeslist-separator">. .</span> ';
- }
-
- /**
- * @param $s
- * @param $rc RecentChange
- * @param $unpatrolled
- * @param $watched
- */
- public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) {
- # 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 = array();
-
- if ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) {
- $params['rcid'] = $rc->mAttribs['rc_id'];
- }
-
- $articlelink = Linker::linkKnown(
- $rc->getTitle(),
- null,
- array( 'class' => 'mw-changeslist-title' ),
- $params
- );
- if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
- $articlelink = '<span class="history-deleted">' . $articlelink . '</span>';
- }
- # To allow for boldening pages watched by this user
- $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
- # RTL/LTR marker
- $articlelink .= $this->getLanguage()->getDirMark();
-
- wfRunHooks( 'ChangesListInsertArticleLink',
- array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) );
-
- $s .= " $articlelink";
- }
-
- /**
- * @param $s
- * @param $rc RecentChange
- */
- public function insertTimestamp( &$s, $rc ) {
- $s .= $this->message['semicolon-separator'] . '<span class="mw-changeslist-date">' .
- $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() ) . '</span> <span class="mw-changeslist-separator">. .</span> ';
- }
-
- /**
- * Insert links to user page, user talk page and eventually a blocking link
- *
- * @param &$s String HTML to update
- * @param &$rc RecentChange
- */
- public function insertUserRelatedLinks( &$s, &$rc ) {
- if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
- $s .= ' <span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
- } else {
- $s .= $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs['rc_user'],
- $rc->mAttribs['rc_user_text'] );
- $s .= Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
- }
- }
-
- /**
- * Insert a formatted action
- *
- * @param $rc RecentChange
- * @return string
- */
- public function insertLogEntry( $rc ) {
- $formatter = LogFormatter::newFromRow( $rc->mAttribs );
- $formatter->setContext( $this->getContext() );
- $formatter->setShowUserToolLinks( true );
- $mark = $this->getLanguage()->getDirMark();
- return $formatter->getActionText() . " $mark" . $formatter->getComment();
- }
-
- /**
- * Insert a formatted comment
- * @param $rc RecentChange
- * @return string
- */
- public function insertComment( $rc ) {
- if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
- if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) {
- return ' <span class="history-deleted">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
- } else {
- return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
- }
- }
- }
-
- /**
- * Check whether to enable recent changes patrol features
- * @return Boolean
- */
- public static function usePatrol() {
- global $wgUser;
- return $wgUser->useRCPatrol();
- }
-
- /**
- * Returns the string which indicates the number of watching users
- * @return string
- */
- protected function numberofWatchingusers( $count ) {
- static $cache = array();
- if( $count > 0 ) {
- if( !isset( $cache[$count] ) ) {
- $cache[$count] = $this->msg( 'number_of_watching_users_RCview' )->numParams( $count )->escaped();
- }
- return $cache[$count];
- } else {
- return '';
- }
- }
-
- /**
- * Determine if said field of a revision is hidden
- * @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;
- }
-
- /**
- * Determine if the current user is allowed to view a particular
- * field of this revision, if it's marked as deleted.
- * @param $rc RCCacheEntry
- * @param $field Integer
- * @param $user User object to check, or null to use $wgUser
- * @return Boolean
- */
- public static function userCan( $rc, $field, User $user = null ) {
- if( $rc->mAttribs['rc_type'] == RC_LOG ) {
- return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $user );
- } else {
- return Revision::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $user );
- }
- }
-
- /**
- * @param $link string
- * @param $watched bool
- * @return string
- */
- protected function maybeWatchedLink( $link, $watched = false ) {
- if( $watched ) {
- return '<strong class="mw-watched">' . $link . '</strong>';
- } else {
- return '<span class="mw-rc-unwatched">' . $link . '</span>';
- }
- }
-
- /** Inserts a rollback link
- *
- * @param $s string
- * @param $rc RecentChange
- */
- public function insertRollback( &$s, &$rc ) {
- if( $rc->mAttribs['rc_type'] != RC_NEW && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) {
- $page = $rc->getTitle();
- /** Check for rollback and edit permissions, disallow special pages, and only
- * show a link on the top-most revision */
- if ( $this->getUser()->isAllowed('rollback') && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] )
- {
- $rev = new Revision( array(
- 'id' => $rc->mAttribs['rc_this_oldid'],
- 'user' => $rc->mAttribs['rc_user'],
- 'user_text' => $rc->mAttribs['rc_user_text'],
- 'deleted' => $rc->mAttribs['rc_deleted']
- ) );
- $rev->setTitle( $page );
- $s .= ' '.Linker::generateRollback( $rev, $this->getContext() );
- }
- }
- }
-
- /**
- * @param $s string
- * @param $rc RecentChange
- * @param $classes
- */
- public function insertTags( &$s, &$rc, &$classes ) {
- if ( empty($rc->mAttribs['ts_tags']) )
- return;
-
- list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' );
- $classes = array_merge( $classes, $newClasses );
- $s .= ' ' . $tagSummary;
- }
-
- public function insertExtra( &$s, &$rc, &$classes ) {
- ## Empty, used for subclassers to add anything special.
- }
-
- protected function showAsUnpatrolled( RecentChange $rc ) {
- $unpatrolled = false;
- if ( !$rc->mAttribs['rc_patrolled'] ) {
- if ( $this->getUser()->useRCPatrol() ) {
- $unpatrolled = true;
- } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_type'] == RC_NEW ) {
- $unpatrolled = true;
- }
- }
- return $unpatrolled;
- }
-}
-
-
-/**
- * Generate a list of changes using the good old system (no javascript)
- */
-class OldChangesList extends ChangesList {
- /**
- * Format a line using the old system (aka without any javascript).
- *
- * @param $rc RecentChange, passed by reference
- * @param $watched Bool (default false)
- * @param $linenumber Int (default null)
- * @return string
- */
- public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
- global $wgRCShowChangedSize;
- wfProfileIn( __METHOD__ );
-
- # Should patrol-related stuff be shown?
- $unpatrolled = $this->showAsUnpatrolled( $rc );
-
- $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience.
- $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] );
-
- $s = '';
- $classes = array();
- // use mw-line-even/mw-line-odd class only if linenumber is given (feature from bug 14468)
- if( $linenumber ) {
- if( $linenumber & 1 ) {
- $classes[] = 'mw-line-odd';
- }
- else {
- $classes[] = 'mw-line-even';
- }
- }
-
- // Indicate watched status on the line to allow for more
- // comprehensive styling.
- $classes[] = $watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched';
-
- // Moved pages (very very old, not supported anymore)
- if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) {
- // Log entries
- } elseif( $rc->mAttribs['rc_log_type'] ) {
- $logtitle = SpecialPage::getTitleFor( 'Log', $rc->mAttribs['rc_log_type'] );
- $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] );
- // Log entries (old format) or log targets, and special pages
- } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) {
- list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] );
- if( $name == 'Log' ) {
- $this->insertLog( $s, $rc->getTitle(), $subpage );
- }
- // Regular entries
- } else {
- $this->insertDiffHist( $s, $rc, $unpatrolled );
- # M, N, b and ! (minor, new, bot and unpatrolled)
- $s .= $this->recentChangesFlags(
- array(
- 'newpage' => $rc->mAttribs['rc_type'] == RC_NEW,
- 'minor' => $rc->mAttribs['rc_minor'],
- 'unpatrolled' => $unpatrolled,
- 'bot' => $rc->mAttribs['rc_bot']
- ),
- ''
- );
- $this->insertArticleLink( $s, $rc, $unpatrolled, $watched );
- }
- # Edit/log timestamp
- $this->insertTimestamp( $s, $rc );
- # Bytes added or removed
- if ( $wgRCShowChangedSize ) {
- $cd = $this->formatCharacterDifference( $rc );
- if ( $cd !== '' ) {
- $s .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
- }
- }
-
- if ( $rc->mAttribs['rc_type'] == RC_LOG ) {
- $s .= $this->insertLogEntry( $rc );
- } else {
- # User tool links
- $this->insertUserRelatedLinks( $s, $rc );
- # LTR/RTL direction mark
- $s .= $this->getLanguage()->getDirMark();
- $s .= $this->insertComment( $rc );
- }
-
- # Tags
- $this->insertTags( $s, $rc, $classes );
- # Rollback
- $this->insertRollback( $s, $rc );
- # For subclasses
- $this->insertExtra( $s, $rc, $classes );
-
- # How many users watch this page
- if( $rc->numberofWatchingusers > 0 ) {
- $s .= ' ' . $this->numberofWatchingusers( $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";
- }
-}
-
-
-/**
- * Generate a list of changes using an Enhanced system (uses javascript).
- */
-class EnhancedChangesList extends ChangesList {
-
- protected $rc_cache;
-
- /**
- * Add the JavaScript file for enhanced changeslist
- * @return String
- */
- public function beginRecentChangesList() {
- $this->rc_cache = array();
- $this->rcMoveIndex = 0;
- $this->rcCacheIndex = 0;
- $this->lastdate = '';
- $this->rclistOpen = false;
- $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' );
- return '';
- }
- /**
- * Format a line for enhanced recentchange (aka with javascript and block of lines).
- *
- * @param $baseRC RecentChange
- * @param $watched bool
- *
- * @return string
- */
- public function recentChangesLine( &$baseRC, $watched = false ) {
- wfProfileIn( __METHOD__ );
-
- # Create a specialised object
- $rc = RCCacheEntry::newFromParent( $baseRC );
-
- $curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] );
-
- # If it's a new day, add the headline and flush the cache
- $date = $this->getLanguage()->userDate( $rc->mAttribs['rc_timestamp'], $this->getUser() );
- $ret = '';
- if( $date != $this->lastdate ) {
- # Process current cache
- $ret = $this->recentChangesBlock();
- $this->rc_cache = array();
- $ret .= Xml::element( 'h4', null, $date ) . "\n";
- $this->lastdate = $date;
- }
-
- # Should patrol-related stuff be shown?
- $rc->unpatrolled = $this->showAsUnpatrolled( $rc );
-
- $showdifflinks = true;
- # Make article link
- $type = $rc->mAttribs['rc_type'];
- $logType = $rc->mAttribs['rc_log_type'];
- // Page moves, very old style, not supported anymore
- if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
- // New unpatrolled pages
- } elseif( $rc->unpatrolled && $type == RC_NEW ) {
- $clink = Linker::linkKnown( $rc->getTitle(), null, array(),
- array( 'rcid' => $rc->mAttribs['rc_id'] ) );
- // Log entries
- } elseif( $type == RC_LOG ) {
- if( $logType ) {
- $logtitle = SpecialPage::getTitleFor( 'Log', $logType );
- $logpage = new LogPage( $logType );
- $logname = $logpage->getName()->escaped();
- $clink = $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logtitle, $logname ) )->escaped();
- } else {
- $clink = Linker::link( $rc->getTitle() );
- }
- $watched = false;
- // Log entries (old format) and special pages
- } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) {
- wfDebug( "Unexpected special page in recentchanges\n" );
- $clink = '';
- // Edits
- } else {
- $clink = Linker::linkKnown( $rc->getTitle() );
- }
-
- # Don't show unusable diff links
- if ( !ChangesList::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) {
- $showdifflinks = false;
- }
-
- $time = $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() );
- $rc->watched = $watched;
- $rc->link = $clink;
- $rc->timestamp = $time;
- $rc->numberofWatchingusers = $baseRC->numberofWatchingusers;
-
- # Make "cur" and "diff" links. Do not use link(), it is too slow if
- # called too many times (50% of CPU time on RecentChanges!).
- $thisOldid = $rc->mAttribs['rc_this_oldid'];
- $lastOldid = $rc->mAttribs['rc_last_oldid'];
- if( $rc->unpatrolled ) {
- $rcIdQuery = array( 'rcid' => $rc->mAttribs['rc_id'] );
- } else {
- $rcIdQuery = array();
- }
- $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $thisOldid );
- $querydiff = $curIdEq + array( 'diff' => $thisOldid, 'oldid' =>
- $lastOldid ) + $rcIdQuery;
-
- if( !$showdifflinks ) {
- $curLink = $this->message['cur'];
- $diffLink = $this->message['diff'];
- } elseif( in_array( $type, array( RC_NEW, RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) {
- if ( $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 {
- $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 || !$lastOldid ) {
- $lastLink = $this->message['last'];
- } elseif( in_array( $type, array( RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) {
- $lastLink = $this->message['last'];
- } else {
- $lastLink = Linker::linkKnown( $rc->getTitle(), $this->message['last'],
- array(), $curIdEq + array('diff' => $thisOldid, 'oldid' => $lastOldid) + $rcIdQuery );
- }
-
- # Make user links
- if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
- $rc->userlink = ' <span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
- } else {
- $rc->userlink = Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
- $rc->usertalklink = Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
- }
-
- $rc->lastlink = $lastLink;
- $rc->curlink = $curLink;
- $rc->difflink = $diffLink;
-
- # Put accumulated information into the cache, for later display
- # Page moves go on their own line
- $title = $rc->getTitle();
- $secureName = $title->getPrefixedDBkey();
- if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
- # Use an @ character to prevent collision with page names
- $this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc);
- } else {
- # Logs are grouped by type
- if( $type == RC_LOG ){
- $secureName = SpecialPage::getTitleFor( 'Log', $logType )->getPrefixedDBkey();
- }
- if( !isset( $this->rc_cache[$secureName] ) ) {
- $this->rc_cache[$secureName] = array();
- }
-
- array_push( $this->rc_cache[$secureName], $rc );
- }
-
- wfProfileOut( __METHOD__ );
-
- return $ret;
- }
-
- /**
- * Enhanced RC group
- * @return string
- */
- protected function recentChangesBlockGroup( $block ) {
- global $wgRCShowChangedSize;
-
- wfProfileIn( __METHOD__ );
-
- # Add the namespace and title of the block as part of the class
- $classes = array( 'mw-collapsible', 'mw-collapsed', 'mw-enhanced-rc' );
- if ( $block[0]->mAttribs['rc_log_type'] ) {
- # Log entry
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-'
- . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] );
- } else {
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns'
- . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] );
- }
- $classes[] = $block[0]->watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched';
- $r = Html::openElement( 'table', array( 'class' => $classes ) ) .
- Html::openElement( 'tr' );
-
- # Collate list of users
- $userlinks = array();
- # Other properties
- $unpatrolled = false;
- $isnew = false;
- $curId = $currentRevision = 0;
- # Some catalyst variables...
- $namehidden = true;
- $allLogs = true;
- foreach( $block as $rcObj ) {
- $oldid = $rcObj->mAttribs['rc_last_oldid'];
- if( $rcObj->mAttribs['rc_type'] == RC_NEW ) {
- $isnew = true;
- }
- // If all log actions to this page were hidden, then don't
- // give the name of the affected page for this block!
- if( !$this->isDeleted( $rcObj, LogPage::DELETED_ACTION ) ) {
- $namehidden = false;
- }
- $u = $rcObj->userlink;
- if( !isset( $userlinks[$u] ) ) {
- $userlinks[$u] = 0;
- }
- if( $rcObj->unpatrolled ) {
- $unpatrolled = true;
- }
- if( $rcObj->mAttribs['rc_type'] != RC_LOG ) {
- $allLogs = false;
- }
- # Get the latest entry with a page_id and oldid
- # since logs may not have these.
- if( !$curId && $rcObj->mAttribs['rc_cur_id'] ) {
- $curId = $rcObj->mAttribs['rc_cur_id'];
- }
- if( !$currentRevision && $rcObj->mAttribs['rc_this_oldid'] ) {
- $currentRevision = $rcObj->mAttribs['rc_this_oldid'];
- }
-
- $bot = $rcObj->mAttribs['rc_bot'];
- $userlinks[$u]++;
- }
-
- # Sort the list and convert to text
- krsort( $userlinks );
- asort( $userlinks );
- $users = array();
- foreach( $userlinks as $userlink => $count) {
- $text = $userlink;
- $text .= $this->getLanguage()->getDirMark();
- if( $count > 1 ) {
- $text .= ' ' . $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->formatNum( $count ) . '×' )->escaped();
- }
- array_push( $users, $text );
- }
-
- $users = ' <span class="changedby">'
- . $this->msg( 'brackets' )->rawParams(
- implode( $this->message['semicolon-separator'], $users )
- )->escaped() . '</span>';
-
- $tl = '<span class="mw-collapsible-toggle mw-enhancedchanges-arrow"></span>';
- $r .= "<td>$tl</td>";
-
- # Main line
- $r .= '<td class="mw-enhanced-rc">' . $this->recentChangesFlags( array(
- 'newpage' => $isnew,
- 'minor' => false,
- 'unpatrolled' => $unpatrolled,
- 'bot' => $bot ,
- ) );
-
- # Timestamp
- $r .= '&#160;'.$block[0]->timestamp.'&#160;</td><td>';
-
- # Article link
- if( $namehidden ) {
- $r .= ' <span class="history-deleted">' . $this->msg( 'rev-deleted-event' )->escaped() . '</span>';
- } elseif( $allLogs ) {
- $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
- } else {
- $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched );
- }
-
- $r .= $this->getLanguage()->getDirMark();
-
- $queryParams['curid'] = $curId;
- # Changes message
- $n = count($block);
- static $nchanges = array();
- if ( !isset( $nchanges[$n] ) ) {
- $nchanges[$n] = $this->msg( 'nchanges' )->numParams( $n )->escaped();
- }
- # Total change link
- $r .= ' ';
- $logtext = '';
- if( !$allLogs ) {
- if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) {
- $logtext .= $nchanges[$n];
- } elseif( $isnew ) {
- $logtext .= $nchanges[$n];
- } else {
- $params = $queryParams;
- $params['diff'] = $currentRevision;
- $params['oldid'] = $oldid;
-
- $logtext .= Linker::link(
- $block[0]->getTitle(),
- $nchanges[$n],
- array(),
- $params,
- array( 'known', 'noclasses' )
- );
- }
- }
-
- # History
- if( $allLogs ) {
- // don't show history link for logs
- } elseif( $namehidden || !$block[0]->getTitle()->exists() ) {
- $logtext .= $this->message['pipe-separator'] . $this->message['hist'];
- } else {
- $params = $queryParams;
- $params['action'] = 'history';
-
- $logtext .= $this->message['pipe-separator'] .
- Linker::linkKnown(
- $block[0]->getTitle(),
- $this->message['hist'],
- array(),
- $params
- );
- }
-
- if( $logtext !== '' ) {
- $r .= $this->msg( 'parentheses' )->rawParams( $logtext )->escaped();
- }
-
- $r .= ' <span class="mw-changeslist-separator">. .</span> ';
-
- # Character difference (does not apply if only log items)
- if( $wgRCShowChangedSize && !$allLogs ) {
- $last = 0;
- $first = count($block) - 1;
- # Some events (like logs) have an "empty" size, so we need to skip those...
- while( $last < $first && $block[$last]->mAttribs['rc_new_len'] === null ) {
- $last++;
- }
- while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === null ) {
- $first--;
- }
- # Get net change
- $chardiff = $this->formatCharacterDifference( $block[$first], $block[$last] );
-
- if( $chardiff == '' ) {
- $r .= ' ';
- } else {
- $r .= ' ' . $chardiff. ' <span class="mw-changeslist-separator">. .</span> ';
- }
- }
-
- $r .= $users;
- $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers);
-
- # Sub-entries
- foreach( $block as $rcObj ) {
- # Classes to apply -- TODO implement
- $classes = array();
- $type = $rcObj->mAttribs['rc_type'];
-
- $r .= '<tr><td></td><td class="mw-enhanced-rc">';
- $r .= $this->recentChangesFlags( array(
- 'newpage' => $type == RC_NEW,
- 'minor' => $rcObj->mAttribs['rc_minor'],
- 'unpatrolled' => $rcObj->unpatrolled,
- 'bot' => $rcObj->mAttribs['rc_bot'],
- ) );
- $r .= '&#160;</td><td class="mw-enhanced-rc-nested"><span class="mw-enhanced-rc-time">';
-
- $params = $queryParams;
-
- if( $rcObj->mAttribs['rc_this_oldid'] != 0 ) {
- $params['oldid'] = $rcObj->mAttribs['rc_this_oldid'];
- }
-
- # Log timestamp
- if( $type == RC_LOG ) {
- $link = $rcObj->timestamp;
- # Revision link
- } elseif( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) {
- $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> ';
- } else {
- if ( $rcObj->unpatrolled && $type == RC_NEW) {
- $params['rcid'] = $rcObj->mAttribs['rc_id'];
- }
-
- $link = Linker::linkKnown(
- $rcObj->getTitle(),
- $rcObj->timestamp,
- array(),
- $params
- );
- if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
- $link = '<span class="history-deleted">'.$link.'</span> ';
- }
- $r .= $link . '</span>';
-
- if ( !$type == RC_LOG || $type == RC_NEW ) {
- $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->curlink . $this->message['pipe-separator'] . $rcObj->lastlink )->escaped();
- }
- $r .= ' <span class="mw-changeslist-separator">. .</span> ';
-
- # Character diff
- if ( $wgRCShowChangedSize ) {
- $cd = $this->formatCharacterDifference( $rcObj );
- if ( $cd !== '' ) {
- $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
- }
- }
-
- if ( $rcObj->mAttribs['rc_type'] == RC_LOG ) {
- $r .= $this->insertLogEntry( $rcObj );
- } else {
- # User links
- $r .= $rcObj->userlink;
- $r .= $rcObj->usertalklink;
- $r .= $this->insertComment( $rcObj );
- }
-
- # Rollback
- $this->insertRollback( $r, $rcObj );
- # Tags
- $this->insertTags( $r, $rcObj, $classes );
-
- $r .= "</td></tr>\n";
- }
- $r .= "</table>\n";
-
- $this->rcCacheIndex++;
-
- wfProfileOut( __METHOD__ );
-
- return $r;
- }
-
- /**
- * Generate HTML for an arrow or placeholder graphic
- * @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;
- $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' );
- $encAlt = htmlspecialchars( $alt );
- $encTitle = htmlspecialchars( $title );
- return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" title=\"$encTitle\" />";
- }
-
- /**
- * Generate HTML for a right- or left-facing arrow,
- * depending on language direction.
- * @return String: HTML "<img>" tag
- */
- protected function sideArrow() {
- $dir = $this->getLanguage()->isRTL() ? 'l' : 'r';
- return $this->arrow( $dir, '+', $this->msg( 'rc-enhanced-expand' )->text() );
- }
-
- /**
- * Generate HTML for a down-facing arrow
- * depending on language direction.
- * @return String: HTML "<img>" tag
- */
- protected function downArrow() {
- return $this->arrow( 'd', '-', $this->msg( 'rc-enhanced-hide' )->text() );
- }
-
- /**
- * Generate HTML for a spacer image
- * @return String: HTML "<img>" tag
- */
- protected function spacerArrow() {
- return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space
- }
-
- /**
- * Enhanced RC ungrouped line.
- *
- * @param $rcObj RecentChange
- * @return String: a HTML formatted line (generated using $r)
- */
- protected function recentChangesBlockLine( $rcObj ) {
- global $wgRCShowChangedSize;
-
- wfProfileIn( __METHOD__ );
- $query['curid'] = $rcObj->mAttribs['rc_cur_id'];
-
- $type = $rcObj->mAttribs['rc_type'];
- $logType = $rcObj->mAttribs['rc_log_type'];
- $classes = array( 'mw-enhanced-rc' );
- if( $logType ) {
- # Log entry
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-'
- . $logType . '-' . $rcObj->mAttribs['rc_title'] );
- } else {
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' .
- $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] );
- }
- $classes[] = $rcObj->watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched';
- $r = Html::openElement( 'table', array( 'class' => $classes ) ) .
- Html::openElement( 'tr' );
-
- $r .= '<td class="mw-enhanced-rc"><span class="mw-enhancedchanges-arrow mw-enhancedchanges-arrow-space"></span>';
- # Flag and Timestamp
- if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
- $r .= '&#160;&#160;&#160;&#160;'; // 4 flags -> 4 spaces
- } else {
- $r .= $this->recentChangesFlags( array(
- 'newpage' => $type == RC_NEW,
- 'minor' => $rcObj->mAttribs['rc_minor'],
- 'unpatrolled' => $rcObj->unpatrolled,
- 'bot' => $rcObj->mAttribs['rc_bot'],
- ) );
- }
- $r .= '&#160;'.$rcObj->timestamp.'&#160;</td><td>';
- # Article or log link
- if( $logType ) {
- $logPage = new LogPage( $logType );
- $logTitle = SpecialPage::getTitleFor( 'Log', $logType );
- $logName = $logPage->getName()->escaped();
- $r .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logTitle, $logName ) )->escaped();
- } else {
- $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched );
- }
- # Diff and hist links
- if ( $type != RC_LOG ) {
- $query['action'] = 'history';
- $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->difflink . $this->message['pipe-separator'] . Linker::linkKnown(
- $rcObj->getTitle(),
- $this->message['hist'],
- array(),
- $query
- ) )->escaped();
- }
- $r .= ' <span class="mw-changeslist-separator">. .</span> ';
- # Character diff
- if ( $wgRCShowChangedSize ) {
- $cd = $this->formatCharacterDifference( $rcObj );
- if ( $cd !== '' ) {
- $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
- }
- }
-
- if ( $type == RC_LOG ) {
- $r .= $this->insertLogEntry( $rcObj );
- } else {
- $r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
- $r .= $this->insertComment( $rcObj );
- $this->insertRollback( $r, $rcObj );
- }
-
- # Tags
- $this->insertTags( $r, $rcObj, $classes );
- # Show how many people are watching this if enabled
- $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
-
- $r .= "</td></tr></table>\n";
-
- wfProfileOut( __METHOD__ );
-
- return $r;
- }
-
- /**
- * If enhanced RC is in use, this function takes the previously cached
- * RC lines, arranges them, and outputs the HTML
- *
- * @return string
- */
- protected function recentChangesBlock() {
- if( count ( $this->rc_cache ) == 0 ) {
- return '';
- }
-
- wfProfileIn( __METHOD__ );
-
- $blockOut = '';
- foreach( $this->rc_cache as $block ) {
- if( count( $block ) < 2 ) {
- $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) );
- } else {
- $blockOut .= $this->recentChangesBlockGroup( $block );
- }
- }
-
- wfProfileOut( __METHOD__ );
-
- return '<div>'.$blockOut.'</div>';
- }
-
- /**
- * Returns text for the end of RC
- * If enhanced RC is in use, returns pretty much all the text
- * @return string
- */
- public function endRecentChangesList() {
- return $this->recentChangesBlock() . parent::endRecentChangesList();
- }
-
-}
diff --git a/includes/Collation.php b/includes/Collation.php
index ad2b94b1..b0252c70 100644
--- a/includes/Collation.php
+++ b/includes/Collation.php
@@ -40,7 +40,7 @@ abstract class Collation {
* @return Collation
*/
static function factory( $collationName ) {
- switch( $collationName ) {
+ switch ( $collationName ) {
case 'uppercase':
return new UppercaseCollation;
case 'identity':
@@ -48,8 +48,12 @@ abstract class Collation {
case 'uca-default':
return new IcuCollation( 'root' );
default:
- # Provide a mechanism for extensions to hook in.
+ $match = array();
+ if ( preg_match( '/^uca-([a-z@=-]+)$/', $collationName, $match ) ) {
+ return new IcuCollation( $match[1] );
+ }
+ # Provide a mechanism for extensions to hook in.
$collationObject = null;
wfRunHooks( 'Collation::factory', array( $collationName, &$collationObject ) );
@@ -58,7 +62,7 @@ abstract class Collation {
}
// If all else fails...
- throw new MWException( __METHOD__.": unknown collation type \"$collationName\"" );
+ throw new MWException( __METHOD__ . ": unknown collation type \"$collationName\"" );
}
}
@@ -144,18 +148,19 @@ class IdentityCollation extends Collation {
}
}
-
class IcuCollation extends Collation {
+ const FIRST_LETTER_VERSION = 1;
+
var $primaryCollator, $mainCollator, $locale;
var $firstLetterData;
/**
* Unified CJK blocks.
*
- * The same definition of a CJK block must be used for both Collation and
- * generateCollationData.php. These blocks are omitted from the first
- * letter data, as an optimisation measure and because the default UCA table
- * is pretty useless for sorting Chinese text anyway. Japanese and Korean
+ * The same definition of a CJK block must be used for both Collation and
+ * generateCollationData.php. These blocks are omitted from the first
+ * letter data, as an optimisation measure and because the default UCA table
+ * is pretty useless for sorting Chinese text anyway. Japanese and Korean
* blocks are not included here, because they are smaller and more useful.
*/
static $cjkBlocks = array(
@@ -176,11 +181,107 @@ class IcuCollation extends Collation {
array( 0x2F800, 0x2FA1F ), // CJK Compatibility Ideographs Supplement
);
+ /**
+ * Additional characters (or character groups) to be considered separate
+ * letters for given languages, or to be removed from the list of such
+ * letters (denoted by keys starting with '-').
+ *
+ * These are additions to (or subtractions from) the data stored in the
+ * first-letters-root.ser file (which among others includes full basic latin,
+ * cyrillic and greek alphabets).
+ *
+ * "Separate letter" is a letter that would have a separate heading/section
+ * for it in a dictionary or a phone book in this language. This data isn't
+ * used for sorting (the ICU library handles that), only for deciding which
+ * characters (or character groups) to use as headings.
+ *
+ * Initially generated based on the primary level of Unicode collation
+ * tailorings available at http://developer.mimer.com/charts/tailorings.htm ,
+ * later modified.
+ *
+ * Empty arrays are intended; this signifies that the data for the language is
+ * available and that there are, in fact, no additional letters to consider.
+ */
+ static $tailoringFirstLetters = array(
+ // Verified by native speakers
+ 'be' => array( "Ё" ),
+ 'be-tarask' => array( "Ё" ),
+ 'en' => array(),
+ 'fi' => array( "Å", "Ä", "Ö" ),
+ 'hu' => array( "Cs", "Dz", "Dzs", "Gy", "Ly", "Ny", "Ö", "Sz", "Ty", "Ü", "Zs" ),
+ 'it' => array(),
+ 'pl' => array( "Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ź", "Ż" ),
+ 'pt' => array(),
+ 'ru' => array(),
+ 'sv' => array( "Å", "Ä", "Ö" ),
+ 'sv@collation=standard' => array( "Å", "Ä", "Ö" ),
+ 'uk' => array( "Ґ", "Ь" ),
+ 'vi' => array( "Ă", "Â", "Đ", "Ê", "Ô", "Ơ", "Ư" ),
+ // Not verified, but likely correct
+ 'af' => array(),
+ 'ast' => array( "Ch", "Ll", "Ñ" ),
+ 'az' => array( "Ç", "Ə", "Ğ", "İ", "Ö", "Ş", "Ü" ),
+ 'bg' => array(),
+ 'br' => array( "Ch", "C'h" ),
+ 'bs' => array( "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ),
+ 'ca' => array(),
+ 'co' => array(),
+ 'cs' => array( "Č", "Ch", "Ř", "Š", "Ž" ),
+ 'cy' => array( "Ch", "Dd", "Ff", "Ng", "Ll", "Ph", "Rh", "Th" ),
+ 'da' => array( "Æ", "Ø", "Å" ),
+ 'de' => array(),
+ 'dsb' => array( "Č", "Ć", "Dź", "Ě", "Ch", "Ł", "Ń", "Ŕ", "Š", "Ś", "Ž", "Ź" ),
+ 'el' => array(),
+ 'eo' => array( "Ĉ", "Ĝ", "Ĥ", "Ĵ", "Ŝ", "Ŭ" ),
+ 'es' => array( "Ñ" ),
+ 'et' => array( "Š", "Ž", "Õ", "Ä", "Ö", "Ü" ),
+ 'eu' => array( "Ñ" ),
+ 'fa' => array( "آ", "ء", "ه" ),
+ 'fo' => array( "Á", "Ð", "Í", "Ó", "Ú", "Ý", "Æ", "Ø", "Å" ),
+ 'fr' => array(),
+ 'fur' => array( "À", "Á", "Â", "È", "Ì", "Ò", "Ù" ),
+ 'fy' => array(),
+ 'ga' => array(),
+ 'gd' => array(),
+ 'gl' => array( "Ch", "Ll", "Ñ" ),
+ 'hr' => array( "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ),
+ 'hsb' => array( "Č", "Dź", "Ě", "Ch", "Ł", "Ń", "Ř", "Š", "Ć", "Ž" ),
+ 'is' => array( "Á", "Ð", "É", "Í", "Ó", "Ú", "Ý", "Þ", "Æ", "Ö", "Å" ),
+ 'kk' => array( "Ү", "І" ),
+ 'kl' => array( "Æ", "Ø", "Å" ),
+ 'ku' => array( "Ç", "Ê", "Î", "Ş", "Û" ),
+ 'ky' => array( "Ё" ),
+ 'la' => array(),
+ 'lb' => array(),
+ 'lt' => array( "Č", "Š", "Ž" ),
+ 'lv' => array( "Č", "Ģ", "Ķ", "Ļ", "Ņ", "Š", "Ž" ),
+ 'mk' => array(),
+ 'mo' => array( "Ă", "Â", "Î", "Ş", "Ţ" ),
+ 'mt' => array( "Ċ", "Ġ", "Għ", "Ħ", "Ż" ),
+ 'nl' => array(),
+ 'no' => array( "Æ", "Ø", "Å" ),
+ 'oc' => array(),
+ 'rm' => array(),
+ 'ro' => array( "Ă", "Â", "Î", "Ş", "Ţ" ),
+ 'rup' => array( "Ă", "Â", "Î", "Ľ", "Ń", "Ş", "Ţ" ),
+ 'sco' => array(),
+ 'sk' => array( "Ä", "Č", "Ch", "Ô", "Š", "Ž" ),
+ 'sl' => array( "Č", "Š", "Ž" ),
+ 'smn' => array( "Á", "Č", "Đ", "Ŋ", "Š", "Ŧ", "Ž", "Æ", "Ø", "Å", "Ä", "Ö" ),
+ 'sq' => array( "Ç", "Dh", "Ë", "Gj", "Ll", "Nj", "Rr", "Sh", "Th", "Xh", "Zh" ),
+ 'sr' => array(),
+ 'tk' => array( "Ç", "Ä", "Ž", "Ň", "Ö", "Ş", "Ü", "Ý" ),
+ 'tl' => array( "Ñ", "Ng" ),
+ 'tr' => array( "Ç", "Ğ", "İ", "Ö", "Ş", "Ü" ),
+ 'tt' => array( "Ә", "Ө", "Ү", "Җ", "Ң", "Һ" ),
+ 'uz' => array( "Ch", "G'", "Ng", "O'", "Sh" ),
+ );
+
const RECORD_LENGTH = 14;
function __construct( $locale ) {
if ( !extension_loaded( 'intl' ) ) {
- throw new MWException( 'An ICU collation was requested, ' .
+ throw new MWException( 'An ICU collation was requested, ' .
'but the intl extension is not available.' );
}
$this->locale = $locale;
@@ -218,8 +319,8 @@ class IcuCollation extends Collation {
// Check for CJK
$firstChar = mb_substr( $string, 0, 1, 'UTF-8' );
- if ( ord( $firstChar ) > 0x7f
- && self::isCjk( utf8ToCodepoint( $firstChar ) ) )
+ if ( ord( $firstChar ) > 0x7f
+ && self::isCjk( utf8ToCodepoint( $firstChar ) ) )
{
return $firstChar;
}
@@ -249,25 +350,37 @@ class IcuCollation extends Collation {
$cacheKey = wfMemcKey( 'first-letters', $this->locale );
$cacheEntry = $cache->get( $cacheKey );
- if ( $cacheEntry ) {
+ if ( $cacheEntry && isset( $cacheEntry['version'] )
+ && $cacheEntry['version'] == self::FIRST_LETTER_VERSION
+ ) {
$this->firstLetterData = $cacheEntry;
return $this->firstLetterData;
}
// Generate data from serialized data file
- $letters = wfGetPrecompiledData( "first-letters-{$this->locale}.ser" );
- if ( $letters === false ) {
- throw new MWException( "MediaWiki does not support ICU locale " .
- "\"{$this->locale}\"" );
+ if ( isset( self::$tailoringFirstLetters[$this->locale] ) ) {
+ $letters = wfGetPrecompiledData( "first-letters-root.ser" );
+ // Append additional characters
+ $letters = array_merge( $letters, self::$tailoringFirstLetters[$this->locale] );
+ // Remove unnecessary ones, if any
+ if ( isset( self::$tailoringFirstLetters['-' . $this->locale] ) ) {
+ $letters = array_diff( $letters, self::$tailoringFirstLetters['-' . $this->locale] );
+ }
+ } else {
+ $letters = wfGetPrecompiledData( "first-letters-{$this->locale}.ser" );
+ if ( $letters === false ) {
+ throw new MWException( "MediaWiki does not support ICU locale " .
+ "\"{$this->locale}\"" );
+ }
}
// Sort the letters.
//
// It's impossible to have the precompiled data file properly sorted,
- // because the sort order changes depending on ICU version. If the
- // array is not properly sorted, the binary search will return random
- // results.
+ // because the sort order changes depending on ICU version. If the
+ // array is not properly sorted, the binary search will return random
+ // results.
//
// We also take this opportunity to remove primary collisions.
$letterMap = array();
@@ -284,9 +397,76 @@ class IcuCollation extends Collation {
}
}
ksort( $letterMap, SORT_STRING );
+ // Remove duplicate prefixes. Basically if something has a sortkey
+ // which is a prefix of some other sortkey, then it is an
+ // expansion and probably should not be considered a section
+ // header.
+ //
+ // For example 'þ' is sometimes sorted as if it is the letters
+ // 'th'. Other times it is its own primary element. Another
+ // example is '₨'. Sometimes its a currency symbol. Sometimes it
+ // is an 'R' followed by an 's'.
+ //
+ // Additionally an expanded element should always sort directly
+ // after its first element due to they way sortkeys work.
+ //
+ // UCA sortkey elements are of variable length but no collation
+ // element should be a prefix of some other element, so I think
+ // this is safe. See:
+ // * https://ssl.icu-project.org/repos/icu/icuhtml/trunk/design/collation/ICU_collation_design.htm
+ // * http://site.icu-project.org/design/collation/uca-weight-allocation
+ //
+ // Additionally, there is something called primary compression to
+ // worry about. Basically, if you have two primary elements that
+ // are more than one byte and both start with the same byte then
+ // the first byte is dropped on the second primary. Additionally
+ // either \x03 or \xFF may be added to mean that the next primary
+ // does not start with the first byte of the first primary.
+ //
+ // This shouldn't matter much, as the first primary is not
+ // changed, and that is what we are comparing against.
+ //
+ // tl;dr: This makes some assumptions about how icu implements
+ // collations. It seems incredibly unlikely these assumptions
+ // will change, but nonetheless they are assumptions.
+
+ $prev = false;
+ $duplicatePrefixes = array();
+ foreach ( $letterMap as $key => $value ) {
+ // Remove terminator byte. Otherwise the prefix
+ // comparison will get hung up on that.
+ $trimmedKey = rtrim( $key, "\0" );
+ if ( $prev === false || $prev === '' ) {
+ $prev = $trimmedKey;
+ // We don't yet have a collation element
+ // to compare against, so continue.
+ continue;
+ }
+
+ // Due to the fact the array is sorted, we only have
+ // to compare with the element directly previous
+ // to the current element (skipping expansions).
+ // An element "X" will always sort directly
+ // before "XZ" (Unless we have "XY", but we
+ // do not update $prev in that case).
+ if ( substr( $trimmedKey, 0, strlen( $prev ) ) === $prev ) {
+ $duplicatePrefixes[] = $key;
+ // If this is an expansion, we don't want to
+ // compare the next element to this element,
+ // but to what is currently $prev
+ continue;
+ }
+ $prev = $trimmedKey;
+ }
+ foreach ( $duplicatePrefixes as $badKey ) {
+ wfDebug( "Removing '{$letterMap[$badKey]}' from first letters." );
+ unset( $letterMap[$badKey] );
+ // This code assumes that unsetting does not change sort order.
+ }
$data = array(
'chars' => array_values( $letterMap ),
- 'keys' => array_keys( $letterMap )
+ 'keys' => array_keys( $letterMap ),
+ 'version' => self::FIRST_LETTER_VERSION,
);
// Reduce memory usage before caching
@@ -320,23 +500,27 @@ class IcuCollation extends Collation {
}
/**
- * Do a binary search, and return the index of the largest item that sorts
+ * Do a binary search, and return the index of the largest item that sorts
* less than or equal to the target value.
*
- * @param $valueCallback array A function to call to get the value with
+ * @param array $valueCallback A function to call to get the value with
* a given array index.
- * @param $valueCount int The number of items accessible via $valueCallback,
+ * @param int $valueCount The number of items accessible via $valueCallback,
* indexed from 0 to $valueCount - 1
- * @param $comparisonCallback array A callback to compare two values, returning
+ * @param array $comparisonCallback A callback to compare two values, returning
* -1, 0 or 1 in the style of strcmp().
- * @param $target string The target value to find.
+ * @param string $target The target value to find.
*
* @return int|bool The item index of the lower bound, or false if the target value
* sorts before all items.
*/
function findLowerBound( $valueCallback, $valueCount, $comparisonCallback, $target ) {
+ if ( $valueCount === 0 ) {
+ return false;
+ }
+
$min = 0;
- $max = $valueCount - 1;
+ $max = $valueCount;
do {
$mid = $min + ( ( $max - $min ) >> 1 );
$item = call_user_func( $valueCallback, $mid );
@@ -351,12 +535,15 @@ class IcuCollation extends Collation {
}
} while ( $min < $max - 1 );
- if ( $min == 0 && $max == 0 && $comparison > 0 ) {
- // Before the first item
- return false;
- } else {
- return $min;
+ if ( $min == 0 ) {
+ $item = call_user_func( $valueCallback, $min );
+ $comparison = call_user_func( $comparisonCallback, $target, $item );
+ if ( $comparison < 0 ) {
+ // Before the first item
+ return false;
+ }
}
+ return $min;
}
static function isCjk( $codepoint ) {
@@ -367,5 +554,55 @@ class IcuCollation extends Collation {
}
return false;
}
-}
+ /**
+ * Return the version of ICU library used by PHP's intl extension,
+ * or false when the extension is not installed of the version
+ * can't be determined.
+ *
+ * The constant INTL_ICU_VERSION this function refers to isn't really
+ * documented. It is available since PHP 5.3.7 (see PHP bug 54561).
+ * This function will return false on older PHPs.
+ *
+ * @since 1.21
+ * @return string|false
+ */
+ static function getICUVersion() {
+ return defined( 'INTL_ICU_VERSION' ) ? INTL_ICU_VERSION : false;
+ }
+
+ /**
+ * Return the version of Unicode appropriate for the version of ICU library
+ * currently in use, or false when it can't be determined.
+ *
+ * @since 1.21
+ * @return string|false
+ */
+ static function getUnicodeVersionForICU() {
+ $icuVersion = IcuCollation::getICUVersion();
+ if ( !$icuVersion ) {
+ return false;
+ }
+
+ $versionPrefix = substr( $icuVersion, 0, 3 );
+ // Source: http://site.icu-project.org/download
+ $map = array(
+ '50.' => '6.2',
+ '49.' => '6.1',
+ '4.8' => '6.0',
+ '4.6' => '6.0',
+ '4.4' => '5.2',
+ '4.2' => '5.1',
+ '4.0' => '5.1',
+ '3.8' => '5.0',
+ '3.6' => '5.0',
+ '3.4' => '4.1',
+ );
+
+ if ( isset( $map[$versionPrefix] ) ) {
+ return $map[$versionPrefix];
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php
index b68fc762..67cb87db 100644
--- a/includes/ConfEditor.php
+++ b/includes/ConfEditor.php
@@ -66,7 +66,6 @@ class ConfEditor {
*/
var $stateStack;
-
/**
* The path stack is a stack of associative arrays with the following elements:
* name The name of top level of the path
@@ -128,7 +127,7 @@ class ConfEditor {
/**
* Edit the text. Returns the edited text.
- * @param $ops Array of operations.
+ * @param array $ops of operations.
*
* Operations are given as an associative array, with members:
* type: One of delete, set, append or insert (required)
@@ -159,6 +158,7 @@ class ConfEditor {
* insert
* Insert a new element at the start of the array.
*
+ * @throws MWException
* @return string
*/
public function edit( $ops ) {
@@ -278,19 +278,23 @@ class ConfEditor {
function getVars() {
$vars = array();
$this->parse();
- foreach( $this->pathInfo as $path => $data ) {
- if ( $path[0] != '$' )
+ foreach ( $this->pathInfo as $path => $data ) {
+ if ( $path[0] != '$' ) {
continue;
+ }
$trimmedPath = substr( $path, 1 );
$name = $data['name'];
- if ( $name[0] == '@' )
+ if ( $name[0] == '@' ) {
continue;
- if ( $name[0] == '$' )
+ }
+ if ( $name[0] == '$' ) {
$name = substr( $name, 1 );
+ }
$parentPath = substr( $trimmedPath, 0,
strlen( $trimmedPath ) - strlen( $name ) );
- if( substr( $parentPath, -1 ) == '/' )
+ if ( substr( $parentPath, -1 ) == '/' ) {
$parentPath = substr( $parentPath, 0, -1 );
+ }
$value = substr( $this->text, $data['valueStartByte'],
$data['valueEndByte'] - $data['valueStartByte']
@@ -306,7 +310,7 @@ class ConfEditor {
* setVar( $arr, 'foo/bar', 'baz', 3 ); will set
* $arr['foo']['bar']['baz'] = 3;
* @param $array array
- * @param $path string slash-delimited path
+ * @param string $path slash-delimited path
* @param $key mixed Key
* @param $value mixed Value
*/
@@ -315,13 +319,15 @@ class ConfEditor {
$target =& $array;
if ( $path !== '' ) {
foreach ( $pathArr as $p ) {
- if( !isset( $target[$p] ) )
+ if ( !isset( $target[$p] ) ) {
$target[$p] = array();
+ }
$target =& $target[$p];
}
}
- if ( !isset( $target[$key] ) )
+ if ( !isset( $target[$key] ) ) {
$target[$key] = $value;
+ }
}
/**
@@ -329,25 +335,30 @@ class ConfEditor {
* @return mixed Parsed value
*/
function parseScalar( $str ) {
- if ( $str !== '' && $str[0] == '\'' )
+ 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] == '"' )
+ }
+ 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' )
+ }
+ if ( substr( $str, 0, 4 ) == 'true' ) {
return true;
- if ( substr( $str, 0, 5 ) == 'false' )
+ }
+ if ( substr( $str, 0, 5 ) == 'false' ) {
return false;
- if ( substr( $str, 0, 4 ) == 'null' )
+ }
+ 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;
@@ -392,6 +403,8 @@ class ConfEditor {
* 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.
+ * @param $pathName
+ * @throws MWException
* @return array
*/
function findDeletionRegion( $pathName ) {
@@ -450,6 +463,8 @@ class ConfEditor {
* or semicolon.
*
* The end position is the past-the-end (end + 1) value as per convention.
+ * @param $pathName
+ * @throws MWException
* @return array
*/
function findValueRegion( $pathName ) {
@@ -533,7 +548,7 @@ class ConfEditor {
*/
function getIndent( $pos, $key = false, $arrowPos = false ) {
$arrowIndent = ' ';
- if ( $pos == 0 || $this->text[$pos-1] == "\n" ) {
+ if ( $pos == 0 || $this->text[$pos - 1] == "\n" ) {
$indentLength = strspn( $this->text, " \t", $pos );
$indent = substr( $this->text, $pos, $indentLength );
} else {
@@ -555,7 +570,7 @@ class ConfEditor {
public function parse() {
$this->initParse();
$this->pushState( 'file' );
- $this->pushPath( '@extra-' . ($this->serial++) );
+ $this->pushPath( '@extra-' . ( $this->serial++ ) );
$token = $this->firstToken();
while ( !$token->isEnd() ) {
@@ -613,19 +628,21 @@ class ConfEditor {
$this->expect( '=' );
$this->skipSpace();
$this->startPathValue();
- if ( $arrayAssign )
+ if ( $arrayAssign ) {
$this->pushState( 'expression', 'array assign end' );
- else
+ } else {
$this->pushState( 'expression', 'statement end' );
+ }
break;
case 'array assign end':
case 'statement end':
$this->endPathValue();
- if ( $state == 'array assign end' )
+ if ( $state == 'array assign end' ) {
$this->popPath();
+ }
$this->skipSpace();
$this->expect( ';' );
- $this->nextPath( '@extra-' . ($this->serial++) );
+ $this->nextPath( '@extra-' . ( $this->serial++ ) );
break;
case 'expression':
$token = $this->skipSpace();
@@ -645,7 +662,7 @@ class ConfEditor {
$this->skipSpace();
$this->expect( '(' );
$this->skipSpace();
- $this->pushPath( '@extra-' . ($this->serial++) );
+ $this->pushPath( '@extra-' . ( $this->serial++ ) );
if ( $this->isAhead( ')' ) ) {
// Empty array
$this->pushState( 'array end' );
@@ -677,7 +694,7 @@ class ConfEditor {
$this->endPathValue();
$this->markComma();
$this->nextToken();
- $this->nextPath( '@extra-' . ($this->serial++) );
+ $this->nextPath( '@extra-' . ( $this->serial++ ) );
// Look ahead to find ending bracket
if ( $this->isAhead( ")" ) ) {
// Found ending bracket, no continuation
@@ -1052,9 +1069,10 @@ class ConfEditorParseError extends MWException {
$lines = StringUtils::explode( "\n", $text );
foreach ( $lines as $lineNum => $line ) {
if ( $lineNum == $this->lineNum - 1 ) {
- return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n";
+ return "$line\n" . str_repeat( ' ', $this->colNum - 1 ) . "^\n";
}
}
+ return '';
}
}
@@ -1089,4 +1107,3 @@ class ConfEditorToken {
return $this->type == 'END';
}
}
-
diff --git a/includes/Cookie.php b/includes/Cookie.php
index 7984d63e..ecf4667d 100644
--- a/includes/Cookie.php
+++ b/includes/Cookie.php
@@ -40,14 +40,15 @@ class Cookie {
/**
* Sets a cookie. Used before a request to set up any individual
- * cookies. Used internally after a request to parse the
+ * cookies. Used internally after a request to parse the
* Set-Cookie headers.
*
- * @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
+ * @param string $value the value of the cookie
+ * @param array $attr possible key/values:
+ * expires A date string
+ * path The path this cookie is used on
+ * domain Domain this cookie is used on
+ * @throws MWException
*/
public function set( $value, $attr ) {
$this->value = $value;
@@ -81,10 +82,11 @@ class Cookie {
* http://publicsuffix.org/
*
* @todo fixme fails to detect 3-letter top-level domains
- * @todo fixme fails to detect 2-letter top-level domains for single-domain use (probably not a big problem in practice, but there are test cases)
+ * @todo fixme fails to detect 2-letter top-level domains for single-domain use (probably
+ * not a big problem in practice, but there are test cases)
*
- * @param $domain String: the domain to validate
- * @param $originDomain String: (optional) the domain the cookie originates from
+ * @param string $domain the domain to validate
+ * @param string $originDomain (optional) the domain the cookie originates from
* @return Boolean
*/
public static function validateCookieDomain( $domain, $originDomain = null ) {
@@ -112,7 +114,7 @@ class Cookie {
}
// Don't allow cookies for "co.uk" or "gov.uk", etc, but allow "supermarket.uk"
- if ( strrpos( $domain, "." ) - strlen( $domain ) == -3 ) {
+ 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;
@@ -141,8 +143,8 @@ class Cookie {
/**
* 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.
+ * @param string $path the path that will be used. Required.
+ * @param string $domain the domain that will be used. Required.
* @return String
*/
public function serializeToHttpRequest( $path, $domain ) {
@@ -164,8 +166,8 @@ class Cookie {
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 ),
+ && substr( $this->domain, 0, 1 ) == '.'
+ && substr_compare( $domain, $this->domain, -strlen( $this->domain ),
strlen( $this->domain ), true ) == 0 ) ) {
return true;
}
@@ -193,10 +195,10 @@ class CookieJar {
private $cookie = array();
/**
- * Set a cookie in the cookie jar. Make sure only one cookie per-name exists.
+ * Set a cookie in the cookie jar. Make sure only one cookie per-name exists.
* @see Cookie::set()
*/
- public function setCookie ( $name, $value, $attr ) {
+ 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.
*/
@@ -231,10 +233,10 @@ class CookieJar {
* Parse the content of an Set-Cookie HTTP Response header.
*
* @param $cookie String
- * @param $domain String: cookie's domain
+ * @param string $domain cookie's domain
* @return null
*/
- public function parseCookieResponseHeader ( $cookie, $domain ) {
+ public function parseCookieResponseHeader( $cookie, $domain ) {
$len = strlen( 'Set-Cookie:' );
if ( substr_compare( 'Set-Cookie:', $cookie, 0, $len, true ) === 0 ) {
diff --git a/includes/DataUpdate.php b/includes/DataUpdate.php
index 377b64c0..7b9ac281 100644
--- a/includes/DataUpdate.php
+++ b/includes/DataUpdate.php
@@ -34,7 +34,7 @@ abstract class DataUpdate implements DeferrableUpdate {
/**
* Constructor
*/
- public function __construct( ) {
+ public function __construct() {
# noop
}
@@ -67,18 +67,20 @@ abstract class DataUpdate implements DeferrableUpdate {
*
* This methods supports transactions logic by first calling beginTransaction()
* on all updates in the array, then calling doUpdate() on each, and, if all goes well,
- * then calling commitTransaction() on each update. If an error occurrs,
- * rollbackTransaction() will be called on any update object that had beginTranscation()
+ * then calling commitTransaction() on each update. If an error occurs,
+ * rollbackTransaction() will be called on any update object that had beginTransaction()
* called but not yet commitTransaction().
*
* This allows for limited transactional logic across multiple backends for storing
* secondary data.
*
- * @static
- * @param $updates array a list of DataUpdate instances
+ * @param array $updates a list of DataUpdate instances
+ * @throws Exception|null
*/
public static function runUpdates( $updates ) {
- if ( empty( $updates ) ) return; # nothing to do
+ if ( empty( $updates ) ) {
+ return; # nothing to do
+ }
$open_transactions = array();
$exception = null;
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 710605ad..f423e623 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -47,7 +47,7 @@
* This is not a valid entry point, perform no further processing unless
* MEDIAWIKI is defined
*/
-if( !defined( 'MEDIAWIKI' ) ) {
+if ( !defined( 'MEDIAWIKI' ) ) {
echo "This file is part of MediaWiki and is not a valid entry point\n";
die( 1 );
}
@@ -55,13 +55,19 @@ if( !defined( 'MEDIAWIKI' ) ) {
/**
* wgConf hold the site configuration.
* Not used for much in a default install.
+ * @since 1.5
*/
$wgConf = new SiteConfiguration;
-/** MediaWiki version number */
-$wgVersion = '1.20.6';
+/**
+ * MediaWiki version number
+ * @since 1.2
+ */
+$wgVersion = '1.22.1';
-/** Name of the site. It must be changed in LocalSettings.php */
+/**
+ * Name of the site. It must be changed in LocalSettings.php
+ */
$wgSitename = 'MediaWiki';
/**
@@ -87,6 +93,7 @@ $wgServer = WebRequest::detectServer();
* Must be fully qualified, even if $wgServer is protocol-relative.
*
* Defaults to $wgServer, expanded to a fully qualified http:// URL if needed.
+ * @since 1.18
*/
$wgCanonicalServer = false;
@@ -104,7 +111,7 @@ $wgCanonicalServer = false;
* Other paths will be set to defaults based on it unless they are directly
* set in LocalSettings.php
*/
-$wgScriptPath = '/wiki';
+$wgScriptPath = '/wiki';
/**
* Whether to support URLs like index.php/Page_title These often break when PHP
@@ -121,11 +128,11 @@ $wgScriptPath = '/wiki';
* The default $wgArticlePath will be set based on this value at runtime, but if
* you have customized it, having this incorrectly set to true can cause
* redirect loops when "pretty URLs" are used.
+ * @since 1.2.1
*/
-$wgUsePathInfo =
- ( strpos( php_sapi_name(), 'cgi' ) === false ) &&
- ( strpos( php_sapi_name(), 'apache2filter' ) === false ) &&
- ( strpos( php_sapi_name(), 'isapi' ) === false );
+$wgUsePathInfo = ( strpos( PHP_SAPI, 'cgi' ) === false ) &&
+ ( strpos( PHP_SAPI, 'apache2filter' ) === false ) &&
+ ( strpos( PHP_SAPI, 'isapi' ) === false );
/**
* The extension to append to script names by default. This can either be .php
@@ -133,9 +140,9 @@ $wgUsePathInfo =
*
* Some hosting providers use PHP 4 for *.php files, and PHP 5 for *.php5. This
* variable is provided to support those providers.
+ * @since 1.11
*/
-$wgScriptExtension = '.php';
-
+$wgScriptExtension = '.php';
/**@}*/
@@ -165,23 +172,17 @@ $wgScriptExtension = '.php';
$wgScript = false;
/**
- * The URL path to redirect.php. This is a script that is used by the Nostalgia
- * skin.
- *
- * Defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}".
- */
-$wgRedirectScript = false;
-
-/**
* The URL path to load.php.
*
* Defaults to "{$wgScriptPath}/load{$wgScriptExtension}".
+ * @since 1.17
*/
$wgLoadScript = false;
/**
* The URL path of the skins directory.
* Defaults to "{$wgScriptPath}/skins".
+ * @since 1.3
*/
$wgStylePath = false;
$wgStyleSheetPath = &$wgStylePath;
@@ -189,6 +190,7 @@ $wgStyleSheetPath = &$wgStylePath;
/**
* The URL path of the skins directory. Should not point to an external domain.
* Defaults to "{$wgScriptPath}/skins".
+ * @since 1.17
*/
$wgLocalStylePath = false;
@@ -202,6 +204,7 @@ $wgExtensionAssetsPath = false;
/**
* Filesystem stylesheets directory.
* Defaults to "{$IP}/skins".
+ * @since 1.3
*/
$wgStyleDirectory = false;
@@ -239,12 +242,14 @@ $wgLogo = false;
/**
* The URL path of the shortcut icon.
+ * @since 1.6
*/
$wgFavicon = '/favicon.ico';
/**
* The URL path of the icon for iPhone and iPod Touch web app bookmarks.
* Defaults to no icon.
+ * @since 1.12
*/
$wgAppleTouchIcon = false;
@@ -261,13 +266,13 @@ $wgAppleTouchIcon = false;
*
* @see wfTempDir()
* @note Default changed to false in MediaWiki 1.20.
- *
*/
$wgTmpDirectory = false;
/**
* If set, this URL is added to the start of $wgUploadPath to form a complete
* upload URL.
+ * @since 1.4
*/
$wgUploadBaseUrl = '';
@@ -276,6 +281,7 @@ $wgUploadBaseUrl = '';
* Full thumbnail URL will be like $wgUploadStashScalerBaseUrl/e/e6/Foo.jpg/123px-Foo.jpg
* where 'e6' are the first two characters of the MD5 hash of the file name.
* If $wgUploadStashScalerBaseUrl is set to false, thumbs are rendered locally as needed.
+ * @since 1.17
*/
$wgUploadStashScalerBaseUrl = false;
@@ -291,6 +297,7 @@ $wgUploadStashScalerBaseUrl = false;
*
* There must be an appropriate script or rewrite rule in place to handle these
* URLs.
+ * @since 1.5
*/
$wgActionPaths = array();
@@ -301,7 +308,9 @@ $wgActionPaths = array();
* @{
*/
-/** Uploads have to be specially set up to be secure */
+/**
+ * Uploads have to be specially set up to be secure
+ */
$wgEnableUploads = false;
/**
@@ -309,10 +318,22 @@ $wgEnableUploads = false;
*/
$wgUploadStashMaxAge = 6 * 3600; // 6 hours
-/** Allows to move images and other media files */
+/**
+ * Allows to move images and other media files
+ */
$wgAllowImageMoving = true;
/**
+ * Enable deferred upload tasks that use the job queue.
+ * Only enable this if job runners are set up for both the
+ * 'AssembleUploadChunks' and 'PublishStashedFile' job types.
+ *
+ * @note If you use suhosin, this setting is incompatible with
+ * suhosin.session.encrypt.
+ */
+$wgEnableAsyncUploads = false;
+
+/**
* These are additional characters that should be replaced with '-' in filenames
*/
$wgIllegalFileChars = ":";
@@ -353,7 +374,7 @@ $wgImgAuthPublicTest = true;
* FSRepo is also supported for backwards compatibility.
*
* - name A unique name for the repository (but $wgLocalFileRepo should be 'local').
- * The name should consist of alpha-numberic characters.
+ * The name should consist of alpha-numeric characters.
* - backend A file backend name (see $wgFileBackends).
*
* For most core repos:
@@ -361,7 +382,9 @@ $wgImgAuthPublicTest = true;
* container : backend container name the zone is in
* directory : root path within container for the zone
* url : base URL to the root of the zone
- * handlerUrl : base script handled URL to the root of the zone
+ * urlsByExt : map of file extension types to base URLs
+ * (useful for using a different cache for videos)
+ * handlerUrl : base script-handled URL to the root of the zone
* (see FileRepo::getZoneHandlerUrl() function)
* Zones default to using "<repo name>-<zone name>" as the container name
* and default to using the container root as the zone's root directory.
@@ -405,7 +428,7 @@ $wgImgAuthPublicTest = true;
*
* ForeignDBRepo:
* - dbType, dbServer, dbUser, dbPassword, dbName, dbFlags
- * equivalent to the corresponding member of $wgDBservers
+ * 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
*
@@ -416,7 +439,7 @@ $wgImgAuthPublicTest = true;
* If you leave $wgLocalFileRepo set to false, Setup will fill in appropriate values.
* Otherwise, set $wgLocalFileRepo to a repository structure as described above.
* If you set $wgUseInstantCommons to true, it will add an entry for Commons.
- * If you set $wgForeignFileRepos to an array of repostory structures, those will
+ * If you set $wgForeignFileRepos to an array of repository structures, those will
* be searched after the local file repo.
* Otherwise, you will only have access to local media files.
*
@@ -424,7 +447,9 @@ $wgImgAuthPublicTest = true;
*/
$wgLocalFileRepo = false;
-/** @see $wgLocalFileRepo */
+/**
+ * @see $wgLocalFileRepo
+ */
$wgForeignFileRepos = array();
/**
@@ -436,14 +461,34 @@ $wgUseInstantCommons = false;
/**
* File backend structure configuration.
+ *
* This is an array of file backend configuration arrays.
* Each backend configuration has the following parameters:
- * - 'name' : A unique name for the backend
- * - 'class' : The file backend class to use
- * - 'wikiId' : A unique string that identifies the wiki (container prefix)
- * - 'lockManager' : The name of a lock manager (see $wgLockManagers)
- *
- * Additional parameters are specific to the class used.
+ * - 'name' : A unique name for the backend
+ * - 'class' : The file backend class to use
+ * - 'wikiId' : A unique string that identifies the wiki (container prefix)
+ * - 'lockManager' : The name of a lock manager (see $wgLockManagers)
+ *
+ * See FileBackend::__construct() for more details.
+ * Additional parameters are specific to the file backend class used.
+ * These settings should be global to all wikis when possible.
+ *
+ * There are two particularly important aspects about each backend:
+ * - a) Whether it is fully qualified or wiki-relative.
+ * By default, the paths of files are relative to the current wiki,
+ * which works via prefixing them with the current wiki ID when accessed.
+ * Setting 'wikiId' forces the backend to be fully qualified by prefixing
+ * all paths with the specified value instead. This can be useful if
+ * multiple wikis need to share the same data. Note that 'name' is *not*
+ * part of any prefix and thus should not be relied upon for namespacing.
+ * - b) Whether it is only defined for some wikis or is defined on all
+ * wikis in the wiki farm. Defining a backend globally is useful
+ * if multiple wikis need to share the same data.
+ * One should be aware of these aspects when configuring a backend for use with
+ * any basic feature or plugin. For example, suppose an extension stores data for
+ * different wikis in different directories and sometimes needs to access data from
+ * a foreign wiki's directory in order to render a page on given wiki. The extension
+ * would need a fully qualified backend that is defined on all wikis in the wiki farm.
*/
$wgFileBackends = array();
@@ -452,16 +497,19 @@ $wgFileBackends = array();
* Each backend configuration has the following parameters:
* - 'name' : A unique name for the lock manager
* - 'class' : The lock manger class to use
- * Additional parameters are specific to the class used.
+ *
+ * See LockManager::__construct() for more details.
+ * Additional parameters are specific to the lock manager class used.
+ * These settings should be global to all wikis.
*/
$wgLockManagers = array();
/**
- * Show EXIF data, on by default if available.
- * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php
+ * Show Exif data, on by default if available.
+ * Requires PHP's Exif extension: http://www.php.net/manual/en/ref.exif.php
*
* @note FOR WINDOWS USERS:
- * To enable EXIF functions, add the following lines to the "Windows
+ * To enable Exif functions, add the following lines to the "Windows
* extensions" section of php.ini:
* @code{.ini}
* extension=extensions/php_mbstring.dll
@@ -492,22 +540,36 @@ $wgUpdateCompatibleMetadata = false;
*/
$wgUseSharedUploads = false;
-/** Full path on the web server where shared uploads can be found */
+/**
+ * Full path on the web server where shared uploads can be found
+ */
$wgSharedUploadPath = "http://commons.wikimedia.org/shared/images";
-/** Fetch commons image description pages and display them on the local wiki? */
+/**
+ * Fetch commons image description pages and display them on the local wiki?
+ */
$wgFetchCommonsDescriptions = false;
-/** Path on the file system where shared uploads can be found. */
+/**
+ * Path on the file system where shared uploads can be found.
+ */
$wgSharedUploadDirectory = "/var/www/wiki3/images";
-/** DB name with metadata about shared directory. Set this to false if the uploads do not come from a wiki. */
+/**
+ * DB name with metadata about shared directory.
+ * Set this to false if the uploads do not come from a wiki.
+ */
$wgSharedUploadDBname = false;
-/** Optional table prefix used in database. */
+/**
+ * Optional table prefix used in database.
+ */
$wgSharedUploadDBprefix = '';
-/** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */
+/**
+ * Cache shared metadata in memcached.
+ * Don't do this if the commons wiki is in a different memcached domain
+ */
$wgCacheSharedUploads = true;
/**
@@ -531,12 +593,40 @@ $wgAllowAsyncCopyUploads = false;
$wgCopyUploadsDomains = array();
/**
+ * Enable copy uploads from Special:Upload. $wgAllowCopyUploads must also be
+ * true. If $wgAllowCopyUploads is true, but this is false, you will only be
+ * able to perform copy uploads from the API or extensions (e.g. UploadWizard).
+ */
+$wgCopyUploadsFromSpecialUpload = false;
+
+/**
* Proxy to use for copy upload requests.
* @since 1.20
*/
$wgCopyUploadProxy = false;
/**
+ * Different timeout for upload by url
+ * This could be useful since when fetching large files, you may want a
+ * timeout longer than the default $wgHTTPTimeout. False means fallback
+ * to default.
+ *
+ * @since 1.22
+ */
+$wgCopyUploadTimeout = false;
+
+/**
+ * Different timeout for upload by url when run as a background job
+ * This could be useful since when fetching large files via job queue,
+ * you may want a different timeout, especially because there is no
+ * http request being kept alive.
+ *
+ * false means fallback to $wgCopyUploadTimeout.
+ * @since 1.22
+ */
+$wgCopyUploadAsyncTimeout = false;
+
+/**
* Max size for uploads, in bytes. If not set to an array, applies to all
* uploads. If set to an array, per upload type maximums can be set, using the
* file and url keys. If the * key is set this value will be used as maximum
@@ -551,9 +641,8 @@ $wgCopyUploadProxy = false;
* @endcode
* Sets the maximum for all uploads to 250 kB except for upload-by-url, which
* will have a maximum of 500 kB.
- *
*/
-$wgMaxUploadSize = 1024*1024*100; # 100MB
+$wgMaxUploadSize = 1024 * 1024 * 100; # 100MB
/**
* Point the upload navigation link to an external URL
@@ -586,6 +675,7 @@ $wgUploadMissingFileUrl = false;
* @endcode
*/
$wgThumbnailScriptPath = false;
+
/**
* @see $wgThumbnailScriptPath
*/
@@ -637,7 +727,7 @@ $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' );
* Files with these extensions will never be allowed as uploads.
* An array of file extensions to blacklist. You should append to this array
* if you want to blacklist additional files.
- * */
+ */
$wgFileBlacklist = array(
# HTML may contain cookie-stealing JavaScript and web bugs
'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht',
@@ -654,7 +744,7 @@ $wgFileBlacklist = array(
*/
$wgMimeTypeBlacklist = array(
# HTML may contain cookie-stealing JavaScript and web bugs
- 'text/html', 'text/javascript', 'text/x-javascript', 'application/x-shellscript',
+ 'text/html', 'text/javascript', 'text/x-javascript', 'application/x-shellscript',
# PHP scripts may execute arbitrary code on the server
'application/x-php', 'text/x-php',
# Other types that may be interpreted by some servers
@@ -712,10 +802,10 @@ $wgUploadSizeWarning = false;
*/
$wgTrustedMediaFormats = array(
MEDIATYPE_BITMAP, //all bitmap formats
- MEDIATYPE_AUDIO, //all audio formats
- MEDIATYPE_VIDEO, //all plain video formats
- "image/svg+xml", //svg (only needed if inline rendering of svg is not supported)
- "application/pdf", //PDF files
+ MEDIATYPE_AUDIO, //all audio formats
+ MEDIATYPE_VIDEO, //all plain video formats
+ "image/svg+xml", //svg (only needed if inline rendering of svg is not supported)
+ "application/pdf", //PDF files
#"application/x-shockwave-flash", //flash/shockwave movie
);
@@ -724,18 +814,35 @@ $wgTrustedMediaFormats = array(
* Each entry in the array maps a MIME type to a class name
*/
$wgMediaHandlers = array(
- 'image/jpeg' => 'JpegHandler',
- 'image/png' => 'PNGHandler',
- 'image/gif' => 'GIFHandler',
- 'image/tiff' => 'TiffHandler',
+ 'image/jpeg' => 'JpegHandler',
+ 'image/png' => 'PNGHandler',
+ 'image/gif' => 'GIFHandler',
+ 'image/tiff' => 'TiffHandler',
'image/x-ms-bmp' => 'BmpHandler',
- 'image/x-bmp' => 'BmpHandler',
- 'image/x-xcf' => 'XCFHandler',
- 'image/svg+xml' => 'SvgHandler', // official
- 'image/svg' => 'SvgHandler', // compat
+ 'image/x-bmp' => 'BmpHandler',
+ 'image/x-xcf' => 'XCFHandler',
+ 'image/svg+xml' => 'SvgHandler', // official
+ 'image/svg' => 'SvgHandler', // compat
'image/vnd.djvu' => 'DjVuHandler', // official
- 'image/x.djvu' => 'DjVuHandler', // compat
- 'image/x-djvu' => 'DjVuHandler', // compat
+ 'image/x.djvu' => 'DjVuHandler', // compat
+ 'image/x-djvu' => 'DjVuHandler', // compat
+);
+
+/**
+ * Plugins for page content model handling.
+ * Each entry in the array maps a model id to a class name.
+ *
+ * @since 1.21
+ */
+$wgContentHandlers = array(
+ // the usual case
+ CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler',
+ // dumb version, no syntax highlighting
+ CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler',
+ // dumb version, no syntax highlighting
+ CONTENT_MODEL_CSS => 'CssContentHandler',
+ // plain text, for use by extensions etc
+ CONTENT_MODEL_TEXT => 'TextContentHandler',
);
/**
@@ -747,15 +854,25 @@ $wgMediaHandlers = array(
* Use Image Magick instead of PHP builtin functions.
*/
$wgUseImageMagick = false;
-/** The convert command shipped with ImageMagick */
+
+/**
+ * The convert command shipped with ImageMagick
+ */
$wgImageMagickConvertCommand = '/usr/bin/convert';
-/** The identify command shipped with ImageMagick */
+
+/**
+ * The identify command shipped with ImageMagick
+ */
$wgImageMagickIdentifyCommand = '/usr/bin/identify';
-/** Sharpening parameter to ImageMagick */
+/**
+ * Sharpening parameter to ImageMagick
+ */
$wgSharpenParameter = '0x0.4';
-/** Reduction in linear dimensions below which sharpening will be enabled */
+/**
+ * Reduction in linear dimensions below which sharpening will be enabled
+ */
$wgSharpenReductionThreshold = 0.85;
/**
@@ -779,7 +896,14 @@ $wgImageMagickTempDir = false;
$wgCustomConvertCommand = false;
/**
- * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some
+ * used for lossless jpeg rotation
+ *
+ * @since 1.21
+ */
+$wgJpegTran = '/usr/bin/jpegtran';
+
+/**
+ * Some tests and extensions use exiv2 to manipulate the Exif metadata in some
* image formats.
*/
$wgExiv2Command = '/usr/bin/exiv2';
@@ -798,21 +922,28 @@ $wgSVGConverters = array(
'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output',
'inkscape' => '$path/inkscape -z -w $width -f $input -e $output',
'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input',
- 'rsvg' => '$path/rsvg -w$width -h$height $input $output',
+ 'rsvg' => '$path/rsvg -w $width -h $height $input $output',
'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output',
'ImagickExt' => array( 'SvgHandler::rasterizeImagickExt' ),
- );
+);
-/** Pick a converter defined in $wgSVGConverters */
+/**
+ * Pick a converter defined in $wgSVGConverters
+ */
$wgSVGConverter = 'ImageMagick';
-/** If not in the executable PATH, specify the SVG converter path. */
+/**
+ * If not in the executable PATH, specify the SVG converter path.
+ */
$wgSVGConverterPath = '';
-/** Don't scale a SVG larger than this */
+/**
+ * Don't scale a SVG larger than this
+ */
$wgSVGMaxSize = 2048;
-/** Don't read SVG metadata beyond this point.
+/**
+ * Don't read SVG metadata beyond this point.
* Default is 1024*256 bytes
*/
$wgSVGMetadataCutoff = 262144;
@@ -844,6 +975,7 @@ $wgAllowTitlesInSVG = false;
* 12.5 million pixels or 3500x3500.
*/
$wgMaxImageArea = 1.25e7;
+
/**
* Force thumbnailing of animated GIFs above this size to a single
* frame instead of an animated thumbnail. As of MW 1.17 this limit
@@ -851,6 +983,7 @@ $wgMaxImageArea = 1.25e7;
* It probably makes sense to keep this equal to $wgMaxImageArea.
*/
$wgMaxAnimatedGifArea = 1.25e7;
+
/**
* Browsers don't support TIFF inline generally...
* For inline display, we need to convert to PNG or JPEG.
@@ -864,7 +997,7 @@ $wgMaxAnimatedGifArea = 1.25e7;
* $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' );
* @endcode
*/
- $wgTiffThumbnailType = false;
+$wgTiffThumbnailType = false;
/**
* If rendered thumbnail files are older than this timestamp, they
@@ -897,11 +1030,13 @@ $wgIgnoreImageErrors = false;
$wgGenerateThumbnailOnParse = true;
/**
-* Show thumbnails for old images on the image description page
-*/
+ * Show thumbnails for old images on the image description page
+ */
$wgShowArchiveThumbnails = true;
-/** Obsolete, always true, kept for compatibility with extensions */
+/**
+ * Obsolete, always true, kept for compatibility with extensions
+ */
$wgUseImageResize = true;
/**
@@ -912,7 +1047,7 @@ $wgUseImageResize = true;
$wgEnableAutoRotation = null;
/**
- * Internal name of virus scanner. This servers as a key to the
+ * Internal name of virus scanner. This serves 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.
*/
@@ -930,7 +1065,7 @@ $wgAntivirus = null;
* "command" is the full command to call the virus scanner - %f will be
* replaced with the name of the file to scan. If not present, the filename
* will be appended to the command. Note that this must be overwritten if the
- * scanner is not in the system path; in that case, plase set
+ * scanner is not in the system path; in that case, please set
* $wgAntivirusSetup[$wgAntivirus]['command'] to the desired command with full
* path.
*
@@ -940,8 +1075,8 @@ $wgAntivirus = null;
* the scan to be failed. This will pass the file if $wgAntivirusRequired
* is not set.
* - An exit code mapped to AV_SCAN_ABORTED causes the function to consider
- * the file to have an usupported format, which is probably imune to
- * virusses. This causes the file to pass.
+ * the file to have an unsupported format, which is probably immune to
+ * viruses. This causes the file to pass.
* - An exit code mapped to AV_NO_VIRUS will cause the file to pass, meaning
* no virus was found.
* - All other codes (like AV_VIRUS_FOUND) will cause the function to report
@@ -955,44 +1090,43 @@ $wgAntivirus = null;
$wgAntivirusSetup = array(
#setup for clamav
- 'clamav' => array (
- 'command' => "clamscan --no-summary ",
-
- 'codemap' => array (
- "0" => AV_NO_VIRUS, # no virus
- "1" => AV_VIRUS_FOUND, # virus found
- "52" => AV_SCAN_ABORTED, # unsupported file format (probably imune)
- "*" => AV_SCAN_FAILED, # else scan failed
+ 'clamav' => array(
+ 'command' => 'clamscan --no-summary ',
+ 'codemap' => array(
+ "0" => AV_NO_VIRUS, # no virus
+ "1" => AV_VIRUS_FOUND, # virus found
+ "52" => AV_SCAN_ABORTED, # unsupported file format (probably immune)
+ "*" => AV_SCAN_FAILED, # else scan failed
),
-
'messagepattern' => '/.*?:(.*)/sim',
),
);
-
-/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. */
+/**
+ * Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected.
+ */
$wgAntivirusRequired = true;
-/** Determines if the mime type of uploaded files should be checked */
+/**
+ * Determines if the mime type of uploaded files should be checked
+ */
$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.
-
-/** Sets the mime type info file to use by MimeMagic.php. */
-$wgMimeInfoFile= "includes/mime.info";
-#$wgMimeInfoFile= null; #use built-in defaults only.
+/**
+ * Sets the mime type definition file to use by MimeMagic.php.
+ * Set to null, to use built-in defaults only.
+ * example: $wgMimeTypeFile = '/etc/mime.types';
+ */
+$wgMimeTypeFile = 'includes/mime.types';
/**
- * Switch for loading the FileInfo extension by PECL at runtime.
- * This should be used only if fileinfo is installed as a shared object
- * or a dynamic library.
+ * Sets the mime type info file to use by MimeMagic.php.
+ * Set to null, to use built-in defaults only.
*/
-$wgLoadFileinfoExtension = false;
+$wgMimeInfoFile = 'includes/mime.info';
-/** Sets an external mime detector program. The command must print only
+/**
+ * Sets an external mime detector program. The command must print only
* the mime type to standard output.
* The name of the file to process will be appended to the command given here.
* If not set or NULL, mime_content_type will be used if available.
@@ -1016,11 +1150,11 @@ $wgTrivialMimeDetection = false;
* array = ( 'rootElement' => 'associatedMimeType' )
*/
$wgXMLMimeTypes = array(
- 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml',
- 'svg' => 'image/svg+xml',
+ 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml',
+ 'svg' => 'image/svg+xml',
'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram',
- 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
- 'html' => 'text/html', // application/xhtml+xml?
+ 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
+ 'html' => 'text/html', // application/xhtml+xml?
);
/**
@@ -1056,12 +1190,13 @@ $wgThumbLimits = array(
/**
* Default parameters for the "<gallery>" tag
*/
-$wgGalleryOptions = array (
+$wgGalleryOptions = array(
'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize
'imageWidth' => 120, // Width of the cells containing images in galleries (in "px")
'imageHeight' => 120, // Height of the cells containing images in galleries (in "px")
'captionLength' => 25, // Length of caption to truncate (in characters)
'showBytes' => true, // Show the filesize in bytes in categories
+ 'mode' => 'traditional',
);
/**
@@ -1077,28 +1212,39 @@ $wgThumbUpright = 0.75;
$wgDirectoryMode = 0777;
/**
+ * Generate and use thumbnails suitable for screens with 1.5 and 2.0 pixel densities.
+ *
+ * This means a 320x240 use of an image on the wiki will also generate 480x360 and 640x480
+ * thumbnails, output via data-src-1-5 and data-src-2-0. Runtime JavaScript switches the
+ * images in after loading the original low-resolution versions depending on the reported
+ * window.devicePixelRatio.
+ */
+$wgResponsiveImages = true;
+
+/**
* @name DJVU settings
* @{
*/
+
/**
* Path of the djvudump executable
* Enable this and $wgDjvuRenderer to enable djvu rendering
+ * example: $wgDjvuDump = 'djvudump';
*/
-# $wgDjvuDump = 'djvudump';
$wgDjvuDump = null;
/**
* Path of the ddjvu DJVU renderer
* Enable this and $wgDjvuDump to enable djvu rendering
+ * example: $wgDjvuRenderer = 'ddjvu';
*/
-# $wgDjvuRenderer = 'ddjvu';
$wgDjvuRenderer = null;
/**
* Path of the djvutxt DJVU text extraction utility
* Enable this and $wgDjvuDump to enable text layer extraction from djvu files
+ * example: $wgDjvuTxt = 'djvutxt';
*/
-# $wgDjvuTxt = 'djvutxt';
$wgDjvuTxt = null;
/**
@@ -1123,10 +1269,12 @@ $wgDjvuToXML = null;
* Set this to false to output the ppm file directly.
*/
$wgDjvuPostProcessor = 'pnmtojpeg';
+
/**
* File extension for the DJVU post processor output
*/
$wgDjvuOutputExtension = 'jpg';
+
/** @} */ # end of DJvu }
/** @} */ # end of file uploads }
@@ -1182,7 +1330,7 @@ $wgEnableUserEmail = true;
* instead of From. ($wgEmergencyContact will be used as From.)
*
* Some mailers (eg sSMTP) set the SMTP envelope sender to the From value,
- * which can cause problems with SPF validation and leak recipient addressses
+ * which can cause problems with SPF validation and leak recipient addresses
* when bounces are sent to the sender.
*/
$wgUserEmailUseReplyTo = false;
@@ -1211,12 +1359,12 @@ $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60;
*
* @code
* $wgSMTP = array(
- * 'host' => 'SMTP domain',
- * 'IDHost' => 'domain for MessageID',
- * 'port' => '25',
- * 'auth' => [true|false],
- * 'username' => [SMTP username],
- * 'password' => [SMTP password],
+ * 'host' => 'SMTP domain',
+ * 'IDHost' => 'domain for MessageID',
+ * 'port' => '25',
+ * 'auth' => [true|false],
+ * 'username' => [SMTP username],
+ * 'password' => [SMTP password],
* );
* @endcode
*/
@@ -1229,13 +1377,20 @@ $wgSMTP = false;
$wgAdditionalMailParams = null;
/**
+ * For parts of the system that have been updated to provide HTML email content, send
+ * both text and HTML parts as the body of the email
+ */
+$wgAllowHTMLEmail = false;
+
+/**
* True: from page editor if s/he opted-in. False: Enotif mails appear to come
* from $wgEmergencyContact
*/
$wgEnotifFromEditor = false;
// TODO move UPO to preferences probably ?
-# If set to true, users get a corresponding option in their preferences and can choose to enable or disable at their discretion
+# If set to true, users get a corresponding option in their preferences and can choose to
+# enable or disable at their discretion
# If set to false, the corresponding input form on the user preference page is suppressed
# It call this to be a "user-preferences-option (UPO)"
@@ -1301,33 +1456,61 @@ $wgEnotifUseRealName = false;
*/
$wgUsersNotifiedOnAllChanges = array();
-
/** @} */ # end of email settings
/************************************************************************//**
* @name Database settings
* @{
*/
-/** Database host name or IP address */
+
+/**
+ * Database host name or IP address
+ */
$wgDBserver = 'localhost';
-/** Database port number (for PostgreSQL) */
+
+/**
+ * Database port number (for PostgreSQL)
+ */
$wgDBport = 5432;
-/** Name of the database */
+
+/**
+ * Name of the database
+ */
$wgDBname = 'my_wiki';
-/** Database username */
+
+/**
+ * Database username
+ */
$wgDBuser = 'wikiuser';
-/** Database user's password */
+
+/**
+ * Database user's password
+ */
$wgDBpassword = '';
-/** Database type */
+
+/**
+ * Database type
+ */
$wgDBtype = 'mysql';
-/** Whether to use SSL in DB connection. */
+
+/**
+ * Whether to use SSL in DB connection.
+ */
$wgDBssl = false;
-/** Whether to use compression in DB connection. */
+
+/**
+ * Whether to use compression in DB connection.
+ */
$wgDBcompress = false;
-/** Separate username for maintenance tasks. Leave as null to use the default. */
+/**
+ * Separate username for maintenance tasks. Leave as null to use the default.
+ */
$wgDBadminuser = null;
-/** Separate password for maintenance tasks. Leave as null to use the default. */
+
+/**
+ * Separate password for maintenance tasks. Leave as null to use the default.
+ */
$wgDBadminpassword = null;
/**
@@ -1338,9 +1521,23 @@ $wgDBadminpassword = null;
*/
$wgSearchType = null;
-/** Table name prefix */
+/**
+ * Alternative search types
+ * Sometimes you want to support multiple search engines for testing. This
+ * allows users to select their search engine of choice via url parameters
+ * to Special:Search and the action=search API. If using this, there's no
+ * need to add $wgSearchType to it, that is handled automatically.
+ */
+$wgSearchTypeAlternatives = null;
+
+/**
+ * Table name prefix
+ */
$wgDBprefix = '';
-/** MySQL table options to use during installation or update */
+
+/**
+ * MySQL table options to use during installation or update
+ */
$wgDBTableOptions = 'ENGINE=InnoDB';
/**
@@ -1351,10 +1548,14 @@ $wgDBTableOptions = 'ENGINE=InnoDB';
*/
$wgSQLMode = '';
-/** Mediawiki schema */
+/**
+ * Mediawiki schema
+ */
$wgDBmwschema = 'mediawiki';
-/** To override default SQLite data directory ($docroot/../data) */
+/**
+ * To override default SQLite data directory ($docroot/../data)
+ */
$wgSQLiteDataDir = '';
/**
@@ -1375,18 +1576,27 @@ $wgAllDBsAreLocalhost = false;
* 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
+ * database. However it is advised to limit what tables you do share as many of
* MediaWiki's tables may have side effects if you try to share them.
- * EXPERIMENTAL
*
* $wgSharedPrefix is the table prefix for the shared database. It defaults to
* $wgDBprefix.
+ *
+ * @deprecated In new code, use the $wiki parameter to wfGetLB() to access
+ * remote databases. Using wfGetLB() allows the shared database to reside on
+ * separate servers to the wiki's own database, with suitable configuration
+ * of $wgLBFactoryConf.
*/
$wgSharedDB = null;
-/** @see $wgSharedDB */
+/**
+ * @see $wgSharedDB
+ */
$wgSharedPrefix = false;
-/** @see $wgSharedDB */
+
+/**
+ * @see $wgSharedDB
+ */
$wgSharedTables = array( 'user', 'user_properties' );
/**
@@ -1406,11 +1616,11 @@ $wgSharedTables = array( 'user', 'user_properties' );
* - DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended)
* - DBO_DEBUG -- equivalent of $wgDebugDumpSql
* - DBO_TRX -- wrap entire request in a transaction
- * - DBO_IGNORE -- ignore errors (not useful in LocalSettings.php)
* - DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
* - DBO_PERSISTENT -- enables persistent database connections
* - DBO_SSL -- uses SSL/TLS encryption in database connections, if available
- * - DBO_COMPRESS -- uses internal compression in database connections, if available
+ * - DBO_COMPRESS -- uses internal compression in database connections,
+ * if available
*
* - max lag: (optional) Maximum replication lag before a slave will taken out of rotation
* - max threads: (optional) Maximum number of running threads
@@ -1449,17 +1659,21 @@ $wgDBservers = false;
*/
$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' );
-/** How long to wait for a slave to catch up to the master */
+/**
+ * How long to wait for a slave to catch up to the master
+ */
$wgMasterWaitTimeout = 10;
-/** File to log database errors to */
+/**
+ * File to log database errors to
+ */
$wgDBerrorLog = false;
/**
* Timezone to use in the error log.
* Defaults to the wiki timezone ($wgLocaltimezone).
*
- * A list of useable timezones can found at:
+ * A list of usable timezones can found at:
* http://php.net/manual/en/timezones.php
*
* @par Examples:
@@ -1475,7 +1689,9 @@ $wgDBerrorLog = false;
*/
$wgDBerrorLogTZ = false;
-/** When to give an error message */
+/**
+ * When to give an error message
+ */
$wgDBClusterTimeout = 10;
/**
@@ -1504,6 +1720,35 @@ $wgDBAvgStatusPoll = 2000;
$wgDBmysql5 = false;
/**
+ * Set true to enable Oracle DCRP (supported from 11gR1 onward)
+ *
+ * To use this feature set to true and use a datasource defined as
+ * POOLED (i.e. in tnsnames definition set server=pooled in connect_data
+ * block).
+ *
+ * Starting from 11gR1 you can use DCRP (Database Resident Connection
+ * Pool) that maintains established sessions and reuses them on new
+ * connections.
+ *
+ * Not completely tested, but it should fall back on normal connection
+ * in case the pool is full or the datasource is not configured as
+ * pooled.
+ * And the other way around; using oci_pconnect on a non pooled
+ * datasource should produce a normal connection.
+ *
+ * When it comes to frequent shortlived DB connections like with MW
+ * Oracle tends to s***. The problem is the driver connects to the
+ * database reasonably fast, but establishing a session takes time and
+ * resources. MW does not rely on session state (as it does not use
+ * features such as package variables) so establishing a valid session
+ * is in this case an unwanted overhead that just slows things down.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ */
+$wgDBOracleDRCP = false;
+
+/**
* Other wikis on this site, can be administered from a single developer
* account.
* Array numeric key => database name
@@ -1516,17 +1761,14 @@ $wgLocalDatabases = array();
* show a more obvious warning.
*/
$wgSlaveLagWarning = 10;
-/** @see $wgSlaveLagWarning */
-$wgSlaveLagCritical = 30;
/**
- * Use old names for change_tags indices.
+ * @see $wgSlaveLagWarning
*/
-$wgOldChangeTagsIndex = false;
+$wgSlaveLagCritical = 30;
/**@}*/ # End of DB settings }
-
/************************************************************************//**
* @name Text storage
* @{
@@ -1535,8 +1777,8 @@ $wgOldChangeTagsIndex = false;
/**
* We can also compress text stored in the 'text' table. If this is set on, new
* revisions will be compressed on page save if zlib support is available. Any
- * compressed revisions will be decompressed on load regardless of this setting
- * *but will not be readable at all* if zlib support is not available.
+ * compressed revisions will be decompressed on load regardless of this setting,
+ * but will not be readable at all* if zlib support is not available.
*/
$wgCompressRevisions = false;
@@ -1560,7 +1802,7 @@ $wgExternalStores = false;
* Create a cluster named 'cluster1' containing three servers:
* @code
* $wgExternalServers = array(
- * 'cluster1' => array( 'srv28', 'srv29', 'srv30' )
+ * 'cluster1' => array( 'srv28', 'srv29', 'srv30' )
* );
* @endcode
*
@@ -1599,23 +1841,36 @@ $wgRevisionCacheExpiry = 0;
* @name Performance hacks and limits
* @{
*/
-/** Disable database-intensive features */
+
+/**
+ * Disable database-intensive features
+ */
$wgMiserMode = false;
-/** Disable all query pages if miser mode is on, not just some */
+
+/**
+ * Disable all query pages if miser mode is on, not just some
+ */
$wgDisableQueryPages = false;
-/** Number of rows to cache in 'querycache' table when miser mode is on */
+
+/**
+ * Number of rows to cache in 'querycache' table when miser mode is on
+ */
$wgQueryCacheLimit = 1000;
-/** Number of links to a page required before it is deemed "wanted" */
+
+/**
+ * Number of links to a page required before it is deemed "wanted"
+ */
$wgWantedPagesThreshold = 1;
-/** Enable slow parser functions */
+
+/**
+ * Enable slow parser functions
+ */
$wgAllowSlowParserFunctions = false;
-/** Allow schema updates */
-$wgAllowSchemaUpdates = true;
/**
- * Do DELETE/INSERT for link updates instead of incremental
+ * Allow schema updates
*/
-$wgUseDumbLinkUpdate = false;
+$wgAllowSchemaUpdates = true;
/**
* Anti-lock flags - bitfield
@@ -1758,13 +2013,13 @@ $wgDBAhandler = 'db3';
/**
* Deprecated alias for $wgSessionsInObjectCache.
*
- * @deprecated Use $wgSessionsInObjectCache
+ * @deprecated since 1.20; Use $wgSessionsInObjectCache
*/
$wgSessionsInMemcached = false;
/**
* Store sessions in an object cache, configured by $wgSessionCacheType. This
- * can be useful to improve performance, or to avoid the locking behaviour of
+ * can be useful to improve performance, or to avoid the locking behavior of
* PHP's default session handler, which tends to prevent multiple requests for
* the same user from acting concurrently.
*/
@@ -1784,11 +2039,15 @@ $wgObjectCacheSessionExpiry = 3600;
*/
$wgSessionHandler = null;
-/** If enabled, will send MemCached debugging information to $wgDebugLogFile */
+/**
+ * If enabled, will send MemCached debugging information to $wgDebugLogFile
+ */
$wgMemCachedDebug = false;
-/** The list of MemCached servers and port numbers */
-$wgMemCachedServers = array( '127.0.0.1:11000' );
+/**
+ * The list of MemCached servers and port numbers
+ */
+$wgMemCachedServers = array( '127.0.0.1:11211' );
/**
* Use persistent connections to MemCached, which are shared across multiple
@@ -1808,16 +2067,10 @@ $wgMemCachedTimeout = 500000;
$wgUseLocalMessageCache = false;
/**
- * Defines format of local cache.
- * - true: Serialized object
- * - false: PHP source file (Warning - security risk)
- */
-$wgLocalMessageCacheSerialized = true;
-
-/**
- * Instead of caching everything, keep track which messages are requested and
- * load only most used messages. This only makes sense if there is lots of
- * interface messages customised in the wiki (like hundreds in many languages).
+ * Instead of caching everything, only cache those messages which have
+ * been customised in the site content language. This means that
+ * MediaWiki:Foo/ja is ignored if MediaWiki:Foo doesn't exist.
+ * This option is probably only useful for translatewiki.net.
*/
$wgAdaptiveMessageCache = false;
@@ -1849,7 +2102,9 @@ $wgLocalisationCacheConf = array(
'manualRecache' => false,
);
-/** Allow client-side caching of pages */
+/**
+ * Allow client-side caching of pages
+ */
$wgCachePages = true;
/**
@@ -1935,7 +2190,8 @@ $wgUseGzip = false;
*/
$wgUseETag = false;
-/** Clock skew or the one-second resolution of time() can occasionally cause cache
+/**
+ * Clock skew or the one-second resolution of time() can occasionally cause cache
* problems when the user requests two pages within a short period of time. This
* variable adds a given number of seconds to vulnerable timestamps, thereby giving
* a grace period.
@@ -1974,13 +2230,18 @@ $wgInvalidateCacheOnLocalSettingsChange = true;
*/
$wgUseSquid = false;
-/** If you run Squid3 with ESI support, enable this (default: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) */
+/**
+ * Send X-Vary-Options header for better caching (requires patched Squid)
+ */
$wgUseXVO = false;
-/** Add X-Forwarded-Proto to the Vary and X-Vary-Options headers for API
+/**
+ * Add X-Forwarded-Proto to the Vary and X-Vary-Options headers for API
* requests and RSS/Atom feeds. Use this if you have an SSL termination setup
* and need to split the cache between HTTP and HTTPS for API requests,
* feed requests and HTTP redirect responses in order to prevent cache
@@ -2028,10 +2289,33 @@ $wgSquidServers = array();
*/
$wgSquidServersNoPurge = array();
-/** Maximum number of titles to purge in any one client operation */
+/**
+ * Maximum number of titles to purge in any one client operation
+ */
$wgMaxSquidPurgeTitles = 400;
/**
+ * Whether to use a Host header in purge requests sent to the proxy servers
+ * configured in $wgSquidServers. Set this to false to support Squid
+ * configured in forward-proxy mode.
+ *
+ * If this is set to true, a Host header will be sent, and only the path
+ * component of the URL will appear on the request line, as if the request
+ * were a non-proxy HTTP 1.1 request. Varnish only supports this style of
+ * request. Squid supports this style of request only if reverse-proxy mode
+ * (http_port ... accel) is enabled.
+ *
+ * If this is set to false, no Host header will be sent, and the absolute URL
+ * will be sent in the request line, as is the standard for an HTTP proxy
+ * request in both HTTP 1.0 and 1.1. This style of request is not supported
+ * by Varnish, but is supported by Squid in either configuration (forward or
+ * reverse).
+ *
+ * @since 1.21
+ */
+$wgSquidPurgeUseHostHeader = true;
+
+/**
* Routing configuration for HTCP multicast purging. Add elements here to
* enable HTCP and determine which purges are sent where. If set to an empty
* array, HTCP is disabled.
@@ -2039,12 +2323,12 @@ $wgMaxSquidPurgeTitles = 400;
* Each key in this array is a regular expression to match against the purged
* URL, or an empty string to match all URLs. The purged URL is matched against
* the regexes in the order specified, and the first rule whose regex matches
- * is used.
+ * is used, all remaining rules will thus be ignored.
*
- * Example configuration to send purges for upload.wikimedia.org to one
+ * @par Example configuration to send purges for upload.wikimedia.org to one
* multicast group and all other purges to another:
* @code
- * $wgHTCPMulticastRouting = array(
+ * $wgHTCPRouting = array(
* '|^https?://upload\.wikimedia\.org|' => array(
* 'host' => '239.128.0.113',
* 'port' => 4827,
@@ -2056,11 +2340,44 @@ $wgMaxSquidPurgeTitles = 400;
* );
* @endcode
*
- * @since 1.20
+ * You can also pass an array of hosts to send purges too. This is useful when
+ * you have several multicast groups or unicast address that should receive a
+ * given purge. Multiple hosts support was introduced in MediaWiki 1.22.
+ *
+ * @par Example of sending purges to multiple hosts:
+ * @code
+ * $wgHTCPRouting = array(
+ * '' => array(
+ * // Purges to text caches using multicast
+ * array( 'host' => '239.128.0.114', 'port' => '4827' ),
+ * // Purges to a hardcoded list of caches
+ * array( 'host' => '10.88.66.1', 'port' => '4827' ),
+ * array( 'host' => '10.88.66.2', 'port' => '4827' ),
+ * array( 'host' => '10.88.66.3', 'port' => '4827' ),
+ * ),
+ * );
+ * @endcode
+ *
+ * @since 1.22
+ *
+ * $wgHTCPRouting replaces $wgHTCPMulticastRouting that was introduced in 1.20.
+ * For back compatibility purposes, whenever its array is empty
+ * $wgHTCPMutlicastRouting will be used as a fallback if it not null.
*
* @see $wgHTCPMulticastTTL
*/
-$wgHTCPMulticastRouting = array();
+$wgHTCPRouting = array();
+
+/**
+ * @deprecated since 1.22, please use $wgHTCPRouting instead.
+ *
+ * Whenever this is set and $wgHTCPRouting evaluates to false, $wgHTCPRouting
+ * will be set to this value.
+ * This is merely for back compatibility.
+ *
+ * @since 1.20
+ */
+$wgHTCPMulticastRouting = null;
/**
* HTCP multicast address. Set this to a multicast IP address to enable HTCP.
@@ -2068,29 +2385,34 @@ $wgHTCPMulticastRouting = array();
* Note that MediaWiki uses the old non-RFC compliant HTCP format, which was
* present in the earliest Squid implementations of the protocol.
*
- * This setting is DEPRECATED in favor of $wgHTCPMulticastRouting , and kept
- * for backwards compatibility only. If $wgHTCPMulticastRouting is set, this
- * setting is ignored. If $wgHTCPMulticastRouting is not set and this setting
- * is, it is used to populate $wgHTCPMulticastRouting.
+ * This setting is DEPRECATED in favor of $wgHTCPRouting , and kept for
+ * backwards compatibility only. If $wgHTCPRouting is set, this setting is
+ * ignored. If $wgHTCPRouting is not set and this setting is, it is used to
+ * populate $wgHTCPRouting.
*
- * @deprecated in favor of $wgHTCPMulticastRouting
+ * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting and since 1.22 in
+ * favor of $wgHTCPRouting.
*/
$wgHTCPMulticastAddress = false;
/**
* HTCP multicast port.
- * @deprecated in favor of $wgHTCPMulticastRouting
+ * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting and since 1.22 in
+ * favor of $wgHTCPRouting.
+ *
* @see $wgHTCPMulticastAddress
*/
$wgHTCPPort = 4827;
/**
* HTCP multicast TTL.
- * @see $wgHTCPMulticastRouting
+ * @see $wgHTCPRouting
*/
$wgHTCPMulticastTTL = 1;
-/** Should forwarded Private IPs be accepted? */
+/**
+ * Should forwarded Private IPs be accepted?
+ */
$wgUsePrivateIPs = false;
/** @} */ # end of HTTP proxy settings
@@ -2100,10 +2422,30 @@ $wgUsePrivateIPs = false;
* @{
*/
-/** Site language code, should be one of ./languages/Language(.*).php */
+/**
+ * Site language code. See languages/Names.php for languages supported by
+ * MediaWiki out of the box. Not all languages listed there have translations,
+ * see languages/messages/ for the list of languages with some localisation.
+ *
+ * Warning: Don't use language codes listed in $wgDummyLanguageCodes like "no"
+ * for Norwegian (use "nb" instead), or things will break unexpectedly.
+ *
+ * This defines the default interface language for all users, but users can
+ * change it in their preferences.
+ *
+ * This also defines the language of pages in the wiki. The content is wrapped
+ * in a html element with lang=XX attribute. This behavior can be overridden
+ * via hooks, see Title::getPageLanguage.
+ */
$wgLanguageCode = 'en';
/**
+ * Language cache size, or really how many languages can we handle
+ * simultaneously without degrading to crawl speed.
+ */
+$wgLangObjCacheSize = 10;
+
+/**
* Some languages need different word forms, usually for different cases.
* Used in Language::convertGrammar().
*
@@ -2114,18 +2456,24 @@ $wgLanguageCode = 'en';
*/
$wgGrammarForms = array();
-/** Treat language links as magic connectors, not inline links */
+/**
+ * Treat language links as magic connectors, not inline links
+ */
$wgInterwikiMagic = true;
-/** Hide interlanguage links from the sidebar */
+/**
+ * Hide interlanguage links from the sidebar
+ */
$wgHideInterlanguageLinks = false;
-/** List of language names or overrides for default names in Names.php */
+/**
+ * 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 mostly leftoffs from renames, or other legacy things.
+ * These codes are mostly left-offs from renames, or other legacy things.
* This array makes them not appear as a selectable language on the installer,
* and excludes them when running the transstat.php script.
*/
@@ -2220,7 +2568,8 @@ $wgBrowserBlackList = array(
'/^Mozilla\/4\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/',
/**
- * MSIE on Mac OS 9 is teh sux0r, converts þ to <thorn>, ð to <eth>, Þ to <THORN> and Ð to <ETH>
+ * MSIE on Mac OS 9 is teh sux0r, converts þ to <thorn>, ð to <eth>,
+ * Þ to <THORN> and Ð to <ETH>
*
* Known useragents:
* - Mozilla/4.0 (compatible; MSIE 5.0; Mac_PowerPC)
@@ -2228,7 +2577,7 @@ $wgBrowserBlackList = array(
* - Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC)
* - [...]
*
- * @link http://en.wikipedia.org/w/index.php?title=User%3A%C6var_Arnfj%F6r%F0_Bjarmason%2Ftestme&diff=12356041&oldid=12355864
+ * @link http://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864
* @link http://en.wikipedia.org/wiki/Template%3AOS9
*/
'/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/',
@@ -2249,24 +2598,17 @@ $wgBrowserBlackList = array(
* requires that the cur table be kept around for those revisions
* to remain viewable.
*
- * maintenance/migrateCurStubs.php can be used to complete the
- * migration in the background once the wiki is back online.
- *
* This option affects the updaters *only*. Any present cur stub
* revisions will be readable at runtime regardless of this setting.
*/
$wgLegacySchemaConversion = false;
/**
- * Enable to allow rewriting dates in page text.
- * DOES NOT FORMAT CORRECTLY FOR MOST LANGUAGES.
- */
-$wgUseDynamicDates = false;
-/**
* Enable dates like 'May 12' instead of '12 May', this only takes effect if
* the interface is set to English.
*/
-$wgAmericanDates = false;
+$wgAmericanDates = false;
+
/**
* For Hindi and Arabic use local numerals instead of Western style (0-9)
* numerals in interface.
@@ -2289,16 +2631,24 @@ $wgMsgCacheExpiry = 86400;
*/
$wgMaxMsgCacheEntrySize = 10000;
-/** Whether to enable language variant conversion. */
+/**
+ * Whether to enable language variant conversion.
+ */
$wgDisableLangConversion = false;
-/** Whether to enable language variant conversion for links. */
+/**
+ * Whether to enable language variant conversion for links.
+ */
$wgDisableTitleConversion = false;
-/** Whether to enable cononical language links in meta data. */
+/**
+ * Whether to enable canonical language links in meta data.
+ */
$wgCanonicalLanguageLinks = true;
-/** Default variant code, if false, the default will be the language code */
+/**
+ * Default variant code, if false, the default will be the language code
+ */
$wgDefaultLanguageVariant = false;
/**
@@ -2319,9 +2669,9 @@ $wgDisabledVariants = array();
*
* @par Example:
* @code
- * $wgLanguageCode = 'sr';
- * $wgVariantArticlePath = '/$2/$1';
- * $wgArticlePath = '/wiki/$1';
+ * $wgLanguageCode = 'sr';
+ * $wgVariantArticlePath = '/$2/$1';
+ * $wgArticlePath = '/wiki/$1';
* @endcode
*
* A link to /wiki/ would be redirected to /sr/Главна_страна
@@ -2354,7 +2704,7 @@ $wgLoginLanguageSelector = false;
* To allow language-specific main page and community
* portal:
* @code
- * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+ * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
* @endcode
*/
$wgForceUIMsgAsContentMsg = array();
@@ -2370,7 +2720,7 @@ $wgForceUIMsgAsContentMsg = array();
* Timezones can be translated by editing MediaWiki messages of type
* timezone-nameinlowercase like timezone-utc.
*
- * A list of useable timezones can found at:
+ * A list of usable timezones can found at:
* http://php.net/manual/en/timezones.php
*
* @par Examples:
@@ -2389,23 +2739,12 @@ $wgLocaltimezone = null;
* for anonymous users and new user accounts.
*
* This setting is used for most date/time displays in the software, and is
- * overrideable in user preferences. It is *not* used for signature timestamps.
+ * overridable in user preferences. It is *not* used for signature timestamps.
*
* By default, this will be set to match $wgLocaltimezone.
*/
$wgLocalTZoffset = null;
-/**
- * If set to true, this will roll back a few bug fixes introduced in 1.19,
- * emulating the 1.18 behaviour, to avoid introducing bug 34832. In 1.19,
- * language variant conversion is disabled in interface messages. Setting this
- * to true re-enables it.
- *
- * @todo This variable should be removed (implicitly false) in 1.20 or earlier.
- */
-$wgBug34832TransitionalRollback = true;
-
-
/** @} */ # End of language/charset settings
/*************************************************************************//**
@@ -2413,52 +2752,44 @@ $wgBug34832TransitionalRollback = true;
* @{
*/
-/** The default Content-Type header. */
-$wgMimeType = 'text/html';
-
-/**
- * The content type used in script tags. This is mostly going to be ignored if
- * $wgHtml5 is true, at least for actual HTML output, since HTML5 doesn't
- * require a MIME type for JavaScript or CSS (those are the default script and
- * style languages).
- */
-$wgJsMimeType = 'text/javascript';
-
/**
- * The HTML document type. Ignored if $wgHtml5 is true, since <!DOCTYPE html>
- * doesn't actually have a doctype part to put this variable's contents in.
+ * The default Content-Type header.
*/
-$wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN';
+$wgMimeType = 'text/html';
/**
- * The URL of the document type declaration. Ignored if $wgHtml5 is true,
- * since HTML5 has no DTD, and <!DOCTYPE html> doesn't actually have a DTD part
- * to put this variable's contents in.
+ * Previously used as content type in HTML script tags. This is now ignored since
+ * HTML5 doesn't require a MIME type for script tags (javascript is the default).
+ * It was also previously used by RawAction to determine the ctype query parameter
+ * value that will result in a javascript response.
+ * @deprecated since 1.22
*/
-$wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd';
+$wgJsMimeType = null;
/**
- * The default xmlns attribute. Ignored if $wgHtml5 is true (or it's supposed
- * to be), since we don't currently support XHTML5, and in HTML5 (i.e., served
- * as text/html) the attribute has no effect, so why bother?
+ * The default xmlns attribute. The option to define this has been removed.
+ * The value of this variable is no longer used by core and is set to a fixed
+ * value in Setup.php for compatibility with extensions that depend on the value
+ * of this variable being set. Such a dependency however is deprecated.
+ * @deprecated since 1.22
*/
-$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
+$wgXhtmlDefaultNamespace = null;
/**
- * Should we output an HTML5 doctype? If false, use XHTML 1.0 Transitional
- * instead, and disable HTML5 features. This may eventually be removed and set
- * to always true. If it's true, a number of other settings will be irrelevant
- * and have no effect.
+ * Previously used to determine if we should output an HTML5 doctype.
+ * This is no longer used as we always output HTML5 now. For compatibility with
+ * extensions that still check the value of this config it's value is now forced
+ * to true by Setup.php.
+ * @deprecated since 1.22
*/
$wgHtml5 = true;
/**
* Defines the value of the version attribute in the &lt;html&gt; tag, if any.
- * This is ignored if $wgHtml5 is false. If $wgAllowRdfaAttributes and
- * $wgHtml5 are both true, and this evaluates to boolean false (like if it's
- * left at the default null value), it will be auto-initialized to the correct
- * value for RDFa+HTML5. As such, you should have no reason to ever actually
- * set this to anything.
+ * If $wgAllowRdfaAttributes is true, and this evaluates to boolean false
+ * (like if it's left at the default null value), it will be auto-initialized
+ * to the correct value for RDFa+HTML5. As such, you should have no reason to
+ * ever actually set this to anything.
*/
$wgHtml5Version = null;
@@ -2469,7 +2800,7 @@ $wgHtml5Version = null;
$wgAllowRdfaAttributes = false;
/**
- * Enabled HTML5 microdata attributes for use in wikitext, if $wgHtml5 is also true.
+ * Enabled HTML5 microdata attributes for use in wikitext.
*/
$wgAllowMicrodataAttributes = false;
@@ -2499,12 +2830,11 @@ $wgWellFormedXml = true;
* @par Example:
* @code
* $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg';
- * @endCode
+ * @endcode
* Normally we wouldn't have to define this in the root "<html>"
* element, but IE needs it there in some circumstances.
*
- * This is ignored if $wgHtml5 is true, for the same reason as
- * $wgXhtmlDefaultNamespace.
+ * This is ignored if $wgMimeType is set to a non-XML mimetype.
*/
$wgXhtmlNamespaces = array();
@@ -2527,12 +2857,12 @@ $wgSiteNotice = '';
/**
* A subtitle to add to the tagline, for skins that have it/
*/
-$wgExtraSubtitle = '';
+$wgExtraSubtitle = '';
/**
* If this is set, a "donate" link will appear in the sidebar. Set it to a URL.
*/
-$wgSiteSupportPage = '';
+$wgSiteSupportPage = '';
/**
* Validate the overall output using tidy and refuse
@@ -2554,34 +2884,11 @@ $wgDefaultSkin = 'vector';
* remove from the .../skins/ directory
*/
$wgSkipSkin = '';
-/** Array for more like $wgSkipSkin. */
-$wgSkipSkins = array();
-
-/**
- * Optionally, we can specify a stylesheet to use for media="handheld".
- * This is recognized by some, but not all, handheld/mobile/PDA browsers.
- * If left empty, compliant handheld browsers won't pick up the skin
- * stylesheet, which is specified for 'screen' media.
- *
- * Can be a complete URL, base-relative path, or $wgStylePath-relative path.
- * Try 'chick/main.css' to apply the Chick styles to the MonoBook HTML.
- *
- * Will also be switched in when 'handheld=yes' is added to the URL, like
- * the 'printable=yes' mode for print media.
- */
-$wgHandheldStyle = false;
/**
- * If set, 'screen' and 'handheld' media specifiers for stylesheets are
- * transformed such that they apply to the iPhone/iPod Touch Mobile Safari,
- * which doesn't recognize 'handheld' but does support media queries on its
- * screen size.
- *
- * Consider only using this if you have a *really good* handheld stylesheet,
- * as iPhone users won't have any way to disable it and use the "grown-up"
- * styles instead.
+ * Array for more like $wgSkipSkin.
*/
-$wgHandheldForIPhone = false;
+$wgSkipSkins = array();
/**
* Allow user Javascript page?
@@ -2604,10 +2911,14 @@ $wgAllowUserCss = false;
*/
$wgAllowUserCssPrefs = true;
-/** Use the site's Javascript page? */
+/**
+ * Use the site's Javascript page?
+ */
$wgUseSiteJs = true;
-/** Use the site's Cascading Style Sheets (CSS)? */
+/**
+ * Use the site's Cascading Style Sheets (CSS)?
+ */
$wgUseSiteCss = true;
/**
@@ -2646,7 +2957,6 @@ $wgEditPageFrameOptions = 'DENY';
* - 'SAMEORIGIN': Allow framing by pages on the same domain.
* - false: Allow all framing.
*/
-
$wgApiFrameOptions = 'DENY';
/**
@@ -2682,9 +2992,10 @@ $wgExperimentalHtmlIds = false;
* The value should be either a string or an array. If it is a string it will be output
* directly as html, however some skins may choose to ignore it. An array is the preferred format
* for the icon, the following keys are used:
- * - src: An absolute url to the image to use for the icon, this is recommended
+ * - src: An absolute url to the image to use for the icon, this is recommended
* but not required, however some skins will ignore icons without an image
- * - url: The url to use in the a element arround the text or icon, if not set an a element will not be outputted
+ * - url: The url to use in the a element around the text or icon, if not set an a element will
+ * not be outputted
* - alt: This is the text form of the icon, it will be displayed without an image in
* skins like Modern or if src is not set, and will otherwise be used as
* the alt="" for the image. This key is required.
@@ -2719,14 +3030,14 @@ $wgUseCombinedLoginLink = false;
* - true = use an icon search button
* - false = use Go & Search buttons
*/
-$wgVectorUseSimpleSearch = false;
+$wgVectorUseSimpleSearch = true;
/**
* 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;
+$wgVectorUseIconWatch = true;
/**
* Display user edit counts in various prominent places.
@@ -2754,16 +3065,24 @@ $wgBetterDirectionality = true;
*/
$wgSend404Code = true;
-
/**
* The $wgShowRollbackEditCount variable is used to show how many edits will be
- * rollback. The numeric value of the varible are the limit up to are counted.
- * If the value is false or 0, the edits are not counted.
+ * rollback. The numeric value of the variable are the limit up to are counted.
+ * If the value is false or 0, the edits are not counted. Disabling this will
+ * furthermore prevent MediaWiki from hiding some useless rollback links.
*
* @since 1.20
*/
$wgShowRollbackEditCount = 10;
+/**
+ * Output a <link rel="canonical"> tag on every page indicating the canonical
+ * server which should be used, i.e. $wgServer or $wgCanonicalServer. Since
+ * detection of the current server is unreliable, the link is sent
+ * unconditionally.
+ */
+$wgEnableCanonicalServerLink = false;
+
/** @} */ # End of output format settings }
/*************************************************************************//**
@@ -2890,21 +3209,21 @@ $wgPreloadJavaScriptMwUtil = false;
*
* @par Example of legacy code:
* @code{,js}
- * if ( window.wgRestrictionEdit ) { ... }
+ * if ( window.wgRestrictionEdit ) { ... }
* @endcode
* or:
* @code{,js}
- * if ( wgIsArticle ) { ... }
+ * if ( wgIsArticle ) { ... }
* @endcode
*
* Instead, one needs to use mw.config.
* @par Example using mw.config global configuration:
* @code{,js}
- * if ( mw.config.exists('wgRestrictionEdit') ) { ... }
+ * if ( mw.config.exists('wgRestrictionEdit') ) { ... }
* @endcode
* or:
* @code{,js}
- * if ( mw.config.get('wgIsArticle') ) { ... }
+ * if ( mw.config.get('wgIsArticle') ) { ... }
* @endcode
*/
$wgLegacyJavaScriptGlobals = true;
@@ -2919,8 +3238,10 @@ $wgLegacyJavaScriptGlobals = true;
*
* If set to a negative number, ResourceLoader will assume there is no query
* string length limit.
+ *
+ * Defaults to a value based on php configuration.
*/
-$wgResourceLoaderMaxQueryLength = -1;
+$wgResourceLoaderMaxQueryLength = false;
/**
* If set to true, JavaScript modules loaded from wiki pages will be parsed
@@ -2948,8 +3269,60 @@ $wgResourceLoaderValidateStaticJS = false;
*/
$wgResourceLoaderExperimentalAsyncLoading = false;
-/** @} */ # End of resource loader settings }
+/**
+ * Global LESS variables. An associative array binding variable names to CSS
+ * string values.
+ *
+ * Because the hashed contents of this array are used to construct the cache key
+ * that ResourceLoader uses to look up LESS compilation results, updating this
+ * array can be used to deliberately invalidate the set of cached results.
+ *
+ * @par Example:
+ * @code
+ * $wgResourceLoaderLESSVars = array(
+ * 'baseFontSize' => '1em',
+ * 'smallFontSize' => '0.75em',
+ * 'WikimediaBlue' => '#006699',
+ * );
+ * @endcode
+ * @since 1.22
+ */
+$wgResourceLoaderLESSVars = array();
+/**
+ * Custom LESS functions. An associative array mapping function name to PHP
+ * callable.
+ *
+ * Changes to LESS functions do not trigger cache invalidation. If you update
+ * the behavior of a LESS function and need to invalidate stale compilation
+ * results, you can touch one of values in $wgResourceLoaderLESSVars, as
+ * documented above.
+ *
+ * @since 1.22
+ */
+$wgResourceLoaderLESSFunctions = array(
+ 'embeddable' => 'ResourceLoaderLESSFunctions::embeddable',
+ 'embed' => 'ResourceLoaderLESSFunctions::embed',
+);
+
+/**
+ * Default import paths for LESS modules. LESS files referenced in @import
+ * statements will be looked up here first, and relative to the importing file
+ * second. To avoid collisions, it's important for the LESS files in these
+ * directories to have a common, predictable file name prefix.
+ *
+ * Extensions need not (and should not) register paths in
+ * $wgResourceLoaderLESSImportPaths. The import path includes the path of the
+ * currently compiling LESS file, which allows each extension to freely import
+ * files from its own tree.
+ *
+ * @since 1.22
+ */
+$wgResourceLoaderLESSImportPaths = array(
+ "$IP/resources/mediawiki.less/",
+);
+
+/** @} */ # End of resource loader settings }
/*************************************************************************//**
* @name Page title and interwiki link settings
@@ -3035,7 +3408,8 @@ $wgNamespaceAliases = array();
* - []{}|# Are needed for link syntax, never enable these
* - <> Causes problems with HTML escaping, don't use
* - % Enabled by default, minor problems with path to query rewrite rules, see below
- * - + Enabled by default, but doesn't work with path to query rewrite rules, corrupted by apache
+ * - + Enabled by default, but doesn't work with path to query rewrite rules,
+ * corrupted by apache
* - ? Enabled by default, but doesn't work with path to PATH_INFO rewrites
*
* All three of these punctuation problems can be avoided by using an alias,
@@ -3069,6 +3443,7 @@ $wgInterwikiExpiry = 10800;
* @name Interwiki caching settings.
* @{
*/
+
/**
*$wgInterwikiCache specifies path to constant database file.
*
@@ -3083,17 +3458,20 @@ $wgInterwikiExpiry = 10800;
* data layout.
*/
$wgInterwikiCache = false;
+
/**
* Specify number of domains to check for messages.
- * - 1: Just wiki(db)-level
- * - 2: wiki and global levels
- * - 3: site levels
+ * - 1: Just wiki(db)-level
+ * - 2: wiki and global levels
+ * - 3: site levels
*/
$wgInterwikiScopes = 3;
+
/**
- * $wgInterwikiFallbackSite - if unable to resolve from cache
+ * Fallback site, if unable to resolve from cache
*/
$wgInterwikiFallbackSite = 'wiki';
+
/** @} */ # end of Interwiki caching settings.
/**
@@ -3133,25 +3511,28 @@ $wgCapitalLinks = true;
*
* @par Example:
* @code
- * $wgCapitalLinkOverrides[ NS_FILE ] = false;
+ * $wgCapitalLinkOverrides[ NS_FILE ] = false;
* @endcode
*/
$wgCapitalLinkOverrides = array();
-/** Which namespaces should support subpages?
+/**
+ * Which namespaces should support subpages?
* See Language.php for a list of namespaces.
*/
$wgNamespacesWithSubpages = array(
- NS_TALK => true,
- NS_USER => true,
- NS_USER_TALK => true,
- NS_PROJECT_TALK => true,
- NS_FILE_TALK => true,
- NS_MEDIAWIKI => true,
+ NS_TALK => true,
+ NS_USER => true,
+ NS_USER_TALK => true,
+ NS_PROJECT => 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
+ NS_TEMPLATE_TALK => true,
+ NS_HELP => true,
+ NS_HELP_TALK => true,
+ NS_CATEGORY_TALK => true
);
/**
@@ -3179,7 +3560,7 @@ $wgMaxRedirects = 1;
* As of now, this only checks special pages. Redirects to pages in
* other namespaces cannot be invalidated by this variable.
*/
-$wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk' );
+$wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk', 'Redirect' );
/** @} */ # End of title and interwiki settings }
@@ -3195,7 +3576,7 @@ $wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk' );
* class The class name
*
* preprocessorClass The preprocessor class. Two classes are currently available:
- * Preprocessor_Hash, which uses plain PHP arrays for tempoarary
+ * Preprocessor_Hash, which uses plain PHP arrays for temporary
* storage, and Preprocessor_DOM, which uses the DOM module for
* temporary storage. Preprocessor_DOM generally uses less memory;
* the speed of the two is roughly the same.
@@ -3215,7 +3596,9 @@ $wgParserConf = array(
#'preprocessorClass' => 'Preprocessor_Hash',
);
-/** Maximum indent level of toc. */
+/**
+ * Maximum indent level of toc.
+ */
$wgMaxTocLevel = 999;
/**
@@ -3225,12 +3608,16 @@ $wgMaxTocLevel = 999;
$wgMaxPPNodeCount = 1000000;
/**
- * A complexity limit on template expansion: the maximum number of nodes
- * generated by Preprocessor::preprocessToObj()
+ * A complexity limit on template expansion: the maximum number of elements
+ * generated by Preprocessor::preprocessToObj(). This allows you to limit the
+ * amount of memory used by the Preprocessor_DOM node cache: testing indicates
+ * that each element uses about 160 bytes of memory on a 64-bit processor, so
+ * this default corresponds to about 155 MB.
+ *
+ * When the limit is exceeded, an exception is thrown.
*/
$wgMaxGeneratedPPNodeCount = 1000000;
-
/**
* Maximum recursion depth for templates within templates.
* The current parser adds two levels to the PHP call stack for each template,
@@ -3239,25 +3626,41 @@ $wgMaxGeneratedPPNodeCount = 1000000;
*/
$wgMaxTemplateDepth = 40;
-/** @see $wgMaxTemplateDepth */
+/**
+ * @see $wgMaxTemplateDepth
+ */
$wgMaxPPExpandDepth = 40;
-/** The external URL protocols */
+/**
+ * The external URL protocols
+ */
$wgUrlProtocols = array(
'http://',
'https://',
'ftp://',
+ 'ftps://', // If we allow ftp:// we should allow the secure version.
+ 'ssh://',
+ 'sftp://', // SFTP > FTP
'irc://',
- 'ircs://', // @bug 28503
+ 'ircs://', // @bug 28503
+ 'xmpp:', // Another open communication protocol
+ 'sip:',
+ 'sips:',
'gopher://',
'telnet://', // Well if we're going to support the above.. -ævar
'nntp://', // @bug 3808 RFC 1738
'worldwind://',
'mailto:',
+ 'tel:', // If we can make emails linkable, why not phone numbers?
+ 'sms:', // Likewise this is standardized too
'news:',
'svn://',
'git://',
'mms://',
+ 'bitcoin:', // Even registerProtocolHandler whitelists this along with mailto:
+ 'magnet:', // No reason to reject torrents over magnet: when they're allowed over http://
+ 'urn:', // Allow URNs to be used in Microdata/RDFa <link ... href="urn:...">s
+ 'geo:', // urls define geo locations, they're useful in Microdata/RDFa and for coordinates
'//', // for protocol-relative URLs
);
@@ -3266,7 +3669,9 @@ $wgUrlProtocols = array(
*/
$wgCleanSignatures = true;
-/** Whether to allow inline image pointing to other websites */
+/**
+ * Whether to allow inline image pointing to other websites
+ */
$wgAllowExternalImages = false;
/**
@@ -3283,7 +3688,8 @@ $wgAllowExternalImages = false;
*/
$wgAllowExternalImagesFrom = '';
-/** If $wgAllowExternalImages is false, you can allow an on-wiki
+/**
+ * If $wgAllowExternalImages is false, you can allow an on-wiki
* whitelist of regular expression fragments to match the image URL
* against. If the image matches one of the regular expression fragments,
* The image will be displayed.
@@ -3319,15 +3725,30 @@ $wgAllowImageTag = false;
* 'extension=tidy.so' to php.ini.
*/
$wgUseTidy = false;
-/** @see $wgUseTidy */
+
+/**
+ * @see $wgUseTidy
+ */
$wgAlwaysUseTidy = false;
-/** @see $wgUseTidy */
+
+/**
+ * @see $wgUseTidy
+ */
$wgTidyBin = 'tidy';
-/** @see $wgUseTidy */
-$wgTidyConf = $IP.'/includes/tidy.conf';
-/** @see $wgUseTidy */
+
+/**
+ * @see $wgUseTidy
+ */
+$wgTidyConf = $IP . '/includes/tidy.conf';
+
+/**
+ * @see $wgUseTidy
+ */
$wgTidyOpts = '';
-/** @see $wgUseTidy */
+
+/**
+ * @see $wgUseTidy
+ */
$wgTidyInternal = extension_loaded( 'tidy' );
/**
@@ -3336,7 +3757,8 @@ $wgTidyInternal = extension_loaded( 'tidy' );
*/
$wgDebugTidy = false;
-/** Allow raw, unchecked HTML in "<html>...</html>" sections.
+/**
+ * Allow raw, unchecked HTML in "<html>...</html>" sections.
* THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions
* TO RESTRICT EDITING to only those that you trust
*/
@@ -3380,8 +3802,9 @@ $wgNoFollowDomainExceptions = array();
$wgAllowDisplayTitle = true;
/**
- * For consistency, restrict DISPLAYTITLE to titles that normalize to the same
- * canonical DB key.
+ * For consistency, restrict DISPLAYTITLE to text that normalizes to the same
+ * canonical DB key. Also disallow some inline CSS rules like display: none;
+ * which can cause the text to be hidden or unselectable.
*/
$wgRestrictDisplayTitle = true;
@@ -3398,12 +3821,13 @@ $wgExpensiveParserFunctionLimit = 100;
$wgPreprocessorCacheThreshold = 1000;
/**
- * Enable interwiki transcluding. Only when iw_trans=1.
+ * Enable interwiki transcluding. Only when iw_trans=1 in the interwiki table.
*/
$wgEnableScaryTranscluding = false;
/**
- * (see next option $wgGlobalDatabase).
+ * Expiry time for transcluded templates cached in transcache database table.
+ * Only used $wgEnableInterwikiTranscluding is set to true.
*/
$wgTranscludeCacheExpiry = 3600;
@@ -3453,7 +3877,8 @@ $wgHitcounterUpdateFreq = 1;
/**
* How many days user must be idle before he is considered inactive. Will affect
- * the number shown on Special:Statistics and Special:ActiveUsers special page.
+ * the number shown on Special:Statistics, Special:ActiveUsers, and the
+ * {{NUMBEROFACTIVEUSERS}} magic word in wikitext.
* You might want to leave this as the default value, to provide comparable
* numbers between different wikis.
*/
@@ -3466,7 +3891,9 @@ $wgActiveUserDays = 30;
* @{
*/
-/** For compatibility with old installations set to false */
+/**
+ * For compatibility with old installations set to false
+ */
$wgPasswordSalt = true;
/**
@@ -3509,7 +3936,7 @@ $wgReservedUsernames = array(
'ScriptImporter', // Default user name used by maintenance/importSiteScripts.php
'msg:double-redirect-fixer', // Automatic double redirect fix
'msg:usermessage-editor', // Default user for leaving user messages
- 'msg:proxyblocker', // For Special:Blockme
+ 'msg:proxyblocker', // For $wgProxyList and Special:Blockme (removed in 1.22)
);
/**
@@ -3517,81 +3944,74 @@ $wgReservedUsernames = array(
* preferences used by anonymous visitors and newly created accounts.
* For instance, to disable section editing links:
* $wgDefaultUserOptions ['editsection'] = 0;
- *
*/
$wgDefaultUserOptions = array(
- 'ccmeonemails' => 0,
- 'cols' => 80,
- 'date' => 'default',
- 'diffonly' => 0,
- 'disablemail' => 0,
- 'disablesuggest' => 0,
- 'editfont' => 'default',
- 'editondblclick' => 0,
- 'editsection' => 1,
+ 'ccmeonemails' => 0,
+ 'cols' => 80,
+ 'date' => 'default',
+ 'diffonly' => 0,
+ 'disablemail' => 0,
+ 'disablesuggest' => 0,
+ 'editfont' => 'default',
+ 'editondblclick' => 0,
+ 'editsection' => 1,
'editsectiononrightclick' => 0,
- 'enotifminoredits' => 0,
- 'enotifrevealaddr' => 0,
- 'enotifusertalkpages' => 1,
- 'enotifwatchlistpages' => 0,
- 'extendwatchlist' => 0,
- 'externaldiff' => 0,
- 'externaleditor' => 0,
- 'fancysig' => 0,
- 'forceeditsummary' => 0,
- 'gender' => 'unknown',
- 'hideminor' => 0,
- 'hidepatrolled' => 0,
- 'imagesize' => 2,
- 'justify' => 0,
- 'math' => 1,
- 'minordefault' => 0,
- 'newpageshidepatrolled' => 0,
- 'nocache' => 0,
- 'noconvertlink' => 0,
- 'norollbackdiff' => 0,
- 'numberheadings' => 0,
- 'previewonfirst' => 0,
- 'previewontop' => 1,
- 'quickbar' => 5,
- 'rcdays' => 7,
- 'rclimit' => 50,
- 'rememberpassword' => 0,
- 'rows' => 25,
- 'searchlimit' => 20,
- 'showhiddencats' => 0,
- 'showjumplinks' => 1,
- 'shownumberswatching' => 1,
- 'showtoc' => 1,
- 'showtoolbar' => 1,
- 'skin' => false,
- 'stubthreshold' => 0,
- 'thumbsize' => 2,
- 'underline' => 2,
- 'uselivepreview' => 0,
- 'usenewrc' => 0,
- 'watchcreations' => 0,
- 'watchdefault' => 0,
- 'watchdeletion' => 0,
- 'watchlistdays' => 3.0,
- 'watchlisthideanons' => 0,
- 'watchlisthidebots' => 0,
- 'watchlisthideliu' => 0,
- 'watchlisthideminor' => 0,
- 'watchlisthideown' => 0,
- 'watchlisthidepatrolled' => 0,
- 'watchmoves' => 0,
- 'wllimit' => 250,
+ 'enotifminoredits' => 0,
+ 'enotifrevealaddr' => 0,
+ 'enotifusertalkpages' => 1,
+ 'enotifwatchlistpages' => 0,
+ 'extendwatchlist' => 0,
+ 'fancysig' => 0,
+ 'forceeditsummary' => 0,
+ 'gender' => 'unknown',
+ 'hideminor' => 0,
+ 'hidepatrolled' => 0,
+ 'imagesize' => 2,
+ 'justify' => 0,
+ 'math' => 1,
+ 'minordefault' => 0,
+ 'newpageshidepatrolled' => 0,
+ 'nocache' => 0,
+ 'noconvertlink' => 0,
+ 'norollbackdiff' => 0,
+ 'numberheadings' => 0,
+ 'previewonfirst' => 0,
+ 'previewontop' => 1,
+ 'rcdays' => 7,
+ 'rclimit' => 50,
+ 'rememberpassword' => 0,
+ 'rows' => 25,
+ 'searchlimit' => 20,
+ 'showhiddencats' => 0,
+ 'shownumberswatching' => 1,
+ 'showtoc' => 1,
+ 'showtoolbar' => 1,
+ 'skin' => false,
+ 'stubthreshold' => 0,
+ 'thumbsize' => 2,
+ 'underline' => 2,
+ 'uselivepreview' => 0,
+ 'usenewrc' => 0,
+ 'vector-simplesearch' => 1,
+ 'watchcreations' => 0,
+ 'watchdefault' => 0,
+ 'watchdeletion' => 0,
+ 'watchlistdays' => 3.0,
+ 'watchlisthideanons' => 0,
+ 'watchlisthidebots' => 0,
+ 'watchlisthideliu' => 0,
+ 'watchlisthideminor' => 0,
+ 'watchlisthideown' => 0,
+ 'watchlisthidepatrolled' => 0,
+ 'watchmoves' => 0,
+ 'wllimit' => 250,
+ 'useeditwarning' => 1,
+ 'prefershttps' => 1,
);
/**
- * Whether or not to allow and use real name fields.
- * @deprecated since 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real
- * names
+ * An array of preferences to not show for the user
*/
-$wgAllowRealName = true;
-
-/** An array of preferences to not show for the user */
$wgHiddenPrefs = array();
/**
@@ -3613,66 +4033,9 @@ $wgInvalidUsernameCharacters = '@';
$wgUserrightsInterwikiDelimiter = '@';
/**
- * 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.
- */
-$wgExternalAuthConf = 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();
-
-/**
* This is to let user authenticate using https when they come from http.
* Based on an idea by George Herbert on wikitech-l:
- * http://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050065.html
+ * http://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html
* @since 1.17
*/
$wgSecureLogin = false;
@@ -3694,7 +4057,9 @@ $wgAutoblockExpiry = 86400;
*/
$wgBlockAllowsUTEdit = false;
-/** Allow sysops to ban users from accessing Emailuser */
+/**
+ * Allow sysops to ban users from accessing Emailuser
+ */
$wgSysopEmailBans = true;
/**
@@ -3745,6 +4110,34 @@ $wgBlockDisablesLogin = false;
$wgWhitelistRead = false;
/**
+ * Pages anonymous user may see, set as an array of regular expressions.
+ *
+ * This function will match the regexp against the title name, which
+ * is without underscore.
+ *
+ * @par Example:
+ * To whitelist [[Main Page]]:
+ * @code
+ * $wgWhitelistReadRegexp = array( "/Main Page/" );
+ * @endcode
+ *
+ * @note Unless ^ and/or $ is specified, a regular expression might match
+ * pages not intended to be whitelisted. The above example will also
+ * whitelist a page named 'Security Main Page'.
+ *
+ * @par Example:
+ * To allow reading any page starting with 'User' regardless of the case:
+ * @code
+ * $wgWhitelistReadRegexp = array( "@^UsEr.*@i" );
+ * @endcode
+ * Will allow both [[User is banned]] and [[User:JohnDoe]]
+ *
+ * @note This will only work if $wgGroupPermissions['*']['read'] is false --
+ * see below. Otherwise, ALL pages are accessible, regardless of this setting.
+ */
+$wgWhitelistReadRegexp = false;
+
+/**
* Should editors be required to have a validated e-mail
* address before being allowed to edit?
*/
@@ -3778,93 +4171,106 @@ $wgGroupPermissions = array();
/** @cond file_level_code */
// Implicit group for all visitors
-$wgGroupPermissions['*']['createaccount'] = true;
-$wgGroupPermissions['*']['read'] = true;
-$wgGroupPermissions['*']['edit'] = true;
-$wgGroupPermissions['*']['createpage'] = true;
-$wgGroupPermissions['*']['createtalk'] = true;
-$wgGroupPermissions['*']['writeapi'] = true;
-//$wgGroupPermissions['*']['patrolmarks'] = false; // let anons see what was patrolled
+$wgGroupPermissions['*']['createaccount'] = true;
+$wgGroupPermissions['*']['read'] = true;
+$wgGroupPermissions['*']['edit'] = true;
+$wgGroupPermissions['*']['createpage'] = true;
+$wgGroupPermissions['*']['createtalk'] = true;
+$wgGroupPermissions['*']['writeapi'] = true;
+$wgGroupPermissions['*']['editmyusercss'] = true;
+$wgGroupPermissions['*']['editmyuserjs'] = true;
+$wgGroupPermissions['*']['viewmywatchlist'] = true;
+$wgGroupPermissions['*']['editmywatchlist'] = true;
+$wgGroupPermissions['*']['viewmyprivateinfo'] = true;
+$wgGroupPermissions['*']['editmyprivateinfo'] = true;
+$wgGroupPermissions['*']['editmyoptions'] = true;
+#$wgGroupPermissions['*']['patrolmarks'] = false; // let anons see what was patrolled
// Implicit group for all logged-in accounts
-$wgGroupPermissions['user']['move'] = true;
-$wgGroupPermissions['user']['move-subpages'] = true;
+$wgGroupPermissions['user']['move'] = true;
+$wgGroupPermissions['user']['move-subpages'] = true;
$wgGroupPermissions['user']['move-rootuserpages'] = true; // can move root userpages
-$wgGroupPermissions['user']['movefile'] = true;
-$wgGroupPermissions['user']['read'] = true;
-$wgGroupPermissions['user']['edit'] = true;
-$wgGroupPermissions['user']['createpage'] = true;
-$wgGroupPermissions['user']['createtalk'] = true;
-$wgGroupPermissions['user']['writeapi'] = true;
-$wgGroupPermissions['user']['upload'] = true;
-$wgGroupPermissions['user']['reupload'] = true;
-$wgGroupPermissions['user']['reupload-shared'] = true;
-$wgGroupPermissions['user']['minoredit'] = true;
-$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok"
-$wgGroupPermissions['user']['sendemail'] = true;
+$wgGroupPermissions['user']['movefile'] = true;
+$wgGroupPermissions['user']['read'] = true;
+$wgGroupPermissions['user']['edit'] = true;
+$wgGroupPermissions['user']['createpage'] = true;
+$wgGroupPermissions['user']['createtalk'] = true;
+$wgGroupPermissions['user']['writeapi'] = true;
+$wgGroupPermissions['user']['upload'] = true;
+$wgGroupPermissions['user']['reupload'] = true;
+$wgGroupPermissions['user']['reupload-shared'] = true;
+$wgGroupPermissions['user']['minoredit'] = true;
+$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok"
+$wgGroupPermissions['user']['sendemail'] = true;
// Implicit group for accounts that pass $wgAutoConfirmAge
$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true;
+$wgGroupPermissions['autoconfirmed']['editsemiprotected'] = true;
// Users with bot privilege can have their edits hidden
// from various log pages by default
-$wgGroupPermissions['bot']['bot'] = true;
-$wgGroupPermissions['bot']['autoconfirmed'] = true;
-$wgGroupPermissions['bot']['nominornewtalk'] = true;
-$wgGroupPermissions['bot']['autopatrol'] = true;
+$wgGroupPermissions['bot']['bot'] = true;
+$wgGroupPermissions['bot']['autoconfirmed'] = true;
+$wgGroupPermissions['bot']['editsemiprotected'] = true;
+$wgGroupPermissions['bot']['nominornewtalk'] = true;
+$wgGroupPermissions['bot']['autopatrol'] = true;
$wgGroupPermissions['bot']['suppressredirect'] = true;
-$wgGroupPermissions['bot']['apihighlimits'] = true;
-$wgGroupPermissions['bot']['writeapi'] = true;
-#$wgGroupPermissions['bot']['editprotected'] = true; // can edit all protected pages without cascade protection enabled
+$wgGroupPermissions['bot']['apihighlimits'] = true;
+$wgGroupPermissions['bot']['writeapi'] = true;
// Most extra permission abilities go to this group
-$wgGroupPermissions['sysop']['block'] = true;
-$wgGroupPermissions['sysop']['createaccount'] = true;
-$wgGroupPermissions['sysop']['delete'] = true;
-$wgGroupPermissions['sysop']['bigdelete'] = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs
-$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text
-$wgGroupPermissions['sysop']['deletedtext'] = true; // can view deleted revision text
-$wgGroupPermissions['sysop']['undelete'] = true;
-$wgGroupPermissions['sysop']['editinterface'] = true;
-$wgGroupPermissions['sysop']['editusercss'] = true;
-$wgGroupPermissions['sysop']['edituserjs'] = true;
-$wgGroupPermissions['sysop']['import'] = true;
-$wgGroupPermissions['sysop']['importupload'] = true;
-$wgGroupPermissions['sysop']['move'] = true;
-$wgGroupPermissions['sysop']['move-subpages'] = true;
+$wgGroupPermissions['sysop']['block'] = true;
+$wgGroupPermissions['sysop']['createaccount'] = true;
+$wgGroupPermissions['sysop']['delete'] = true;
+// can be separately configured for pages with > $wgDeleteRevisionsLimit revs
+$wgGroupPermissions['sysop']['bigdelete'] = true;
+// can view deleted history entries, but not see or restore the text
+$wgGroupPermissions['sysop']['deletedhistory'] = true;
+// can view deleted revision text
+$wgGroupPermissions['sysop']['deletedtext'] = true;
+$wgGroupPermissions['sysop']['undelete'] = true;
+$wgGroupPermissions['sysop']['editinterface'] = true;
+$wgGroupPermissions['sysop']['editusercss'] = true;
+$wgGroupPermissions['sysop']['edituserjs'] = true;
+$wgGroupPermissions['sysop']['import'] = true;
+$wgGroupPermissions['sysop']['importupload'] = true;
+$wgGroupPermissions['sysop']['move'] = true;
+$wgGroupPermissions['sysop']['move-subpages'] = true;
$wgGroupPermissions['sysop']['move-rootuserpages'] = true;
-$wgGroupPermissions['sysop']['patrol'] = true;
-$wgGroupPermissions['sysop']['autopatrol'] = true;
-$wgGroupPermissions['sysop']['protect'] = true;
-$wgGroupPermissions['sysop']['proxyunbannable'] = true;
-$wgGroupPermissions['sysop']['rollback'] = true;
-$wgGroupPermissions['sysop']['upload'] = true;
-$wgGroupPermissions['sysop']['reupload'] = true;
-$wgGroupPermissions['sysop']['reupload-shared'] = true;
-$wgGroupPermissions['sysop']['unwatchedpages'] = true;
-$wgGroupPermissions['sysop']['autoconfirmed'] = true;
-$wgGroupPermissions['sysop']['ipblock-exempt'] = true;
-$wgGroupPermissions['sysop']['blockemail'] = true;
-$wgGroupPermissions['sysop']['markbotedits'] = true;
-$wgGroupPermissions['sysop']['apihighlimits'] = true;
-$wgGroupPermissions['sysop']['browsearchive'] = true;
-$wgGroupPermissions['sysop']['noratelimit'] = true;
-$wgGroupPermissions['sysop']['movefile'] = true;
-$wgGroupPermissions['sysop']['unblockself'] = true;
+$wgGroupPermissions['sysop']['patrol'] = true;
+$wgGroupPermissions['sysop']['autopatrol'] = true;
+$wgGroupPermissions['sysop']['protect'] = true;
+$wgGroupPermissions['sysop']['editprotected'] = true;
+$wgGroupPermissions['sysop']['proxyunbannable'] = true;
+$wgGroupPermissions['sysop']['rollback'] = true;
+$wgGroupPermissions['sysop']['upload'] = true;
+$wgGroupPermissions['sysop']['reupload'] = true;
+$wgGroupPermissions['sysop']['reupload-shared'] = true;
+$wgGroupPermissions['sysop']['unwatchedpages'] = true;
+$wgGroupPermissions['sysop']['autoconfirmed'] = true;
+$wgGroupPermissions['sysop']['editsemiprotected'] = true;
+$wgGroupPermissions['sysop']['ipblock-exempt'] = true;
+$wgGroupPermissions['sysop']['blockemail'] = true;
+$wgGroupPermissions['sysop']['markbotedits'] = true;
+$wgGroupPermissions['sysop']['apihighlimits'] = true;
+$wgGroupPermissions['sysop']['browsearchive'] = true;
+$wgGroupPermissions['sysop']['noratelimit'] = true;
+$wgGroupPermissions['sysop']['movefile'] = true;
+$wgGroupPermissions['sysop']['unblockself'] = true;
$wgGroupPermissions['sysop']['suppressredirect'] = true;
-#$wgGroupPermissions['sysop']['upload_by_url'] = true;
-#$wgGroupPermissions['sysop']['mergehistory'] = true;
+#$wgGroupPermissions['sysop']['upload_by_url'] = true;
+#$wgGroupPermissions['sysop']['mergehistory'] = true;
// Permission to change users' group assignments
-$wgGroupPermissions['bureaucrat']['userrights'] = true;
+$wgGroupPermissions['bureaucrat']['userrights'] = true;
$wgGroupPermissions['bureaucrat']['noratelimit'] = true;
// Permission to change users' groups assignments across wikis
#$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true;
// Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth
#$wgGroupPermissions['bureaucrat']['override-export-depth'] = true;
-#$wgGroupPermissions['sysop']['deletelogentry'] = true;
-#$wgGroupPermissions['sysop']['deleterevision'] = true;
+#$wgGroupPermissions['sysop']['deletelogentry'] = true;
+#$wgGroupPermissions['sysop']['deleterevision'] = true;
// To hide usernames from users and Sysops
#$wgGroupPermissions['suppress']['hideuser'] = true;
// To hide revisions/log items from users and Sysops
@@ -3918,11 +4324,12 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
* @endcode
* This allows users in the '*' group (i.e. any user) to remove themselves from
* any group that they happen to be in.
- *
*/
$wgGroupsAddToSelf = array();
-/** @see $wgGroupsAddToSelf */
+/**
+ * @see $wgGroupsAddToSelf
+ */
$wgGroupsRemoveFromSelf = array();
/**
@@ -3942,11 +4349,37 @@ $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' );
* dictates the order on the protection form's lists.
*
* - '' will be ignored (i.e. unprotected)
- * - 'sysop' is quietly rewritten to 'protect' for backwards compatibility
+ * - 'autoconfirmed' is quietly rewritten to 'editsemiprotected' for backwards compatibility
+ * - 'sysop' is quietly rewritten to 'editprotected' for backwards compatibility
*/
$wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' );
/**
+ * Restriction levels that can be used with cascading protection
+ *
+ * A page can only be protected with cascading protection if the
+ * requested restriction level is included in this array.
+ *
+ * 'autoconfirmed' is quietly rewritten to 'editsemiprotected' for backwards compatibility.
+ * 'sysop' is quietly rewritten to 'editprotected' for backwards compatibility.
+ */
+$wgCascadingRestrictionLevels = array( 'sysop' );
+
+/**
+ * Restriction levels that should be considered "semiprotected"
+ *
+ * Certain places in the interface recognize a dichotomy between "protected"
+ * and "semiprotected", without further distinguishing the specific levels. In
+ * general, if anyone can be eligible to edit a protection level merely by
+ * reaching some condition in $wgAutopromote, it should probably be considered
+ * "semiprotected".
+ *
+ * 'autoconfirmed' is quietly rewritten to 'editsemiprotected' for backwards compatibility.
+ * 'sysop' is not changed, since it really shouldn't be here.
+ */
+$wgSemiprotectedRestrictionLevels = array( 'autoconfirmed' );
+
+/**
* Set the minimum permissions required to edit pages in each
* namespace. If you list more than one permission, a user must
* have all of them to edit pages in that namespace.
@@ -3962,7 +4395,7 @@ $wgNamespaceProtection = array();
* namespaces constants (NS_USER, NS_MAIN...).
*
* Among other things, this may be useful to enforce read-restrictions
- * which may otherwise be bypassed by using the template machanism.
+ * which may otherwise be bypassed by using the template mechanism.
*/
$wgNonincludableNamespaces = array();
@@ -4037,11 +4470,11 @@ $wgAutopromote = array(
*
* The format is:
* @code
- * array( event => criteria, ... )
+ * array( event => criteria, ... )
* @endcode
* Where event is either:
- * - 'onEdit' (when user edits)
- * - 'onView' (when user views the wiki)
+ * - 'onEdit' (when user edits)
+ * - 'onView' (when user views the wiki)
*
* Criteria has the same format as $wgAutopromote
*
@@ -4082,7 +4515,10 @@ $wgAutopromoteOnceLogInRC = true;
* @endcode
*/
$wgAddGroups = array();
-/** @see $wgAddGroups */
+
+/**
+ * @see $wgAddGroups
+ */
$wgRemoveGroups = array();
/**
@@ -4100,7 +4536,8 @@ $wgDeleteRevisionsLimit = 0;
/**
* Number of accounts each IP address may create, 0 to disable.
*
- * @warning Requires memcached */
+ * @warning Requires memcached
+ */
$wgAccountCreationThrottle = 0;
/**
@@ -4116,7 +4553,9 @@ $wgAccountCreationThrottle = 0;
*/
$wgSpamRegex = array();
-/** Same as the above except for edit summaries */
+/**
+ * Same as the above except for edit summaries
+ */
$wgSummarySpamRegex = array();
/**
@@ -4170,6 +4609,13 @@ $wgSorbsUrl = array();
$wgProxyWhitelist = array();
/**
+ * Whether to look at the X-Forwarded-For header's list of (potentially spoofed)
+ * IPs and apply IP blocks to them. This allows for IP blocks to work with correctly-configured
+ * (transparent) proxies without needing to block the proxies themselves.
+ */
+$wgApplyIpBlocksToXff = false;
+
+/**
* Simple rate limiter options to brake edit floods.
*
* Maximum number actions allowed in the given number of seconds; after that
@@ -4191,25 +4637,39 @@ $wgProxyWhitelist = array();
*/
$wgRateLimits = array(
'edit' => array(
- 'anon' => null, // for any and all anonymous edits (aggregate)
- 'user' => null, // for each logged-in user
+ 'anon' => null, // for any and all anonymous edits (aggregate)
+ 'user' => null, // for each logged-in user
'newbie' => null, // for each recent (autoconfirmed) account; overrides 'user'
- 'ip' => null, // for each anon and recent account
- 'subnet' => null, // ... with final octet removed
- ),
+ 'ip' => null, // for each anon and recent account
+ 'subnet' => null, // ... within a /24 subnet in IPv4 or /64 in IPv6
+ ),
'move' => array(
- 'user' => null,
+ 'user' => null,
'newbie' => null,
- 'ip' => null,
+ 'ip' => null,
'subnet' => null,
- ),
- 'mailpassword' => array(
+ ),
+ 'mailpassword' => array( // triggering password resets emails
'anon' => null,
- ),
- 'emailuser' => array(
+ ),
+ 'emailuser' => array( // emailing other users using MediaWiki
'user' => null,
- ),
- );
+ ),
+ 'linkpurge' => array( // purges of link tables
+ 'anon' => null,
+ 'user' => null,
+ 'newbie' => null,
+ 'ip' => null,
+ 'subnet' => null,
+ ),
+ 'renderfile' => array( // files rendered via thumb.php or thumb_handler.php
+ 'anon' => null,
+ 'user' => null,
+ 'newbie' => null,
+ 'ip' => null,
+ 'subnet' => null,
+ ),
+);
/**
* Set to a filename to log rate limiter hits.
@@ -4225,6 +4685,7 @@ $wgRateLimitsExcludedIPs = array();
/**
* Log IP addresses in the recentchanges table; can be accessed only by
* extensions (e.g. CheckUser) or a DB admin
+ * Used for retroactive autoblocks
*/
$wgPutIPinRC = true;
@@ -4249,26 +4710,24 @@ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
*/
/**
- * If you enable this, every editor's IP address will be scanned for open HTTP
- * proxies.
- *
- * @warning Don't enable this. Many sysops will report "hostile TCP port scans"
- * to your ISP and ask for your server to be shut down.
- * You have been warned.
- *
+ * This should always be customised in LocalSettings.php
*/
-$wgBlockOpenProxies = false;
-/** Port we want to scan for a proxy */
-$wgProxyPorts = array( 80, 81, 1080, 3128, 6588, 8000, 8080, 8888, 65506 );
-/** Script used to scan */
-$wgProxyScriptPath = "$IP/maintenance/proxy_check.php";
-/** */
-$wgProxyMemcExpiry = 86400;
-/** This should always be customised in LocalSettings.php */
$wgSecretKey = false;
-/** big list of banned IP addresses, in the keys not the values */
+
+/**
+ * Big list of banned IP addresses.
+ *
+ * This can have the following formats:
+ * - An array of addresses, either in the values
+ * or the keys (for backward compatibility)
+ * - A string, in that case this is the path to a file
+ * containing the list of IP addresses, one per line
+ */
$wgProxyList = array();
-/** deprecated */
+
+/**
+ * @deprecated since 1.14
+ */
$wgProxyKey = false;
/** @} */ # end of proxy scanner settings
@@ -4281,7 +4740,7 @@ $wgProxyKey = false;
/**
* Default cookie expiration time. Setting to 0 makes all cookies session-only.
*/
-$wgCookieExpiration = 180*86400;
+$wgCookieExpiration = 180 * 86400;
/**
* Set to set an explicit domain on the login cookies eg, "justthis.domain.org"
@@ -4289,7 +4748,6 @@ $wgCookieExpiration = 180*86400;
*/
$wgCookieDomain = '';
-
/**
* Set this variable if you want to restrict cookies to a certain path within
* the domain specified by $wgCookieDomain.
@@ -4337,13 +4795,17 @@ $wgHttpOnlyBlacklist = array(
'/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/',
);
-/** A list of cookies that vary the cache (for use by extensions) */
+/**
+ * A list of cookies that vary the cache (for use by extensions)
+ */
$wgCacheVaryCookies = array();
-/** Override to customise the session name */
+/**
+ * Override to customise the session name
+ */
$wgSessionName = false;
-/** @} */ # end of cookie settings }
+/** @} */ # end of cookie settings }
/************************************************************************//**
* @name LaTeX (mathematical formulas)
@@ -4358,7 +4820,7 @@ $wgSessionName = false;
*/
$wgUseTeX = false;
-/* @} */ # end LaTeX }
+/** @} */ # end LaTeX }
/************************************************************************//**
* @name Profiling, testing and debugging
@@ -4497,26 +4959,36 @@ $wgDevelopmentWarnings = false;
*/
$wgDeprecationReleaseLimit = false;
-/** Only record profiling info for pages that took longer than this */
+/**
+ * Only record profiling info for pages that took longer than this
+ */
$wgProfileLimit = 0.0;
-/** Don't put non-profiling info into log file */
+/**
+ * Don't put non-profiling info into log file
+ */
$wgProfileOnly = false;
/**
* Log sums from profiling into "profiling" table in db.
*
* You have to create a 'profiling' table in your database before using
- * this feature, see maintenance/archives/patch-profiling.sql
+ * this feature. Run set $wgProfileToDatabase to true in
+ * LocalSettings.php and run maintenance/update.php or otherwise
+ * manually add patch-profiling.sql to your database.
*
* To enable profiling, edit StartProfiler.php
*/
$wgProfileToDatabase = false;
-/** If true, print a raw call tree instead of per-function report */
+/**
+ * If true, print a raw call tree instead of per-function report
+ */
$wgProfileCallTree = false;
-/** Should application server host be put into profiling table */
+/**
+ * Should application server host be put into profiling table
+ */
$wgProfilePerHost = false;
/**
@@ -4533,10 +5005,25 @@ $wgUDPProfilerHost = '127.0.0.1';
*/
$wgUDPProfilerPort = '3811';
-/** Detects non-matching wfProfileIn/wfProfileOut calls */
+/**
+ * Format string for the UDP profiler. The UDP profiler invokes sprintf() with
+ * (profile id, count, cpu, cpu_sq, real, real_sq, entry name) as arguments.
+ * You can use sprintf's argument numbering/swapping capability to repeat,
+ * re-order or omit fields.
+ *
+ * @see $wgStatsFormatString
+ * @since 1.22
+ */
+$wgUDPProfilerFormatString = "%s - %d %f %f %f %f %s\n";
+
+/**
+ * Detects non-matching wfProfileIn/wfProfileOut calls
+ */
$wgDebugProfiling = false;
-/** Output debug message on every wfProfileIn/wfProfileOut */
+/**
+ * Output debug message on every wfProfileIn/wfProfileOut
+ */
$wgDebugFunctionEntry = false;
/**
@@ -4555,12 +5042,33 @@ $wgStatsMethod = 'cache';
*/
$wgAggregateStatsID = false;
-/** Whereas to count the number of time an article is viewed.
+/**
+ * When $wgStatsMethod is 'udp', this variable specifies how stats should be
+ * formatted. Its value should be a format string suitable for a sprintf()
+ * invocation with (id, count, key) arguments, where 'id' is either
+ * $wgAggregateStatsID or the DB name, 'count' is the value by which the metric
+ * is being incremented, and 'key' is the metric name.
+ *
+ * @see $wgUDPProfilerFormatString
+ * @see $wgAggregateStatsID
+ * @since 1.22
+ */
+$wgStatsFormatString = "stats/%s - %s 1 1 1 1 %s\n";
+
+/**
+ * Whereas to count the number of time an article is viewed.
* Does not work if pages are cached (for example with squid).
*/
$wgDisableCounters = false;
/**
+ * InfoAction retrieves a list of transclusion links (both to and from).
+ * This number puts a limit on that query in the case of highly transcluded
+ * templates.
+ */
+$wgPageInfoTransclusionLimit = 50;
+
+/**
* Set this to an integer to only do synchronous site_stats updates
* one every *this many* updates. The other requests go into pending
* delta values in $wgMemc. Make sure that $wgMemc is a global cache.
@@ -4621,7 +5129,6 @@ $wgJavaScriptTestConfig = array(
),
);
-
/**
* Overwrite the caching key prefix with custom value.
* @since 1.19
@@ -4650,7 +5157,7 @@ $wgDebugToolbar = false;
$wgDisableTextSearch = false;
/**
- * Set to true to have nicer highligted text in search results,
+ * Set to true to have nicer highlighted text in search results,
* by default off due to execution overhead
*/
$wgAdvancedSearchHighlighting = false;
@@ -4676,11 +5183,10 @@ $wgCountTotalSearchHits = false;
/**
* Template for OpenSearch suggestions, defaults to API action=opensearch
*
- * Sites with heavy load would tipically have these point to a custom
+ * Sites with heavy load would typically have these point to a custom
* PHP wrapper to avoid firing up mediawiki for every keystroke
*
* Placeholders: {searchTerms}
- *
*/
$wgOpenSearchTemplate = false;
@@ -4724,7 +5230,7 @@ $wgNamespacesToBeSearchedDefault = array(
*/
$wgNamespacesToBeSearchedHelp = array(
NS_PROJECT => true,
- NS_HELP => true,
+ NS_HELP => true,
);
/**
@@ -4732,7 +5238,6 @@ $wgNamespacesToBeSearchedHelp = array(
* logged-in users.
* Useful for big wikis to maintain different search profiles for anonymous and
* logged-in users.
- *
*/
$wgSearchEverythingOnlyLoggedIn = false;
@@ -4751,10 +5256,10 @@ $wgDisableInternalSearch = false;
* To forward to Google you'd have something like:
* @code
* $wgSearchForwardUrl =
- * 'http://www.google.com/search?q=$1' .
- * '&domains=http://example.com' .
- * '&sitesearch=http://example.com' .
- * '&ie=utf-8&oe=utf-8';
+ * 'http://www.google.com/search?q=$1' .
+ * '&domains=http://example.com' .
+ * '&sitesearch=http://example.com' .
+ * '&ie=utf-8&oe=utf-8';
* @endcode
*/
$wgSearchForwardUrl = null;
@@ -4768,14 +5273,14 @@ $wgUseTwoButtonsSearchForm = true;
/**
* Array of namespaces to generate a Google sitemap for when the
- * maintenance/generateSitemap.php script is run, or false if one is to be ge-
- * nerated for all namespaces.
+ * maintenance/generateSitemap.php script is run, or false if one is to be
+ * generated for all namespaces.
*/
$wgSitemapNamespaces = false;
/**
* Custom namespace priorities for sitemaps. Setting this will allow you to
- * set custom priorities to namsepaces when sitemaps are generated using the
+ * set custom priorities to namespaces when sitemaps are generated using the
* maintenance/generateSitemap.php script.
*
* This should be a map of namespace IDs to priority
@@ -4805,7 +5310,7 @@ $wgEnableSearchContributorsByIP = true;
/**
* Path to the GNU diff3 utility. If the file doesn't exist, edit conflicts will
- * fall back to the old behaviour (no merging).
+ * fall back to the old behavior (no merging).
*/
$wgDiff3 = '/usr/bin/diff3';
@@ -4816,7 +5321,7 @@ $wgDiff = '/usr/bin/diff';
/**
* 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)
+ * Internally only Category: pages apply, but using this extensions (e.g. Semantic MediaWiki)
* can specify namespaces of pages they have special treatment for
*/
$wgPreviewOnOpenNamespaces = array(
@@ -4824,12 +5329,8 @@ $wgPreviewOnOpenNamespaces = array(
);
/**
- * Activate external editor interface for files and pages
- * See http://www.mediawiki.org/wiki/Manual:External_editors
+ * Go button goes straight to the edit screen if the article doesn't exist.
*/
-$wgUseExternalEditor = true;
-
-/** Go button goes straight to the edit screen if the article doesn't exist. */
$wgGoToEdit = false;
/**
@@ -4858,12 +5359,14 @@ $wgUseAutomaticEditSummaries = true;
* @cond file_level_code
* Set $wgCommandLineMode if it's not set already, to avoid notices
*/
-if( !isset( $wgCommandLineMode ) ) {
+if ( !isset( $wgCommandLineMode ) ) {
$wgCommandLineMode = false;
}
/** @endcond */
-/** For colorized maintenance script output, is your terminal background dark ? */
+/**
+ * For colorized maintenance script output, is your terminal background dark ?
+ */
$wgCommandLineDarkBg = false;
/**
@@ -4904,6 +5407,11 @@ $wgReadOnlyFile = false;
$wgUpgradeKey = false;
/**
+ * Fully specified path to git binary
+ */
+$wgGitBin = '/usr/bin/git';
+
+/**
* Map GIT repository URLs to viewer URLs to provide links in Special:Version
*
* Key is a pattern passed to preg_match() and preg_replace(),
@@ -4911,12 +5419,14 @@ $wgUpgradeKey = false;
* The value is the replacement for the key (it can contain $1, etc.)
* %h will be replaced by the short SHA-1 (7 first chars) and %H by the
* full SHA-1 of the HEAD revision.
+ * %r will be replaced with a URL-encoded version of $1.
*
* @since 1.20
*/
$wgGitRepositoryViewers = array(
- 'https://gerrit.wikimedia.org/r/p/(.*)' => 'https://gerrit.wikimedia.org/r/gitweb?p=$1;h=%H',
- 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' => 'https://gerrit.wikimedia.org/r/gitweb?p=$1;h=%H',
+ 'https://gerrit.wikimedia.org/r/p/(.*)' => 'https://git.wikimedia.org/commit/%r/%H',
+ 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)'
+ => 'https://git.wikimedia.org/commit/%r/%H',
);
/** @} */ # End of maintenance }
@@ -4952,11 +5462,15 @@ $wgRCLinkDays = array( 1, 3, 7, 14, 30 );
/**
* Send recent changes updates via UDP. The updates will be formatted for IRC.
* Set this to the IP address of the receiver.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPAddress = false;
/**
* Port number for RC updates
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPPort = false;
@@ -4965,42 +5479,108 @@ $wgRC2UDPPort = false;
* This can be used to identify the wiki. A script is available called
* mxircecho.py which listens on a UDP port, and uses a prefix ending in a
* tab to identify the IRC channel to send the log line to.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPPrefix = '';
/**
* If this is set to true, $wgLocalInterwiki will be prepended to links in the
* IRC feed. If this is set to a string, that string will be used as the prefix.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPInterwikiPrefix = false;
/**
* Set to true to omit "bot" edits (by users with the bot permission) from the
* UDP feed.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPOmitBots = false;
/**
+ * Destinations to which notifications about recent changes
+ * should be sent.
+ *
+ * As of MediaWiki 1.22, the only supported 'engine' parameter option in core
+ * is 'UDPRCFeedEngine', which is used to send recent changes over UDP to the
+ * specified server.
+ * The common options are:
+ * * 'uri' -- the address to which the notices are to be sent.
+ * * 'formatter' -- the class name (implementing RCFeedFormatter) which will
+ * produce the text to send.
+ * * 'omit_bots' -- whether the bot edits should be in the feed
+ * The IRC-specific options are:
+ * * 'add_interwiki_prefix' -- whether the titles should be prefixed with
+ * $wgLocalInterwiki.
+ * The JSON-specific options are:
+ * * 'channel' -- if set, the 'channel' parameter is also set in JSON values.
+ *
+ * To ensure backwards-compatability, whenever $wgRC2UDPAddress is set, a
+ * 'default' feed will be created reusing the deprecated $wgRC2UDP* variables.
+ *
+ * @example $wgRCFeeds['example'] = array(
+ * 'formatter' => 'JSONRCFeedFormatter',
+ * 'uri' => "udp://localhost:1336",
+ * 'add_interwiki_prefix' => false,
+ * 'omit_bots' => true,
+ * );
+ * @example $wgRCFeeds['exampleirc'] = array(
+ * 'formatter' => 'IRCColourfulRCFeedFormatter',
+ * 'uri' => "udp://localhost:1338",
+ * 'add_interwiki_prefix' => false,
+ * 'omit_bots' => true,
+ * );
+ * @since 1.22
+ */
+$wgRCFeeds = array();
+
+/**
+ * Used by RecentChange::getEngine to find the correct engine to use for a given URI scheme.
+ * Keys are scheme names, values are names of engine classes.
+ */
+$wgRCEngines = array(
+ 'redis' => 'RedisPubSubFeedEngine',
+ 'udp' => 'UDPRCFeedEngine',
+);
+
+/**
* Enable user search in Special:Newpages
* This is really a temporary hack around an index install bug on some Wikipedias.
* Kill it once fixed.
*/
$wgEnableNewpagesUserFilter = true;
-/** Use RC Patrolling to check for vandalism */
+/**
+ * Use RC Patrolling to check for vandalism
+ */
$wgUseRCPatrol = true;
-/** Use new page patrolling to check new pages on Special:Newpages */
+/**
+ * Use new page patrolling to check new pages on Special:Newpages
+ */
$wgUseNPPatrol = true;
-/** Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages */
+/**
+ * Log autopatrol actions to the log table
+ */
+$wgLogAutopatrol = true;
+
+/**
+ * Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages
+ */
$wgFeed = true;
-/** Set maximum number of results to return in syndication feeds (RSS, Atom) for
- * eg Recentchanges, Newpages. */
+/**
+ * Set maximum number of results to return in syndication feeds (RSS, Atom) for
+ * eg Recentchanges, Newpages.
+ */
$wgFeedLimit = 50;
-/** _Minimum_ timeout for cached Recentchanges feed, in seconds.
+/**
+ * _Minimum_ timeout for cached Recentchanges feed, in seconds.
* A cached version will continue to be served out even if changes
* are made, until this many seconds runs out since the last render.
*
@@ -5009,11 +5589,14 @@ $wgFeedLimit = 50;
*/
$wgFeedCacheTimeout = 60;
-/** When generating Recentchanges RSS/Atom feed, diffs will not be generated for
- * pages larger than this size. */
+/**
+ * When generating Recentchanges RSS/Atom feed, diffs will not be generated for
+ * pages larger than this size.
+ */
$wgFeedDiffCutoff = 32768;
-/** Override the site's default RSS/ATOM feed for recentchanges that appears on
+/**
+ * Override the site's default RSS/ATOM feed for recentchanges that appears on
* every page. Some sites might have a different feed they'd like to promote
* instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one).
* Should be a format as key (either 'rss' or 'atom') and an URL to the feed
@@ -5032,7 +5615,7 @@ $wgOverrideSiteFeed = array();
* $wgOut->isSyndicated() is true.
*/
$wgFeedClasses = array(
- 'rss' => 'RSSFeed',
+ 'rss' => 'RSSFeed',
'atom' => 'AtomFeed',
);
@@ -5042,11 +5625,19 @@ $wgFeedClasses = array(
*/
$wgAdvertisedFeedTypes = array( 'atom' );
-/** Show watching users in recent changes, watchlist and page history views */
+/**
+ * Show watching users in recent changes, watchlist and page history views
+ */
$wgRCShowWatchingUsers = false; # UPO
-/** Show watching users in Page views */
+
+/**
+ * Show watching users in Page views
+ */
$wgPageShowWatchingUsers = false;
-/** Show the amount of changed characters in recent changes */
+
+/**
+ * Show the amount of changed characters in recent changes
+ */
$wgRCShowChangedSize = true;
/**
@@ -5058,7 +5649,8 @@ $wgRCChangedSizeThreshold = 500;
/**
* Show "Updated (since my last visit)" marker in RC view, watchlist and history
- * view for watched pages with new changes */
+ * view for watched pages with new changes
+ */
$wgShowUpdatedMarker = true;
/**
@@ -5078,6 +5670,42 @@ $wgAllowCategorizedRecentChanges = false;
*/
$wgUseTagFilter = true;
+/**
+ * If set to an integer, pages that are watched by this many users or more
+ * will not require the unwatchedpages permission to view the number of
+ * watchers.
+ *
+ * @since 1.21
+ */
+$wgUnwatchedPageThreshold = false;
+
+/**
+ * Flags (letter symbols) shown in recent changes and watchlist to indicate
+ * certain types of edits.
+ *
+ * To register a new one:
+ * @code
+ * $wgRecentChangesFlags['flag'] => array(
+ * 'letter' => 'letter-msg',
+ * 'title' => 'tooltip-msg'
+ * );
+ * @endcode
+ *
+ * Optional 'class' allows to set a css class different than the flag name.
+ *
+ * @since 1.22
+ */
+$wgRecentChangesFlags = array(
+ 'newpage' => array( 'letter' => 'newpageletter',
+ 'title' => 'recentchanges-label-newpage' ),
+ 'minor' => array( 'letter' => 'minoreditletter',
+ 'title' => 'recentchanges-label-minor', 'class' => 'minoredit' ),
+ 'bot' => array( 'letter' => 'boteditletter',
+ 'title' => 'recentchanges-label-bot', 'class' => 'botedit' ),
+ 'unpatrolled' => array( 'letter' => 'unpatrolledletter',
+ 'title' => 'recentchanges-label-unpatrolled' ),
+);
+
/** @} */ # end RC/watchlist }
/************************************************************************//**
@@ -5095,15 +5723,17 @@ $wgUseTagFilter = true;
$wgRightsPage = null;
/**
- * Set this to specify an external URL containing details about the content license used on your wiki.
+ * Set this to specify an external URL containing details about the content license used on your
+ * wiki.
* If $wgRightsPage is set then this setting is ignored.
*/
$wgRightsUrl = null;
/**
- * If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link.
- * If using $wgRightsUrl then this value must be specified. If using $wgRightsPage then the name of the
- * page will also be used as the link if this variable is not set.
+ * If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the
+ * link.
+ * If using $wgRightsUrl then this value must be specified. If using $wgRightsPage then the name
+ * of the page will also be used as the link if this variable is not set.
*/
$wgRightsText = null;
@@ -5123,7 +5753,9 @@ $wgLicenseTerms = false;
*/
$wgCopyrightIcon = null;
-/** Set this to true if you want detailed copyright information forms on Upload. */
+/**
+ * Set this to true if you want detailed copyright information forms on Upload.
+ */
$wgUseCopyrightUpload = false;
/**
@@ -5135,8 +5767,10 @@ $wgUseCopyrightUpload = false;
*/
$wgMaxCredits = 0;
-/** If there are more than $wgMaxCredits authors, show $wgMaxCredits of them.
- * Otherwise, link to a separate credits page. */
+/**
+ * If there are more than $wgMaxCredits authors, show $wgMaxCredits of them.
+ * Otherwise, link to a separate credits page.
+ */
$wgShowCreditsIfMax = true;
/** @} */ # end of copyright and credits settings }
@@ -5180,8 +5814,8 @@ $wgExportAllowHistory = true;
$wgExportMaxHistory = 0;
/**
-* Return distinct author list (when not returning full history)
-*/
+ * Return distinct author list (when not returning full history)
+ */
$wgExportAllowListContributors = false;
/**
@@ -5198,13 +5832,13 @@ $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;
/**
-* Whether to allow exporting the entire wiki into a single file
-*/
+ * Whether to allow exporting the entire wiki into a single file
+ */
$wgExportAllowAll = false;
/** @} */ # end of import/export }
@@ -5239,6 +5873,13 @@ $wgExtensionFunctions = array();
$wgExtensionMessagesFiles = array();
/**
+ * Array of files with list(s) of extension entry points to be used in
+ * maintenance/mergeMessageFileList.php
+ * @since 1.22
+ */
+$wgExtensionEntryPointListFiles = array();
+
+/**
* Parser output hooks.
* This is an associative array where the key is an extension-defined tag
* (typically the extension name), and the value is a PHP callback.
@@ -5255,6 +5896,11 @@ $wgExtensionMessagesFiles = array();
$wgParserOutputHooks = array();
/**
+ * Whether to include the NewPP limit report as a HTML comment
+ */
+$wgEnableParserLimitReporting = true;
+
+/**
* List of valid skin names.
* The key should be the name in all lower case, the value should be a properly
* cased name for the skin. This value will be prefixed with "Skin" to create the
@@ -5287,7 +5933,7 @@ $wgAutoloadClasses = array();
* 'version' => 1.9,
* 'path' => __FILE__,
* 'author' => 'Foo Barstein',
- * 'url' => 'http://wwww.example.com/Example%20Extension/',
+ * 'url' => 'http://www.example.com/Example%20Extension/',
* 'description' => 'An example extension',
* 'descriptionmsg' => 'exampleextension-desc',
* );
@@ -5296,6 +5942,11 @@ $wgAutoloadClasses = array();
* 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, ... ),
+ *
+ * author can be a string or an array of strings. Authors can be linked using
+ * the regular wikitext link syntax. To have an internationalized version of
+ * "and others" show, add an element "...". This element can also be linked,
+ * for instance "[http://example ...]".
*/
$wgExtensionCredits = array();
@@ -5320,17 +5971,24 @@ $wgAuth = null;
* @endcode
* - A function with some data:
* @code
- * $wgHooks['event_name'][] = array($function, $data);
+ * $wgHooks['event_name'][] = array( $function, $data );
* @endcode
* - A an object method:
* @code
- * $wgHooks['event_name'][] = array($object, 'method');
+ * $wgHooks['event_name'][] = array( $object, 'method' );
+ * @endcode
+ * - A closure:
+ * @code
+ * $wgHooks['event_name'][] = function ( $hookParam ) {
+ * // Handler code goes here.
+ * };
* @endcode
*
* @warning You should always append to an event array or you will end up
* deleting a previous registered hook.
*
- * @todo Does it support PHP closures?
+ * @warning Hook handlers should be registered at file scope. Registering
+ * handlers after file scope can lead to unexpected results due to caching.
*/
$wgHooks = array();
@@ -5342,16 +6000,18 @@ $wgJobClasses = array(
'refreshLinks' => 'RefreshLinksJob',
'refreshLinks2' => 'RefreshLinksJob2',
'htmlCacheUpdate' => 'HTMLCacheUpdateJob',
- 'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible
'sendMail' => 'EmaillingJob',
'enotifNotify' => 'EnotifNotifyJob',
'fixDoubleRedirect' => 'DoubleRedirectJob',
'uploadFromUrl' => 'UploadFromUrlJob',
+ 'AssembleUploadChunks' => 'AssembleUploadChunksJob',
+ 'PublishStashedFile' => 'PublishStashedFileJob',
+ 'null' => 'NullJob'
);
/**
-
- * Jobs that must be explicitly requested, i.e. aren't run by job runners unless special flags are set.
+ * Jobs that must be explicitly requested, i.e. aren't run by job runners unless
+ * special flags are set. The values here are keys of $wgJobClasses.
*
* These can be:
* - Very long-running jobs.
@@ -5359,7 +6019,25 @@ $wgJobClasses = array(
* - Jobs that you want to run on specialized machines ( like transcoding, or a particular
* machine on your cluster has 'outside' web access you could restrict uploadFromUrl )
*/
-$wgJobTypesExcludedFromDefaultQueue = array();
+$wgJobTypesExcludedFromDefaultQueue = array( 'AssembleUploadChunks', 'PublishStashedFile' );
+
+/**
+ * Map of job types to configuration arrays.
+ * This determines which queue class and storage system is used for each job type.
+ * Job types that do not have explicit configuration will use the 'default' config.
+ * These settings should be global to all wikis.
+ */
+$wgJobTypeConf = array(
+ 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random' ),
+);
+
+/**
+ * Which aggregator to use for tracking which queues have jobs.
+ * These settings should be global to all wikis.
+ */
+$wgJobQueueAggregator = array(
+ 'class' => 'JobQueueAggregatorMemc'
+);
/**
* Additional functions to be performed with updateSpecialPages.
@@ -5486,7 +6164,7 @@ $wgLogRestrictions = array(
*
* @par Example:
* @code
- * $wgFilterLogTypes => array(
+ * $wgFilterLogTypes = array(
* 'move' => true,
* 'import' => false,
* );
@@ -5513,16 +6191,16 @@ $wgFilterLogTypes = array(
* where TYPE is your log type, yoy don't need to use this array.
*/
$wgLogNames = array(
- '' => 'all-logs-page',
- 'block' => 'blocklogpage',
+ '' => 'all-logs-page',
+ 'block' => 'blocklogpage',
'protect' => 'protectlogpage',
- 'rights' => 'rightslog',
- 'delete' => 'dellogpage',
- 'upload' => 'uploadlogpage',
- 'move' => 'movelogpage',
- 'import' => 'importlogpage',
- 'patrol' => 'patrol-log-page',
- 'merge' => 'mergelog',
+ 'rights' => 'rightslog',
+ 'delete' => 'dellogpage',
+ 'upload' => 'uploadlogpage',
+ 'move' => 'movelogpage',
+ 'import' => 'importlogpage',
+ 'patrol' => 'patrol-log-page',
+ 'merge' => 'mergelog',
'suppress' => 'suppressionlog',
);
@@ -5536,16 +6214,16 @@ $wgLogNames = array(
* where TYPE is your log type, yoy don't need to use this array.
*/
$wgLogHeaders = array(
- '' => 'alllogstext',
- 'block' => 'blocklogtext',
+ '' => 'alllogstext',
+ 'block' => 'blocklogtext',
'protect' => 'protectlogtext',
- 'rights' => 'rightslogtext',
- 'delete' => 'dellogpagetext',
- 'upload' => 'uploadlogpagetext',
- 'move' => 'movelogpagetext',
- 'import' => 'importlogpagetext',
- 'patrol' => 'patrol-log-header',
- 'merge' => 'mergelogpagetext',
+ 'rights' => 'rightslogtext',
+ 'delete' => 'dellogpagetext',
+ 'upload' => 'uploadlogpagetext',
+ 'move' => 'movelogpagetext',
+ 'import' => 'importlogpagetext',
+ 'patrol' => 'patrol-log-header',
+ 'merge' => 'mergelogpagetext',
'suppress' => 'suppressionlogtext',
);
@@ -5556,23 +6234,21 @@ $wgLogHeaders = array(
* Extensions with custom log types may add to this array.
*/
$wgLogActions = array(
- 'block/block' => 'blocklogentry',
- 'block/unblock' => 'unblocklogentry',
- 'block/reblock' => 'reblock-logentry',
- 'protect/protect' => 'protectedarticle',
- 'protect/modify' => 'modifiedarticleprotection',
- 'protect/unprotect' => 'unprotectedarticle',
- 'protect/move_prot' => 'movedarticleprotection',
- 'rights/rights' => 'rightslogentry',
- 'rights/autopromote' => 'rightslogentry-autopromote',
- 'upload/upload' => 'uploadedimage',
- 'upload/overwrite' => 'overwroteimage',
- 'upload/revert' => 'uploadedimage',
- 'import/upload' => 'import-logentry-upload',
- 'import/interwiki' => 'import-logentry-interwiki',
- 'merge/merge' => 'pagemerge-logentry',
- 'suppress/block' => 'blocklogentry',
- 'suppress/reblock' => 'reblock-logentry',
+ 'block/block' => 'blocklogentry',
+ 'block/unblock' => 'unblocklogentry',
+ 'block/reblock' => 'reblock-logentry',
+ 'protect/protect' => 'protectedarticle',
+ 'protect/modify' => 'modifiedarticleprotection',
+ 'protect/unprotect' => 'unprotectedarticle',
+ 'protect/move_prot' => 'movedarticleprotection',
+ 'upload/upload' => 'uploadedimage',
+ 'upload/overwrite' => 'overwroteimage',
+ 'upload/revert' => 'uploadedimage',
+ 'import/upload' => 'import-logentry-upload',
+ 'import/interwiki' => 'import-logentry-interwiki',
+ 'merge/merge' => 'pagemerge-logentry',
+ 'suppress/block' => 'blocklogentry',
+ 'suppress/reblock' => 'reblock-logentry',
);
/**
@@ -5582,16 +6258,18 @@ $wgLogActions = array(
* @see LogFormatter
*/
$wgLogActionsHandlers = array(
- 'move/move' => 'MoveLogFormatter',
- 'move/move_redir' => 'MoveLogFormatter',
- 'delete/delete' => 'DeleteLogFormatter',
- 'delete/restore' => 'DeleteLogFormatter',
- 'delete/revision' => 'DeleteLogFormatter',
- 'delete/event' => 'DeleteLogFormatter',
+ 'move/move' => 'MoveLogFormatter',
+ 'move/move_redir' => 'MoveLogFormatter',
+ 'delete/delete' => 'DeleteLogFormatter',
+ 'delete/restore' => 'DeleteLogFormatter',
+ 'delete/revision' => 'DeleteLogFormatter',
+ 'delete/event' => 'DeleteLogFormatter',
'suppress/revision' => 'DeleteLogFormatter',
- 'suppress/event' => 'DeleteLogFormatter',
- 'suppress/delete' => 'DeleteLogFormatter',
- 'patrol/patrol' => 'PatrolLogFormatter',
+ 'suppress/event' => 'DeleteLogFormatter',
+ 'suppress/delete' => 'DeleteLogFormatter',
+ 'patrol/patrol' => 'PatrolLogFormatter',
+ 'rights/rights' => 'RightsLogFormatter',
+ 'rights/autopromote' => 'RightsLogFormatter',
);
/**
@@ -5620,113 +6298,14 @@ $wgDisableQueryPageUpdate = false;
/**
* List of special pages, followed by what subtitle they should go under
* at Special:SpecialPages
+ *
+ * @deprecated since 1.21 Override SpecialPage::getGroupName instead
*/
-$wgSpecialPageGroups = array(
- 'DoubleRedirects' => 'maintenance',
- 'BrokenRedirects' => 'maintenance',
- 'Lonelypages' => 'maintenance',
- 'Uncategorizedpages' => 'maintenance',
- 'Uncategorizedcategories' => 'maintenance',
- 'Uncategorizedimages' => 'maintenance',
- 'Uncategorizedtemplates' => 'maintenance',
- 'Unusedcategories' => 'maintenance',
- 'Unusedimages' => 'maintenance',
- 'Protectedpages' => 'maintenance',
- 'Protectedtitles' => 'maintenance',
- 'Unusedtemplates' => 'maintenance',
- 'Withoutinterwiki' => 'maintenance',
- 'Longpages' => 'maintenance',
- 'Shortpages' => 'maintenance',
- 'Ancientpages' => 'maintenance',
- 'Deadendpages' => 'maintenance',
- 'Wantedpages' => 'maintenance',
- 'Wantedcategories' => 'maintenance',
- 'Wantedfiles' => 'maintenance',
- 'Wantedtemplates' => 'maintenance',
- 'Unwatchedpages' => 'maintenance',
- 'Fewestrevisions' => 'maintenance',
-
- 'Userlogin' => 'login',
- 'Userlogout' => 'login',
- 'CreateAccount' => 'login',
-
- 'Recentchanges' => 'changes',
- 'Recentchangeslinked' => 'changes',
- 'Watchlist' => 'changes',
- 'Newimages' => 'changes',
- 'Newpages' => 'changes',
- 'Log' => 'changes',
- 'Tags' => 'changes',
-
- 'Upload' => 'media',
- 'Listfiles' => 'media',
- 'MIMEsearch' => 'media',
- 'FileDuplicateSearch' => 'media',
- 'Filepath' => 'media',
-
- 'Listusers' => 'users',
- 'Activeusers' => 'users',
- 'Listgrouprights' => 'users',
- 'BlockList' => 'users',
- 'Contributions' => 'users',
- 'Emailuser' => 'users',
- 'Listadmins' => 'users',
- 'Listbots' => 'users',
- 'Userrights' => 'users',
- 'Block' => 'users',
- 'Unblock' => 'users',
- 'Preferences' => 'users',
- 'ChangeEmail' => 'users',
- 'ChangePassword' => 'users',
- 'DeletedContributions' => 'users',
- 'PasswordReset' => 'users',
-
- 'Mostlinked' => 'highuse',
- 'Mostlinkedcategories' => 'highuse',
- 'Mostlinkedtemplates' => 'highuse',
- 'Mostcategories' => 'highuse',
- 'Mostimages' => 'highuse',
- 'Mostinterwikis' => 'highuse',
- 'Mostrevisions' => 'highuse',
-
- 'Allpages' => 'pages',
- 'Prefixindex' => 'pages',
- 'Listredirects' => 'pages',
- 'Categories' => 'pages',
- 'Disambiguations' => 'pages',
-
- 'Randompage' => 'redirects',
- 'Randomredirect' => 'redirects',
- 'Mypage' => 'redirects',
- 'Mytalk' => 'redirects',
- 'Mycontributions' => 'redirects',
- 'Search' => 'redirects',
- 'LinkSearch' => 'redirects',
-
- 'ComparePages' => 'pagetools',
- 'Movepage' => 'pagetools',
- 'MergeHistory' => 'pagetools',
- 'Revisiondelete' => 'pagetools',
- 'Undelete' => 'pagetools',
- 'Export' => 'pagetools',
- 'Import' => 'pagetools',
- 'Whatlinkshere' => 'pagetools',
-
- 'Statistics' => 'wiki',
- 'Version' => 'wiki',
- 'Lockdb' => 'wiki',
- 'Unlockdb' => 'wiki',
- 'Allmessages' => 'wiki',
- 'Popularpages' => 'wiki',
-
- 'Specialpages' => 'other',
- 'Blockme' => 'other',
- 'Booksources' => 'other',
- 'JavaScriptTest' => 'other',
-);
-
-/** Whether or not to sort special pages in Special:Specialpages */
+$wgSpecialPageGroups = array();
+/**
+ * Whether or not to sort special pages in Special:Specialpages
+ */
$wgSortSpecialPages = true;
/**
@@ -5759,24 +6338,24 @@ $wgMaxRedirectLinksRetrieved = 500;
* Unsetting core actions will probably cause things to complain loudly.
*/
$wgActions = array(
- 'credits' => true,
- 'delete' => true,
- 'edit' => true,
- 'history' => true,
- 'info' => true,
- 'markpatrolled' => true,
- 'protect' => true,
- 'purge' => true,
- 'raw' => true,
- 'render' => true,
- 'revert' => true,
+ 'credits' => true,
+ 'delete' => true,
+ 'edit' => true,
+ 'history' => true,
+ 'info' => true,
+ 'markpatrolled' => true,
+ 'protect' => true,
+ 'purge' => true,
+ 'raw' => true,
+ 'render' => true,
+ 'revert' => true,
'revisiondelete' => true,
- 'rollback' => true,
- 'submit' => true,
- 'unprotect' => true,
- 'unwatch' => true,
- 'view' => true,
- 'watch' => true,
+ 'rollback' => true,
+ 'submit' => true,
+ 'unprotect' => true,
+ 'unwatch' => true,
+ 'view' => true,
+ 'watch' => true,
);
/**
@@ -5818,14 +6397,14 @@ $wgNamespaceRobotPolicies = array();
/**
* Robot policies per article. These override the per-namespace robot policies.
- * Must be in the form of an array where the key part is a properly canonical-
- * ised text form title and the value is a robot policy.
+ * Must be in the form of an array where the key part is a properly canonicalised
+ * text form title and the value is a robot policy.
*
* @par Example:
* @code
* $wgArticleRobotPolicies = array(
- * 'Main Page' => 'noindex,follow',
- * 'User:Bob' => 'index,follow',
+ * 'Main Page' => 'noindex,follow',
+ * 'User:Bob' => 'index,follow',
* );
* @endcode
*
@@ -5882,6 +6461,22 @@ $wgEnableAPI = true;
$wgEnableWriteAPI = true;
/**
+ *
+ * WARNING: SECURITY THREAT - debug use only
+ *
+ * Disables many security checks in the API for debugging purposes.
+ * This flag should never be used on the production servers, as it introduces
+ * a number of potential security holes. Even when enabled, the validation
+ * will still be performed, but instead of failing, API will return a warning.
+ * Also, there will always be a warning notifying that this flag is set.
+ * At this point, the flag allows GET requests to go through for modules
+ * requiring POST.
+ *
+ * @since 1.21
+ */
+$wgDebugAPI = false;
+
+/**
* API module extensions.
* Associative array mapping module name to class name.
* Extension modules may override the core modules.
@@ -5893,6 +6488,12 @@ $wgAPIPropModules = array();
$wgAPIListModules = array();
/**
+ * This variable is ignored. To add your module to the API, please add it to $wgAPI*Modules
+ * @deprecated since 1.21
+ */
+$wgAPIGeneratorModules = array();
+
+/**
* Maximum amount of rows to scan in a DB query in the API
* The default value is generally fine
*/
@@ -5919,7 +6520,17 @@ $wgAPIRequestLog = false;
/**
* Set the timeout for the API help text cache. If set to 0, caching disabled
*/
-$wgAPICacheHelpTimeout = 60*60;
+$wgAPICacheHelpTimeout = 60 * 60;
+
+/**
+ * The ApiQueryQueryPages module should skip pages that are redundant to true
+ * API queries.
+ */
+$wgAPIUselessQueryPages = array(
+ 'MIMEsearch', // aiprop=mime
+ 'LinkSearch', // list=exturlusage
+ 'FileDuplicateSearch', // prop=duplicatefiles
+);
/**
* Enable AJAX framework
@@ -5961,10 +6572,10 @@ $wgAjaxLicensePreview = true;
* @par Example:
* @code
* $wgCrossSiteAJAXdomains = array(
- * 'www.mediawiki.org',
- * '*.wikipedia.org',
- * '*.wikimedia.org',
- * '*.wiktionary.org',
+ * 'www.mediawiki.org',
+ * '*.wikipedia.org',
+ * '*.wikimedia.org',
+ * '*.wiktionary.org',
* );
* @endcode
*/
@@ -5975,7 +6586,6 @@ $wgCrossSiteAJAXdomains = array();
* even if they match one of the domains allowed by $wgCrossSiteAJAXdomains
* Uses the same syntax as $wgCrossSiteAJAXdomains
*/
-
$wgCrossSiteAJAXdomainExceptions = array();
/** @} */ # End AJAX and API }
@@ -5988,7 +6598,7 @@ $wgCrossSiteAJAXdomainExceptions = array();
/**
* Maximum amount of virtual memory available to shell processes under linux, in KB.
*/
-$wgMaxShellMemory = 102400;
+$wgMaxShellMemory = 307200;
/**
* Maximum file size created by shell processes under linux, in KB
@@ -5997,11 +6607,42 @@ $wgMaxShellMemory = 102400;
$wgMaxShellFileSize = 102400;
/**
- * Maximum CPU time in seconds for shell processes under linux
+ * Maximum CPU time in seconds for shell processes under Linux
*/
$wgMaxShellTime = 180;
/**
+ * Maximum wall clock time (i.e. real time, of the kind the clock on the wall
+ * would measure) in seconds for shell processes under Linux
+ */
+$wgMaxShellWallClockTime = 180;
+
+/**
+ * Under Linux: a cgroup directory used to constrain memory usage of shell
+ * commands. The directory must be writable by the user which runs MediaWiki.
+ *
+ * If specified, this is used instead of ulimit, which is inaccurate, and
+ * causes malloc() to return NULL, which exposes bugs in C applications, making
+ * them segfault or deadlock.
+ *
+ * A wrapper script will create a cgroup for each shell command that runs, as
+ * a subgroup of the specified cgroup. If the memory limit is exceeded, the
+ * kernel will send a SIGKILL signal to a process in the subgroup.
+ *
+ * @par Example:
+ * @code
+ * mkdir -p /sys/fs/cgroup/memory/mediawiki
+ * mkdir -m 0777 /sys/fs/cgroup/memory/mediawiki/job
+ * echo '$wgShellCgroup = "/sys/fs/cgroup/memory/mediawiki/job";' >> LocalSettings.php
+ * @endcode
+ *
+ * The reliability of cgroup cleanup can be improved by installing a
+ * notify_on_release script in the root cgroup, see e.g.
+ * https://gerrit.wikimedia.org/r/#/c/40784
+ */
+$wgShellCgroup = false;
+
+/**
* Executable path of the PHP cli binary (php/php5). Should be set up on install.
*/
$wgPhpCli = '/usr/bin/php';
@@ -6035,6 +6676,12 @@ $wgAsyncHTTPTimeout = 25;
*/
$wgHTTPProxy = false;
+/**
+ * Timeout for connections done internally (in seconds)
+ * Only works for curl
+ */
+$wgHTTPConnectTimeout = 5e0;
+
/** @} */ # End HTTP client }
/************************************************************************//**
@@ -6061,78 +6708,25 @@ $wgUpdateRowsPerJob = 500;
*/
$wgUpdateRowsPerQuery = 100;
-/** @} */ # End job queue }
-
-/************************************************************************//**
- * @name HipHop compilation
- * @{
- */
-
-/**
- * The build directory for HipHop compilation.
- * Defaults to '$IP/maintenance/hiphop/build'.
- */
-$wgHipHopBuildDirectory = false;
-
-/**
- * The HipHop build type. Can be either "Debug" or "Release".
- */
-$wgHipHopBuildType = 'Debug';
-
/**
- * Number of parallel processes to use during HipHop compilation, or "detect"
- * to guess from system properties.
- */
-$wgHipHopCompilerProcs = 'detect';
-
-/**
- * Filesystem extensions directory. Defaults to $IP/../extensions.
- *
- * To compile extensions with HipHop, set $wgExtensionsDirectory correctly,
- * and use code like:
- * @code
- * require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) );
- * @endcode
+ * Do not purge all the pages that use a page when it is edited
+ * if there are more than this many such pages. This is used to
+ * avoid invalidating a large portion of the squid/parser cache.
*
- * to include the extension setup file from LocalSettings.php. It is not
- * necessary to set this variable unless you use MWInit::extensionSetupPath().
+ * This setting should factor in any squid/parser cache expiry settings.
*/
-$wgExtensionsDirectory = false;
-
-/**
- * A list of files that should be compiled into a HipHop build, in addition to
- * those listed in $wgAutoloadClasses. Add to this array in an extension setup
- * file in order to add files to the build.
- *
- * The files listed here must either be either absolute paths under $IP or
- * under $wgExtensionsDirectory, or paths relative to the virtual source root
- * "$IP/..", i.e. starting with "phase3" for core files, and "extensions" for
- * extension files.
- */
-$wgCompiledFiles = array();
-
-/** @} */ # End of HipHop compilation }
+$wgMaxBacklinksInvalidate = false;
+/** @} */ # End job queue }
/************************************************************************//**
- * @name Mobile support
+ * @name Miscellaneous
* @{
*/
/**
- * Name of the class used for mobile device detection, must be inherited from
- * IDeviceDetector.
- */
-$wgDeviceDetectionClass = 'DeviceDetection';
-
-/** @} */ # End of Mobile support }
-
-/************************************************************************//**
- * @name Miscellaneous
- * @{
+ * Name of the external diff engine to use
*/
-
-/** Name of the external diff engine to use */
$wgExternalDiffEngine = false;
/**
@@ -6202,13 +6796,57 @@ $wgPoolCounterConf = null;
$wgUploadMaintenance = false;
/**
- * Allows running of selenium tests via maintenance/tests/RunSeleniumTests.php
+ * Associative array mapping namespace IDs to the name of the content model pages in that namespace
+ * should have by default (use the CONTENT_MODEL_XXX constants). If no special content type is
+ * defined for a given namespace, pages in that namespace will use the CONTENT_MODEL_WIKITEXT
+ * (except for the special case of JS and CS pages).
+ *
+ * @since 1.21
+ */
+$wgNamespaceContentModels = array();
+
+/**
+ * How to react if a plain text version of a non-text Content object is requested using
+ * ContentHandler::getContentText():
+ *
+ * * 'ignore': return null
+ * * 'fail': throw an MWException
+ * * 'serialize': serialize to default format
+ *
+ * @since 1.21
*/
-$wgEnableSelenium = false;
-$wgSeleniumTestConfigs = array();
-$wgSeleniumConfigFile = null;
-$wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
-$wgDBtestpassword = '';
+$wgContentHandlerTextFallback = 'ignore';
+
+/**
+ * Set to false to disable use of the database fields introduced by the ContentHandler facility.
+ * This way, the ContentHandler facility can be used without any additional information in the
+ * database. A page's content model is then derived solely from the page's title. This however
+ * means that changing a page's default model (e.g. using $wgNamespaceContentModels) will break
+ * the page and/or make the content inaccessible. This also means that pages can not be moved to
+ * a title that would default to a different content model.
+ *
+ * Overall, with $wgContentHandlerUseDB = false, no database updates are needed, but content
+ * handling is less robust and less flexible.
+ *
+ * @since 1.21
+ */
+$wgContentHandlerUseDB = true;
+
+/**
+ * Determines which types of text are parsed as wikitext. This does not imply that these kinds
+ * of texts are also rendered as wikitext, it only means that links, magic words, etc will have
+ * the effect on the database they would have on a wikitext page.
+ *
+ * @todo On the long run, it would be nice to put categories etc into a separate structure,
+ * or at least parse only the contents of comments in the scripts.
+ *
+ * @since 1.21
+ */
+$wgTextModelsToParse = array(
+ CONTENT_MODEL_WIKITEXT, // Just for completeness, wikitext will always be parsed.
+ CONTENT_MODEL_JAVASCRIPT, // Make categories etc work, people put them into comments.
+ CONTENT_MODEL_CSS, // Make categories etc work, people put them into comments.
+);
/**
* Whether the user must enter their password to change their e-mail address
@@ -6218,6 +6856,21 @@ $wgDBtestpassword = '';
$wgRequirePasswordforEmailChange = true;
/**
+ * Register handlers for specific types of sites.
+ *
+ * @since 1.20
+ */
+$wgSiteTypes = array(
+ 'mediawiki' => 'MediaWikiSite',
+);
+
+/**
+ * Formerly a list of files for HipHop compilation
+ * @deprecated since 1.22
+ */
+$wgCompiledFiles = array();
+
+/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker
* @}
diff --git a/includes/DeferredUpdates.php b/includes/DeferredUpdates.php
index b4989a69..c385f138 100644
--- a/includes/DeferredUpdates.php
+++ b/includes/DeferredUpdates.php
@@ -34,7 +34,7 @@ interface DeferrableUpdate {
}
/**
- * Class for mananging the deferred updates.
+ * Class for managing the deferred updates.
*
* @since 1.19
*/
@@ -64,9 +64,19 @@ class DeferredUpdates {
}
/**
+ * Add a callable update. In a lot of cases, we just need a callback/closure,
+ * defining a new DeferrableUpdate object is not necessary
+ * @see MWCallableUpdate::__construct()
+ * @param callable $callable
+ */
+ public static function addCallableUpdate( $callable ) {
+ self::addUpdate( new MWCallableUpdate( $callable ) );
+ }
+
+ /**
* Do any deferred updates and clear the list
*
- * @param $commit String: set to 'commit' to commit after every update to
+ * @param string $commit set to 'commit' to commit after every update to
* prevent lock contention
*/
public static function doUpdates( $commit = '' ) {
@@ -92,14 +102,14 @@ class DeferredUpdates {
$update->doUpdate();
if ( $doCommit && $dbw->trxLevel() ) {
- $dbw->commit( __METHOD__ );
+ $dbw->commit( __METHOD__, 'flush' );
}
} catch ( MWException $e ) {
// We don't want exceptions thrown during deferred updates to
// be reported to the user since the output is already sent.
// Instead we just log them.
if ( !$e instanceof ErrorPageError ) {
- wfDebugLog( 'exception', $e->getLogMessage() );
+ MWExceptionHandler::logException( $e );
}
}
}
diff --git a/includes/Defines.php b/includes/Defines.php
index be9f9816..86c5520b 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -39,7 +39,7 @@ define( 'MW_SPECIALPAGE_VERSION', 2 );
define( 'DBO_DEBUG', 1 );
define( 'DBO_NOBUFFER', 2 );
define( 'DBO_IGNORE', 4 );
-define( 'DBO_TRX', 8 );
+define( 'DBO_TRX', 8 ); // automatically start transaction on first query
define( 'DBO_DEFAULT', 16 );
define( 'DBO_PERSISTENT', 32 );
define( 'DBO_SYSDBA', 64 ); //for oracle maintenance
@@ -54,13 +54,12 @@ define( 'DBO_COMPRESS', 512 );
*/
define( 'DB_SLAVE', -1 ); # Read from the slave (or only server)
define( 'DB_MASTER', -2 ); # Write to master (or only server)
-define( 'DB_LAST', -3 ); # Whatever database was used last
/**@}*/
# Obsolete aliases
define( 'DB_READ', -1 );
define( 'DB_WRITE', -2 );
-
+define( 'DB_LAST', -3 ); # deprecated since 2008, usage throws exception
/**@{
* Virtual namespaces; don't appear in the page database
@@ -138,7 +137,7 @@ define( 'MEDIATYPE_ARCHIVE', 'ARCHIVE' ); // archive file (zip, tar, etc)
*/
define( 'AV_NO_VIRUS', 0 ); #scan ok, no virus found
define( 'AV_VIRUS_FOUND', 1 ); #virus found!
-define( 'AV_SCAN_ABORTED', -1 ); #scan aborted, the file is probably imune
+define( 'AV_SCAN_ABORTED', -1 ); #scan aborted, the file is probably immune
define( 'AV_SCAN_FAILED', false ); #scan failed (scanner not found or error in scanner)
/**@}*/
@@ -171,11 +170,12 @@ define( 'MW_DATE_ISO', 'ISO 8601' );
/**@{
* RecentChange type identifiers
*/
-define( 'RC_EDIT', 0);
-define( 'RC_NEW', 1);
-define( 'RC_MOVE', 2); // obsolete
-define( 'RC_LOG', 3);
-define( 'RC_MOVE_OVER_REDIRECT', 4); // obsolete
+define( 'RC_EDIT', 0 );
+define( 'RC_NEW', 1 );
+define( 'RC_MOVE', 2 ); // obsolete
+define( 'RC_LOG', 3 );
+define( 'RC_MOVE_OVER_REDIRECT', 4 ); // obsolete
+define( 'RC_EXTERNAL', 5 );
/**@}*/
/**@{
@@ -197,15 +197,14 @@ define( 'EDIT_AUTOSUMMARY', 64 );
define( 'LIST_COMMA', 0 );
define( 'LIST_AND', 1 );
define( 'LIST_SET', 2 );
-define( 'LIST_NAMES', 3);
-define( 'LIST_OR', 4);
-define( 'LIST_SET_PREPARED', 8); // List of (?, ?, ?) for DatabaseIbm_db2
+define( 'LIST_NAMES', 3 );
+define( 'LIST_OR', 4 );
/**@}*/
/**
* Unicode and normalisation related
*/
-require_once __DIR__.'/normal/UtfNormalDefines.php';
+require_once __DIR__ . '/normal/UtfNormalDefines.php';
/**@{
* Hook support constants
@@ -213,6 +212,7 @@ require_once __DIR__.'/normal/UtfNormalDefines.php';
define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 );
define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 );
define( 'MW_SUPPORTS_LOCALISATIONCACHE', 1 );
+define( 'MW_SUPPORTS_CONTENTHANDLER', 1 );
/**@}*/
/** Support for $wgResourceModules */
@@ -225,7 +225,7 @@ define( 'MW_SUPPORTS_RESOURCE_MODULES', 1 );
define( 'OT_HTML', 1 );
define( 'OT_WIKI', 2 );
define( 'OT_PREPROCESS', 3 );
-define( 'OT_MSG' , 3 ); // b/c alias for OT_PREPROCESS
+define( 'OT_MSG', 3 ); // b/c alias for OT_PREPROCESS
define( 'OT_PLAIN', 4 );
/**@}*/
@@ -237,11 +237,6 @@ define( 'SFH_OBJECT_ARGS', 2 );
/**@}*/
/**
- * Flags for Parser::setLinkHook
- */
-define( 'SLH_PATTERN', 1 );
-
-/**
* Flags for Parser::replaceLinkHolders
*/
define( 'RLH_FOR_UPDATE', 1 );
@@ -261,7 +256,7 @@ define( 'APCOND_BLOCKED', 8 );
define( 'APCOND_ISBOT', 9 );
/**@}*/
-/**
+/** @{
* Protocol constants for wfExpandUrl()
*/
define( 'PROTO_HTTP', 'http://' );
@@ -270,3 +265,35 @@ define( 'PROTO_RELATIVE', '//' );
define( 'PROTO_CURRENT', null );
define( 'PROTO_CANONICAL', 1 );
define( 'PROTO_INTERNAL', 2 );
+/**@}*/
+
+/**@{
+ * Content model ids, used by Content and ContentHandler.
+ * These IDs will be exposed in the API and XML dumps.
+ *
+ * Extensions that define their own content model IDs should take
+ * care to avoid conflicts. Using the extension name as a prefix is recommended,
+ * for example 'myextension-somecontent'.
+ */
+define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' );
+define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
+define( 'CONTENT_MODEL_CSS', 'css' );
+define( 'CONTENT_MODEL_TEXT', 'text' );
+/**@}*/
+
+/**@{
+ * Content formats, used by Content and ContentHandler.
+ * These should be MIME types, and will be exposed in the API and XML dumps.
+ *
+ * Extensions are free to use the below formats, or define their own.
+ * It is recommended to stick with the conventions for MIME types.
+ */
+define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext
+define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages
+define( 'CONTENT_FORMAT_CSS', 'text/css' ); // for css pages
+define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api and for extensions
+define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions
+define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions
+/**@}*/
diff --git a/includes/DeprecatedGlobal.php b/includes/DeprecatedGlobal.php
index 4d7b9689..d48bd0b0 100644
--- a/includes/DeprecatedGlobal.php
+++ b/includes/DeprecatedGlobal.php
@@ -27,7 +27,7 @@
*/
class DeprecatedGlobal extends StubObject {
- // The m's are to stay consistent with parent class.
+ // The m's are to stay consistent with parent class.
protected $mRealValue, $mVersion;
function __construct( $name, $realValue, $version = false ) {
diff --git a/includes/EditPage.php b/includes/EditPage.php
index b762cad1..530e2674 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -40,90 +40,90 @@ class EditPage {
/**
* Status: Article successfully updated
*/
- const AS_SUCCESS_UPDATE = 200;
+ const AS_SUCCESS_UPDATE = 200;
/**
* Status: Article successfully created
*/
- const AS_SUCCESS_NEW_ARTICLE = 201;
+ const AS_SUCCESS_NEW_ARTICLE = 201;
/**
* Status: Article update aborted by a hook function
*/
- const AS_HOOK_ERROR = 210;
+ const AS_HOOK_ERROR = 210;
/**
* Status: A hook function returned an error
*/
- const AS_HOOK_ERROR_EXPECTED = 212;
+ const AS_HOOK_ERROR_EXPECTED = 212;
/**
- * Status: User is blocked from editting this page
+ * Status: User is blocked from editing this page
*/
- const AS_BLOCKED_PAGE_FOR_USER = 215;
+ const AS_BLOCKED_PAGE_FOR_USER = 215;
/**
* Status: Content too big (> $wgMaxArticleSize)
*/
- const AS_CONTENT_TOO_BIG = 216;
+ const AS_CONTENT_TOO_BIG = 216;
/**
* Status: User cannot edit? (not used)
*/
- const AS_USER_CANNOT_EDIT = 217;
+ const AS_USER_CANNOT_EDIT = 217;
/**
* Status: this anonymous user is not allowed to edit this page
*/
- const AS_READ_ONLY_PAGE_ANON = 218;
+ const AS_READ_ONLY_PAGE_ANON = 218;
/**
* Status: this logged in user is not allowed to edit this page
*/
- const AS_READ_ONLY_PAGE_LOGGED = 219;
+ const AS_READ_ONLY_PAGE_LOGGED = 219;
/**
* Status: wiki is in readonly mode (wfReadOnly() == true)
*/
- const AS_READ_ONLY_PAGE = 220;
+ const AS_READ_ONLY_PAGE = 220;
/**
* Status: rate limiter for action 'edit' was tripped
*/
- const AS_RATE_LIMITED = 221;
+ const AS_RATE_LIMITED = 221;
/**
- * Status: article was deleted while editting and param wpRecreate == false or form
+ * Status: article was deleted while editing and param wpRecreate == false or form
* was not posted
*/
- const AS_ARTICLE_WAS_DELETED = 222;
+ const AS_ARTICLE_WAS_DELETED = 222;
/**
* Status: user tried to create this page, but is not allowed to do that
- * ( Title->usercan('create') == false )
+ * ( Title->userCan('create') == false )
*/
- const AS_NO_CREATE_PERMISSION = 223;
+ const AS_NO_CREATE_PERMISSION = 223;
/**
* Status: user tried to create a blank page
*/
- const AS_BLANK_ARTICLE = 224;
+ const AS_BLANK_ARTICLE = 224;
/**
* Status: (non-resolvable) edit conflict
*/
- const AS_CONFLICT_DETECTED = 225;
+ const AS_CONFLICT_DETECTED = 225;
/**
* Status: no edit summary given and the user has forceeditsummary set and the user is not
- * editting in his own userspace or talkspace and wpIgnoreBlankSummary == false
+ * editing in his own userspace or talkspace and wpIgnoreBlankSummary == false
*/
- const AS_SUMMARY_NEEDED = 226;
+ const AS_SUMMARY_NEEDED = 226;
/**
* Status: user tried to create a new section without content
*/
- const AS_TEXTBOX_EMPTY = 228;
+ const AS_TEXTBOX_EMPTY = 228;
/**
* Status: article is too big (> $wgMaxArticleSize), after merging in the new section
@@ -133,32 +133,57 @@ class EditPage {
/**
* not used
*/
- const AS_OK = 230;
+ const AS_OK = 230;
/**
- * Status: WikiPage::doEdit() was unsuccessfull
+ * Status: WikiPage::doEdit() was unsuccessful
*/
- const AS_END = 231;
+ const AS_END = 231;
/**
* Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex
*/
- const AS_SPAM_ERROR = 232;
+ const AS_SPAM_ERROR = 232;
/**
* Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
*/
- const AS_IMAGE_REDIRECT_ANON = 233;
+ const AS_IMAGE_REDIRECT_ANON = 233;
/**
* Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
*/
- const AS_IMAGE_REDIRECT_LOGGED = 234;
+ const AS_IMAGE_REDIRECT_LOGGED = 234;
+
+ /**
+ * Status: can't parse content
+ */
+ const AS_PARSE_ERROR = 240;
/**
* HTML id and name for the beginning of the edit form.
*/
- const EDITFORM_ID = 'editform';
+ const EDITFORM_ID = 'editform';
+
+ /**
+ * Prefix of key for cookie used to pass post-edit state.
+ * The revision id edited is added after this
+ */
+ const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
+
+ /**
+ * Duration of PostEdit cookie, in seconds.
+ * The cookie will be removed instantly if the JavaScript runs.
+ *
+ * Otherwise, though, we don't want the cookies to accumulate.
+ * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible limit of only 20 cookies per domain.
+ * This still applies at least to some versions of IE without full updates:
+ * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx
+ *
+ * A value of 20 minutes should be enough to take into account slow loads and minor
+ * clock skew while still avoiding cookie accumulation when JavaScript is turned off.
+ */
+ const POST_EDIT_COOKIE_DURATION = 1200;
/**
* @var Article
@@ -214,6 +239,7 @@ class EditPage {
var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
+ var $contentModel = null, $contentFormat = null;
# Placeholders for text injection by hooks (must be HTML)
# extensions should take care to _append_ to the present value
@@ -225,20 +251,32 @@ class EditPage {
public $editFormTextBottom = '';
public $editFormTextAfterContent = '';
public $previewTextAfterContent = '';
- public $mPreloadText = '';
+ public $mPreloadContent = null;
- /* $didSave should be set to true whenever an article was succesfully altered. */
+ /* $didSave should be set to true whenever an article was successfully altered. */
public $didSave = false;
public $undidRev = 0;
public $suppressIntro = false;
/**
+ * Set to true to allow editing of non-text content types.
+ *
+ * @var bool
+ */
+ public $allowNonTextContent = false;
+
+ /**
* @param $article Article
*/
public function __construct( Article $article ) {
$this->mArticle = $article;
$this->mTitle = $article->getTitle();
+
+ $this->contentModel = $this->mTitle->getContentModel();
+
+ $handler = ContentHandler::getForModelID( $this->contentModel );
+ $this->contentFormat = $handler->getDefaultFormat();
}
/**
@@ -267,7 +305,7 @@ class EditPage {
/**
* Get the context title object.
- * If not set, $wgTitle will be returned. This behavior might changed in
+ * If not set, $wgTitle will be returned. This behavior might change in
* the future to return $this->mTitle instead.
*
* @return Title object
@@ -359,11 +397,10 @@ 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->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
+ $this->isCssSubpage = $this->mTitle->isCssSubpage();
+ $this->isJsSubpage = $this->mTitle->isJsSubpage();
$this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
- $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
# Show applicable editing introductions
if ( $this->formtype == 'initial' || $this->firsttime ) {
@@ -392,10 +429,13 @@ class EditPage {
wfProfileOut( __METHOD__ );
return;
}
- if ( !$this->mTitle->getArticleID() )
+
+ if ( !$this->mTitle->getArticleID() ) {
wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
- else
+ } else {
wfRunHooks( 'EditFormInitialText', array( $this ) );
+ }
+
}
$this->showEditForm();
@@ -436,8 +476,9 @@ class EditPage {
* "View source for ..." page displaying the source code after the error message.
*
* @since 1.19
- * @param $permErrors Array of permissions errors, as returned by
+ * @param array $permErrors of permissions errors, as returned by
* Title::getUserPermissionsErrors().
+ * @throws PermissionsError
*/
protected function displayPermissionsError( array $permErrors ) {
global $wgRequest, $wgOut;
@@ -446,19 +487,20 @@ class EditPage {
// The edit page was reached via a red link.
// Redirect to the article page and let them click the edit tab if
// they really want a permission error.
- $wgOut->redirect( $this->mTitle->getFullUrl() );
+ $wgOut->redirect( $this->mTitle->getFullURL() );
return;
}
- $content = $this->getContent();
+ $content = $this->getContentObject();
# Use the normal message if there's nothing to display
- if ( $this->firsttime && $content === '' ) {
+ if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
$action = $this->mTitle->exists() ? 'edit' :
( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
throw new PermissionsError( $action, $permErrors );
}
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setPageTitle( wfMessage( 'viewsource-title', $this->getContextTitle()->getPrefixedText() ) );
$wgOut->addBacklinkSubtitle( $this->getContextTitle() );
$wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) );
@@ -467,17 +509,20 @@ class EditPage {
# If the user made changes, preserve them when showing the markup
# (This happens when a user is blocked during edit, for instance)
if ( !$this->firsttime ) {
- $content = $this->textbox1;
+ $text = $this->textbox1;
$wgOut->addWikiMsg( 'viewyourtext' );
} else {
+ $text = $this->toEditText( $content );
$wgOut->addWikiMsg( 'viewsourcetext' );
}
- $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
+ $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
Linker::formatTemplates( $this->getTemplates() ) ) );
+ $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+
if ( $this->mTitle->exists() ) {
$wgOut->returnToMain( null, $this->mTitle );
}
@@ -497,7 +542,7 @@ class EditPage {
// The edit page was reached via a red link.
// Redirect to the article page and let them click the edit tab if
// they really want a permission error.
- $wgOut->redirect( $this->mTitle->getFullUrl() );
+ $wgOut->redirect( $this->mTitle->getFullURL() );
} else {
$wgOut->readOnlyPage( $source, $protected, $reasons, $action );
}
@@ -520,11 +565,11 @@ class EditPage {
// Nothing *to* preview for new sections
return false;
} elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) {
- // Standard preference behaviour
+ // Standard preference behavior
return true;
} elseif ( !$this->mTitle->exists() &&
- isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) &&
- $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
+ isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) &&
+ $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
{
// Categories are special
return true;
@@ -554,13 +599,15 @@ class EditPage {
}
/**
- * Does this EditPage class support section editing?
- * This is used by EditPage subclasses to indicate their ui cannot handle section edits
+ * Returns whether section editing is supported for the current page.
+ * Subclasses may override this to replace the default behavior, which is
+ * to check ContentHandler::supportsSections.
*
- * @return bool
+ * @return bool true if this edit page supports sections, false otherwise.
*/
protected function isSectionEditSupported() {
- return true;
+ $contentHandler = ContentHandler::getForTitle( $this->mTitle );
+ return $contentHandler->supportsSections();
}
/**
@@ -568,13 +615,20 @@ class EditPage {
* @param $request WebRequest
*/
function importFormData( &$request ) {
- global $wgLang, $wgUser;
+ global $wgContLang, $wgUser;
wfProfileIn( __METHOD__ );
# Section edit can come from either the form or a link
$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
+ if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
+ wfProfileOut( __METHOD__ );
+ throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
+ }
+
+ $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
+
if ( $request->wasPosted() ) {
# These fields need to be checked for encoding.
# Also remove trailing whitespace, but don't remove _initial_
@@ -586,13 +640,15 @@ class EditPage {
// modified by subclasses
wfProfileIn( get_class( $this ) . "::importContentFormData" );
$textbox1 = $this->importContentFormData( $request );
- if ( isset( $textbox1 ) )
+ if ( $textbox1 !== null ) {
$this->textbox1 = $textbox1;
+ }
+
wfProfileOut( get_class( $this ) . "::importContentFormData" );
}
# Truncate for whole multibyte characters
- $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 255 );
+ $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
# If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
# header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
@@ -604,12 +660,17 @@ class EditPage {
# currently doing double duty as both edit summary and section title. Right now this
# is just to allow API edits to work around this limitation, but this should be
# incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
- $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
+ $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
$this->edittime = $request->getVal( 'wpEdittime' );
$this->starttime = $request->getVal( 'wpStarttime' );
+ $undidRev = $request->getInt( 'wpUndidRevision' );
+ if ( $undidRev ) {
+ $this->undidRev = $undidRev;
+ }
+
$this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
@@ -661,7 +722,7 @@ class EditPage {
$this->starttime = null;
}
- $this->recreate = $request->getCheck( 'wpRecreate' );
+ $this->recreate = $request->getCheck( 'wpRecreate' );
$this->minoredit = $request->getCheck( 'wpMinoredit' );
$this->watchthis = $request->getCheck( 'wpWatchthis' );
@@ -679,18 +740,18 @@ class EditPage {
} else {
# Not a posted form? Start with nothing.
wfDebug( __METHOD__ . ": Not a posted form.\n" );
- $this->textbox1 = '';
- $this->summary = '';
+ $this->textbox1 = '';
+ $this->summary = '';
$this->sectiontitle = '';
- $this->edittime = '';
- $this->starttime = wfTimestampNow();
- $this->edit = false;
- $this->preview = false;
- $this->save = false;
- $this->diff = false;
- $this->minoredit = false;
- $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters
- $this->recreate = false;
+ $this->edittime = '';
+ $this->starttime = wfTimestampNow();
+ $this->edit = false;
+ $this->preview = false;
+ $this->save = false;
+ $this->diff = false;
+ $this->minoredit = false;
+ $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overridden by request parameters
+ $this->recreate = false;
// When creating a new section, we can preload a section title by passing it as the
// preloadtitle parameter in the URL (Bug 13100)
@@ -711,10 +772,17 @@ class EditPage {
}
}
+ $this->oldid = $request->getInt( 'oldid' );
+
$this->bot = $request->getBool( 'bot', true );
$this->nosummary = $request->getBool( 'nosummary' );
- $this->oldid = $request->getInt( 'oldid' );
+ $content_handler = ContentHandler::getForTitle( $this->mTitle );
+ $this->contentModel = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
+ $this->contentFormat = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
+
+ #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
+ #TODO: check if the desired content model supports the given content format!
$this->live = $request->getCheck( 'live' );
$this->editintro = $request->getText( 'editintro',
@@ -730,7 +798,7 @@ class EditPage {
/**
* 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
+ * this method should be overridden and return the page text that will be used
* for saving, preview parsing and so on...
*
* @param $request WebRequest
@@ -747,7 +815,13 @@ class EditPage {
function initialiseForm() {
global $wgUser;
$this->edittime = $this->mArticle->getTimestamp();
- $this->textbox1 = $this->getContent( false );
+
+ $content = $this->getContentObject( false ); #TODO: track content object?!
+ if ( $content === false ) {
+ return false;
+ }
+ $this->textbox1 = $this->toEditText( $content );
+
// activate checkboxes if user wants them to be always active
# Sort out the "watch" checkbox
if ( $wgUser->getOption( 'watchdefault' ) ) {
@@ -766,43 +840,73 @@ class EditPage {
if ( $this->textbox1 === false ) {
return false;
}
- wfProxyCheck();
return true;
}
/**
* Fetch initial editing page content.
*
- * @param $def_text string
+ * @param $def_text string|bool
* @return mixed string on success, $def_text for invalid sections
* @private
+ * @deprecated since 1.21, get WikiPage::getContent() instead.
*/
- function getContent( $def_text = '' ) {
- global $wgOut, $wgRequest, $wgParser;
+ function getContent( $def_text = false ) {
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+ $def_content = $this->toEditContent( $def_text );
+ } else {
+ $def_content = false;
+ }
+
+ $content = $this->getContentObject( $def_content );
+
+ // Note: EditPage should only be used with text based content anyway.
+ return $this->toEditText( $content );
+ }
+
+ /**
+ * @param Content|null $def_content The default value to return
+ *
+ * @return mixed Content on success, $def_content for invalid sections
+ *
+ * @since 1.21
+ */
+ protected function getContentObject( $def_content = null ) {
+ global $wgOut, $wgRequest;
wfProfileIn( __METHOD__ );
- $text = false;
+ $content = false;
// For message page not locally set, use the i18n message.
// For other non-existent articles, use preload text if any.
if ( !$this->mTitle->exists() || $this->section == 'new' ) {
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
# If this is a system message, get the default text.
- $text = $this->mTitle->getDefaultMessageText();
+ $msg = $this->mTitle->getDefaultMessageText();
+
+ $content = $this->toEditContent( $msg );
}
- if ( $text === false ) {
+ if ( $content === false ) {
# If requested, preload some text.
$preload = $wgRequest->getVal( 'preload',
// Custom preload text for new sections
$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
- $text = $this->getPreloadedText( $preload );
+
+ $content = $this->getPreloadedContent( $preload );
}
// For existing pages, get text based on "undo" or section parameters.
} else {
if ( $this->section != '' ) {
// Get section edit text (returns $def_text for invalid sections)
- $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
+ $orig = $this->getOriginalContent();
+ $content = $orig ? $orig->getSection( $this->section ) : null;
+
+ if ( !$content ) {
+ $content = $def_content;
+ }
} else {
$undoafter = $wgRequest->getInt( 'undoafter' );
$undo = $wgRequest->getInt( 'undo' );
@@ -818,15 +922,16 @@ class EditPage {
# Sanity check, make sure it's the right page,
# the revisions exist and they were not deleted.
- # Otherwise, $text will be left as-is.
+ # Otherwise, $content will be left as-is.
if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
$undorev->getPage() == $oldrev->getPage() &&
$undorev->getPage() == $this->mTitle->getArticleID() &&
!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
!$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
- $text = $this->mArticle->getUndoText( $undorev, $oldrev );
- if ( $text === false ) {
+ $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
+
+ if ( $content === false ) {
# Warn the user that something went wrong
$undoMsg = 'failure';
} else {
@@ -836,7 +941,19 @@ class EditPage {
# If we just undid one rev, use an autosummary
$firstrev = $oldrev->getNext();
if ( $firstrev && $firstrev->getId() == $undo ) {
- $undoSummary = wfMessage( 'undo-summary', $undo, $undorev->getUserText() )->inContentLanguage()->text();
+ $userText = $undorev->getUserText();
+ if ( $userText === '' ) {
+ $undoSummary = wfMessage(
+ 'undo-summary-username-hidden',
+ $undo
+ )->inContentLanguage()->text();
+ } else {
+ $undoSummary = wfMessage(
+ 'undo-summary',
+ $undo,
+ $userText
+ )->inContentLanguage()->text();
+ }
if ( $this->summary === '' ) {
$this->summary = $undoSummary;
} else {
@@ -854,19 +971,20 @@ class EditPage {
$undoMsg = 'norev';
}
+ // Messages: undo-success, undo-failure, undo-norev
$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
$this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
}
- if ( $text === false ) {
- $text = $this->getOriginalContent();
+ if ( $content === false ) {
+ $content = $this->getOriginalContent();
}
}
}
wfProfileOut( __METHOD__ );
- return $text;
+ return $content;
}
/**
@@ -876,38 +994,55 @@ class EditPage {
* section replaced in its context (using WikiPage::replaceSection())
* to the original text of the edit.
*
- * This difers from Article::getContent() that when a missing revision is
- * encountered the result will be an empty string and not the
+ * This differs from Article::getContent() that when a missing revision is
+ * encountered the result will be null and not the
* 'missing-revision' message.
*
* @since 1.19
- * @return string
+ * @return Content|null
*/
private function getOriginalContent() {
if ( $this->section == 'new' ) {
- return $this->getCurrentText();
+ return $this->getCurrentContent();
}
$revision = $this->mArticle->getRevisionFetched();
if ( $revision === null ) {
- return '';
+ if ( !$this->contentModel ) {
+ $this->contentModel = $this->getTitle()->getContentModel();
+ }
+ $handler = ContentHandler::getForModelID( $this->contentModel );
+
+ return $handler->makeEmptyContent();
}
- return $this->mArticle->getContent();
+ $content = $revision->getContent();
+ return $content;
}
/**
- * Get the actual text of the page. This is basically similar to
- * WikiPage::getRawText() except that when the page doesn't exist an empty
- * string is returned instead of false.
+ * Get the current content of the page. This is basically similar to
+ * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
+ * content object is returned instead of null.
*
- * @since 1.19
- * @return string
+ * @since 1.21
+ * @return Content
*/
- private function getCurrentText() {
- $text = $this->mArticle->getRawText();
- if ( $text === false ) {
- return '';
+ protected function getCurrentContent() {
+ $rev = $this->mArticle->getRevision();
+ $content = $rev ? $rev->getContent( Revision::RAW ) : null;
+
+ if ( $content === false || $content === null ) {
+ if ( !$this->contentModel ) {
+ $this->contentModel = $this->getTitle()->getContentModel();
+ }
+ $handler = ContentHandler::getForModelID( $this->contentModel );
+
+ return $handler->makeEmptyContent();
} else {
- return $text;
+ # nasty side-effect, but needed for consistency
+ $this->contentModel = $rev->getContentModel();
+ $this->contentFormat = $rev->getContentFormat();
+
+ return $content;
}
}
@@ -915,47 +1050,111 @@ class EditPage {
* Use this method before edit() to preload some text into the edit box
*
* @param $text string
+ * @deprecated since 1.21, use setPreloadedContent() instead.
*/
public function setPreloadedText( $text ) {
- $this->mPreloadText = $text;
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+
+ $content = $this->toEditContent( $text );
+
+ $this->setPreloadedContent( $content );
+ }
+
+ /**
+ * Use this method before edit() to preload some content into the edit box
+ *
+ * @param $content Content
+ *
+ * @since 1.21
+ */
+ public function setPreloadedContent( Content $content ) {
+ $this->mPreloadContent = $content;
}
/**
* Get the contents to be preloaded into the box, either set by
* an earlier setPreloadText() or by loading the given page.
*
- * @param $preload String: representing the title to preload from.
+ * @param string $preload representing the title to preload from.
+ *
* @return String
+ *
+ * @deprecated since 1.21, use getPreloadedContent() instead
*/
protected function getPreloadedText( $preload ) {
- global $wgUser, $wgParser;
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+
+ $content = $this->getPreloadedContent( $preload );
+ $text = $this->toEditText( $content );
+
+ return $text;
+ }
+
+ /**
+ * Get the contents to be preloaded into the box, either set by
+ * an earlier setPreloadText() or by loading the given page.
+ *
+ * @param string $preload representing the title to preload from.
+ *
+ * @return Content
+ *
+ * @since 1.21
+ */
+ protected function getPreloadedContent( $preload ) {
+ global $wgUser;
- if ( !empty( $this->mPreloadText ) ) {
- return $this->mPreloadText;
+ if ( !empty( $this->mPreloadContent ) ) {
+ return $this->mPreloadContent;
}
+ $handler = ContentHandler::getForTitle( $this->getTitle() );
+
if ( $preload === '' ) {
- return '';
+ return $handler->makeEmptyContent();
}
$title = Title::newFromText( $preload );
# Check for existence to avoid getting MediaWiki:Noarticletext
- if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
- return '';
+ if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
+ //TODO: somehow show a warning to the user!
+ return $handler->makeEmptyContent();
}
$page = WikiPage::factory( $title );
if ( $page->isRedirect() ) {
$title = $page->getRedirectTarget();
# Same as before
- if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
- return '';
+ if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
+ //TODO: somehow show a warning to the user!
+ return $handler->makeEmptyContent();
}
$page = WikiPage::factory( $title );
}
$parserOptions = ParserOptions::newFromUser( $wgUser );
- return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
+ $content = $page->getContent( Revision::RAW );
+
+ if ( !$content ) {
+ //TODO: somehow show a warning to the user!
+ return $handler->makeEmptyContent();
+ }
+
+ if ( $content->getModel() !== $handler->getModelID() ) {
+ $converted = $content->convert( $handler->getModelID() );
+
+ if ( !$converted ) {
+ //TODO: somehow show a warning to the user!
+ wfDebug( "Attempt to preload incompatible content: "
+ . "can't convert " . $content->getModel()
+ . " to " . $handler->getModelID() );
+
+ return $handler->makeEmptyContent();
+ }
+
+ $content = $converted;
+ }
+
+ return $content->preloadTransform( $title, $parserOptions );
}
/**
@@ -974,7 +1173,35 @@ class EditPage {
}
/**
+ * Sets post-edit cookie indicating the user just saved a particular revision.
+ *
+ * This uses a temporary cookie for each revision ID so separate saves will never
+ * interfere with each other.
+ *
+ * The cookie is deleted in the mediawiki.action.view.postEdit JS module after
+ * the redirect. It must be clearable by JavaScript code, so it must not be
+ * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config
+ * variable.
+ *
+ * We use a path of '/' since wgCookiePath is not exposed to JS
+ *
+ * If the variable were set on the server, it would be cached, which is unwanted
+ * since the post-edit state should only apply to the load right after the save.
+ */
+ protected function setPostEditCookie() {
+ $revisionId = $this->mArticle->getLatest();
+ $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
+
+ $response = RequestContext::getMain()->getRequest()->response();
+ $response->setcookie( $postEditKey, '1', time() + self::POST_EDIT_COOKIE_DURATION, array(
+ 'path' => '/',
+ 'httpOnly' => false,
+ ) );
+ }
+
+ /**
* Attempt submission
+ * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
* @return bool false if output is done, true if the rest of the form should be displayed
*/
function attemptSave() {
@@ -987,6 +1214,9 @@ class EditPage {
// FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status
if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) {
$this->didSave = true;
+ if ( !$resultDetails['nullEdit'] ) {
+ $this->setPostEditCookie();
+ }
}
switch ( $status->value ) {
@@ -1003,9 +1233,13 @@ class EditPage {
case self::AS_HOOK_ERROR:
return false;
+ case self::AS_PARSE_ERROR:
+ $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
+ return true;
+
case self::AS_SUCCESS_NEW_ARTICLE:
$query = $resultDetails['redirect'] ? 'redirect=no' : '';
- $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
+ $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
$wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
return false;
@@ -1067,10 +1301,62 @@ class EditPage {
}
/**
+ * Run hooks that can filter edits just before they get saved.
+ *
+ * @param Content $content the Content to filter.
+ * @param Status $status for reporting the outcome to the caller
+ * @param User $user the user performing the edit
+ *
+ * @return bool
+ */
+ protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
+ // Run old style post-section-merge edit filter
+ if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
+ array( $this, $content, &$this->hookError, $this->summary ) ) ) {
+
+ # Error messages etc. could be handled within the hook...
+ $status->fatal( 'hookaborted' );
+ $status->value = self::AS_HOOK_ERROR;
+ return false;
+ } elseif ( $this->hookError != '' ) {
+ # ...or the hook could be expecting us to produce an error
+ $status->fatal( 'hookaborted' );
+ $status->value = self::AS_HOOK_ERROR_EXPECTED;
+ return false;
+ }
+
+ // Run new style post-section-merge edit filter
+ if ( !wfRunHooks( 'EditFilterMergedContent',
+ array( $this->mArticle->getContext(), $content, $status, $this->summary,
+ $user, $this->minoredit ) ) ) {
+
+ # Error messages etc. could be handled within the hook...
+ // XXX: $status->value may already be something informative...
+ $this->hookError = $status->getWikiText();
+ $status->fatal( 'hookaborted' );
+ $status->value = self::AS_HOOK_ERROR;
+ return false;
+ } elseif ( !$status->isOK() ) {
+ # ...or the hook could be expecting us to produce an error
+ // FIXME this sucks, we should just use the Status object throughout
+ $this->hookError = $status->getWikiText();
+ $status->fatal( 'hookaborted' );
+ $status->value = self::AS_HOOK_ERROR_EXPECTED;
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
* Attempt submission (no UI)
*
- * @param $result
- * @param $bot bool
+ * @param array $result array to add statuses to, currently with the possible keys:
+ * spam - string - Spam string from content if any spam is detected by matchSpamRegex
+ * sectionanchor - string - Section anchor for a section save
+ * nullEdit - boolean - Set if doEditContent is OK. True if null edit, false otherwise.
+ * redirect - boolean - Set if doEditContent is OK. True if resulting revision is a redirect
+ * @param bool $bot True if edit is being made under the bot right.
*
* @return Status object, possibly with a message, but always with one of the AS_* constants in $status->value,
*
@@ -1083,7 +1369,7 @@ class EditPage {
$status = Status::newGood();
- wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__ . '-checks' );
if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) {
@@ -1091,25 +1377,66 @@ class EditPage {
$status->fatal( 'hookaborted' );
$status->value = self::AS_HOOK_ERROR;
wfProfileOut( __METHOD__ . '-checks' );
- wfProfileOut( __METHOD__ );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ $spam = $wgRequest->getText( 'wpAntispam' );
+ if ( $spam !== '' ) {
+ wfDebugLog(
+ 'SimpleAntiSpam',
+ $wgUser->getName() .
+ ' editing "' .
+ $this->mTitle->getPrefixedText() .
+ '" submitted bogus field "' .
+ $spam .
+ '"'
+ );
+ $status->fatal( 'spamprotectionmatch', false );
+ $status->value = self::AS_SPAM_ERROR;
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ try {
+ # Construct Content object
+ $textbox_content = $this->toEditContent( $this->textbox1 );
+ } catch ( MWContentSerializationException $ex ) {
+ $status->fatal( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+ $status->value = self::AS_PARSE_ERROR;
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return $status;
}
# Check image redirect
if ( $this->mTitle->getNamespace() == NS_FILE &&
- Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
+ $textbox_content->isRedirect() &&
!$wgUser->isAllowed( 'upload' ) ) {
$code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
$status->setResult( false, $code );
wfProfileOut( __METHOD__ . '-checks' );
- wfProfileOut( __METHOD__ );
+ wfProfileOut( __METHOD__ );
return $status;
}
# Check for spam
$match = self::matchSummarySpamRegex( $this->summary );
+ if ( $match === false && $this->section == 'new' ) {
+ # $wgSpamRegex is enforced on this new heading/summary because, unlike
+ # regular summaries, it is added to the actual wikitext.
+ if ( $this->sectiontitle !== '' ) {
+ # This branch is taken when the API is used with the 'sectiontitle' parameter.
+ $match = self::matchSpamRegex( $this->sectiontitle );
+ } else {
+ # This branch is taken when the "Add Topic" user interface is used, or the API
+ # is used with the 'summary' parameter.
+ $match = self::matchSpamRegex( $this->summary );
+ }
+ }
if ( $match === false ) {
$match = self::matchSpamRegex( $this->textbox1 );
}
@@ -1183,7 +1510,7 @@ class EditPage {
wfProfileOut( __METHOD__ );
return $status;
}
- if ( $wgUser->pingLimiter() ) {
+ if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
$status->fatal( 'actionthrottledtext' );
$status->value = self::AS_RATE_LIMITED;
wfProfileOut( __METHOD__ . '-checks' );
@@ -1209,7 +1536,7 @@ class EditPage {
if ( $new ) {
// Late check for create permission, just in case *PARANOIA*
- if ( !$this->mTitle->userCan( 'create' ) ) {
+ if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
$status->fatal( 'nocreatetext' );
$status->value = self::AS_NO_CREATE_PERMISSION;
wfDebug( __METHOD__ . ": no create permission\n" );
@@ -1217,35 +1544,34 @@ class EditPage {
return $status;
}
- # Don't save a new article if it's blank.
- if ( $this->textbox1 == '' ) {
+ // Don't save a new page if it's blank or if it's a MediaWiki:
+ // message with content equivalent to default (allow empty pages
+ // in this case to disable messages, see bug 50124)
+ $defaultMessageText = $this->mTitle->getDefaultMessageText();
+ if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
+ $defaultText = $defaultMessageText;
+ } else {
+ $defaultText = '';
+ }
+
+ if ( $this->textbox1 === $defaultText ) {
$status->setResult( false, self::AS_BLANK_ARTICLE );
wfProfileOut( __METHOD__ );
return $status;
}
- // 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...
- $status->fatal( 'hookaborted' );
- $status->value = self::AS_HOOK_ERROR;
- wfProfileOut( __METHOD__ );
- return $status;
- } elseif ( $this->hookError != '' ) {
- # ...or the hook could be expecting us to produce an error
- $status->fatal( 'hookaborted' );
- $status->value = self::AS_HOOK_ERROR_EXPECTED;
+ if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
wfProfileOut( __METHOD__ );
return $status;
}
- $text = $this->textbox1;
+ $content = $textbox_content;
+
$result['sectionanchor'] = '';
if ( $this->section == 'new' ) {
if ( $this->sectiontitle !== '' ) {
// Insert the section title above the content.
- $text = wfMessage( 'newsectionheaderdefaultlevel', $this->sectiontitle )
- ->inContentLanguage()->text() . "\n\n" . $text;
+ $content = $content->addSectionHeader( $this->sectiontitle );
// Jump to the new section
$result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
@@ -1260,8 +1586,7 @@ class EditPage {
}
} elseif ( $this->summary !== '' ) {
// Insert the section title above the content.
- $text = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )
- ->inContentLanguage()->text() . "\n\n" . $text;
+ $content = $content->addSectionHeader( $this->summary );
// Jump to the new section
$result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
@@ -1275,10 +1600,13 @@ class EditPage {
$status->value = self::AS_SUCCESS_NEW_ARTICLE;
- } else {
+ } else { # not $new
# Article exists. Check for edit conflict.
+
+ $this->mArticle->clear(); # Force reload of dates, etc.
$timestamp = $this->mArticle->getTimestamp();
+
wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
if ( $timestamp != $this->edittime ) {
@@ -1295,41 +1623,46 @@ class EditPage {
$this->isConflict = false;
wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
}
- } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(), $wgUser->getId(), $this->edittime ) ) {
+ } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(),
+ $wgUser->getId(), $this->edittime ) ) {
# Suppress edit conflict with self, except for section edits where merging is required.
wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
$this->isConflict = false;
}
}
- // If sectiontitle is set, use it, otherwise use the summary as the section title (for
- // backwards compatibility with old forms/bots).
+ // If sectiontitle is set, use it, otherwise use the summary as the section title.
if ( $this->sectiontitle !== '' ) {
$sectionTitle = $this->sectiontitle;
} else {
$sectionTitle = $this->summary;
}
+ $content = null;
+
if ( $this->isConflict ) {
- wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
- $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
+ wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
+ . " (article time '{$timestamp}')\n" );
+
+ $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
} else {
- wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
- $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
+ wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
+ $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
}
- if ( is_null( $text ) ) {
+
+ if ( is_null( $content ) ) {
wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
$this->isConflict = true;
- $text = $this->textbox1; // do not try to merge here!
+ $content = $textbox_content; // do not try to merge here!
} elseif ( $this->isConflict ) {
# Attempt merge
- if ( $this->mergeChangesInto( $text ) ) {
+ if ( $this->mergeChangesIntoContent( $content ) ) {
// Successful merge! Maybe we should tell the user the good news?
$this->isConflict = false;
wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
} else {
$this->section = '';
- $this->textbox1 = $text;
+ $this->textbox1 = ContentHandler::getContentText( $content );
wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
}
}
@@ -1340,58 +1673,45 @@ class EditPage {
return $status;
}
- // 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...
- $status->fatal( 'hookaborted' );
- $status->value = self::AS_HOOK_ERROR;
- wfProfileOut( __METHOD__ );
- return $status;
- } elseif ( $this->hookError != '' ) {
- # ...or the hook could be expecting us to produce an error
- $status->fatal( 'hookaborted' );
- $status->value = self::AS_HOOK_ERROR_EXPECTED;
+ if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
wfProfileOut( __METHOD__ );
return $status;
}
- # Handle the user preference to force summaries here, but not for null edits
- if ( $this->section != 'new' && !$this->allowBlankSummary
- && $this->getOriginalContent() != $text
- && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
- {
- if ( md5( $this->summary ) == $this->autoSumm ) {
+ if ( $this->section == 'new' ) {
+ // Handle the user preference to force summaries here
+ if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
$this->missingSummary = true;
- $status->fatal( 'missingsummary' );
+ $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
$status->value = self::AS_SUMMARY_NEEDED;
wfProfileOut( __METHOD__ );
return $status;
}
- }
- # And a similar thing for new sections
- if ( $this->section == 'new' && !$this->allowBlankSummary ) {
- if ( trim( $this->summary ) == '' ) {
- $this->missingSummary = true;
- $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
- $status->value = self::AS_SUMMARY_NEEDED;
+ // Do not allow the user to post an empty comment
+ if ( $this->textbox1 == '' ) {
+ $this->missingComment = true;
+ $status->fatal( 'missingcommenttext' );
+ $status->value = self::AS_TEXTBOX_EMPTY;
wfProfileOut( __METHOD__ );
return $status;
}
+ } elseif ( !$this->allowBlankSummary
+ && !$content->equals( $this->getOriginalContent() )
+ && !$content->isRedirect()
+ && md5( $this->summary ) == $this->autoSumm
+ ) {
+ $this->missingSummary = true;
+ $status->fatal( 'missingsummary' );
+ $status->value = self::AS_SUMMARY_NEEDED;
+ wfProfileOut( __METHOD__ );
+ return $status;
}
# All's well
wfProfileIn( __METHOD__ . '-sectionanchor' );
$sectionanchor = '';
if ( $this->section == 'new' ) {
- if ( $this->textbox1 == '' ) {
- $this->missingComment = true;
- $status->fatal( 'missingcommenttext' );
- $status->value = self::AS_TEXTBOX_EMPTY;
- wfProfileOut( __METHOD__ . '-sectionanchor' );
- wfProfileOut( __METHOD__ );
- return $status;
- }
if ( $this->sectiontitle !== '' ) {
$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
// If no edit summary was specified, create one automatically from the section
@@ -1428,14 +1748,14 @@ class EditPage {
// merged the section into full text. Clear the section field
// so that later submission of conflict forms won't try to
// replace that into a duplicated mess.
- $this->textbox1 = $text;
+ $this->textbox1 = $this->toEditText( $content );
$this->section = '';
$status->value = self::AS_SUCCESS_UPDATE;
}
// Check for length errors again now that the section is merged in
- $this->kblength = (int)( strlen( $text ) / 1024 );
+ $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
if ( $this->kblength > $wgMaxArticleSize ) {
$this->tooBig = true;
$status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
@@ -1448,14 +1768,10 @@ class EditPage {
( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
( $bot ? EDIT_FORCE_BOT : 0 );
- $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
+ $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
+ false, null, $this->contentFormat );
- if ( $doEditStatus->isOK() ) {
- $result['redirect'] = Title::newFromRedirect( $text ) !== null;
- $this->commitWatch();
- wfProfileOut( __METHOD__ );
- return $status;
- } else {
+ if ( !$doEditStatus->isOK() ) {
// Failure from doEdit()
// Show the edit conflict page for certain recognized errors from doEdit(),
// but don't show it for errors from extension hooks
@@ -1470,63 +1786,109 @@ class EditPage {
wfProfileOut( __METHOD__ );
return $doEditStatus;
}
+
+ $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
+ if ( $result['nullEdit'] ) {
+ // We don't know if it was a null edit until now, so increment here
+ $wgUser->pingLimiter( 'linkpurge' );
+ }
+ $result['redirect'] = $content->isRedirect();
+ $this->updateWatchlist();
+ wfProfileOut( __METHOD__ );
+ return $status;
}
/**
- * Commit the change of watch status
+ * Register the change of watch status
*/
- protected function commitWatch() {
+ protected function updateWatchlist() {
global $wgUser;
- if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) {
+
+ if ( $wgUser->isLoggedIn()
+ && $this->watchthis != $wgUser->isWatched( $this->mTitle, WatchedItem::IGNORE_USER_RIGHTS )
+ ) {
+ $fname = __METHOD__;
+ $title = $this->mTitle;
+ $watch = $this->watchthis;
+
+ // Do this in its own transaction to reduce contention...
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin( __METHOD__ );
- if ( $this->watchthis ) {
- WatchAction::doWatch( $this->mTitle, $wgUser );
- } else {
- WatchAction::doUnwatch( $this->mTitle, $wgUser );
- }
- $dbw->commit( __METHOD__ );
+ $dbw->onTransactionIdle( function() use ( $dbw, $title, $watch, $wgUser, $fname ) {
+ $dbw->begin( $fname );
+ WatchAction::doWatchOrUnwatch( $watch, $title, $wgUser );
+ $dbw->commit( $fname );
+ } );
}
}
/**
- * @private
- * @todo document
+ * Attempts to merge text content with base and current revisions
*
* @param $editText string
*
* @return bool
+ * @deprecated since 1.21, use mergeChangesIntoContent() instead
*/
function mergeChangesInto( &$editText ) {
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+
+ $editContent = $this->toEditContent( $editText );
+
+ $ok = $this->mergeChangesIntoContent( $editContent );
+
+ if ( $ok ) {
+ $editText = $this->toEditText( $editContent );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to do 3-way merge of edit content with a base revision
+ * and current content, in case of edit conflict, in whichever way appropriate
+ * for the content type.
+ *
+ * @since 1.21
+ *
+ * @param $editContent
+ *
+ * @return bool
+ */
+ private function mergeChangesIntoContent( &$editContent ) {
wfProfileIn( __METHOD__ );
$db = wfGetDB( DB_MASTER );
// This is the revision the editor started from
$baseRevision = $this->getBaseRevision();
- if ( is_null( $baseRevision ) ) {
+ $baseContent = $baseRevision ? $baseRevision->getContent() : null;
+
+ if ( is_null( $baseContent ) ) {
wfProfileOut( __METHOD__ );
return false;
}
- $baseText = $baseRevision->getText();
// The current state, we want to merge updates into it
$currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
- if ( is_null( $currentRevision ) ) {
+ $currentContent = $currentRevision ? $currentRevision->getContent() : null;
+
+ if ( is_null( $currentContent ) ) {
wfProfileOut( __METHOD__ );
return false;
}
- $currentText = $currentRevision->getText();
- $result = '';
- if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
- $editText = $result;
+ $handler = ContentHandler::getForModelID( $baseContent->getModel() );
+
+ $result = $handler->merge3( $baseContent, $editContent, $currentContent );
+
+ if ( $result ) {
+ $editContent = $result;
wfProfileOut( __METHOD__ );
return true;
- } else {
- wfProfileOut( __METHOD__ );
- return false;
}
+
+ wfProfileOut( __METHOD__ );
+ return false;
}
/**
@@ -1558,11 +1920,11 @@ class EditPage {
}
/**
- * Check given input text against $wgSpamRegex, and return the text of the first match.
+ * Check given input text against $wgSummarySpamRegex, and return the text of the first match.
*
* @param $text string
*
- * @return string|bool matching string or false
+ * @return string|bool matching string or false
*/
public static function matchSummarySpamRegex( $text ) {
global $wgSummarySpamRegex;
@@ -1589,10 +1951,16 @@ class EditPage {
global $wgOut, $wgUser;
$wgOut->addModules( 'mediawiki.action.edit' );
+ $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
if ( $wgUser->getOption( 'uselivepreview', false ) ) {
$wgOut->addModules( 'mediawiki.action.edit.preview' );
}
+
+ if ( $wgUser->getOption( 'useeditwarning', false ) ) {
+ $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
+ }
+
// Bug #19334: textarea jumps when editing articles in IE8
$wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' );
@@ -1632,15 +2000,15 @@ class EditPage {
if ( $namespace == NS_MEDIAWIKI ) {
# Show a warning if editing an interface message
$wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
- } else if( $namespace == NS_FILE ) {
+ } elseif ( $namespace == NS_FILE ) {
# Show a hint to shared repo
$file = wfFindFile( $this->mTitle );
- if( $file && !$file->isLocal() ) {
+ if ( $file && !$file->isLocal() ) {
$descUrl = $file->getDescriptionUrl();
# there must be a description url to show a hint to shared repo
- if( $descUrl ) {
- if( !$this->mTitle->exists() ) {
- $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", array (
+ if ( $descUrl ) {
+ if ( !$this->mTitle->exists() ) {
+ $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", array(
'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
) );
} else {
@@ -1690,10 +2058,13 @@ class EditPage {
# Give a notice if the user is editing a deleted/moved page...
if ( !$this->mTitle->exists() ) {
LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle,
- '', array( 'lim' => 10,
- 'conds' => array( "log_action != 'revision'" ),
- 'showIfEmpty' => false,
- 'msgKey' => array( 'recreate-moveddeleted-warn' ) )
+ '',
+ array(
+ 'lim' => 10,
+ 'conds' => array( "log_action != 'revision'" ),
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'recreate-moveddeleted-warn' )
+ )
);
}
}
@@ -1711,17 +2082,77 @@ class EditPage {
// Added using template syntax, to take <noinclude>'s into account.
$wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle );
return true;
- } else {
- return false;
}
- } else {
- return false;
}
+ return false;
+ }
+
+ /**
+ * Gets an editable textual representation of $content.
+ * The textual representation can be turned by into a Content object by the
+ * toEditContent() method.
+ *
+ * If $content is null or false or a string, $content is returned unchanged.
+ *
+ * If the given Content object is not of a type that can be edited using the text base EditPage,
+ * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
+ * content.
+ *
+ * @param Content|null|bool|string $content
+ * @return String the editable text form of the content.
+ *
+ * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true.
+ */
+ protected function toEditText( $content ) {
+ if ( $content === null || $content === false ) {
+ return $content;
+ }
+
+ if ( is_string( $content ) ) {
+ return $content;
+ }
+
+ if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
+ throw new MWException( "This content model can not be edited as text: "
+ . ContentHandler::getLocalizedName( $content->getModel() ) );
+ }
+
+ return $content->serialize( $this->contentFormat );
+ }
+
+ /**
+ * Turns the given text into a Content object by unserializing it.
+ *
+ * If the resulting Content object is not of a type that can be edited using the text base EditPage,
+ * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
+ * content.
+ *
+ * @param string|null|bool $text Text to unserialize
+ * @return Content The content object created from $text. If $text was false or null, false resp. null will be
+ * returned instead.
+ *
+ * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent
+ * and $this->allowNonTextContent is not true.
+ */
+ protected function toEditContent( $text ) {
+ if ( $text === false || $text === null ) {
+ return $text;
+ }
+
+ $content = ContentHandler::makeContent( $text, $this->getTitle(),
+ $this->contentModel, $this->contentFormat );
+
+ if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
+ throw new MWException( "This content model can not be edited as text: "
+ . ContentHandler::getLocalizedName( $content->getModel() ) );
+ }
+
+ return $content;
}
/**
* Send the edit form and related headers to $wgOut
- * @param $formCallback Callback that takes an OutputPage parameter; will be called
+ * @param $formCallback Callback|null that takes an OutputPage parameter; will be called
* during form output near the top, for captchas and the like.
*/
function showEditForm( $formCallback = null ) {
@@ -1767,6 +2198,8 @@ class EditPage {
}
}
+ // @todo add EditForm plugin interface and use it here!
+ // search for textarea1 and textares2, and allow EditForm to override all uses.
$wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
'enctype' => 'multipart/form-data' ) ) );
@@ -1775,6 +2208,14 @@ class EditPage {
call_user_func_array( $formCallback, array( &$wgOut ) );
}
+ // Add an empty field to trip up spambots
+ $wgOut->addHTML(
+ Xml::openElement( 'div', array( 'id' => 'antispam-container', 'style' => 'display: none;' ) )
+ . Html::rawElement( 'label', array( 'for' => 'wpAntiSpam' ), wfMessage( 'simpleantispam-label' )->parse() )
+ . Xml::element( 'input', array( 'type' => 'text', 'name' => 'wpAntispam', 'id' => 'wpAntispam', 'value' => '' ) )
+ . Xml::closeElement( 'div' )
+ );
+
wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) );
// Put these up at the top to ensure they aren't lost on early form submission
@@ -1800,7 +2241,7 @@ class EditPage {
}
# When the summary is hidden, also hide them on preview/show changes
- if( $this->nosummary ) {
+ if ( $this->nosummary ) {
$wgOut->addHTML( Html::hidden( 'nosummary', true ) );
}
@@ -1820,7 +2261,7 @@ class EditPage {
}
if ( $this->hasPresetSummary ) {
- // If a summary has been preset using &summary= we dont want to prompt for
+ // If a summary has been preset using &summary= we don't want to prompt for
// a different summary. Only prompt for a summary if the summary is blanked.
// (Bug 17416)
$this->autoSumm = md5( '' );
@@ -1831,6 +2272,9 @@ class EditPage {
$wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
+ $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
+ $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
+
if ( $this->section == 'new' ) {
$this->showSummaryInput( true, $this->summary );
$wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
@@ -1843,12 +2287,14 @@ class EditPage {
}
if ( $this->isConflict ) {
- // In an edit conflict bypass the overrideable content form method
+ // In an edit conflict bypass the overridable 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->textbox2 = $this->textbox1;
- $this->textbox1 = $this->getCurrentText();
+
+ $content = $this->getCurrentContent();
+ $this->textbox1 = $this->toEditText( $content );
$this->showTextbox1();
} else {
@@ -1873,8 +2319,19 @@ class EditPage {
$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'hiddencats' ),
Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
+ $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'limitreport' ),
+ self::getPreviewLimitReport( $this->mParserOutput ) ) );
+
+ $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+
if ( $this->isConflict ) {
- $this->showConflict();
+ try {
+ $this->showConflict();
+ } catch ( MWContentSerializationException $ex ) {
+ // this can't really happen, but be nice if it does.
+ $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+ $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+ }
}
$wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
@@ -1909,30 +2366,8 @@ class EditPage {
$wgOut->addWikiMsg( 'talkpagetext' );
}
- # Optional notices on a per-namespace and per-page basis
- $editnotice_ns = 'editnotice-' . $this->mTitle->getNamespace();
- $editnotice_ns_message = wfMessage( $editnotice_ns );
- if ( $editnotice_ns_message->exists() ) {
- $wgOut->addWikiText( $editnotice_ns_message->plain() );
- }
- if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) {
- $parts = explode( '/', $this->mTitle->getDBkey() );
- $editnotice_base = $editnotice_ns;
- while ( count( $parts ) > 0 ) {
- $editnotice_base .= '-' . array_shift( $parts );
- $editnotice_base_msg = wfMessage( $editnotice_base );
- if ( $editnotice_base_msg->exists() ) {
- $wgOut->addWikiText( $editnotice_base_msg->plain() );
- }
- }
- } else {
- # Even if there are no subpages in namespace, we still don't want / in MW ns.
- $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->mTitle->getDBkey() );
- $editnoticeMsg = wfMessage( $editnoticeText );
- if ( $editnoticeMsg->exists() ) {
- $wgOut->addWikiText( $editnoticeMsg->plain() );
- }
- }
+ // Add edit notices
+ $wgOut->addHTML( implode( "\n", $this->mTitle->getEditNotices( $this->oldid ) ) );
if ( $this->isConflict ) {
$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
@@ -1948,7 +2383,7 @@ class EditPage {
if ( $this->section != '' && $this->section != 'new' ) {
if ( !$this->summary && !$this->preview && !$this->diff ) {
- $sectionTitle = self::extractSectionTitle( $this->textbox1 );
+ $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
if ( $sectionTitle !== false ) {
$this->summary = "/* $sectionTitle */ ";
}
@@ -1980,7 +2415,7 @@ class EditPage {
if ( $revision ) {
// Let sysop know that this will make private content public if saved
- if ( !$revision->userCan( Revision::DELETED_TEXT ) ) {
+ if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
$wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
$wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
@@ -2014,10 +2449,13 @@ class EditPage {
$wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) );
}
if ( $this->formtype !== 'preview' ) {
- if ( $this->isCssSubpage )
+ if ( $this->isCssSubpage ) {
$wgOut->wrapWikiMsg( "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", array( 'usercssyoucanpreview' ) );
- if ( $this->isJsSubpage )
+ }
+
+ if ( $this->isJsSubpage ) {
$wgOut->wrapWikiMsg( "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", array( 'userjsyoucanpreview' ) );
+ }
}
}
}
@@ -2073,7 +2511,6 @@ class EditPage {
$this->showHeaderCopyrightWarning();
}
-
/**
* Standard summary input and label (wgSummary), abstracted so EditPage
* subclasses may reorganize the form.
@@ -2081,15 +2518,15 @@ class EditPage {
* inferred by the id given to the input. You can remove them both by
* passing array( 'id' => false ) to $userInputAttrs.
*
- * @param $summary string The value of the summary input
- * @param $labelText string The html to place inside the label
- * @param $inputAttrs array of attrs to use on the input
- * @param $spanLabelAttrs array of attrs to use on the span inside the label
+ * @param string $summary The value of the summary input
+ * @param string $labelText The html to place inside the label
+ * @param array $inputAttrs of attrs to use on the input
+ * @param array $spanLabelAttrs 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 ) {
- // Note: the maxlength is overriden in JS to 255 and to make it use UTF-8 bytes, not characters.
+ // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
$inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array(
'id' => 'wpSummary',
'maxlength' => '200',
@@ -2118,7 +2555,7 @@ class EditPage {
* @param $isSubjectPreview Boolean: true if this is the section subject/title
* up top, or false if this is the comment summary
* down below the textarea
- * @param $summary String: The text of the summary to display
+ * @param string $summary The text of the summary to display
* @return String
*/
protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
@@ -2144,18 +2581,22 @@ class EditPage {
* @param $isSubjectPreview Boolean: true if this is the section subject/title
* up top, or false if this is the comment summary
* down below the textarea
- * @param $summary String: the text of the summary to display
+ * @param string $summary the text of the summary to display
* @return String
*/
protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
- if ( !$summary || ( !$this->preview && !$this->diff ) )
+ // avoid spaces in preview, gets always trimmed on save
+ $summary = trim( $summary );
+ if ( !$summary || ( !$this->preview && !$this->diff ) ) {
return "";
+ }
global $wgParser;
- if ( $isSubjectPreview )
- $summary = wfMessage( 'newsectionsummary', $wgParser->stripSectionName( $summary ) )
+ if ( $isSubjectPreview ) {
+ $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
->inContentLanguage()->text();
+ }
$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
@@ -2174,8 +2615,9 @@ class EditPage {
HTML
);
- if ( !$this->checkUnicodeCompliantBrowser() )
+ if ( !$this->checkUnicodeCompliantBrowser() ) {
$wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
+ }
}
protected function showFormAfterText() {
@@ -2212,8 +2654,8 @@ HTML
* The $textoverride method can be used by subclasses overriding showContentForm
* to pass back to this method.
*
- * @param $customAttribs array of html attributes to use in the textarea
- * @param $textoverride String: optional text to override $this->textarea1 with
+ * @param array $customAttribs 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 ) {
if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
@@ -2255,10 +2697,10 @@ HTML
$this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
}
- protected function showTextbox( $content, $name, $customAttribs = array() ) {
+ protected function showTextbox( $text, $name, $customAttribs = array() ) {
global $wgOut, $wgUser;
- $wikitext = $this->safeUnicodeOutput( $content );
+ $wikitext = $this->safeUnicodeOutput( $text );
if ( strval( $wikitext ) !== '' ) {
// Ensure there's a newline at the end, otherwise adding lines
// is awkward.
@@ -2269,7 +2711,7 @@ HTML
$attribs = $customAttribs + array(
'accesskey' => ',',
- 'id' => $name,
+ 'id' => $name,
'cols' => $wgUser->getIntOption( 'cols' ),
'rows' => $wgUser->getIntOption( 'rows' ),
'style' => '' // avoid php notices when appending preferences (appending allows customAttribs['style'] to still work
@@ -2285,13 +2727,15 @@ HTML
protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
global $wgOut;
$classes = array();
- if ( $isOnTop )
+ if ( $isOnTop ) {
$classes[] = 'ontop';
+ }
$attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) );
- if ( $this->formtype != 'preview' )
+ if ( $this->formtype != 'preview' ) {
$attribs['style'] = 'display: none;';
+ }
$wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
@@ -2302,7 +2746,12 @@ HTML
$wgOut->addHTML( '</div>' );
if ( $this->formtype == 'diff' ) {
- $this->showDiff();
+ try {
+ $this->showDiff();
+ } catch ( MWContentSerializationException $ex ) {
+ $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+ $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+ }
}
}
@@ -2310,7 +2759,7 @@ HTML
* Append preview output to $wgOut.
* Includes category rendering if this is a category page.
*
- * @param $text String: the HTML to be output for the preview.
+ * @param string $text the HTML to be output for the preview.
*/
protected function showPreview( $text ) {
global $wgOut;
@@ -2334,32 +2783,51 @@ HTML
* save and then make a comparison.
*/
function showDiff() {
- global $wgUser, $wgContLang, $wgParser, $wgOut;
+ global $wgUser, $wgContLang, $wgOut;
$oldtitlemsg = 'currentrev';
# if message does not exist, show diff against the preloaded default
- if( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
$oldtext = $this->mTitle->getDefaultMessageText();
- if( $oldtext !== false ) {
+ if ( $oldtext !== false ) {
$oldtitlemsg = 'defaultmessagetext';
+ $oldContent = $this->toEditContent( $oldtext );
+ } else {
+ $oldContent = null;
}
} else {
- $oldtext = $this->mArticle->getRawText();
+ $oldContent = $this->getCurrentContent();
}
- $newtext = $this->mArticle->replaceSection(
- $this->section, $this->textbox1, $this->summary, $this->edittime );
- wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
+ $textboxContent = $this->toEditContent( $this->textbox1 );
- $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
- $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
+ $newContent = $this->mArticle->replaceSectionContent(
+ $this->section, $textboxContent,
+ $this->summary, $this->edittime );
+
+ if ( $newContent ) {
+ ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
+ wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
+
+ $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+ $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
+ }
- if ( $oldtext !== false || $newtext != '' ) {
+ if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
$oldtitle = wfMessage( $oldtitlemsg )->parse();
$newtitle = wfMessage( 'yourtext' )->parse();
- $de = new DifferenceEngine( $this->mArticle->getContext() );
- $de->setText( $oldtext, $newtext );
+ if ( !$oldContent ) {
+ $oldContent = $newContent->getContentHandler()->makeEmptyContent();
+ }
+
+ if ( !$newContent ) {
+ $newContent = $oldContent->getContentHandler()->makeEmptyContent();
+ }
+
+ $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
+ $de->setContent( $oldContent, $newContent );
+
$difftext = $de->getDiff( $oldtitle, $newtitle );
$de->showDiffStyle();
} else {
@@ -2416,7 +2884,15 @@ HTML
return self::getCopyrightWarning( $this->mTitle );
}
- public static function getCopyrightWarning( $title ) {
+ /**
+ * Get the copyright warning, by default returns wikitext
+ *
+ * @param Title $title
+ * @param string $format output format, valid values are any function of
+ * a Message object
+ * @return string
+ */
+ public static function getCopyrightWarning( $title, $format = 'plain' ) {
global $wgRightsText;
if ( $wgRightsText ) {
$copywarnMsg = array( 'copyrightwarning',
@@ -2430,7 +2906,60 @@ HTML
wfRunHooks( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) );
return "<div id=\"editpage-copywarn\">\n" .
- call_user_func_array( 'wfMessage', $copywarnMsg )->plain() . "\n</div>";
+ call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
+ }
+
+ /**
+ * Get the Limit report for page previews
+ *
+ * @since 1.22
+ * @param ParserOutput $output ParserOutput object from the parse
+ * @return string HTML
+ */
+ public static function getPreviewLimitReport( $output ) {
+ if ( !$output || !$output->getLimitReportData() ) {
+ return '';
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ $limitReport = Html::rawElement( 'div', array( 'class' => 'mw-limitReportExplanation' ),
+ wfMessage( 'limitreport-title' )->parseAsBlock()
+ );
+
+ // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
+ $limitReport .= Html::openElement( 'div', array( 'class' => 'preview-limit-report-wrapper' ) );
+
+ $limitReport .= Html::openElement( 'table', array(
+ 'class' => 'preview-limit-report wikitable'
+ ) ) .
+ Html::openElement( 'tbody' );
+
+ foreach ( $output->getLimitReportData() as $key => $value ) {
+ if ( wfRunHooks( 'ParserLimitReportFormat',
+ array( $key, $value, &$limitReport, true, true )
+ ) ) {
+ $keyMsg = wfMessage( $key );
+ $valueMsg = wfMessage( array( "$key-value-html", "$key-value" ) );
+ if ( !$valueMsg->exists() ) {
+ $valueMsg = new RawMessage( '$1' );
+ }
+ if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
+ $limitReport .= Html::openElement( 'tr' ) .
+ Html::rawElement( 'th', null, $keyMsg->parse() ) .
+ Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
+ Html::closeElement( 'tr' );
+ }
+ }
+ }
+
+ $limitReport .= Html::closeElement( 'tbody' ) .
+ Html::closeElement( 'table' ) .
+ Html::closeElement( 'div' );
+
+ wfProfileOut( __METHOD__ );
+
+ return $limitReport;
}
protected function showStandardInputs( &$tabindex = 2 ) {
@@ -2455,7 +2984,9 @@ HTML
$cancel = $this->getCancelLink();
if ( $cancel !== '' ) {
- $cancel .= wfMessage( 'pipe-separator' )->text();
+ $cancel .= Html::element( 'span',
+ array( 'class' => 'mw-editButtons-pipe-separator' ),
+ wfMessage( 'pipe-separator' )->text() );
}
$edithelpurl = Skin::makeInternalOrExternalUrl( wfMessage( 'edithelppage' )->inContentLanguage()->text() );
$edithelp = '<a target="helpwindow" href="' . $edithelpurl . '">' .
@@ -2463,7 +2994,9 @@ HTML
wfMessage( 'newwindow' )->parse();
$wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
$wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
- $wgOut->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" );
+ $wgOut->addHTML( "</div><!-- editButtons -->\n" );
+ wfRunHooks( 'EditPage::showStandardInputs:options', array( $this, $wgOut, &$tabindex ) );
+ $wgOut->addHTML( "</div><!-- editOptions -->\n" );
}
/**
@@ -2476,8 +3009,12 @@ HTML
if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
- $de = new DifferenceEngine( $this->mArticle->getContext() );
- $de->setText( $this->textbox2, $this->textbox1 );
+ $content1 = $this->toEditContent( $this->textbox1 );
+ $content2 = $this->toEditContent( $this->textbox2 );
+
+ $handler = ContentHandler::getForModelID( $this->contentModel );
+ $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
+ $de->setContent( $content2, $content1 );
$de->showDiff(
wfMessage( 'yourtext' )->parse(),
wfMessage( 'storedversion' )->text()
@@ -2548,40 +3085,47 @@ HTML
$dbr = wfGetDB( DB_SLAVE );
$data = $dbr->selectRow(
array( 'logging', 'user' ),
- array( 'log_type',
- 'log_action',
- 'log_timestamp',
- 'log_user',
- 'log_namespace',
- 'log_title',
- 'log_comment',
- 'log_params',
- 'log_deleted',
- 'user_name' ),
- array( 'log_namespace' => $this->mTitle->getNamespace(),
- 'log_title' => $this->mTitle->getDBkey(),
- 'log_type' => 'delete',
- 'log_action' => 'delete',
- 'user_id=log_user' ),
+ array(
+ 'log_type',
+ 'log_action',
+ 'log_timestamp',
+ 'log_user',
+ 'log_namespace',
+ 'log_title',
+ 'log_comment',
+ 'log_params',
+ 'log_deleted',
+ 'user_name'
+ ), array(
+ 'log_namespace' => $this->mTitle->getNamespace(),
+ 'log_title' => $this->mTitle->getDBkey(),
+ 'log_type' => 'delete',
+ 'log_action' => 'delete',
+ 'user_id=log_user'
+ ),
__METHOD__,
array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' )
);
// Quick paranoid permission checks...
if ( is_object( $data ) ) {
- if ( $data->log_deleted & LogPage::DELETED_USER )
+ if ( $data->log_deleted & LogPage::DELETED_USER ) {
$data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
- if ( $data->log_deleted & LogPage::DELETED_COMMENT )
+ }
+
+ if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
$data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
+ }
}
return $data;
}
/**
* Get the rendered text for previewing.
+ * @throws MWException
* @return string
*/
function getPreviewText() {
- global $wgOut, $wgUser, $wgParser, $wgRawHtml, $wgLang;
+ global $wgOut, $wgUser, $wgRawHtml, $wgLang;
wfProfileIn( __METHOD__ );
@@ -2600,82 +3144,96 @@ HTML
return $parsedNote;
}
- if ( $this->mTriedSave && !$this->mTokenOk ) {
- if ( $this->mTokenOkExceptSuffix ) {
- $note = wfMessage( 'token_suffix_mismatch' )->plain();
- } else {
- $note = wfMessage( 'session_fail_preview' )->plain();
- }
- } elseif ( $this->incompleteForm ) {
- $note = wfMessage( 'edit_form_incomplete' )->plain();
- } else {
- $note = wfMessage( 'previewnote' )->plain() .
- ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
- }
+ $note = '';
+
+ try {
+ $content = $this->toEditContent( $this->textbox1 );
- $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+ $previewHTML = '';
+ if ( !wfRunHooks( 'AlternateEditPreview', array( $this, &$content, &$previewHTML, &$this->mParserOutput ) ) ) {
+ wfProfileOut( __METHOD__ );
+ return $previewHTML;
+ }
- $parserOptions->setEditSection( false );
- $parserOptions->setIsPreview( true );
- $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
+ if ( $this->mTriedSave && !$this->mTokenOk ) {
+ if ( $this->mTokenOkExceptSuffix ) {
+ $note = wfMessage( 'token_suffix_mismatch' )->plain();
- # don't parse non-wikitext pages, show message about preview
- if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
- if ( $this->mTitle->isCssJsSubpage() ) {
- $level = 'user';
- } elseif ( $this->mTitle->isCssOrJsPage() ) {
- $level = 'site';
- } else {
- $level = false;
- }
-
- # Used messages to make sure grep find them:
- # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
- $class = 'mw-code';
- if ( $level ) {
- if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
- $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>";
- $class .= " mw-css";
- } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
- $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>";
- $class .= " mw-js";
} else {
- throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
+ $note = wfMessage( 'session_fail_preview' )->plain();
}
- $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
- $previewHTML = $parserOutput->getText();
+ } elseif ( $this->incompleteForm ) {
+ $note = wfMessage( 'edit_form_incomplete' )->plain();
} else {
- $previewHTML = '';
+ $note = wfMessage( 'previewnote' )->plain() .
+ ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
}
- $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
- } else {
- $toparse = $this->textbox1;
+ $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+ $parserOptions->setEditSection( false );
+ $parserOptions->setIsPreview( true );
+ $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
- # If we're adding a comment, we need to show the
- # summary as the headline
- if ( $this->section == "new" && $this->summary != "" ) {
- $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse;
- }
+ # don't parse non-wikitext pages, show message about preview
+ if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
+ if ( $this->mTitle->isCssJsSubpage() ) {
+ $level = 'user';
+ } elseif ( $this->mTitle->isCssOrJsPage() ) {
+ $level = 'site';
+ } else {
+ $level = false;
+ }
- wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
+ if ( $content->getModel() == CONTENT_MODEL_CSS ) {
+ $format = 'css';
+ } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
+ $format = 'js';
+ } else {
+ $format = false;
+ }
- $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
- $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
+ # Used messages to make sure grep find them:
+ # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
+ if ( $level && $format ) {
+ $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage( "{$level}{$format}preview" )->text() . "</div>";
+ }
+ }
- $rt = Title::newFromRedirectArray( $this->textbox1 );
+ $rt = $content->getRedirectChain();
if ( $rt ) {
$previewHTML = $this->mArticle->viewRedirect( $rt, false );
} else {
- $previewHTML = $parserOutput->getText();
- }
- $this->mParserOutput = $parserOutput;
- $wgOut->addParserOutputNoText( $parserOutput );
+ # If we're adding a comment, we need to show the
+ # summary as the headline
+ if ( $this->section === "new" && $this->summary !== "" ) {
+ $content = $content->addSectionHeader( $this->summary );
+ }
+
+ $hook_args = array( $this, &$content );
+ ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
+ wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
+
+ $parserOptions->enableLimitReport();
+
+ # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
+ # But it's now deprecated, so never mind
- if ( count( $parserOutput->getWarnings() ) ) {
- $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+ $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+ $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
+
+ $previewHTML = $parserOutput->getText();
+ $this->mParserOutput = $parserOutput;
+ $wgOut->addParserOutputNoText( $parserOutput );
+
+ if ( count( $parserOutput->getWarnings() ) ) {
+ $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+ }
}
+ } catch ( MWContentSerializationException $ex ) {
+ $m = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+ $note .= "\n\n" . $m->parse();
+ $previewHTML = '';
}
if ( $this->isConflict ) {
@@ -2688,9 +3246,9 @@ HTML
'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
$wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
- $pageLang = $this->mTitle->getPageLanguage();
- $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
- 'class' => 'mw-content-' . $pageLang->getDir() );
+ $pageViewLang = $this->mTitle->getPageViewLanguage();
+ $attribs = array( 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
+ 'class' => 'mw-content-' . $pageViewLang->getDir() );
$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
wfProfileOut( __METHOD__ );
@@ -2887,8 +3445,8 @@ HTML
* Returns an array of html code of the following checkboxes:
* minor and watch
*
- * @param $tabindex int Current tabindex
- * @param $checked Array of checkbox => bool, where bool indicates the checked
+ * @param int $tabindex Current tabindex
+ * @param array $checked of checkbox => bool, where bool indicates the checked
* status of the checkbox
*
* @return array
@@ -2904,9 +3462,9 @@ HTML
$minorLabel = wfMessage( 'minoredit' )->parse();
if ( $wgUser->isAllowed( 'minoredit' ) ) {
$attribs = array(
- 'tabindex' => ++$tabindex,
+ 'tabindex' => ++$tabindex,
'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
- 'id' => 'wpMinoredit',
+ 'id' => 'wpMinoredit',
);
$checkboxes['minor'] =
Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
@@ -2920,9 +3478,9 @@ HTML
$checkboxes['watch'] = '';
if ( $wgUser->isLoggedIn() ) {
$attribs = array(
- 'tabindex' => ++$tabindex,
+ 'tabindex' => ++$tabindex,
'accesskey' => wfMessage( 'accesskey-watch' )->text(),
- 'id' => 'wpWatchthis',
+ 'id' => 'wpWatchthis',
);
$checkboxes['watch'] =
Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
@@ -2938,7 +3496,7 @@ HTML
* Returns an array of html code of the following buttons:
* save, diff, preview and live
*
- * @param $tabindex int Current tabindex
+ * @param int $tabindex Current tabindex
*
* @return array
*/
@@ -2946,37 +3504,37 @@ HTML
$buttons = array();
$temp = array(
- 'id' => 'wpSave',
- 'name' => 'wpSave',
- 'type' => 'submit',
- 'tabindex' => ++$tabindex,
- 'value' => wfMessage( 'savearticle' )->text(),
+ 'id' => 'wpSave',
+ 'name' => 'wpSave',
+ 'type' => 'submit',
+ 'tabindex' => ++$tabindex,
+ 'value' => wfMessage( 'savearticle' )->text(),
'accesskey' => wfMessage( 'accesskey-save' )->text(),
- 'title' => wfMessage( 'tooltip-save' )->text() . ' [' . wfMessage( 'accesskey-save' )->text() . ']',
+ 'title' => wfMessage( 'tooltip-save' )->text() . ' [' . wfMessage( 'accesskey-save' )->text() . ']',
);
$buttons['save'] = Xml::element( 'input', $temp, '' );
++$tabindex; // use the same for preview and live preview
$temp = array(
- 'id' => 'wpPreview',
- 'name' => 'wpPreview',
- 'type' => 'submit',
- 'tabindex' => $tabindex,
- 'value' => wfMessage( 'showpreview' )->text(),
+ 'id' => 'wpPreview',
+ 'name' => 'wpPreview',
+ 'type' => 'submit',
+ 'tabindex' => $tabindex,
+ 'value' => wfMessage( 'showpreview' )->text(),
'accesskey' => wfMessage( 'accesskey-preview' )->text(),
- 'title' => wfMessage( 'tooltip-preview' )->text() . ' [' . wfMessage( 'accesskey-preview' )->text() . ']',
+ 'title' => wfMessage( 'tooltip-preview' )->text() . ' [' . wfMessage( 'accesskey-preview' )->text() . ']',
);
$buttons['preview'] = Xml::element( 'input', $temp, '' );
$buttons['live'] = '';
$temp = array(
- 'id' => 'wpDiff',
- 'name' => 'wpDiff',
- 'type' => 'submit',
- 'tabindex' => ++$tabindex,
- 'value' => wfMessage( 'showdiff' )->text(),
+ 'id' => 'wpDiff',
+ 'name' => 'wpDiff',
+ 'type' => 'submit',
+ 'tabindex' => ++$tabindex,
+ 'value' => wfMessage( 'showdiff' )->text(),
'accesskey' => wfMessage( 'accesskey-diff' )->text(),
- 'title' => wfMessage( 'tooltip-diff' )->text() . ' [' . wfMessage( 'accesskey-diff' )->text() . ']',
+ 'title' => wfMessage( 'tooltip-diff' )->text() . ' [' . wfMessage( 'accesskey-diff' )->text() . ']',
);
$buttons['diff'] = Xml::element( 'input', $temp, '' );
@@ -3067,7 +3625,7 @@ HTML
/**
* Produce the stock "your edit contains spam" page
*
- * @param $match string Text which triggered one or more filters
+ * @param string|bool $match Text which triggered one or more filters
* @deprecated since 1.17 Use method spamPageWithContent() instead
*/
static function spamPage( $match = false ) {
@@ -3096,7 +3654,7 @@ HTML
global $wgOut, $wgLang;
$this->textbox2 = $this->textbox1;
- if( is_array( $match ) ){
+ if ( is_array( $match ) ) {
$match = $wgLang->listToText( $match );
}
$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
@@ -3210,7 +3768,7 @@ HTML
* @private
*/
function makesafe( $invalue ) {
- // Armor existing references for reversability.
+ // Armor existing references for reversibility.
$invalue = strtr( $invalue, array( "&#x" => "&#x0" ) );
$bytesleft = 0;
@@ -3262,7 +3820,7 @@ HTML
$i++;
} while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
- // Do some sanity checks. These aren't needed for reversability,
+ // Do some sanity checks. These aren't needed for reversibility,
// but should help keep the breakage down if the editor
// breaks one of the entities whilst editing.
if ( ( substr( $invalue, $i, 1 ) == ";" ) and ( strlen( $hexstring ) <= 6 ) ) {
@@ -3275,7 +3833,7 @@ HTML
$result .= substr( $invalue, $i, 1 );
}
}
- // reverse the transform that we made for reversability reasons.
+ // reverse the transform that we made for reversibility reasons.
return strtr( $result, array( "&#x0" => "&#x" ) );
}
}
diff --git a/includes/Exception.php b/includes/Exception.php
index 714f73e8..5bad88c2 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -30,7 +30,6 @@
* @ingroup Exception
*/
class MWException extends Exception {
- var $logId;
/**
* Should the exception use $wgOut to output the error?
@@ -45,6 +44,16 @@ class MWException extends Exception {
}
/**
+ * Whether to log this exception in the exception debug log.
+ *
+ * @since 1.23
+ * @return boolean
+ */
+ function isLoggable() {
+ return true;
+ }
+
+ /**
* Can the extension use the Message class/wfMessage to get i18n-ed messages?
*
* @return bool
@@ -64,8 +73,8 @@ class MWException extends Exception {
/**
* Run hook to allow extensions to modify the text of the exception
*
- * @param $name string: class name of the exception
- * @param $args array: arguments to pass to the callback functions
+ * @param string $name class name of the exception
+ * @param array $args arguments to pass to the callback functions
* @return string|null string to output or null if any hook has been called
*/
function runHooks( $name, $args = array() ) {
@@ -75,15 +84,15 @@ class MWException extends Exception {
return null; // Just silently ignore
}
- if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) {
+ if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[$name] ) ) {
return null;
}
- $hooks = $wgExceptionHooks[ $name ];
+ $hooks = $wgExceptionHooks[$name];
$callargs = array_merge( array( $this ), $args );
foreach ( $hooks as $hook ) {
- if ( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) { // 'function' or array( 'class', hook' )
+ if ( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) { // 'function' or array( 'class', hook' )
$result = call_user_func_array( $hook, $callargs );
} else {
$result = null;
@@ -99,8 +108,8 @@ class MWException extends Exception {
/**
* Get a message from i18n
*
- * @param $key string: message name
- * @param $fallback string: default message if the message cache can't be
+ * @param string $key message name
+ * @param string $fallback default message if the message cache can't be
* called by the exception
* The function also has other parameters that are arguments for the message
* @return string message with arguments replaced
@@ -126,13 +135,12 @@ class MWException extends Exception {
global $wgShowExceptionDetails;
if ( $wgShowExceptionDetails ) {
- return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) .
- '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
+ return '<p>' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) .
+ '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) .
"</p>\n";
} else {
- return
- "<div class=\"errorbox\">" .
- '[' . $this->getLogId() . '] ' .
+ return "<div class=\"errorbox\">" .
+ '[' . MWExceptionHandler::getLogId( $this ) . '] ' .
gmdate( 'Y-m-d H:i:s' ) .
": Fatal exception of type " . get_class( $this ) . "</div>\n" .
"<!-- Set \$wgShowExceptionDetails = true; " .
@@ -152,8 +160,8 @@ class MWException extends Exception {
global $wgShowExceptionDetails;
if ( $wgShowExceptionDetails ) {
- return $this->getMessage() .
- "\nBacktrace:\n" . $this->getTraceAsString() . "\n";
+ return MWExceptionHandler::getLogMessage( $this ) .
+ "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $this ) . "\n";
} else {
return "Set \$wgShowExceptionDetails = true; " .
"in LocalSettings.php to show detailed debugging information.\n";
@@ -170,43 +178,28 @@ class MWException extends Exception {
}
/**
- * Get a random ID for this error.
- * This allows to link the exception to its correspoding log entry when
- * $wgShowExceptionDetails is set to false.
+ * Get a the ID for this error.
*
+ * @since 1.20
+ * @deprecated since 1.22 Use MWExceptionHandler::getLogId instead.
* @return string
*/
function getLogId() {
- if ( $this->logId === null ) {
- $this->logId = wfRandomString( 8 );
- }
- return $this->logId;
+ wfDeprecated( __METHOD__, '1.22' );
+ return MWExceptionHandler::getLogId( $this );
}
/**
* Return the requested URL and point to file and line number from which the
* exception occurred
*
+ * @since 1.8
+ * @deprecated since 1.22 Use MWExceptionHandler::getLogMessage instead.
* @return string
*/
function getLogMessage() {
- global $wgRequest;
-
- $id = $this->getLogId();
- $file = $this->getFile();
- $line = $this->getLine();
- $message = $this->getMessage();
-
- if ( isset( $wgRequest ) && !$wgRequest instanceof FauxRequest ) {
- $url = $wgRequest->getRequestURL();
- if ( !$url ) {
- $url = '[no URL]';
- }
- } else {
- $url = '[no req]';
- }
-
- return "[$id] $url Exception from line $line of $file: $message";
+ wfDeprecated( __METHOD__, '1.22' );
+ return MWExceptionHandler::getLogMessage( $this );
}
/**
@@ -248,26 +241,20 @@ class MWException extends Exception {
* It will be either HTML or plain text based on isCommandLine().
*/
function report() {
- global $wgLogExceptionBacktrace;
- $log = $this->getLogMessage();
+ global $wgMimeType;
- if ( $log ) {
- if ( $wgLogExceptionBacktrace ) {
- wfDebugLog( 'exception', $log . "\n" . $this->getTraceAsString() . "\n" );
- } else {
- wfDebugLog( 'exception', $log );
- }
- }
+ MWExceptionHandler::logException( $this );
if ( defined( 'MW_API' ) ) {
// Unhandled API exception, we can't be sure that format printer is alive
header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) );
- wfHttpError(500, 'Internal Server Error', $this->getText() );
+ wfHttpError( 500, 'Internal Server Error', $this->getText() );
} elseif ( self::isCommandLine() ) {
MWExceptionHandler::printError( $this->getText() );
} else {
header( "HTTP/1.1 500 MediaWiki exception" );
header( "Status: 500 MediaWiki exception", true );
+ header( "Content-Type: $wgMimeType; charset=utf-8", true );
$this->reportHTML();
}
@@ -320,20 +307,26 @@ class ErrorPageError extends MWException {
/**
* Note: these arguments are keys into wfMessage(), not text!
*
- * @param $title string|Message Message key (string) for page title, or a Message object
- * @param $msg string|Message Message key (string) for error text, or a Message object
- * @param $params array with parameters to wfMessage()
+ * @param string|Message $title Message key (string) for page title, or a Message object
+ * @param string|Message $msg Message key (string) for error text, or a Message object
+ * @param array $params with parameters to wfMessage()
*/
function __construct( $title, $msg, $params = null ) {
$this->title = $title;
$this->msg = $msg;
$this->params = $params;
- if( $msg instanceof Message ){
- parent::__construct( $msg );
+ // Bug 44111: Messages in the log files should be in English and not
+ // customized by the local wiki. So get the default English version for
+ // passing to the parent constructor. Our overridden report() below
+ // makes sure that the page shown to the user is not forced to English.
+ if ( $msg instanceof Message ) {
+ $enMsg = clone( $msg );
} else {
- parent::__construct( wfMessage( $msg )->text() );
+ $enMsg = wfMessage( $msg, $params );
}
+ $enMsg->inLanguage( 'en' )->useDatabase( false );
+ parent::__construct( $enMsg->text() );
}
function report() {
@@ -354,8 +347,8 @@ class ErrorPageError extends MWException {
*/
class BadTitleError extends ErrorPageError {
/**
- * @param $msg string|Message A message key (default: 'badtitletext')
- * @param $params Array parameter to wfMessage()
+ * @param string|Message $msg A message key (default: 'badtitletext')
+ * @param array $params parameter to wfMessage()
*/
function __construct( $msg = 'badtitletext', $params = null ) {
parent::__construct( 'badtitle', $msg, $params );
@@ -423,7 +416,7 @@ class PermissionsError extends ErrorPageError {
* @ingroup Exception
*/
class ReadOnlyError extends ErrorPageError {
- public function __construct(){
+ public function __construct() {
parent::__construct(
'readonly',
'readonlytext',
@@ -439,14 +432,14 @@ class ReadOnlyError extends ErrorPageError {
* @ingroup Exception
*/
class ThrottledError extends ErrorPageError {
- public function __construct(){
+ public function __construct() {
parent::__construct(
'actionthrottled',
'actionthrottledtext'
);
}
- public function report(){
+ public function report() {
global $wgOut;
$wgOut->setStatusCode( 503 );
parent::report();
@@ -460,65 +453,34 @@ class ThrottledError extends ErrorPageError {
* @ingroup Exception
*/
class UserBlockedError extends ErrorPageError {
- public function __construct( Block $block ){
- global $wgLang, $wgRequest;
-
- $blocker = $block->getBlocker();
- if ( $blocker instanceof User ) { // local user
- $blockerUserpage = $block->getBlocker()->getUserPage();
- $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]";
- } else { // foreign user
- $link = $blocker;
- }
-
- $reason = $block->mReason;
- if( $reason == '' ) {
- $reason = wfMessage( 'blockednoreason' )->text();
- }
-
- /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
- * This could be a username, an IP range, or a single IP. */
- $intended = $block->getTarget();
-
- parent::__construct(
- 'blockedtitle',
- $block->mAuto ? 'autoblockedtext' : 'blockedtext',
- array(
- $link,
- $reason,
- $wgRequest->getIP(),
- $block->getByName(),
- $block->getId(),
- $wgLang->formatExpiry( $block->mExpiry ),
- $intended,
- $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true )
- )
- );
+ public function __construct( Block $block ) {
+ // @todo FIXME: Implement a more proper way to get context here.
+ $params = $block->getPermissionsError( RequestContext::getMain() );
+ parent::__construct( 'blockedtitle', array_shift( $params ), $params );
}
}
/**
* Shows a generic "user is not logged in" error page.
*
- * This is essentially an ErrorPageError exception which by default use the
+ * This is essentially an ErrorPageError exception which by default uses the
* 'exception-nologin' as a title and 'exception-nologin-text' for the message.
* @see bug 37627
* @since 1.20
*
* @par Example:
* @code
- * if( $user->isAnon ) {
+ * if( $user->isAnon() ) {
* throw new UserNotLoggedIn();
* }
* @endcode
*
- * Please note the parameters are mixed up compared to ErrorPageError, this
- * is done to be able to simply specify a reason whitout overriding the default
- * title.
+ * Note the parameter order differs from ErrorPageError, this allows you to
+ * simply specify a reason without overriding the default title.
*
* @par Example:
* @code
- * if( $user->isAnon ) {
+ * if( $user->isAnon() ) {
* throw new UserNotLoggedIn( 'action-require-loggedin' );
* }
* @endcode
@@ -533,11 +495,11 @@ class UserNotLoggedIn extends ErrorPageError {
* @param $titleMsg A message key to set the page title.
* Optional, default: 'exception-nologin'
* @param $params Parameters to wfMessage().
- * Optiona, default: null
+ * Optional, default: null
*/
public function __construct(
$reasonMsg = 'exception-nologin-text',
- $titleMsg = 'exception-nologin',
+ $titleMsg = 'exception-nologin',
$params = null
) {
parent::__construct( $titleMsg, $reasonMsg, $params );
@@ -558,24 +520,48 @@ class HttpError extends MWException {
* Constructor
*
* @param $httpCode Integer: HTTP status code to send to the client
- * @param $content String|Message: content of the message
- * @param $header String|Message: content of the header (\<title\> and \<h1\>)
+ * @param string|Message $content content of the message
+ * @param string|Message $header content of the header (\<title\> and \<h1\>)
*/
- public function __construct( $httpCode, $content, $header = null ){
+ public function __construct( $httpCode, $content, $header = null ) {
parent::__construct( $content );
$this->httpCode = (int)$httpCode;
$this->header = $header;
$this->content = $content;
}
+ /**
+ * Returns the HTTP status code supplied to the constructor.
+ *
+ * @return int
+ */
+ public function getStatusCode() {
+ return $this->httpCode;
+ }
+
+ /**
+ * Report the HTTP error.
+ * Sends the appropriate HTTP status code and outputs an
+ * HTML page with an error message.
+ */
public function report() {
$httpMessage = HttpStatus::getMessage( $this->httpCode );
- header( "Status: {$this->httpCode} {$httpMessage}" );
+ header( "Status: {$this->httpCode} {$httpMessage}", true, $this->httpCode );
header( 'Content-type: text/html; charset=utf-8' );
+ print $this->getHTML();
+ }
+
+ /**
+ * Returns HTML for reporting the HTTP error.
+ * This will be a minimal but complete HTML document.
+ *
+ * @return string HTML
+ */
+ public function getHTML() {
if ( $this->header === null ) {
- $header = $httpMessage;
+ $header = HttpStatus::getMessage( $this->httpCode );
} elseif ( $this->header instanceof Message ) {
$header = $this->header->escaped();
} else {
@@ -588,7 +574,7 @@ class HttpError extends MWException {
$content = htmlspecialchars( $this->content );
}
- print "<!DOCTYPE html>\n".
+ return "<!DOCTYPE html>\n" .
"<html><head><title>$header</title></head>\n" .
"<body><h1>$header</h1><p>$content</p></body></html>\n";
}
@@ -625,8 +611,10 @@ class MWExceptionHandler {
$message = "MediaWiki internal error.\n\n";
if ( $wgShowExceptionDetails ) {
- $message .= 'Original exception: ' . $e->__toString() . "\n\n" .
- 'Exception caught inside exception handler: ' . $e2->__toString();
+ $message .= 'Original exception: ' . self::getLogMessage( $e ) .
+ "\nBacktrace:\n" . self::getRedactedTraceAsString( $e ) .
+ "\n\nException caught inside exception handler: " . self::getLogMessage( $e2 ) .
+ "\nBacktrace:\n" . self::getRedactedTraceAsString( $e2 );
} else {
$message .= "Exception caught inside exception handler.\n\n" .
"Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " .
@@ -642,11 +630,11 @@ class MWExceptionHandler {
}
}
} else {
- $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" .
- $e->__toString() . "\n";
+ $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"";
if ( $wgShowExceptionDetails ) {
- $message .= "\n" . $e->getTraceAsString() . "\n";
+ $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" .
+ self::getRedactedTraceAsString( $e ) . "\n";
}
if ( $cmdLine ) {
@@ -661,7 +649,7 @@ class MWExceptionHandler {
* Print a message, if possible to STDERR.
* Use this in command line mode only (see isCommandLine)
*
- * @param $message string Failure text
+ * @param string $message Failure text
*/
public static function printError( $message ) {
# NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602).
@@ -669,7 +657,7 @@ class MWExceptionHandler {
if ( defined( 'STDERR' ) ) {
fwrite( STDERR, $message );
} else {
- echo( $message );
+ echo $message;
}
}
@@ -692,11 +680,145 @@ class MWExceptionHandler {
// Final cleanup
if ( $wgFullyInitialised ) {
try {
- wfLogProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition
- } catch ( Exception $e ) {}
+ // uses $wgRequest, hence the $wgFullyInitialised condition
+ wfLogProfilingData();
+ } catch ( Exception $e ) {
+ }
}
// Exit value should be nonzero for the benefit of shell jobs
exit( 1 );
}
+
+ /**
+ * Generate a string representation of an exception's stack trace
+ *
+ * Like Exception::getTraceAsString, but replaces argument values with
+ * argument type or class name.
+ *
+ * @param Exception $e
+ * @return string
+ */
+ public static function getRedactedTraceAsString( Exception $e ) {
+ $text = '';
+
+ foreach ( self::getRedactedTrace( $e ) as $level => $frame ) {
+ if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
+ $text .= "#{$level} {$frame['file']}({$frame['line']}): ";
+ } else {
+ // 'file' and 'line' are unset for calls via call_user_func (bug 55634)
+ // This matches behaviour of Exception::getTraceAsString to instead
+ // display "[internal function]".
+ $text .= "#{$level} [internal function]: ";
+ }
+
+ if ( isset( $frame['class'] ) ) {
+ $text .= $frame['class'] . $frame['type'] . $frame['function'];
+ } else {
+ $text .= $frame['function'];
+ }
+
+ if ( isset( $frame['args'] ) ) {
+ $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
+ } else {
+ $text .= "()\n";
+ }
+ }
+
+ $level = $level + 1;
+ $text .= "#{$level} {main}";
+
+ return $text;
+ }
+
+ /**
+ * Return a copy of an exception's backtrace as an array.
+ *
+ * Like Exception::getTrace, but replaces each element in each frame's
+ * argument array with the name of its class (if the element is an object)
+ * or its type (if the element is a PHP primitive).
+ *
+ * @since 1.22
+ * @param Exception $e
+ * @return array
+ */
+ public static function getRedactedTrace( Exception $e ) {
+ return array_map( function ( $frame ) {
+ if ( isset( $frame['args'] ) ) {
+ $frame['args'] = array_map( function ( $arg ) {
+ return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
+ }, $frame['args'] );
+ }
+ return $frame;
+ }, $e->getTrace() );
+ }
+
+
+ /**
+ * Get the ID for this error.
+ *
+ * The ID is saved so that one can match the one output to the user (when
+ * $wgShowExceptionDetails is set to false), to the entry in the debug log.
+ *
+ * @since 1.22
+ * @param Exception $e
+ * @return string
+ */
+ public static function getLogId( Exception $e ) {
+ if ( !isset( $e->_mwLogId ) ) {
+ $e->_mwLogId = wfRandomString( 8 );
+ }
+ return $e->_mwLogId;
+ }
+
+ /**
+ * Return the requested URL and point to file and line number from which the
+ * exception occurred.
+ *
+ * @since 1.22
+ * @param Exception $e
+ * @return string
+ */
+ public static function getLogMessage( Exception $e ) {
+ global $wgRequest;
+
+ $id = self::getLogId( $e );
+ $file = $e->getFile();
+ $line = $e->getLine();
+ $message = $e->getMessage();
+
+ if ( isset( $wgRequest ) && !$wgRequest instanceof FauxRequest ) {
+ $url = $wgRequest->getRequestURL();
+ if ( !$url ) {
+ $url = '[no URL]';
+ }
+ } else {
+ $url = '[no req]';
+ }
+
+ return "[$id] $url Exception from line $line of $file: $message";
+ }
+
+ /**
+ * Log an exception to the exception log (if enabled).
+ *
+ * This method must not assume the exception is an MWException,
+ * it is also used to handle PHP errors or errors from other libraries.
+ *
+ * @since 1.22
+ * @param Exception $e
+ */
+ public static function logException( Exception $e ) {
+ global $wgLogExceptionBacktrace;
+
+ if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
+ $log = self::getLogMessage( $e );
+ if ( $wgLogExceptionBacktrace ) {
+ wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() . "\n" );
+ } else {
+ wfDebugLog( 'exception', $log );
+ }
+ }
+ }
+
}
diff --git a/includes/Export.php b/includes/Export.php
index f01fb237..98de4c00 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -31,8 +31,8 @@
* @ingroup SpecialPage Dump
*/
class WikiExporter {
- var $list_authors = false ; # Return distinct author list (when not returning full history)
- var $author_list = "" ;
+ var $list_authors = false; # Return distinct author list (when not returning full history)
+ var $author_list = "";
var $dumpUploads = false;
var $dumpUploadFileContents = false;
@@ -63,7 +63,7 @@ class WikiExporter {
* @return string
*/
public static function schemaVersion() {
- return "0.7";
+ return "0.8";
}
/**
@@ -80,17 +80,17 @@ class WikiExporter {
* offset: non-inclusive offset at which to start the query
* limit: maximum number of rows to return
* dir: "asc" or "desc" timestamp order
- * @param $buffer Int: one of WikiExporter::BUFFER or WikiExporter::STREAM
- * @param $text Int: one of WikiExporter::TEXT or WikiExporter::STUB
+ * @param int $buffer one of WikiExporter::BUFFER or WikiExporter::STREAM
+ * @param int $text one of WikiExporter::TEXT or WikiExporter::STUB
*/
- function __construct( &$db, $history = WikiExporter::CURRENT,
+ function __construct( $db, $history = WikiExporter::CURRENT,
$buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) {
- $this->db =& $db;
+ $this->db = $db;
$this->history = $history;
- $this->buffer = $buffer;
- $this->writer = new XmlDumpWriter();
- $this->sink = new DumpOutput();
- $this->text = $text;
+ $this->buffer = $buffer;
+ $this->writer = new XmlDumpWriter();
+ $this->sink = new DumpOutput();
+ $this->text = $text;
}
/**
@@ -126,7 +126,7 @@ class WikiExporter {
/**
* Dumps a series of page and revision records for those pages
* in the database falling within the page_id range given.
- * @param $start Int: inclusive lower limit (this id is included)
+ * @param int $start inclusive lower limit (this id is included)
* @param $end Int: Exclusive upper limit (this id is not included)
* If 0, no upper limit.
*/
@@ -141,7 +141,7 @@ class WikiExporter {
/**
* Dumps a series of page and revision records for those pages
* in the database with revisions falling within the rev_id range given.
- * @param $start Int: inclusive lower limit (this id is included)
+ * @param int $start inclusive lower limit (this id is included)
* @param $end Int: Exclusive upper limit (this id is not included)
* If 0, no upper limit.
*/
@@ -226,7 +226,7 @@ class WikiExporter {
foreach ( $res as $row ) {
$this->author_list .= "<contributor>" .
"<username>" .
- htmlentities( $row->rev_user_text ) .
+ htmlentities( $row->rev_user_text ) .
"</username>" .
"<id>" .
$row->rev_user .
@@ -249,9 +249,13 @@ class WikiExporter {
$where = array( 'user_id = log_user' );
# Hide private logs
$hideLogs = LogEventsList::getExcludeClause( $this->db );
- if ( $hideLogs ) $where[] = $hideLogs;
+ if ( $hideLogs ) {
+ $where[] = $hideLogs;
+ }
# Add on any caller specified conditions
- if ( $cond ) $where[] = $cond;
+ if ( $cond ) {
+ $where[] = $cond;
+ }
# Get logging table name for logging.* clause
$logging = $this->db->tableName( 'logging' );
@@ -296,6 +300,7 @@ class WikiExporter {
}
// Inform caller about problem
+ wfProfileOut( __METHOD__ );
throw $e;
}
# For page dumps...
@@ -330,7 +335,7 @@ class WikiExporter {
$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
+ if ( $this->list_authors && $cond != '' ) { // List authors, if so desired
$this->do_list_authors( $cond );
}
$join['revision'] = array( 'INNER JOIN', 'page_id=rev_page AND page_latest=rev_id' );
@@ -348,7 +353,7 @@ class WikiExporter {
$join['revision'] = array( 'INNER JOIN', 'page_id=rev_page' );
$opts['ORDER BY'] = array( 'rev_page ASC', 'rev_id ASC' );
} else {
- # Uknown history specification parameter?
+ # Unknown history specification parameter?
wfProfileOut( __METHOD__ );
throw new MWException( __METHOD__ . " given invalid history dump type." );
}
@@ -427,10 +432,10 @@ class WikiExporter {
protected function outputPageStream( $resultset ) {
$last = null;
foreach ( $resultset as $row ) {
- if ( is_null( $last ) ||
+ if ( $last === null ||
$last->page_namespace != $row->page_namespace ||
- $last->page_title != $row->page_title ) {
- if ( isset( $last ) ) {
+ $last->page_title != $row->page_title ) {
+ if ( $last !== null ) {
$output = '';
if ( $this->dumpUploads ) {
$output .= $this->writer->writeUploads( $last, $this->dumpUploadFileContents );
@@ -445,7 +450,7 @@ class WikiExporter {
$output = $this->writer->writeRevision( $row );
$this->sink->writeRevision( $row, $output );
}
- if ( isset( $last ) ) {
+ if ( $last !== null ) {
$output = '';
if ( $this->dumpUploads ) {
$output .= $this->writer->writeUploads( $last, $this->dumpUploadFileContents );
@@ -498,7 +503,7 @@ class XmlDumpWriter {
'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
- "http://www.mediawiki.org/xml/export-$ver.xsd",
+ "http://www.mediawiki.org/xml/export-$ver.xsd", #TODO: how do we get a new version up there?
'version' => $ver,
'xml:lang' => $wgLanguageCode ),
null ) .
@@ -541,7 +546,7 @@ class XmlDumpWriter {
* @return string
*/
function homelink() {
- return Xml::element( 'base', array(), Title::newMainPage()->getCanonicalUrl() );
+ return Xml::element( 'base', array(), Title::newMainPage()->getCanonicalURL() );
}
/**
@@ -563,8 +568,9 @@ class XmlDumpWriter {
foreach ( $wgContLang->getFormattedNamespaces() as $ns => $title ) {
$spaces .= ' ' .
Xml::element( 'namespace',
- array( 'key' => $ns,
- 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
+ array(
+ 'key' => $ns,
+ 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
), $title ) . "\n";
}
$spaces .= " </namespaces>";
@@ -593,7 +599,7 @@ class XmlDumpWriter {
$out = " <page>\n";
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
$out .= ' ' . Xml::elementClean( 'title', array(), self::canonicalTitle( $title ) ) . "\n";
- $out .= ' ' . Xml::element( 'ns', array(), strval( $row->page_namespace) ) . "\n";
+ $out .= ' ' . Xml::element( 'ns', array(), strval( $row->page_namespace ) ) . "\n";
$out .= ' ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n";
if ( $row->page_is_redirect ) {
$page = WikiPage::factory( $title );
@@ -634,37 +640,31 @@ class XmlDumpWriter {
function writeRevision( $row ) {
wfProfileIn( __METHOD__ );
- $out = " <revision>\n";
+ $out = " <revision>\n";
$out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n";
- if( $row->rev_parent_id ) {
+ if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
$out .= " " . Xml::element( 'parentid', null, strval( $row->rev_parent_id ) ) . "\n";
}
$out .= $this->writeTimestamp( $row->rev_timestamp );
- if ( $row->rev_deleted & Revision::DELETED_USER ) {
+ if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_USER ) ) {
$out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
} else {
$out .= $this->writeContributor( $row->rev_user, $row->rev_user_text );
}
- if ( $row->rev_minor_edit ) {
- $out .= " <minor/>\n";
+ if ( isset( $row->rev_minor_edit ) && $row->rev_minor_edit ) {
+ $out .= " <minor/>\n";
}
- if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
+ if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_COMMENT ) ) {
$out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif ( $row->rev_comment != '' ) {
$out .= " " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
}
- if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
- $out .= " " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
- } else {
- $out .= " <sha1/>\n";
- }
-
$text = '';
- if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
+ if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
$out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif ( isset( $row->old_text ) ) {
// Raw text from the database may have invalid chars
@@ -679,6 +679,34 @@ class XmlDumpWriter {
"" ) . "\n";
}
+ if ( isset( $row->rev_sha1 ) && $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
+ $out .= " " . Xml::element( 'sha1', null, strval( $row->rev_sha1 ) ) . "\n";
+ } else {
+ $out .= " <sha1/>\n";
+ }
+
+ if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model ) ) {
+ $content_model = strval( $row->rev_content_model );
+ } else {
+ // probably using $wgContentHandlerUseDB = false;
+ // @todo test!
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $content_model = ContentHandler::getDefaultModelFor( $title );
+ }
+
+ $out .= " " . Xml::element( 'model', null, strval( $content_model ) ) . "\n";
+
+ if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
+ $content_format = strval( $row->rev_content_format );
+ } else {
+ // probably using $wgContentHandlerUseDB = false;
+ // @todo test!
+ $content_handler = ContentHandler::getForModelID( $content_model );
+ $content_format = $content_handler->getDefaultFormat();
+ }
+
+ $out .= " " . Xml::element( 'format', null, strval( $content_format ) ) . "\n";
+
wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
$out .= " </revision>\n";
@@ -698,7 +726,7 @@ class XmlDumpWriter {
function writeLogItem( $row ) {
wfProfileIn( __METHOD__ );
- $out = " <logitem>\n";
+ $out = " <logitem>\n";
$out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
$out .= $this->writeTimestamp( $row->log_timestamp, " " );
@@ -736,7 +764,7 @@ class XmlDumpWriter {
/**
* @param $timestamp string
- * @param $indent string Default to six spaces
+ * @param string $indent Default to six spaces
* @return string
*/
function writeTimestamp( $timestamp, $indent = " " ) {
@@ -747,7 +775,7 @@ class XmlDumpWriter {
/**
* @param $id
* @param $text string
- * @param $indent string Default to six spaces
+ * @param string $indent Default to six spaces
* @return string
*/
function writeContributor( $id, $text, $indent = " " ) {
@@ -796,10 +824,13 @@ class XmlDumpWriter {
$archiveName = '';
}
if ( $dumpContents ) {
+ $be = $file->getRepo()->getBackend();
# Dump file as base64
# Uses only XML-safe characters, so does not need escaping
+ # @TODO: too bad this loads the contents into memory (script might swap)
$contents = ' <contents encoding="base64">' .
- chunk_split( base64_encode( file_get_contents( $file->getPath() ) ) ) .
+ chunk_split( base64_encode(
+ $be->getFileContents( array( 'src' => $file->getPath() ) ) ) ) .
" </contents>\n";
} else {
$contents = '';
@@ -815,7 +846,7 @@ class XmlDumpWriter {
" " . $comment . "\n" .
" " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
$archiveName .
- " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
+ " " . Xml::element( 'src', null, $file->getCanonicalURL() ) . "\n" .
" " . Xml::element( 'size', null, $file->getSize() ) . "\n" .
" " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" .
" " . Xml::element( 'rel', null, $file->getRel() ) . "\n" .
@@ -849,9 +880,8 @@ class XmlDumpWriter {
}
}
-
/**
- * Base class for output stream; prints to stdout or buffer or whereever.
+ * Base class for output stream; prints to stdout or buffer or wherever.
* @ingroup Dump
*/
class DumpOutput {
@@ -918,7 +948,6 @@ class DumpOutput {
* @param $newname mixed File name. May be a string or an array with one element
*/
function closeRenameAndReopen( $newname ) {
- return;
}
/**
@@ -926,10 +955,9 @@ class DumpOutput {
* Use this for the last piece of a file written out
* at specified checkpoints (e.g. every n hours).
* @param $newname mixed File name. May be a string or an array with one element
- * @param $open bool If true, a new file with the old filename will be opened again for writing (default: false)
+ * @param bool $open If true, a new file with the old filename will be opened again for writing (default: false)
*/
function closeAndRename( $newname, $open = false ) {
- return;
}
/**
@@ -938,7 +966,7 @@ class DumpOutput {
* @return null
*/
function getFilenames() {
- return NULL;
+ return null;
}
}
@@ -987,7 +1015,7 @@ class DumpFileOutput extends DumpOutput {
* @throws MWException
*/
function renameOrException( $newname ) {
- if (! rename( $this->filename, $newname ) ) {
+ if ( !rename( $this->filename, $newname ) ) {
throw new MWException( __METHOD__ . ": rename of file {$this->filename} to $newname failed\n" );
}
}
@@ -1050,7 +1078,7 @@ class DumpPipeOutput extends DumpFileOutput {
*/
function __construct( $command, $file = null ) {
if ( !is_null( $file ) ) {
- $command .= " > " . wfEscapeShellArg( $file );
+ $command .= " > " . wfEscapeShellArg( $file );
}
$this->startCommand( $command );
@@ -1106,7 +1134,7 @@ class DumpPipeOutput extends DumpFileOutput {
$this->renameOrException( $newname );
if ( $open ) {
$command = $this->command;
- $command .= " > " . wfEscapeShellArg( $this->filename );
+ $command .= " > " . wfEscapeShellArg( $this->filename );
$this->startCommand( $command );
}
}
@@ -1166,7 +1194,7 @@ class Dump7ZipOutput extends DumpPipeOutput {
// Suppress annoying useless crap from p7zip
// Unfortunately this could suppress real error messages too
$command .= ' >' . wfGetNull() . ' 2>&1';
- return( $command );
+ return $command;
}
/**
@@ -1325,6 +1353,7 @@ class DumpNamespaceFilter extends DumpFilter {
/**
* @param $sink DumpOutput
* @param $param
+ * @throws MWException
*/
function __construct( &$sink, $param ) {
parent::__construct( $sink );
@@ -1338,7 +1367,7 @@ class DumpNamespaceFilter extends DumpFilter {
"NS_PROJECT_TALK" => NS_PROJECT_TALK,
"NS_FILE" => NS_FILE,
"NS_FILE_TALK" => NS_FILE_TALK,
- "NS_IMAGE" => NS_IMAGE, // NS_IMAGE is an alias for NS_FILE
+ "NS_IMAGE" => NS_IMAGE, // NS_IMAGE is an alias for NS_FILE
"NS_IMAGE_TALK" => NS_IMAGE_TALK,
"NS_MEDIAWIKI" => NS_MEDIAWIKI,
"NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK,
@@ -1378,7 +1407,6 @@ class DumpNamespaceFilter extends DumpFilter {
}
}
-
/**
* Dump output filter to include only the last revision in each page sequence.
* @ingroup Dump
@@ -1423,7 +1451,7 @@ class DumpLatestFilter extends DumpFilter {
}
/**
- * Base class for output stream; prints to stdout or buffer or whereever.
+ * Base class for output stream; prints to stdout or buffer or wherever.
* @ingroup Dump
*/
class DumpMultiWriter {
@@ -1506,7 +1534,7 @@ class DumpMultiWriter {
function getFilenames() {
$filenames = array();
for ( $i = 0; $i < $this->count; $i++ ) {
- $filenames[] = $this->sinks[$i]->getFilenames();
+ $filenames[] = $this->sinks[$i]->getFilenames();
}
return $filenames;
}
diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php
deleted file mode 100644
index 34683253..00000000
--- a/includes/ExternalEdit.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-/**
- * External editors support
- *
- * License: Public domain
- *
- * @file
- * @author Erik Moeller <moeller@scireview.de>
- */
-
-/**
- * Support for external editors to modify both text and files
- * in external applications. It works as follows: MediaWiki
- * sends a meta-file with the MIME type 'application/x-external-editor'
- * to the client. The user has to associate that MIME type with
- * a helper application (a reference implementation in Perl
- * can be found in extensions/ee), which will launch the editor,
- * and save the modified data back to the server.
- *
- */
-class ExternalEdit extends ContextSource {
-
- /**
- * Array of URLs to link to
- * @var Array
- */
- private $urls;
-
- /**
- * Constructor
- * @param $context IContextSource context to use
- * @param $urls array
- */
- public function __construct( IContextSource $context, array $urls = array() ) {
- $this->setContext( $context );
- $this->urls = $urls;
- }
-
- /**
- * Check whether external edit or diff should be used.
- *
- * @param $context IContextSource context to use
- * @param $type String can be either 'edit' or 'diff'
- * @return Bool
- */
- public static function useExternalEngine( IContextSource $context, $type ) {
- global $wgUseExternalEditor;
-
- if ( !$wgUseExternalEditor ) {
- return false;
- }
-
- $pref = $type == 'diff' ? 'externaldiff' : 'externaleditor';
- $request = $context->getRequest();
-
- return !$request->getVal( 'internaledit' ) &&
- ( $context->getUser()->getOption( $pref ) || $request->getVal( 'externaledit' ) );
- }
-
- /**
- * Output the information for the external editor
- */
- public function execute() {
- global $wgContLang, $wgScript, $wgScriptPath, $wgCanonicalServer;
-
- $this->getOutput()->disable();
-
- $response = $this->getRequest()->response();
- $response->header( 'Content-type: application/x-external-editor; charset=utf-8' );
- $response->header( 'Cache-control: no-cache' );
-
- $special = $wgContLang->getNsText( NS_SPECIAL );
-
- # $type can be "Edit text", "Edit file" or "Diff text" at the moment
- # See the protocol specifications at [[m:Help:External editors/Tech]] for
- # details.
- if ( count( $this->urls ) ) {
- $urls = $this->urls;
- $type = "Diff text";
- } elseif ( $this->getRequest()->getVal( 'mode' ) == 'file' ) {
- $type = "Edit file";
- $image = wfLocalFile( $this->getTitle() );
- if ( $image ) {
- $urls = array(
- 'File' => array(
- 'Extension' => $image->getExtension(),
- 'URL' => $image->getCanonicalURL()
- )
- );
- } else{
- $urls = array();
- }
- } else {
- $type = "Edit text";
- # *.wiki file extension is used by some editors for syntax
- # highlighting, so we follow that convention
- $urls = array( 'File' => array(
- 'Extension' => 'wiki',
- 'URL' => $this->getTitle()->getCanonicalURL(
- array( 'action' => 'edit', 'internaledit' => 'true' ) )
- ) );
- }
-
- $files = '';
- foreach( $urls as $key => $vars ) {
- $files .= "\n[$key]\n";
- foreach( $vars as $varname => $varval ) {
- $files .= "$varname=$varval\n";
- }
- }
-
- $url = $this->getTitle()->getFullURL(
- $this->getRequest()->appendQueryValue( 'internaledit', 1, true ) );
-
- $control = <<<CONTROL
-; You're seeing this file because you're using Mediawiki's External Editor feature.
-; This is probably because you selected use external editor in your preferences.
-; To edit normally, either disable that preference or go to the URL:
-; $url
-; See http://www.mediawiki.org/wiki/Manual:External_editors for details.
-[Process]
-Type=$type
-Engine=MediaWiki
-Script={$wgCanonicalServer}{$wgScript}
-Server={$wgCanonicalServer}
-Path={$wgScriptPath}
-Special namespace=$special
-$files
-CONTROL;
- echo $control;
- }
-}
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
deleted file mode 100644
index 61d4ef7c..00000000
--- a/includes/ExternalStore.php
+++ /dev/null
@@ -1,172 +0,0 @@
-<?php
-/**
- * Data storage in external repositories.
- *
- * 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
- */
-
-/**
- * @defgroup ExternalStorage ExternalStorage
- */
-
-/**
- * Constructor class for data kept in external repositories
- *
- * External repositories might be populated by maintenance/async
- * scripts, thus partial moving of data may be possible, as well
- * as possibility to have any storage format (i.e. for archives)
- *
- * @ingroup ExternalStorage
- */
-class ExternalStore {
- 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 string|bool The text stored or false on error
- */
- static function fetchFromURL( $url, $params = array() ) {
- global $wgExternalStores;
-
- if( !$wgExternalStores )
- return false;
-
- $parts = explode( '://', $url, 2 );
-
- if ( count( $parts ) != 2 ) {
- return false;
- }
-
- list( $proto, $path ) = $parts;
-
- if ( $path == '' ) { // Bad URL
- return false;
- }
-
- $store = self::getStoreObject( $proto, $params );
- if ( $store === false )
- return false;
- return $store->fetchFromURL( $url );
- }
-
- /**
- * 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, $params = array() ) {
- global $wgExternalStores;
- if( !$wgExternalStores )
- return false;
- /* Protocol not enabled */
- if( !in_array( $proto, $wgExternalStores ) )
- return false;
-
- $class = 'ExternalStore' . ucfirst( $proto );
- /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */
- if( !MWInit::classExists( $class ) ) {
- return false;
- }
-
- 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.
- * @param $url
- * @param $data
- * @param $params array
- * @return string|bool The URL of the stored data item, or false on error
- */
- static function insert( $url, $data, $params = array() ) {
- list( $proto, $params ) = explode( '://', $url, 2 );
- $store = self::getStoreObject( $proto, $params );
- if ( $store === false ) {
- return false;
- } else {
- return $store->store( $params, $data );
- }
- }
-
- /**
- * Like insert() above, but does more of the work for us.
- * This function does not need a url param, it builds it by
- * itself. It also fails-over to the next possible clusters.
- *
- * @param $data String
- * @param $storageParams Array: associative array of parameters for the ExternalStore object.
- * @return string The URL of the stored data item, or false on error
- */
- public static function insertToDefault( $data, $storageParams = array() ) {
- global $wgDefaultExternalStore;
- $tryStores = (array)$wgDefaultExternalStore;
- $error = false;
- while ( count( $tryStores ) > 0 ) {
- $index = mt_rand(0, count( $tryStores ) - 1);
- $storeUrl = $tryStores[$index];
- wfDebug( __METHOD__.": trying $storeUrl\n" );
- list( $proto, $params ) = explode( '://', $storeUrl, 2 );
- $store = self::getStoreObject( $proto, $storageParams );
- if ( $store === false ) {
- throw new MWException( "Invalid external storage protocol - $storeUrl" );
- }
- try {
- $url = $store->store( $params, $data ); // Try to save the object
- } catch ( DBConnectionError $error ) {
- $url = false;
- } catch( DBQueryError $error ) {
- $url = false;
- }
- if ( $url ) {
- return $url; // Done!
- } else {
- unset( $tryStores[$index] ); // Don't try this one again!
- $tryStores = array_values( $tryStores ); // Must have consecutive keys
- wfDebugLog( 'ExternalStorage', "Unable to store text to external storage $storeUrl" );
- }
- }
- // All stores failed
- if ( $error ) {
- // Rethrow the last connection error
- throw $error;
- } else {
- throw new MWException( "Unable to store text to external storage" );
- }
- }
-
- /**
- * @param $data
- * @param $wiki
- *
- * @return string
- */
- public static function insertToForeignDefault( $data, $wiki ) {
- return self::insertToDefault( $data, array( 'wiki' => $wiki ) );
- }
-}
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
deleted file mode 100644
index 6f2b33e1..00000000
--- a/includes/ExternalStoreDB.php
+++ /dev/null
@@ -1,185 +0,0 @@
-<?php
-/**
- * External storage in SQL database.
- *
- * 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
- */
-
-/**
- * DB accessable external objects
- * @ingroup ExternalStorage
- */
-class ExternalStoreDB {
-
- 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 ) {
- $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false;
-
- return wfGetLBFactory()->getExternalLB( $cluster, $wiki );
- }
-
- /**
- * Get a slave database connection for the specified cluster
- *
- * @param $cluster String: cluster name
- * @return DatabaseBase object
- */
- function &getSlave( $cluster ) {
- global $wgDefaultExternalStore;
-
- $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false;
- $lb =& $this->getLoadBalancer( $cluster );
-
- if ( !in_array( "DB://" . $cluster, (array)$wgDefaultExternalStore ) ) {
- wfDebug( "read only external store" );
- $lb->allowLagged(true);
- } else {
- wfDebug( "writable external store" );
- }
-
- return $lb->getConnection( DB_SLAVE, array(), $wiki );
- }
-
- /**
- * 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, array(), $wiki );
- }
-
- /**
- * 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 ) ) {
- $table = 'blobs';
- }
- return $table;
- }
-
- /**
- * Fetch data from given URL
- * @param $url String: an url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage.
- * @return mixed
- */
- function fetchFromURL( $url ) {
- $path = explode( '/', $url );
- $cluster = $path[2];
- $id = $path[3];
- if ( isset( $path[4] ) ) {
- $itemID = $path[4];
- } else {
- $itemID = false;
- }
-
- $ret =& $this->fetchBlob( $cluster, $id, $itemID );
-
- if ( $itemID !== false && $ret !== false ) {
- return $ret->getItem( $itemID );
- }
- return $ret;
- }
-
- /**
- * Fetch a blob item out of the database; a cache of the last-loaded
- * blob will be kept so that multiple loads out of a multi-item blob
- * can avoid redundant database access and decompression.
- * @param $cluster
- * @param $id
- * @param $itemID
- * @return mixed
- * @private
- */
- function &fetchBlob( $cluster, $id, $itemID ) {
- /**
- * One-step cache variable to hold base blobs; operations that
- * pull multiple revisions may often pull multiple times from
- * the same blob. By keeping the last-used one open, we avoid
- * redundant unserialization and decompression overhead.
- */
- static $externalBlobCache = array();
-
- $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/";
- if( isset( $externalBlobCache[$cacheID] ) ) {
- wfDebug( "ExternalStoreDB::fetchBlob cache hit on $cacheID\n" );
- return $externalBlobCache[$cacheID];
- }
-
- wfDebug( "ExternalStoreDB::fetchBlob cache miss on $cacheID\n" );
-
- $dbr =& $this->getSlave( $cluster );
- $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', array( 'blob_id' => $id ), __METHOD__ );
- if ( $ret === false ) {
- wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master fallback on $cacheID\n" );
- // Try the master
- $dbw =& $this->getMaster( $cluster );
- $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', array( 'blob_id' => $id ), __METHOD__ );
- if( $ret === false) {
- wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master failed to find $cacheID\n" );
- }
- }
- if( $itemID !== false && $ret !== false ) {
- // Unserialise object; caller extracts item
- $ret = unserialize( $ret );
- }
-
- $externalBlobCache = array( $cacheID => &$ret );
- return $ret;
- }
-
- /**
- * Insert a data item into a given cluster
- *
- * @param $cluster String: the cluster name
- * @param $data String: the data item
- * @return string URL
- */
- function store( $cluster, $data ) {
- $dbw = $this->getMaster( $cluster );
- $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' );
- $dbw->insert( $this->getTable( $dbw ),
- 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->commit( __METHOD__ );
- }
- return "DB://$cluster/$id";
- }
-}
diff --git a/includes/ExternalUser.php b/includes/ExternalUser.php
deleted file mode 100644
index 9a01deb7..00000000
--- a/includes/ExternalUser.php
+++ /dev/null
@@ -1,309 +0,0 @@
-<?php
-/**
- * Authentication with a foreign database
- *
- * Copyright © 2009 Aryeh Gregor
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * @defgroup ExternalUser ExternalUser
- */
-
-/**
- * A class intended to supplement, and perhaps eventually replace, AuthPlugin.
- * See: http://www.mediawiki.org/wiki/ExternalAuth
- *
- * The class represents a user whose data is in a foreign database. The
- * database may have entirely different conventions from MediaWiki, but it's
- * assumed to at least support the concept of a user id (possibly not an
- * integer), a user name (possibly not meeting MediaWiki's username
- * requirements), and a password.
- *
- * @ingroup ExternalUser
- */
-abstract class ExternalUser {
- protected function __construct() {}
-
- /**
- * Wrappers around initFrom*().
- */
-
- /**
- * @param $name string
- * @return mixed ExternalUser, or false on failure
- */
- public static function newFromName( $name ) {
- global $wgExternalAuthType;
- if ( is_null( $wgExternalAuthType ) ) {
- return false;
- }
- $obj = new $wgExternalAuthType;
- if ( !$obj->initFromName( $name ) ) {
- return false;
- }
- return $obj;
- }
-
- /**
- * @param $id string
- * @return mixed ExternalUser, or false on failure
- */
- public static function newFromId( $id ) {
- global $wgExternalAuthType;
- if ( is_null( $wgExternalAuthType ) ) {
- return false;
- }
- $obj = new $wgExternalAuthType;
- if ( !$obj->initFromId( $id ) ) {
- return false;
- }
- return $obj;
- }
-
- /**
- * @return mixed ExternalUser, or false on failure
- */
- public static function newFromCookie() {
- global $wgExternalAuthType;
- if ( is_null( $wgExternalAuthType ) ) {
- return false;
- }
- $obj = new $wgExternalAuthType;
- if ( !$obj->initFromCookie() ) {
- return false;
- }
- return $obj;
- }
-
- /**
- * Creates the object corresponding to the given User object, assuming the
- * user exists on the wiki and is linked to an external account. If either
- * of these is false, this will return false.
- *
- * This is a wrapper around newFromId().
- *
- * @param $user User
- * @return ExternalUser|bool False on failure
- */
- 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 60f7600d..efa213fb 100644
--- a/includes/FakeTitle.php
+++ b/includes/FakeTitle.php
@@ -114,9 +114,9 @@ class FakeTitle extends Title {
function getParentCategories() { $this->error(); }
function getParentCategoryTree( $children = array() ) { $this->error(); }
function pageCond() { $this->error(); }
- function getPreviousRevisionID( $revId, $flags=0 ) { $this->error(); }
- function getNextRevisionID( $revId, $flags=0 ) { $this->error(); }
- function getFirstRevision( $flags=0 ) { $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( $flags = 0 ) { $this->error(); }
function countRevisionsBetween( $old, $new ) { $this->error(); }
diff --git a/includes/Fallback.php b/includes/Fallback.php
index 4b138c11..cdf6c88e 100644
--- a/includes/Fallback.php
+++ b/includes/Fallback.php
@@ -35,13 +35,13 @@ class Fallback {
if ( substr( $to, -8 ) == '//IGNORE' ) {
$to = substr( $to, 0, strlen( $to ) - 8 );
}
- if( strcasecmp( $from, $to ) == 0 ) {
+ if ( strcasecmp( $from, $to ) == 0 ) {
return $string;
}
- if( strcasecmp( $from, 'utf-8' ) == 0 ) {
+ if ( strcasecmp( $from, 'utf-8' ) == 0 ) {
return utf8_decode( $string );
}
- if( strcasecmp( $to, 'utf-8' ) == 0 ) {
+ if ( strcasecmp( $to, 'utf-8' ) == 0 ) {
return utf8_encode( $string );
}
return $string;
@@ -64,12 +64,12 @@ class Fallback {
* @return string
*/
public static function mb_substr( $str, $start, $count = 'end' ) {
- if( $start != 0 ) {
+ if ( $start != 0 ) {
$split = self::mb_substr_split_unicode( $str, intval( $start ) );
$str = substr( $str, $split );
}
- if( $count !== 'end' ) {
+ if ( $count !== 'end' ) {
$split = self::mb_substr_split_unicode( $str, intval( $count ) );
$str = substr( $str, 0, $split );
}
@@ -83,14 +83,14 @@ class Fallback {
* @return int
*/
public static function mb_substr_split_unicode( $str, $splitPos ) {
- if( $splitPos == 0 ) {
+ if ( $splitPos == 0 ) {
return 0;
}
$byteLen = strlen( $str );
- if( $splitPos > 0 ) {
- if( $splitPos > 256 ) {
+ 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.
@@ -104,7 +104,7 @@ class Fallback {
$bytePos = 0;
}
- while( $charPos++ < $splitPos ) {
+ while ( $charPos++ < $splitPos ) {
++$bytePos;
// Move past any tail bytes
while ( $bytePos < $byteLen && $str[$bytePos] >= "\x80" && $str[$bytePos] < "\xc0" ) {
@@ -115,7 +115,7 @@ class Fallback {
$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 ) {
+ while ( $bytePos > 0 && $charPos-- >= $splitPosX ) {
--$bytePos;
// Move past any tail bytes
while ( $bytePos > 0 && $str[$bytePos] >= "\x80" && $str[$bytePos] < "\xc0" ) {
@@ -138,24 +138,23 @@ class Fallback {
$total = 0;
// Count ASCII bytes
- for( $i = 0; $i < 0x80; $i++ ) {
+ for ( $i = 0; $i < 0x80; $i++ ) {
$total += $counts[$i];
}
// Count multibyte sequence heads
- for( $i = 0xc0; $i < 0xff; $i++ ) {
+ for ( $i = 0xc0; $i < 0xff; $i++ ) {
$total += $counts[$i];
}
return $total;
}
-
/**
* 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
+ * @param string $offset optional start position
+ * @param string $encoding optional encoding; ignored
* @return int
*/
public static function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) {
@@ -164,7 +163,7 @@ class Fallback {
$ar = array();
preg_match( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset );
- if( isset( $ar[0][1] ) ) {
+ if ( isset( $ar[0][1] ) ) {
return $ar[0][1];
} else {
return false;
@@ -175,8 +174,8 @@ class Fallback {
* 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
+ * @param string $offset optional start position
+ * @param string $encoding optional encoding; ignored
* @return int
*/
public static function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) {
@@ -185,29 +184,11 @@ class Fallback {
$ar = array();
preg_match_all( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset );
- if( isset( $ar[0] ) && count( $ar[0] ) > 0 &&
+ 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;
}
}
-
- /**
- * Fallback implementation of stream_resolve_include_path()
- * Native stream_resolve_include_path is available for PHP 5 >= 5.3.2
- * @param $filename String
- * @return String
- */
- public static function stream_resolve_include_path( $filename ) {
- $pathArray = explode( PATH_SEPARATOR, get_include_path() );
- foreach ( $pathArray as $path ) {
- $fullFilename = $path . DIRECTORY_SEPARATOR . $filename;
- if ( file_exists( $fullFilename ) ) {
- return $fullFilename;
- }
- }
- return false;
- }
-
}
diff --git a/includes/Feed.php b/includes/Feed.php
index f9dbf5ba..635b04e4 100644
--- a/includes/Feed.php
+++ b/includes/Feed.php
@@ -52,11 +52,11 @@ class FeedItem {
/**
* Constructor
*
- * @param $title String|Title Item's title
+ * @param string|Title $title 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 string $url URL uniquely designating the item.
+ * @param string $date Item's date
+ * @param string $author Author's user name
* @param $comments String
*/
function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
@@ -72,7 +72,7 @@ class FeedItem {
/**
* Encode $string so that it can be safely embedded in a XML document
*
- * @param $string String: string to encode
+ * @param string $string string to encode
* @return String
*/
public function xmlEncode( $string ) {
@@ -95,7 +95,7 @@ class FeedItem {
/**
* set the unique id of an item
*
- * @param $uniqueId String: unique id for the item
+ * @param string $uniqueId 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 ) {
@@ -170,7 +170,7 @@ class FeedItem {
/**
* Quickie hack... strip out wikilinks to more legible form from the comment.
*
- * @param $text String: wikitext
+ * @param string $text wikitext
* @return String
*/
public static function stripComment( $text ) {
@@ -243,9 +243,9 @@ abstract class ChannelFeed extends FeedItem {
*/
function contentType() {
global $wgRequest;
- $ctype = $wgRequest->getVal('ctype','application/xml');
- $allowedctypes = array('application/xml','text/xml','application/rss+xml','application/atom+xml');
- return (in_array($ctype, $allowedctypes) ? $ctype : 'application/xml');
+ $ctype = $wgRequest->getVal( 'ctype', 'application/xml' );
+ $allowedctypes = array( 'application/xml', 'text/xml', 'application/rss+xml', 'application/atom+xml' );
+ return ( in_array( $ctype, $allowedctypes ) ? $ctype : 'application/xml' );
}
/**
@@ -282,7 +282,7 @@ class RSSFeed extends ChannelFeed {
}
/**
- * Ouput an RSS 2.0 header
+ * Output an RSS 2.0 header
*/
function outHeader() {
global $wgVersion;
@@ -306,19 +306,19 @@ class RSSFeed extends ChannelFeed {
function outItem( $item ) {
?>
<item>
- <title><?php print $item->getTitle() ?></title>
- <link><?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ) ?></link>
- <guid<?php if( !$item->rssIsPermalink ) print ' isPermaLink="false"' ?>><?php print $item->getUniqueId() ?></guid>
+ <title><?php print $item->getTitle(); ?></title>
+ <link><?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?></link>
+ <guid<?php if ( !$item->rssIsPermalink ) { print ' isPermaLink="false"'; } ?>><?php print $item->getUniqueId(); ?></guid>
<description><?php print $item->getDescription() ?></description>
- <?php if( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ) ?></pubDate><?php } ?>
- <?php if( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor() ?></dc:creator><?php }?>
- <?php if( $item->getComments() ) { ?><comments><?php print wfExpandUrl( $item->getComments(), PROTO_CURRENT ) ?></comments><?php }?>
+ <?php if ( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ); ?></pubDate><?php } ?>
+ <?php if ( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor(); ?></dc:creator><?php }?>
+ <?php if ( $item->getComments() ) { ?><comments><?php print wfExpandUrl( $item->getComments(), PROTO_CURRENT ); ?></comments><?php }?>
</item>
<?php
}
/**
- * Ouput an RSS 2.0 footer
+ * Output an RSS 2.0 footer
*/
function outFooter() {
?>
@@ -362,7 +362,7 @@ class AtomFeed extends ChannelFeed {
}
/**
- * Atom 1.0 requires a unique, opaque IRI as a unique indentifier
+ * Atom 1.0 requires a unique, opaque IRI as a unique identifier
* for every feed we create. For now just use the URL, but who
* can tell if that's right? If we put options on the feed, do we
* have to change the id? Maybe? Maybe not.
@@ -392,15 +392,15 @@ class AtomFeed extends ChannelFeed {
global $wgMimeType;
?>
<entry>
- <id><?php print $item->getUniqueId() ?></id>
- <title><?php print $item->getTitle() ?></title>
- <link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ) ?>"/>
- <?php if( $item->getDate() ) { ?>
- <updated><?php print $this->formatTime( $item->getDate() ) ?>Z</updated>
+ <id><?php print $item->getUniqueId(); ?></id>
+ <title><?php print $item->getTitle(); ?></title>
+ <link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?>"/>
+ <?php if ( $item->getDate() ) { ?>
+ <updated><?php print $this->formatTime( $item->getDate() ); ?>Z</updated>
<?php } ?>
<summary type="html"><?php print $item->getDescription() ?></summary>
- <?php if( $item->getAuthor() ) { ?><author><name><?php print $item->getAuthor() ?></name></author><?php }?>
+ <?php if ( $item->getAuthor() ) { ?><author><name><?php print $item->getAuthor(); ?></name></author><?php }?>
</entry>
<?php /* @todo FIXME: Need to add comments
@@ -409,7 +409,7 @@ class AtomFeed extends ChannelFeed {
}
/**
- * Outputs the footer for Atom 1.0 feed (basicly '\</feed\>').
+ * Outputs the footer for Atom 1.0 feed (basically '\</feed\>').
*/
function outFooter() {?>
</feed><?php
diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php
index 11b2675d..22cb52be 100644
--- a/includes/FeedUtils.php
+++ b/includes/FeedUtils.php
@@ -33,13 +33,13 @@ class FeedUtils {
* If the feed should be purged; $timekey and $key will be removed from
* $messageMemc
*
- * @param $timekey String: cache key of the timestamp of the last item
- * @param $key String: cache key of feed's content
+ * @param string $timekey cache key of the timestamp of the last item
+ * @param string $key cache key of feed's content
*/
public static function checkPurge( $timekey, $key ) {
global $wgRequest, $wgUser, $messageMemc;
$purge = $wgRequest->getVal( 'action' ) === 'purge';
- if ( $purge && $wgUser->isAllowed('purge') ) {
+ if ( $purge && $wgUser->isAllowed( 'purge' ) ) {
$messageMemc->delete( $timekey );
$messageMemc->delete( $key );
}
@@ -48,7 +48,7 @@ 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
+ * @param string $type feed type, as requested by the user
* @return Boolean
*/
public static function checkFeedOutput( $type ) {
@@ -59,7 +59,7 @@ class FeedUtils {
return false;
}
- if( !isset( $wgFeedClasses[$type] ) ) {
+ if ( !isset( $wgFeedClasses[$type] ) ) {
$wgOut->addWikiMsg( 'feed-invalid' );
return false;
}
@@ -77,17 +77,17 @@ class FeedUtils {
$titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
$timestamp = wfTimestamp( TS_MW, $row->rc_timestamp );
$actiontext = '';
- if( $row->rc_type == RC_LOG ) {
+ if ( $row->rc_type == RC_LOG ) {
$rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows
$actiontext = LogFormatter::newFromRow( $rcRow )->getActionText();
}
return self::formatDiffRow( $titleObj,
$row->rc_last_oldid, $row->rc_this_oldid,
$timestamp,
- ($row->rc_deleted & Revision::DELETED_COMMENT)
- ? wfMessage('rev-deleted-comment')->escaped()
+ $row->rc_deleted & Revision::DELETED_COMMENT
+ ? wfMessage( 'rev-deleted-comment' )->escaped()
: $row->rc_comment,
- $actiontext
+ $actiontext
);
}
@@ -98,15 +98,15 @@ class FeedUtils {
* @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
+ * @param string $comment new revision's comment
+ * @param string $actiontext text of the action; in case of log event
* @return String
*/
- public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
+ public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext = '' ) {
global $wgFeedDiffCutoff, $wgLang;
wfProfileIn( __METHOD__ );
- # log enties
+ // log entries
$completeText = '<p>' . implode( ' ',
array_filter(
array(
@@ -115,19 +115,19 @@ class FeedUtils {
// NOTE: Check permissions for anonymous users, not current user.
// No "privileged" version should end up in the cache.
- // Most feed readers will not log in anway.
+ // Most feed readers will not log in anyway.
$anon = new User();
$accErrors = $title->getUserPermissionsErrors( 'read', $anon, true );
// Can't diff special pages, unreadable pages or pages with no new revision
// to compare against: just return the text.
- if( $title->getNamespace() < 0 || $accErrors || !$newid ) {
+ if ( $title->getNamespace() < 0 || $accErrors || !$newid ) {
wfProfileOut( __METHOD__ );
return $completeText;
}
- if( $oldid ) {
- wfProfileIn( __METHOD__."-dodiff" );
+ if ( $oldid ) {
+ wfProfileIn( __METHOD__ . "-dodiff" );
#$diffText = $de->getDiff( wfMessage( 'revisionasof',
# $wgLang->timeanddate( $timestamp ),
@@ -138,13 +138,23 @@ class FeedUtils {
$diffText = '';
// Don't bother generating the diff if we won't be able to show it
if ( $wgFeedDiffCutoff > 0 ) {
- $de = new DifferenceEngine( $title, $oldid, $newid );
- $diffText = $de->getDiff(
- wfMessage( 'previousrevision' )->text(), // hack
- wfMessage( 'revisionasof',
- $wgLang->timeanddate( $timestamp ),
- $wgLang->date( $timestamp ),
- $wgLang->time( $timestamp ) )->text() );
+ $rev = Revision::newFromId( $oldid );
+
+ if ( !$rev ) {
+ $diffText = false;
+ } else {
+ $context = clone RequestContext::getMain();
+ $context->setTitle( $title );
+
+ $contentHandler = $rev->getContentHandler();
+ $de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid );
+ $diffText = $de->getDiff(
+ wfMessage( 'previousrevision' )->text(), // hack
+ wfMessage( 'revisionasof',
+ $wgLang->timeanddate( $timestamp ),
+ $wgLang->date( $timestamp ),
+ $wgLang->time( $timestamp ) )->text() );
+ }
}
if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) {
@@ -158,20 +168,40 @@ class FeedUtils {
$diffText = UtfNormal::cleanUp( $diffText );
$diffText = self::applyDiffStyle( $diffText );
}
- wfProfileOut( __METHOD__."-dodiff" );
+ wfProfileOut( __METHOD__ . "-dodiff" );
} else {
$rev = Revision::newFromId( $newid );
- if( $wgFeedDiffCutoff <= 0 || is_null( $rev ) ) {
- $newtext = '';
+ if ( $wgFeedDiffCutoff <= 0 || is_null( $rev ) ) {
+ $newContent = ContentHandler::getForTitle( $title )->makeEmptyContent();
} else {
- $newtext = $rev->getText();
+ $newContent = $rev->getContent();
}
- if ( $wgFeedDiffCutoff <= 0 || strlen( $newtext ) > $wgFeedDiffCutoff ) {
+
+ if ( $newContent instanceof TextContent ) {
+ // only textual content has a "source view".
+ $text = $newContent->getNativeData();
+
+ if ( $wgFeedDiffCutoff <= 0 || strlen( $text ) > $wgFeedDiffCutoff ) {
+ $html = null;
+ } else {
+ $html = nl2br( htmlspecialchars( $text ) );
+ }
+ } else {
+ //XXX: we could get an HTML representation of the content via getParserOutput, but that may
+ // contain JS magic and generally may not be suitable for inclusion in a feed.
+ // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
+ //Compare also ApiFeedContributions::feedItemDesc
+ $html = null;
+ }
+
+ if ( $html === null ) {
+
// Omit large new page diffs, bug 29110
+ // Also use diff link for non-textual content
$diffText = self::getDiffLink( $title, $newid );
} else {
$diffText = '<p><b>' . wfMessage( 'newpage' )->text() . '</b></p>' .
- '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
+ '<div>' . $html . '</div>';
}
}
$completeText .= $diffText;
@@ -190,10 +220,11 @@ class FeedUtils {
* @return string
*/
protected static function getDiffLink( Title $title, $newid, $oldid = null ) {
- $queryParameters = ($oldid == null)
- ? "diff={$newid}"
- : "diff={$newid}&oldid={$oldid}" ;
- $diffUrl = $title->getFullUrl( $queryParameters );
+ $queryParameters = array( 'diff' => $newid );
+ if ( $oldid != null ) {
+ $queryParameters['oldid'] = $oldid;
+ }
+ $diffUrl = $title->getFullURL( $queryParameters );
$diffLink = Html::element( 'a', array( 'href' => $diffUrl ),
wfMessage( 'showdiff' )->inContentLanguage()->text() );
@@ -206,21 +237,21 @@ class FeedUtils {
* 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
+ * @param string $text diff's HTML output
* @return String: modified HTML
*/
public static function applyDiffStyle( $text ) {
$styles = array(
'diff' => 'background-color: white; color:black;',
- 'diff-otitle' => 'background-color: white; color:black;',
- 'diff-ntitle' => 'background-color: white; color:black;',
- 'diff-addedline' => 'background: #cfc; color:black; font-size: smaller;',
- 'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;',
- 'diff-context' => 'background: #eee; color:black; font-size: smaller;',
- 'diffchange' => 'color: red; font-weight: bold; text-decoration: none;',
+ 'diff-otitle' => 'background-color: white; color:black; text-align: center;',
+ 'diff-ntitle' => 'background-color: white; color:black; text-align: center;',
+ 'diff-addedline' => 'color:black; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;',
+ 'diff-deletedline' => 'color:black; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;',
+ 'diff-context' => 'background-color: #f9f9f9; color: #333333; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #e6e6e6; vertical-align: top; white-space: pre-wrap;',
+ 'diffchange' => 'font-weight: bold; text-decoration: none;',
);
- foreach( $styles as $class => $style ) {
+ foreach ( $styles as $class => $style ) {
$text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/",
"\\1style=\"$style\"\\3", $text );
}
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index e75ad729..65d82b87 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -80,20 +80,20 @@ class FileDeleteForm {
$this->oldimage = $wgRequest->getText( 'oldimage', false );
$token = $wgRequest->getText( 'wpEditToken' );
# Flag to hide all contents of the archived revisions
- $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision');
+ $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
- if( $this->oldimage ) {
+ if ( $this->oldimage ) {
$this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage );
}
- if( !self::haveDeletableFile($this->file, $this->oldfile, $this->oldimage) ) {
+ if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) {
$wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
$wgOut->addReturnTo( $this->title );
return;
}
// Perform the deletion if appropriate
- if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
+ if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
$deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
$deleteReason = $wgRequest->getText( 'wpReason' );
@@ -109,24 +109,18 @@ class FileDeleteForm {
$status = self::doDelete( $this->title, $this->file, $this->oldimage, $reason, $suppress, $wgUser );
- if( !$status->isGood() ) {
+ if ( !$status->isGood() ) {
$wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" );
$wgOut->addWikiText( '<div class="error">' . $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) . '</div>' );
}
- if( $status->ok ) {
+ if ( $status->ok ) {
$wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
$wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) );
// Return to the main page if we just deleted all versions of the
// file, otherwise go back to the description page
$wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
- if ( $wgUser->isLoggedIn() && $wgRequest->getCheck( 'wpWatch' ) != $wgUser->isWatched( $this->title ) ) {
- if ( $wgRequest->getCheck( 'wpWatch' ) ) {
- WatchAction::doWatch( $this->title, $wgUser );
- } else {
- WatchAction::doUnwatch( $this->title, $wgUser );
- }
- }
+ WatchAction::doWatchOrUnwatch( $wgRequest->getCheck( 'wpWatch' ), $this->title, $wgUser );
}
return;
}
@@ -140,10 +134,11 @@ class FileDeleteForm {
*
* @param $title Title object
* @param File $file: file object
- * @param $oldimage String: archive name
- * @param $reason String: reason of the deletion
+ * @param string $oldimage archive name
+ * @param string $reason reason of the deletion
* @param $suppress Boolean: whether to mark all deleted versions as restricted
* @param $user User object performing the request
+ * @throws MWException
* @return bool|Status
*/
public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null ) {
@@ -152,13 +147,13 @@ class FileDeleteForm {
$user = $wgUser;
}
- if( $oldimage ) {
+ if ( $oldimage ) {
$page = null;
$status = $file->deleteOld( $oldimage, $reason, $suppress );
- if( $status->ok ) {
+ if ( $status->ok ) {
// Need to do a log item
$logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
- if( trim( $reason ) != '' ) {
+ if ( trim( $reason ) != '' ) {
$logComment .= wfMessage( 'colon-separator' )
->inContentLanguage()->text() . $reason;
}
@@ -186,7 +181,7 @@ class FileDeleteForm {
// or revision is missing, so check for isOK() rather than isGood()
if ( $deleteStatus->isOK() ) {
$status = $file->delete( $reason, $suppress );
- if( $status->isOK() ) {
+ if ( $status->isOK() ) {
$dbw->commit( __METHOD__ );
} else {
$dbw->rollback( __METHOD__ );
@@ -212,7 +207,7 @@ class FileDeleteForm {
private function showForm() {
global $wgOut, $wgUser, $wgRequest;
- if( $wgUser->isAllowed( 'suppressrevision' ) ) {
+ if ( $wgUser->isAllowed( 'suppressrevision' ) ) {
$suppress = "<tr id=\"wpDeleteSuppressRow\">
<td></td>
<td class='mw-input'><strong>" .
@@ -257,7 +252,7 @@ class FileDeleteForm {
"</td>
</tr>
{$suppress}";
- if( $wgUser->isLoggedIn() ) {
+ if ( $wgUser->isLoggedIn() ) {
$form .= "
<tr>
<td></td>
@@ -308,12 +303,12 @@ class FileDeleteForm {
* showing an appropriate message depending upon whether
* it's a current file or an old version
*
- * @param $message String: message base
+ * @param string $message message base
* @return String
*/
private function prepareMessage( $message ) {
global $wgLang;
- if( $this->oldimage ) {
+ if ( $this->oldimage ) {
return wfMessage(
"{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
wfEscapeWikiText( $this->title->getText() ),
@@ -343,7 +338,7 @@ class FileDeleteForm {
*
* @return bool
*/
- public static function isValidOldSpec($oldimage) {
+ public static function isValidOldSpec( $oldimage ) {
return strlen( $oldimage ) >= 16
&& strpos( $oldimage, '/' ) === false
&& strpos( $oldimage, '\\' ) === false;
@@ -359,7 +354,7 @@ class FileDeleteForm {
* @param $oldimage File
* @return bool
*/
- public static function haveDeletableFile(&$file, &$oldfile, $oldimage) {
+ public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) {
return $oldimage
? $oldfile && $oldfile->exists() && $oldfile->isLocal()
: $file && $file->exists() && $file->isLocal();
@@ -374,10 +369,11 @@ class FileDeleteForm {
$q = array();
$q['action'] = 'delete';
- if( $this->oldimage )
+ if ( $this->oldimage ) {
$q['oldimage'] = $this->oldimage;
+ }
- return $this->title->getLocalUrl( $q );
+ return $this->title->getLocalURL( $q );
}
/**
diff --git a/includes/ForkController.php b/includes/ForkController.php
index 448bc03b..ced45af6 100644
--- a/includes/ForkController.php
+++ b/includes/ForkController.php
@@ -53,7 +53,7 @@ class ForkController {
const RESTART_ON_ERROR = 1;
public function __construct( $numProcs, $flags = 0 ) {
- if ( php_sapi_name() != 'cli' ) {
+ if ( PHP_SAPI != 'cli' ) {
throw new MWException( "ForkController cannot be used from the web." );
}
$this->procsToStart = $numProcs;
@@ -121,7 +121,9 @@ 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 ) {
@@ -140,7 +142,7 @@ class ForkController {
// Don't share DB, storage, or memcached connections
wfGetLBFactory()->destroyInstance();
FileBackendGroup::destroySingleton();
- LockManagerGroup::destroySingleton();
+ LockManagerGroup::destroySingletons();
ObjectCache::clear();
$wgMemc = null;
}
diff --git a/includes/FormOptions.php b/includes/FormOptions.php
index 33bbd86a..54822e32 100644
--- a/includes/FormOptions.php
+++ b/includes/FormOptions.php
@@ -2,7 +2,7 @@
/**
* Helper class to keep track of options when mixing links and form elements.
*
- * Copyright © 2008, Niklas Laxstiröm
+ * Copyright © 2008, Niklas Laxström
* Copyright © 2011, Antoine Musso
*
* This program is free software; you can redistribute it and/or modify
@@ -22,13 +22,14 @@
*
* @file
* @author Niklas Laxström
- * @author Antoine Musso
+ * @author Antoine Musso
*/
/**
* Helper class to keep track of options when mixing links and form elements.
*
- * @todo This badly need some examples and tests :-)
+ * @todo This badly needs some examples and tests :) The usage in SpecialRecentchanges class is a
+ * good ersatz in the meantime.
*/
class FormOptions implements ArrayAccess {
/** @name Type constants
@@ -50,12 +51,26 @@ class FormOptions implements ArrayAccess {
/* @} */
/**
- * @todo Document!
+ * Map of known option names to information about them.
+ *
+ * Each value is an array with the following keys:
+ * - 'default' - the default value as passed to add()
+ * - 'value' - current value, start with null, can be set by various functions
+ * - 'consumed' - true/false, whether the option was consumed using
+ * consumeValue() or consumeValues()
+ * - 'type' - one of the type constants (but never AUTO)
*/
protected $options = array();
# Setting up
+ /**
+ * Add an option to be handled by this FormOptions instance.
+ *
+ * @param string $name Request parameter name
+ * @param mixed $default Default value when the request parameter is not present
+ * @param int $type One of the type constants (optional, defaults to AUTO)
+ */
public function add( $name, $default, $type = self::AUTO ) {
$option = array();
$option['default'] = $default;
@@ -71,19 +86,25 @@ class FormOptions implements ArrayAccess {
$this->options[$name] = $option;
}
+ /**
+ * Remove an option being handled by this FormOptions instance. This is the inverse of add().
+ *
+ * @param string $name Request parameter name
+ */
public function delete( $name ) {
$this->validateName( $name, true );
unset( $this->options[$name] );
}
/**
- * Used to find out which type the data is.
- * All types are defined in the 'Type constants' section of this class
- * Please note we do not support detection of INTNULL MediaWiki type
- * which will be assumed as INT if the data is an integer.
+ * Used to find out which type the data is. All types are defined in the 'Type constants' section
+ * of this class.
+ *
+ * Detection of the INTNULL type is not supported; INT will be assumed if the data is an integer,
+ * MWException will be thrown if it's null.
*
- * @param $data Mixed: value to guess type for
- * @exception MWException Unsupported datatype
+ * @param mixed $data Value to guess the type for
+ * @throws MWException If unable to guess the type
* @return int Type constant
*/
public static function guessType( $data ) {
@@ -101,11 +122,12 @@ class FormOptions implements ArrayAccess {
# Handling values
/**
- * Verify the given option name exist.
+ * Verify that the given option name exists.
*
- * @param $name String: option name
- * @param $strict Boolean: throw an exception when the option does not exist (default false)
- * @return Boolean: true if option exist, false otherwise
+ * @param string $name Option name
+ * @param bool $strict Throw an exception when the option doesn't exist instead of returning false
+ * @throws MWException
+ * @return bool True if the option exists, false otherwise
*/
public function validateName( $name, $strict = false ) {
if ( !isset( $this->options[$name] ) ) {
@@ -115,16 +137,17 @@ class FormOptions implements ArrayAccess {
return false;
}
}
+
return true;
}
/**
* Use to set the value of an option.
*
- * @param $name String: option name
- * @param $value Mixed: value for the option
- * @param $force Boolean: whether to set the value when it is equivalent to the default value for this option (default false).
- * @return null
+ * @param string $name Option name
+ * @param mixed $value Value for the option
+ * @param bool $force Whether to set the value when it is equivalent to the default value for this
+ * option (default false).
*/
public function setValue( $name, $value, $force = false ) {
$this->validateName( $name, true );
@@ -138,11 +161,10 @@ class FormOptions implements ArrayAccess {
}
/**
- * Get the value for the given option name.
- * Internally use getValueReal()
+ * Get the value for the given option name. Uses getValueReal() internally.
*
- * @param $name String: option name
- * @return Mixed
+ * @param string $name Option name
+ * @return mixed
*/
public function getValue( $name ) {
$this->validateName( $name, true );
@@ -151,9 +173,10 @@ class FormOptions implements ArrayAccess {
}
/**
- * @todo Document
- * @param $option Array: array structure describing the option
- * @return Mixed. Value or the default value if it is null
+ * Return current option value, based on a structure taken from $options.
+ *
+ * @param array $option Array structure describing the option
+ * @return mixed Value, or the default value if it is null
*/
protected function getValueReal( $option ) {
if ( $option['value'] !== null ) {
@@ -165,9 +188,8 @@ class FormOptions implements ArrayAccess {
/**
* Delete the option value.
- * This will make future calls to getValue() return the default value.
- * @param $name String: option name
- * @return null
+ * This will make future calls to getValue() return the default value.
+ * @param string $name Option name
*/
public function reset( $name ) {
$this->validateName( $name, true );
@@ -175,9 +197,13 @@ class FormOptions implements ArrayAccess {
}
/**
- * @todo Document
- * @param $name String: option name
- * @return null
+ * Get the value of given option and mark it as 'consumed'. Consumed options are not returned
+ * by getUnconsumedValues().
+ *
+ * @see consumeValues()
+ * @throws MWException If the option does not exist
+ * @param string $name Option name
+ * @return mixed Value, or the default value if it is null
*/
public function consumeValue( $name ) {
$this->validateName( $name, true );
@@ -187,11 +213,15 @@ class FormOptions implements ArrayAccess {
}
/**
- * @todo Document
- * @param $names Array: array of option names
- * @return null
+ * Get the values of given options and mark them as 'consumed'. Consumed options are not returned
+ * by getUnconsumedValues().
+ *
+ * @see consumeValue()
+ * @throws MWException If any option does not exist
+ * @param array $names Array of option names as strings
+ * @return array Array of option values, or the default values if they are null
*/
- public function consumeValues( /*Array*/ $names ) {
+ public function consumeValues( $names ) {
$out = array();
foreach ( $names as $name ) {
@@ -205,13 +235,12 @@ class FormOptions implements ArrayAccess {
/**
* Validate and set an option integer value
- * The value will be altered to fit in the range.
+ * The value will be altered to fit in the range.
*
- * @param $name String: option name
- * @param $min Int: minimum value
- * @param $max Int: maximum value
- * @exception MWException Option is not of type int
- * @return null
+ * @param string $name option name
+ * @param int $min minimum value
+ * @param int $max maximum value
+ * @throws MWException If option is not of type INT
*/
public function validateIntBounds( $name, $min, $max ) {
$this->validateName( $name, true );
@@ -227,9 +256,10 @@ class FormOptions implements ArrayAccess {
}
/**
- * Getting the data out for use
- * @param $all Boolean: whether to include unchanged options (default: false)
- * @return Array
+ * Get all remaining values which have not been consumed by consumeValue() or consumeValues().
+ *
+ * @param bool $all Whether to include unchanged options (default: false)
+ * @return array
*/
public function getUnconsumedValues( $all = false ) {
$values = array();
@@ -247,7 +277,7 @@ class FormOptions implements ArrayAccess {
/**
* Return options modified as an array ( name => value )
- * @return Array
+ * @return array
*/
public function getChangedValues() {
$values = array();
@@ -262,8 +292,8 @@ class FormOptions implements ArrayAccess {
}
/**
- * Format options to an array ( name => value)
- * @return Array
+ * Format options to an array ( name => value )
+ * @return array
*/
public function getAllValues() {
$values = array();
@@ -277,24 +307,38 @@ class FormOptions implements ArrayAccess {
# Reading values
- public function fetchValuesFromRequest( WebRequest $r, $values = false ) {
- if ( !$values ) {
- $values = array_keys( $this->options );
+ /**
+ * Fetch values for all options (or selected options) from the given WebRequest, making them
+ * available for accessing with getValue() or consumeValue() etc.
+ *
+ * @param WebRequest $r The request to fetch values from
+ * @param array $optionKeys Which options to fetch the values for (default:
+ * all of them). Note that passing an empty array will also result in
+ * values for all keys being fetched.
+ * @throws MWException If the type of any option is invalid
+ */
+ public function fetchValuesFromRequest( WebRequest $r, $optionKeys = null ) {
+ if ( !$optionKeys ) {
+ $optionKeys = array_keys( $this->options );
}
- foreach ( $values as $name ) {
+ foreach ( $optionKeys as $name ) {
$default = $this->options[$name]['default'];
$type = $this->options[$name]['type'];
- switch( $type ) {
+ switch ( $type ) {
case self::BOOL:
- $value = $r->getBool( $name, $default ); break;
+ $value = $r->getBool( $name, $default );
+ break;
case self::INT:
- $value = $r->getInt( $name, $default ); break;
+ $value = $r->getInt( $name, $default );
+ break;
case self::STRING:
- $value = $r->getText( $name, $default ); break;
+ $value = $r->getText( $name, $default );
+ break;
case self::INTNULL:
- $value = $r->getIntOrNull( $name ); break;
+ $value = $r->getIntOrNull( $name );
+ break;
default:
throw new MWException( 'Unsupported datatype' );
}
@@ -306,29 +350,26 @@ class FormOptions implements ArrayAccess {
}
/** @name ArrayAccess functions
- * Those function implements PHP ArrayAccess interface
+ * These functions implement the ArrayAccess PHP interface.
* @see http://php.net/manual/en/class.arrayaccess.php
*/
/* @{ */
- /**
- * Whether option exist
- * @return bool
- */
+ /** Whether the option exists. */
public function offsetExists( $name ) {
return isset( $this->options[$name] );
}
- /**
- * Retrieve an option value
- * @return Mixed
- */
+
+ /** Retrieve an option value. */
public function offsetGet( $name ) {
return $this->getValue( $name );
}
- /** Set an option to given value */
+
+ /** Set an option to given value. */
public function offsetSet( $name, $value ) {
$this->setValue( $name, $value );
}
- /** Delete the option */
+
+ /** Delete the option. */
public function offsetUnset( $name ) {
$this->delete( $name );
}
diff --git a/includes/GitInfo.php b/includes/GitInfo.php
index c3c30733..f49f9be1 100644
--- a/includes/GitInfo.php
+++ b/includes/GitInfo.php
@@ -41,10 +41,18 @@ class GitInfo {
private static $viewers = false;
/**
- * @param $dir string The root directory of the repo where the .git dir can be found
+ * @param string $dir The root directory of the repo where the .git dir can be found
*/
public function __construct( $dir ) {
- $this->basedir = "{$dir}/.git/";
+ $this->basedir = "{$dir}/.git";
+ if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
+ $GITfile = file_get_contents( $this->basedir );
+ if ( strlen( $GITfile ) > 8 && substr( $GITfile, 0, 8 ) === 'gitdir: ' ) {
+ $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
+ $isAbsolute = $path[0] === '/' || substr( $path, 1, 1 ) === ':';
+ $this->basedir = $isAbsolute ? $path : "{$dir}/{$path}";
+ }
+ }
}
/**
@@ -62,7 +70,7 @@ class GitInfo {
/**
* Check if a string looks like a hex encoded SHA1 hash
*
- * @param $str string The string to check
+ * @param string $str The string to check
* @return bool Whether or not the string looks like a SHA1
*/
public static function isSHA1( $str ) {
@@ -102,7 +110,7 @@ class GitInfo {
}
// If not a SHA1 it may be a ref:
- $REFfile = "{$this->basedir}{$HEAD}";
+ $REFfile = "{$this->basedir}/{$HEAD}";
if ( !is_readable( $REFfile ) ) {
return false;
}
@@ -113,6 +121,32 @@ class GitInfo {
}
/**
+ * Return the commit date of HEAD entry of the git code repository
+ *
+ * @since 1.22
+ * @return int|bool Commit date (UNIX timestamp) or false
+ */
+ public function getHeadCommitDate() {
+ global $wgGitBin;
+
+ if ( !is_file( $wgGitBin ) || !is_executable( $wgGitBin ) ) {
+ return false;
+ }
+
+ $environment = array( "GIT_DIR" => $this->basedir );
+ $cmd = wfEscapeShellArg( $wgGitBin ) . " show -s --format=format:%ct HEAD";
+ $retc = false;
+ $commitDate = wfShellExec( $cmd, $retc, $environment );
+
+ if ( $retc !== 0 ) {
+ return false;
+ } else {
+ return (int)$commitDate;
+ }
+
+ }
+
+ /**
* Return the name of the current branch, or HEAD if not found
* @return string The branch name, HEAD, or false
*/
@@ -128,7 +162,7 @@ class GitInfo {
/**
* Get an URL to a web viewer link to the HEAD revision.
*
- * @return string|bool string if an URL is available or false otherwise.
+ * @return string|bool string if a URL is available or false otherwise.
*/
public function getHeadViewUrl() {
$config = "{$this->basedir}/config";
@@ -143,7 +177,7 @@ class GitInfo {
if ( isset( $configArray['remote origin'] ) ) {
$remote = $configArray['remote origin'];
} else {
- foreach( $configArray as $sectionName => $sectionConf ) {
+ foreach ( $configArray as $sectionName => $sectionConf ) {
if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
$remote = $sectionConf;
}
@@ -158,14 +192,15 @@ class GitInfo {
if ( substr( $url, -4 ) !== '.git' ) {
$url .= '.git';
}
- foreach( self::getViewers() as $repo => $viewer ) {
+ foreach ( self::getViewers() as $repo => $viewer ) {
$pattern = '#^' . $repo . '$#';
- if ( preg_match( $pattern, $url ) ) {
+ if ( preg_match( $pattern, $url, $matches ) ) {
$viewerUrl = preg_replace( $pattern, $viewer, $url );
$headSHA1 = $this->getHeadSHA1();
$replacements = array(
'%h' => substr( $headSHA1, 0, 7 ),
- '%H' => $headSHA1
+ '%H' => $headSHA1,
+ '%r' => urlencode( $matches[1] ),
);
return strtr( $viewerUrl, $replacements );
}
@@ -204,7 +239,7 @@ class GitInfo {
protected static function getViewers() {
global $wgGitRepositoryViewers;
- if( self::$viewers === false ) {
+ if ( self::$viewers === false ) {
self::$viewers = $wgGitRepositoryViewers;
wfRunHooks( 'GitViewers', array( &self::$viewers ) );
}
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 50758c89..77c09e53 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -35,7 +35,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
* PHP extensions may be included here.
*/
-if( !function_exists( 'iconv' ) ) {
+if ( !function_exists( 'iconv' ) ) {
/**
* @codeCoverageIgnore
* @return string
@@ -50,7 +50,7 @@ if ( !function_exists( 'mb_substr' ) ) {
* @codeCoverageIgnore
* @return string
*/
- function mb_substr( $str, $start, $count='end' ) {
+ function mb_substr( $str, $start, $count = 'end' ) {
return Fallback::mb_substr( $str, $start, $count );
}
@@ -73,7 +73,7 @@ if ( !function_exists( 'mb_strlen' ) ) {
}
}
-if( !function_exists( 'mb_strpos' ) ) {
+if ( !function_exists( 'mb_strpos' ) ) {
/**
* @codeCoverageIgnore
* @return int
@@ -84,7 +84,7 @@ if( !function_exists( 'mb_strpos' ) ) {
}
-if( !function_exists( 'mb_strrpos' ) ) {
+if ( !function_exists( 'mb_strrpos' ) ) {
/**
* @codeCoverageIgnore
* @return int
@@ -94,25 +94,16 @@ if( !function_exists( 'mb_strrpos' ) ) {
}
}
-
-// Support for Wietse Venema's taint feature
-if ( !function_exists( 'istainted' ) ) {
+// gzdecode function only exists in PHP >= 5.4.0
+// http://php.net/gzdecode
+if ( !function_exists( 'gzdecode' ) ) {
/**
* @codeCoverageIgnore
- * @return int
+ * @return string
*/
- function istainted( $var ) {
- return 0;
+ function gzdecode( $data ) {
+ return gzinflate( substr( $data, 10, -8 ) );
}
- /** @codeCoverageIgnore */
- function taint( $var, $level = 0 ) {}
- /** @codeCoverageIgnore */
- function untaint( $var, $level = 0 ) {}
- define( 'TC_HTML', 1 );
- define( 'TC_SHELL', 1 );
- define( 'TC_MYSQL', 1 );
- define( 'TC_PCRE', 1 );
- define( 'TC_SELF', 1 );
}
/// @endcond
@@ -127,19 +118,19 @@ function wfArrayDiff2( $a, $b ) {
}
/**
- * @param $a
- * @param $b
+ * @param $a array|string
+ * @param $b array|string
* @return int
*/
function wfArrayDiff2_cmp( $a, $b ) {
- if ( !is_array( $a ) ) {
+ if ( is_string( $a ) && is_string( $b ) ) {
return strcmp( $a, $b );
} elseif ( count( $a ) !== count( $b ) ) {
return count( $a ) < count( $b ) ? -1 : 1;
} else {
reset( $a );
reset( $b );
- while( ( list( , $valueA ) = each( $a ) ) && ( list( , $valueB ) = each( $b ) ) ) {
+ while ( ( list( , $valueA ) = each( $a ) ) && ( list( , $valueB ) = each( $b ) ) ) {
$cmp = strcmp( $valueA, $valueB );
if ( $cmp !== 0 ) {
return $cmp;
@@ -151,14 +142,16 @@ function wfArrayDiff2_cmp( $a, $b ) {
/**
* Array lookup
- * Returns an array where the values in the first array are replaced by the
- * values in the second array with the corresponding keys
+ * Returns an array where the values in array $b are replaced by the
+ * values in array $a with the corresponding keys
*
+ * @deprecated since 1.22; use array_intersect_key()
* @param $a Array
* @param $b Array
* @return array
*/
function wfArrayLookup( $a, $b ) {
+ wfDeprecated( __FUNCTION__, '1.22' );
return array_flip( array_intersect( array_flip( $a ), array_keys( $b ) ) );
}
@@ -168,7 +161,8 @@ function wfArrayLookup( $a, $b ) {
* @param $key String|Int
* @param $value Mixed
* @param $default Mixed
- * @param $changed Array to alter
+ * @param array $changed to alter
+ * @throws MWException
*/
function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) {
if ( is_null( $changed ) ) {
@@ -183,11 +177,13 @@ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) {
* Backwards array plus for people who haven't bothered to read the PHP manual
* XXX: will not darn your socks for you.
*
+ * @deprecated since 1.22; use array_replace()
* @param $array1 Array
* @param [$array2, [...]] Arrays
* @return Array
*/
function wfArrayMerge( $array1/* ... */ ) {
+ wfDeprecated( __FUNCTION__, '1.22' );
$args = func_get_args();
$args = array_reverse( $args, true );
$out = array();
@@ -232,8 +228,8 @@ function wfMergeErrorArrays( /*...*/ ) {
/**
* Insert array into another array after the specified *KEY*
*
- * @param $array Array: The array.
- * @param $insert Array: The array to insert.
+ * @param array $array The array.
+ * @param array $insert The array to insert.
* @param $after Mixed: The key to insert after
* @return Array
*/
@@ -262,7 +258,7 @@ function wfArrayInsertAfter( array $array, array $insert, $after ) {
*/
function wfObjectToArray( $objOrArray, $recursive = true ) {
$array = array();
- if( is_object( $objOrArray ) ) {
+ if ( is_object( $objOrArray ) ) {
$objOrArray = get_object_vars( $objOrArray );
}
foreach ( $objOrArray as $key => $value ) {
@@ -277,24 +273,6 @@ function wfObjectToArray( $objOrArray, $recursive = true ) {
}
/**
- * Wrapper around array_map() which also taints variables
- *
- * @param $function Callback
- * @param $input Array
- * @return Array
- */
-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;
-}
-
-/**
* Get a random decimal value between 0 and 1, in a way
* not likely to give duplicate values for any realistic
* number of articles.
@@ -311,19 +289,19 @@ function wfRandom() {
}
/**
- * Get a random string containing a number of pesudo-random hex
+ * Get a random string containing a number of pseudo-random hex
* characters.
* @note This is not secure, if you are trying to generate some sort
* of token please use MWCryptRand instead.
*
- * @param $length int The length of the string to generate
+ * @param int $length The length of the string to generate
* @return String
* @since 1.20
*/
function wfRandomString( $length = 32 ) {
$str = '';
- while ( strlen( $str ) < $length ) {
- $str .= dechex( mt_rand() );
+ for ( $n = 0; $n < $length; $n += 7 ) {
+ $str .= sprintf( '%07x', mt_rand() & 0xfffffff );
}
return substr( $str, 0, $length );
}
@@ -349,7 +327,7 @@ function wfRandomString( $length = 32 ) {
*
* @param $s String:
* @return string
-*/
+ */
function wfUrlencode( $s ) {
static $needle;
if ( is_null( $s ) ) {
@@ -379,8 +357,8 @@ function wfUrlencode( $s ) {
* "days=7&limit=100". Options in the first array override options in the second.
* Options set to null or false will not be output.
*
- * @param $array1 Array ( String|Array )
- * @param $array2 Array ( String|Array )
+ * @param array $array1 ( String|Array )
+ * @param array $array2 ( String|Array )
* @param $prefix String
* @return String
*/
@@ -391,7 +369,7 @@ function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) {
$cgi = '';
foreach ( $array1 as $key => $value ) {
- if ( !is_null($value) && $value !== false ) {
+ if ( !is_null( $value ) && $value !== false ) {
if ( $cgi != '' ) {
$cgi .= '&';
}
@@ -422,11 +400,11 @@ function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) {
/**
* This is the logical opposite of wfArrayToCgi(): it accepts a query string as
- * its argument and returns the same string in array form. This allows compa-
- * tibility with legacy functions that accept raw query strings instead of nice
+ * its argument and returns the same string in array form. This allows compatibility
+ * with legacy functions that accept raw query strings instead of nice
* arrays. Of course, keys and values are urldecode()d.
*
- * @param $query String: query string
+ * @param string $query query string
* @return array Array version of input
*/
function wfCgiToArray( $query ) {
@@ -480,8 +458,8 @@ function wfAppendQuery( $url, $query ) {
if ( is_array( $query ) ) {
$query = wfArrayToCgi( $query );
}
- if( $query != '' ) {
- if( false === strpos( $url, '?' ) ) {
+ if ( $query != '' ) {
+ if ( false === strpos( $url, '?' ) ) {
$url .= '?';
} else {
$url .= '&';
@@ -506,7 +484,7 @@ function wfAppendQuery( $url, $query ) {
* @todo this won't work with current-path-relative URLs
* like "subdir/foo.html", etc.
*
- * @param $url String: either fully-qualified or a local path + query
+ * @param string $url either fully-qualified or a local path + query
* @param $defaultProto Mixed: one of the PROTO_* constants. Determines the
* protocol to use if $url or $wgServer is
* protocol-relative
@@ -576,7 +554,7 @@ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) {
* @todo Need to integrate this into wfExpandUrl (bug 32168)
*
* @since 1.19
- * @param $urlParts Array URL parts, as output from wfParseUrl
+ * @param array $urlParts URL parts, as output from wfParseUrl
* @return string URL assembled from its component parts
*/
function wfAssembleUrl( $urlParts ) {
@@ -628,7 +606,7 @@ function wfAssembleUrl( $urlParts ) {
*
* @todo Need to integrate this into wfExpandUrl (bug 32168)
*
- * @param $urlPath String URL path, potentially containing dot-segments
+ * @param string $urlPath URL path, potentially containing dot-segments
* @return string URL path with all dot-segments removed
*/
function wfRemoveDotSegments( $urlPath ) {
@@ -705,7 +683,7 @@ function wfRemoveDotSegments( $urlPath ) {
/**
* Returns a regular expression of url protocols
*
- * @param $includeProtocolRelative bool If false, remove '//' from the returned protocol list.
+ * @param bool $includeProtocolRelative If false, remove '//' from the returned protocol list.
* DO NOT USE this directly, use wfUrlProtocolsWithoutProtRel() instead
* @return String
*/
@@ -765,7 +743,7 @@ function wfUrlProtocolsWithoutProtRel() {
* 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly
* 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2))
*
- * @param $url String: a URL to parse
+ * @param string $url a URL to parse
* @return Array: bits of the URL in an associative array, per PHP docs
*/
function wfParseUrl( $url ) {
@@ -808,9 +786,14 @@ function wfParseUrl( $url ) {
if ( !isset( $bits['host'] ) ) {
$bits['host'] = '';
- /* parse_url loses the third / for file:///c:/ urls (but not on variants) */
- if ( substr( $bits['path'], 0, 1 ) !== '/' ) {
- $bits['path'] = '/' . $bits['path'];
+ // bug 45069
+ if ( isset( $bits['path'] ) ) {
+ /* parse_url loses the third / for file:///c:/ urls (but not on variants) */
+ if ( substr( $bits['path'], 0, 1 ) !== '/' ) {
+ $bits['path'] = '/' . $bits['path'];
+ }
+ } else {
+ $bits['path'] = '';
}
}
@@ -845,8 +828,6 @@ function wfExpandIRI_callback( $matches ) {
return urldecode( $matches[1] );
}
-
-
/**
* Make URL indexes, appropriate for the el_index field of externallinks.
*
@@ -903,17 +884,17 @@ function wfMakeUrlIndexes( $url ) {
/**
* Check whether a given URL has a domain that occurs in a given set of domains
- * @param $url string URL
- * @param $domains array Array of domains (strings)
+ * @param string $url URL
+ * @param array $domains Array of domains (strings)
* @return bool True if the host part of $url ends in one of the strings in $domains
*/
function wfMatchesDomainList( $url, $domains ) {
$bits = wfParseUrl( $url );
if ( is_array( $bits ) && isset( $bits['host'] ) ) {
+ $host = '.' . $bits['host'];
foreach ( (array)$domains as $domain ) {
- // FIXME: This gives false positives. http://nds-nl.wikipedia.org will match nl.wikipedia.org
- // We should use something that interprets dots instead
- if ( substr( $bits['host'], -strlen( $domain ) ) === $domain ) {
+ $domain = '.' . $domain;
+ if ( substr( $host, -strlen( $domain ) ) === $domain ) {
return true;
}
}
@@ -932,7 +913,7 @@ function wfMatchesDomainList( $url, $domains ) {
* $wgDebugComments - if on, some debug items may appear in comments in the HTML output.
*
* @param $text String
- * @param $logonly Bool: set true to avoid appearing in HTML when $wgDebugComments is set
+ * @param bool $logonly set true to avoid appearing in HTML when $wgDebugComments is set
*/
function wfDebug( $text, $logonly = false ) {
global $wgDebugLogFile, $wgProfileOnly, $wgDebugRawPage, $wgDebugLogPrefix;
@@ -950,14 +931,12 @@ function wfDebug( $text, $logonly = false ) {
MWDebug::debugMsg( $text );
}
- if ( wfRunHooks( 'Debug', array( $text, null /* no log group */ ) ) ) {
- 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 );
- $text = $wgDebugLogPrefix . $text;
- wfErrorLog( $text, $wgDebugLogFile );
- }
+ 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 );
+ $text = $wgDebugLogPrefix . $text;
+ wfErrorLog( $text, $wgDebugLogFile );
}
}
@@ -1004,11 +983,11 @@ function wfDebugTimer() {
/**
* Send a line giving PHP memory usage.
*
- * @param $exact Bool: print exact values instead of kilobytes (default: false)
+ * @param bool $exact print exact values instead of kilobytes (default: false)
*/
function wfDebugMem( $exact = false ) {
$mem = memory_get_usage();
- if( !$exact ) {
+ if ( !$exact ) {
$mem = floor( $mem / 1024 ) . ' kilobytes';
} else {
$mem .= ' bytes';
@@ -1022,28 +1001,26 @@ function wfDebugMem( $exact = false ) {
*
* @param $logGroup String
* @param $text String
- * @param $public Bool: whether to log the event in the public log if no private
+ * @param bool $public whether to log the event in the public log if no private
* log file is specified, (default true)
*/
function wfDebugLog( $logGroup, $text, $public = true ) {
global $wgDebugLogGroups;
$text = trim( $text ) . "\n";
- if( isset( $wgDebugLogGroups[$logGroup] ) ) {
+ if ( isset( $wgDebugLogGroups[$logGroup] ) ) {
$time = wfTimestamp( TS_DB );
$wiki = wfWikiID();
$host = wfHostname();
- if ( wfRunHooks( 'Debug', array( $text, $logGroup ) ) ) {
- wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] );
- }
+ wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] );
} elseif ( $public === true ) {
- wfDebug( $text, true );
+ wfDebug( "[$logGroup] $text", false );
}
}
/**
* Log for database errors
*
- * @param $text String: database error message.
+ * @param string $text database error message.
*/
function wfLogDBError( $text ) {
global $wgDBerrorLog, $wgDBerrorLogTZ;
@@ -1076,9 +1053,9 @@ function wfLogDBError( $text ) {
* Throws a warning that $function is deprecated
*
* @param $function String
- * @param $version String|bool: Version of MediaWiki that the function was deprecated in (Added in 1.19).
- * @param $component String|bool: Added in 1.19.
- * @param $callerOffset integer: How far up the callstack is the original
+ * @param string|bool $version Version of MediaWiki that the function was deprecated in (Added in 1.19).
+ * @param string|bool $component Added in 1.19.
+ * @param $callerOffset integer: How far up the call stack is the original
* caller. 2 = function that called the function that called
* wfDeprecated (Added in 1.20)
*
@@ -1090,16 +1067,29 @@ function wfDeprecated( $function, $version = false, $component = false, $callerO
/**
* Send a warning either to the debug log or in a PHP error depending on
- * $wgDevelopmentWarnings
+ * $wgDevelopmentWarnings. To log warnings in production, use wfLogWarning() instead.
*
- * @param $msg String: message to send
+ * @param string $msg message to send
* @param $callerOffset Integer: number of items 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
+ * @param $level Integer: PHP error level; defaults to E_USER_NOTICE;
+ * only used when $wgDevelopmentWarnings is true
*/
function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
- MWDebug::warning( $msg, $callerOffset + 1, $level );
+ MWDebug::warning( $msg, $callerOffset + 1, $level, 'auto' );
+}
+
+/**
+ * Send a warning as a PHP error and the debug log. This is intended for logging
+ * warnings in production. For logging development warnings, use WfWarn instead.
+ *
+ * @param $msg String: message to send
+ * @param $callerOffset Integer: number of items to go back in the backtrace to
+ * find the correct caller (1 = function calling wfLogWarning, ...)
+ * @param $level Integer: PHP error level; defaults to E_USER_WARNING
+ */
+function wfLogWarning( $msg, $callerOffset = 1, $level = E_USER_WARNING ) {
+ MWDebug::warning( $msg, $callerOffset + 1, $level, 'production' );
}
/**
@@ -1109,7 +1099,8 @@ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
* send lines to the specified port, prefixed by the specified prefix and a space.
*
* @param $text String
- * @param $file String filename
+ * @param string $file filename
+ * @throws MWException
*/
function wfErrorLog( $text, $file ) {
if ( substr( $file, 0, 4 ) == 'udp:' ) {
@@ -1173,6 +1164,8 @@ function wfLogProfilingData() {
global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest;
global $wgProfileLimit, $wgUser;
+ StatCounter::singleton()->flush();
+
$profiler = Profiler::instance();
# Profiling must actually be enabled...
@@ -1212,9 +1205,18 @@ function wfLogProfilingData() {
if ( $wgUser->isItemLoaded( 'id' ) && $wgUser->isAnon() ) {
$forward .= ' anon';
}
+
+ // Command line script uses a FauxRequest object which does not have
+ // any knowledge about an URL and throw an exception instead.
+ try {
+ $requestUrl = $wgRequest->getRequestURL();
+ } catch ( MWException $e ) {
+ $requestUrl = 'n/a';
+ }
+
$log = sprintf( "%s\t%04.3f\t%s\n",
gmdate( 'YmdHis' ), $elapsed,
- urldecode( $wgRequest->getRequestURL() . $forward ) );
+ urldecode( $requestUrl . $forward ) );
wfErrorLog( $log . $profiler->getOutput(), $wgDebugLogFile );
}
@@ -1224,83 +1226,38 @@ function wfLogProfilingData() {
*
* @param $key String
* @param $count Int
+ * @return void
*/
function wfIncrStats( $key, $count = 1 ) {
- global $wgStatsMethod;
-
- $count = intval( $count );
-
- if( $wgStatsMethod == 'udp' ) {
- global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname, $wgAggregateStatsID;
- static $socket;
-
- $id = $wgAggregateStatsID !== false ? $wgAggregateStatsID : $wgDBname;
-
- if ( !$socket ) {
- $socket = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
- $statline = "stats/{$id} - 1 1 1 1 1 -total\n";
- socket_sendto(
- $socket,
- $statline,
- strlen( $statline ),
- 0,
- $wgUDPProfilerHost,
- $wgUDPProfilerPort
- );
- }
- $statline = "stats/{$id} - {$count} 1 1 1 1 {$key}\n";
- wfSuppressWarnings();
- socket_sendto(
- $socket,
- $statline,
- strlen( $statline ),
- 0,
- $wgUDPProfilerHost,
- $wgUDPProfilerPort
- );
- wfRestoreWarnings();
- } elseif( $wgStatsMethod == 'cache' ) {
- global $wgMemc;
- $key = wfMemcKey( 'stats', $key );
- if ( is_null( $wgMemc->incr( $key, $count ) ) ) {
- $wgMemc->add( $key, $count );
- }
- } else {
- // Disabled
- }
+ StatCounter::singleton()->incr( $key, $count );
}
/**
- * Check if the wiki read-only lock file is present. This can be used to lock
- * off editing functions, but doesn't guarantee that the database will not be
- * modified.
+ * Check whether the wiki is in read-only mode.
*
* @return bool
*/
function wfReadOnly() {
- global $wgReadOnlyFile, $wgReadOnly;
-
- if ( !is_null( $wgReadOnly ) ) {
- return (bool)$wgReadOnly;
- }
- if ( $wgReadOnlyFile == '' ) {
- return false;
- }
- // Set $wgReadOnly for faster access next time
- if ( is_file( $wgReadOnlyFile ) ) {
- $wgReadOnly = file_get_contents( $wgReadOnlyFile );
- } else {
- $wgReadOnly = false;
- }
- return (bool)$wgReadOnly;
+ return wfReadOnlyReason() !== false;
}
/**
- * @return bool
+ * Get the value of $wgReadOnly or the contents of $wgReadOnlyFile.
+ *
+ * @return string|bool: String when in read-only mode; false otherwise
*/
function wfReadOnlyReason() {
- global $wgReadOnly;
- wfReadOnly();
+ global $wgReadOnly, $wgReadOnlyFile;
+
+ if ( $wgReadOnly === null ) {
+ // Set $wgReadOnly for faster access next time
+ if ( is_file( $wgReadOnlyFile ) && filesize( $wgReadOnlyFile ) > 0 ) {
+ $wgReadOnly = file_get_contents( $wgReadOnlyFile );
+ } else {
+ $wgReadOnly = false;
+ }
+ }
+
return $wgReadOnly;
}
@@ -1322,27 +1279,27 @@ function wfReadOnlyReason() {
function wfGetLangObj( $langcode = false ) {
# Identify which language to get or create a language object for.
# Using is_object here due to Stub objects.
- if( is_object( $langcode ) ) {
+ if ( is_object( $langcode ) ) {
# Great, we already have the object (hopefully)!
return $langcode;
}
global $wgContLang, $wgLanguageCode;
- if( $langcode === true || $langcode === $wgLanguageCode ) {
+ if ( $langcode === true || $langcode === $wgLanguageCode ) {
# $langcode is the language code of the wikis content language object.
# or it is a boolean and value is true
return $wgContLang;
}
global $wgLang;
- if( $langcode === false || $langcode === $wgLang->getCode() ) {
+ if ( $langcode === false || $langcode === $wgLang->getCode() ) {
# $langcode is the language code of user language object.
# or it was a boolean and value is false
return $wgLang;
}
$validCodes = array_keys( Language::fetchLanguageNames() );
- if( in_array( $langcode, $validCodes ) ) {
+ if ( in_array( $langcode, $validCodes ) ) {
# $langcode corresponds to a valid language.
return Language::factory( $langcode );
}
@@ -1366,9 +1323,13 @@ function wfUILang() {
}
/**
- * This is the new function for getting translated interface messages.
- * See the Message class for documentation how to use them.
- * The intention is that this function replaces all old wfMsg* functions.
+ * This is the function for getting translated interface messages.
+ *
+ * @see Message class for documentation how to use them.
+ * @see https://www.mediawiki.org/wiki/Manual:Messages_API
+ *
+ * This function replaces all old wfMsg* functions.
+ *
* @param $key \string Message key.
* Varargs: normal message parameters.
* @return Message
@@ -1393,7 +1354,7 @@ function wfMessage( $key /*...*/) {
*/
function wfMessageFallback( /*...*/ ) {
$args = func_get_args();
- return MWFunction::callArray( 'Message::newFallbackSequence', $args );
+ return call_user_func_array( 'Message::newFallbackSequence', $args );
}
/**
@@ -1404,7 +1365,7 @@ function wfMessageFallback( /*...*/ ) {
*
* @deprecated since 1.18
*
- * @param $key String: lookup key for the message, usually
+ * @param string $key lookup key for the message, usually
* defined in languages/Language.php
*
* Parameters to the message, which can be used to insert variable text into
@@ -1416,6 +1377,8 @@ function wfMessageFallback( /*...*/ ) {
* @return String
*/
function wfMsg( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
$args = func_get_args();
array_shift( $args );
return wfMsgReal( $key, $args );
@@ -1430,6 +1393,8 @@ function wfMsg( $key ) {
* @return String
*/
function wfMsgNoTrans( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
$args = func_get_args();
array_shift( $args );
return wfMsgReal( $key, $args, true, false, false );
@@ -1456,16 +1421,18 @@ function wfMsgNoTrans( $key ) {
*
* @deprecated since 1.18
*
- * @param $key String: lookup key for the message, usually
+ * @param string $key lookup key for the message, usually
* defined in languages/Language.php
* @return String
*/
function wfMsgForContent( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
global $wgForceUIMsgAsContentMsg;
$args = func_get_args();
array_shift( $args );
$forcontent = true;
- if( is_array( $wgForceUIMsgAsContentMsg ) &&
+ if ( is_array( $wgForceUIMsgAsContentMsg ) &&
in_array( $key, $wgForceUIMsgAsContentMsg ) )
{
$forcontent = false;
@@ -1482,11 +1449,13 @@ function wfMsgForContent( $key ) {
* @return String
*/
function wfMsgForContentNoTrans( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
global $wgForceUIMsgAsContentMsg;
$args = func_get_args();
array_shift( $args );
$forcontent = true;
- if( is_array( $wgForceUIMsgAsContentMsg ) &&
+ if ( is_array( $wgForceUIMsgAsContentMsg ) &&
in_array( $key, $wgForceUIMsgAsContentMsg ) )
{
$forcontent = false;
@@ -1499,7 +1468,7 @@ function wfMsgForContentNoTrans( $key ) {
*
* @deprecated since 1.18
*
- * @param $key String: key to get.
+ * @param string $key key to get.
* @param $args
* @param $useDB Boolean
* @param $forContent Mixed: Language code, or false for user lang, true for content lang.
@@ -1507,6 +1476,8 @@ function wfMsgForContentNoTrans( $key ) {
* @return String: the requested message.
*/
function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform = true ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
wfProfileIn( __METHOD__ );
$message = wfMsgGetKey( $key, $useDB, $forContent, $transform );
$message = wfMsgReplaceArgs( $message, $args );
@@ -1521,17 +1492,19 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform
*
* @param $key String
* @param $useDB Bool
- * @param $langCode String: Code of the language to get the message for, or
+ * @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 $transform Boolean: whether to parse magic words, etc.
* @return string
*/
function wfMsgGetKey( $key, $useDB = true, $langCode = false, $transform = true ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
wfRunHooks( 'NormalizeMessageKey', array( &$key, &$useDB, &$langCode, &$transform ) );
$cache = MessageCache::singleton();
$message = $cache->get( $key, $useDB, $langCode );
- if( $message === false ) {
+ if ( $message === false ) {
$message = '&lt;' . htmlspecialchars( $key ) . '&gt;';
} elseif ( $transform ) {
$message = $cache->transform( $message );
@@ -1558,7 +1531,7 @@ function wfMsgReplaceArgs( $message, $args ) {
$args = array_values( $args[0] );
}
$replacementKeys = array();
- foreach( $args as $n => $param ) {
+ foreach ( $args as $n => $param ) {
$replacementKeys['$' . ( $n + 1 )] = $param;
}
$message = strtr( $message, $replacementKeys );
@@ -1581,6 +1554,8 @@ function wfMsgReplaceArgs( $message, $args ) {
* @return string
*/
function wfMsgHtml( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
$args = func_get_args();
array_shift( $args );
return wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $key ) ), $args );
@@ -1600,6 +1575,8 @@ function wfMsgHtml( $key ) {
* @return string
*/
function wfMsgWikiHtml( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
$args = func_get_args();
array_shift( $args );
return wfMsgReplaceArgs(
@@ -1613,8 +1590,8 @@ function wfMsgWikiHtml( $key ) {
*
* @deprecated since 1.18
*
- * @param $key String: key of the message
- * @param $options Array: processing rules. Can take the following options:
+ * @param string $key key of the message
+ * @param array $options processing rules. Can take the following options:
* <i>parse</i>: parses wikitext to HTML
* <i>parseinline</i>: parses wikitext to HTML and removes the surrounding
* p's added by parser or tidy
@@ -1625,22 +1602,24 @@ function wfMsgWikiHtml( $key ) {
* <i>content</i>: fetch message for content language instead of interface
* Also can accept a single associative argument, of the form 'language' => 'xx':
* <i>language</i>: Language object or language code to fetch message for
- * (overriden by <i>content</i>).
+ * (overridden by <i>content</i>).
* Behavior for conflicting options (e.g., parse+parseinline) is undefined.
*
* @return String
*/
function wfMsgExt( $key, $options ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
$args = func_get_args();
array_shift( $args );
array_shift( $args );
$options = (array)$options;
- foreach( $options as $arrayKey => $option ) {
- if( !preg_match( '/^[0-9]+|language$/', $arrayKey ) ) {
+ foreach ( $options as $arrayKey => $option ) {
+ if ( !preg_match( '/^[0-9]+|language$/', $arrayKey ) ) {
# An unknown index, neither numeric nor "language"
wfWarn( "wfMsgExt called with incorrect parameter key $arrayKey", 1, E_USER_WARNING );
- } elseif( preg_match( '/^[0-9]+$/', $arrayKey ) && !in_array( $option,
+ } elseif ( preg_match( '/^[0-9]+$/', $arrayKey ) && !in_array( $option,
array( 'parse', 'parseinline', 'escape', 'escapenoentities',
'replaceafter', 'parsemag', 'content' ) ) ) {
# A numeric index with unknown value
@@ -1648,11 +1627,11 @@ function wfMsgExt( $key, $options ) {
}
}
- if( in_array( 'content', $options, true ) ) {
+ if ( in_array( 'content', $options, true ) ) {
$forContent = true;
$langCode = true;
$langCodeObj = null;
- } elseif( array_key_exists( 'language', $options ) ) {
+ } elseif ( array_key_exists( 'language', $options ) ) {
$forContent = false;
$langCode = wfGetLangObj( $options['language'] );
$langCodeObj = $langCode;
@@ -1664,13 +1643,13 @@ function wfMsgExt( $key, $options ) {
$string = wfMsgGetKey( $key, /*DB*/true, $langCode, /*Transform*/false );
- if( !in_array( 'replaceafter', $options, true ) ) {
+ if ( !in_array( 'replaceafter', $options, true ) ) {
$string = wfMsgReplaceArgs( $string, $args );
}
$messageCache = MessageCache::singleton();
$parseInline = in_array( 'parseinline', $options, true );
- if( in_array( 'parse', $options, true ) || $parseInline ) {
+ if ( in_array( 'parse', $options, true ) || $parseInline ) {
$string = $messageCache->parse( $string, null, true, !$forContent, $langCodeObj );
if ( $string instanceof ParserOutput ) {
$string = $string->getText();
@@ -1678,7 +1657,7 @@ function wfMsgExt( $key, $options ) {
if ( $parseInline ) {
$m = array();
- if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
+ if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
$string = $m[1];
}
}
@@ -1693,7 +1672,7 @@ function wfMsgExt( $key, $options ) {
$string = Sanitizer::escapeHtmlAllowEntities( $string );
}
- if( in_array( 'replaceafter', $options, true ) ) {
+ if ( in_array( 'replaceafter', $options, true ) ) {
$string = wfMsgReplaceArgs( $string, $args );
}
@@ -1702,8 +1681,8 @@ function wfMsgExt( $key, $options ) {
/**
* Since wfMsg() and co suck, they don't return false if the message key they
- * looked up didn't exist but a XHTML string, this function checks for the
- * nonexistance of messages by checking the MessageCache::get() result directly.
+ * looked up didn't exist but instead the key wrapped in <>'s, this function checks for the
+ * nonexistence of messages by checking the MessageCache::get() result directly.
*
* @deprecated since 1.18. Use Message::isDisabled().
*
@@ -1711,6 +1690,8 @@ function wfMsgExt( $key, $options ) {
* @return Boolean True if the message *doesn't* exist.
*/
function wfEmptyMsg( $key ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
return MessageCache::singleton()->get( $key, /*useDB*/true, /*content*/false ) === false;
}
@@ -1718,9 +1699,12 @@ function wfEmptyMsg( $key ) {
* Throw a debugging exception. This function previously once exited the process,
* but now throws an exception instead, with similar results.
*
- * @param $msg String: message shown when dying.
+ * @deprecated since 1.22; just throw an MWException yourself
+ * @param string $msg message shown when dying.
+ * @throws MWException
*/
function wfDebugDieBacktrace( $msg = '' ) {
+ wfDeprecated( __FUNCTION__, '1.22' );
throw new MWException( $msg );
}
@@ -1737,7 +1721,7 @@ function wfHostname() {
# Hostname overriding
global $wgOverrideHostname;
- if( $wgOverrideHostname !== false ) {
+ if ( $wgOverrideHostname !== false ) {
# Set static and skip any detection
$host = $wgOverrideHostname;
return $host;
@@ -1749,7 +1733,7 @@ function wfHostname() {
} else {
$uname = false;
}
- if( is_array( $uname ) && isset( $uname['nodename'] ) ) {
+ if ( is_array( $uname ) && isset( $uname['nodename'] ) ) {
$host = $uname['nodename'];
} elseif ( getenv( 'COMPUTERNAME' ) ) {
# Windows computer name
@@ -1783,20 +1767,20 @@ function wfReportTime() {
*
* With Zend Optimizer 3.2.0 loaded, this causes segfaults under somewhat
* murky circumstances, which may be triggered in part by stub objects
- * or other fancy talkin'.
+ * or other fancy talking'.
*
* Will return an empty array if Zend Optimizer is detected or if
* debug_backtrace is disabled, otherwise the output from
* debug_backtrace() (trimmed).
*
- * @param $limit int This parameter can be used to limit the number of stack frames returned
+ * @param int $limit This parameter can be used to limit the number of stack frames returned
*
* @return array of backtrace information
*/
function wfDebugBacktrace( $limit = 0 ) {
static $disabled = null;
- if( extension_loaded( 'Zend Optimizer' ) ) {
+ if ( extension_loaded( 'Zend Optimizer' ) ) {
wfDebug( "Zend Optimizer detected; skipping debug_backtrace for safety.\n" );
return array();
}
@@ -1836,14 +1820,14 @@ function wfBacktrace() {
$msg = "<ul>\n";
}
$backtrace = wfDebugBacktrace();
- foreach( $backtrace as $call ) {
- if( isset( $call['file'] ) ) {
+ foreach ( $backtrace as $call ) {
+ if ( isset( $call['file'] ) ) {
$f = explode( DIRECTORY_SEPARATOR, $call['file'] );
$file = $f[count( $f ) - 1];
} else {
$file = '-';
}
- if( isset( $call['line'] ) ) {
+ if ( isset( $call['line'] ) ) {
$line = $call['line'];
} else {
$line = '-';
@@ -1853,7 +1837,7 @@ function wfBacktrace() {
} else {
$msg .= '<li>' . $file . ' line ' . $line . ' calls ';
}
- if( !empty( $call['class'] ) ) {
+ if ( !empty( $call['class'] ) ) {
$msg .= $call['class'] . $call['type'];
}
$msg .= $call['function'] . '()';
@@ -1895,7 +1879,7 @@ function wfGetCaller( $level = 2 ) {
* Return a string consisting of callers in the stack. Useful sometimes
* for profiling specific points.
*
- * @param $limit int The maximum depth of the stack frame to return, or false for
+ * @param int $limit The maximum depth of the stack frame to return, or false for
* the entire stack.
* @return String
*/
@@ -1920,10 +1904,8 @@ function wfFormatStackFrame( $frame ) {
$frame['function'];
}
-
/* Some generic result counters, pulled out of SearchEngine */
-
/**
* @todo document
*
@@ -1941,8 +1923,8 @@ function wfShowingResults( $offset, $limit ) {
* @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
+ * @param string $query optional URL query parameter string
+ * @param bool $atend optional param for specified if this is the last page
* @return String
* @deprecated in 1.19; use Language::viewPrevNext() instead
*/
@@ -1953,11 +1935,11 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
$query = wfCgiToArray( $query );
- if( is_object( $link ) ) {
+ if ( is_object( $link ) ) {
$title = $link;
} else {
$title = Title::newFromText( $link );
- if( is_null( $title ) ) {
+ if ( is_null( $title ) ) {
return false;
}
}
@@ -1966,23 +1948,6 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
}
/**
- * Make a list item, used by various special pages
- *
- * @param $page String Page link
- * @param $details String Text between brackets
- * @param $oppositedm Boolean Add the direction mark opposite to your
- * language, to display text properly
- * @return String
- * @deprecated since 1.19; use Language::specialList() instead
- */
-function wfSpecialList( $page, $details, $oppositedm = true ) {
- wfDeprecated( __METHOD__, '1.19' );
-
- global $wgLang;
- return $wgLang->specialList( $page, $details, $oppositedm );
-}
-
-/**
* @todo document
* @todo FIXME: We may want to blacklist some broken browsers
*
@@ -1993,16 +1958,16 @@ function wfClientAcceptsGzip( $force = false ) {
static $result = null;
if ( $result === null || $force ) {
$result = false;
- if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
+ if ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
# @todo FIXME: We may want to blacklist some broken browsers
$m = array();
- if( preg_match(
+ if ( preg_match(
'/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/',
$_SERVER['HTTP_ACCEPT_ENCODING'],
$m )
)
{
- if( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) {
+ if ( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) {
$result = false;
return $result;
}
@@ -2018,8 +1983,8 @@ function wfClientAcceptsGzip( $force = false ) {
* Obtain the offset and limit values from the request string;
* used in special pages
*
- * @param $deflimit Int default limit if none supplied
- * @param $optionname String Name of a user preference to check against
+ * @param int $deflimit default limit if none supplied
+ * @param string $optionname Name of a user preference to check against
* @return array
*
*/
@@ -2034,26 +1999,58 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
* is achieved by substituting certain characters with HTML entities.
* As required by the callers, "<nowiki>" is not used.
*
- * @param $text String: text to be escaped
+ * @param string $text text to be escaped
* @return String
*/
function wfEscapeWikiText( $text ) {
- $text = strtr( "\n$text", array(
- '"' => '&#34;', '&' => '&#38;', "'" => '&#39;', '<' => '&#60;',
- '=' => '&#61;', '>' => '&#62;', '[' => '&#91;', ']' => '&#93;',
- '{' => '&#123;', '|' => '&#124;', '}' => '&#125;',
- "\n#" => "\n&#35;", "\n*" => "\n&#42;",
- "\n:" => "\n&#58;", "\n;" => "\n&#59;",
- '://' => '&#58;//', 'ISBN ' => 'ISBN&#32;', 'RFC ' => 'RFC&#32;',
- ) );
- return substr( $text, 1 );
+ static $repl = null, $repl2 = null;
+ if ( $repl === null ) {
+ $repl = array(
+ '"' => '&#34;', '&' => '&#38;', "'" => '&#39;', '<' => '&#60;',
+ '=' => '&#61;', '>' => '&#62;', '[' => '&#91;', ']' => '&#93;',
+ '{' => '&#123;', '|' => '&#124;', '}' => '&#125;', ';' => '&#59;',
+ "\n#" => "\n&#35;", "\r#" => "\r&#35;",
+ "\n*" => "\n&#42;", "\r*" => "\r&#42;",
+ "\n:" => "\n&#58;", "\r:" => "\r&#58;",
+ "\n " => "\n&#32;", "\r " => "\r&#32;",
+ "\n\n" => "\n&#10;", "\r\n" => "&#13;\n",
+ "\n\r" => "\n&#13;", "\r\r" => "\r&#13;",
+ "\n\t" => "\n&#9;", "\r\t" => "\r&#9;", // "\n\t\n" is treated like "\n\n"
+ "\n----" => "\n&#45;---", "\r----" => "\r&#45;---",
+ '__' => '_&#95;', '://' => '&#58;//',
+ );
+
+ // We have to catch everything "\s" matches in PCRE
+ foreach ( array( 'ISBN', 'RFC', 'PMID' ) as $magic ) {
+ $repl["$magic "] = "$magic&#32;";
+ $repl["$magic\t"] = "$magic&#9;";
+ $repl["$magic\r"] = "$magic&#13;";
+ $repl["$magic\n"] = "$magic&#10;";
+ $repl["$magic\f"] = "$magic&#12;";
+ }
+
+ // And handle protocols that don't use "://"
+ global $wgUrlProtocols;
+ $repl2 = array();
+ foreach ( $wgUrlProtocols as $prot ) {
+ if ( substr( $prot, -1 ) === ':' ) {
+ $repl2[] = preg_quote( substr( $prot, 0, -1 ), '/' );
+ }
+ }
+ $repl2 = $repl2 ? '/\b(' . join( '|', $repl2 ) . '):/i' : '/^(?!)/';
+ }
+ $text = substr( strtr( "\n$text", $repl ), 1 );
+ $text = preg_replace( $repl2, '$1&#58;', $text );
+ return $text;
}
/**
- * Get the current unix timetstamp with microseconds. Useful for profiling
+ * Get the current unix timestamp with microseconds. Useful for profiling
+ * @deprecated since 1.22; call microtime() directly
* @return Float
*/
function wfTime() {
+ wfDeprecated( __FUNCTION__, '1.22' );
return microtime( true );
}
@@ -2155,14 +2152,14 @@ function wfHttpError( $code, $label, $desc ) {
* @param $resetGzipEncoding Bool
*/
function wfResetOutputBuffers( $resetGzipEncoding = true ) {
- if( $resetGzipEncoding ) {
+ if ( $resetGzipEncoding ) {
// Suppress Content-Encoding and Content-Length
// headers from 1.10+s wfOutputHandler
global $wgDisableOutputCompression;
$wgDisableOutputCompression = true;
}
- while( $status = ob_get_status() ) {
- if( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) {
+ while ( $status = ob_get_status() ) {
+ if ( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) {
// Probably from zlib.output_compression or other
// PHP-internal setting which can't be removed.
//
@@ -2170,13 +2167,13 @@ function wfResetOutputBuffers( $resetGzipEncoding = true ) {
// output behavior.
break;
}
- if( !ob_end_clean() ) {
+ if ( !ob_end_clean() ) {
// Could not remove output buffer handler; abort now
// to avoid getting in some kind of infinite loop.
break;
}
- if( $resetGzipEncoding ) {
- if( $status['name'] == 'ob_gzhandler' ) {
+ if ( $resetGzipEncoding ) {
+ if ( $status['name'] == 'ob_gzhandler' ) {
// Reset the 'Content-Encoding' field set by this handler
// so we can start fresh.
header_remove( 'Content-Encoding' );
@@ -2207,12 +2204,12 @@ function wfClearOutputBuffers() {
* factors
*
* @param $accept String
- * @param $def String default
+ * @param string $def default
* @return Array
*/
function wfAcceptToPrefs( $accept, $def = '*/*' ) {
# No arg means accept anything (per HTTP spec)
- if( !$accept ) {
+ if ( !$accept ) {
return array( $def => 1.0 );
}
@@ -2220,7 +2217,7 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) {
$parts = explode( ',', $accept );
- foreach( $parts as $part ) {
+ foreach ( $parts as $part ) {
# @todo FIXME: Doesn't deal with params like 'text/html; level=1'
$values = explode( ';', trim( $part ) );
$match = array();
@@ -2247,13 +2244,13 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) {
* @private
*/
function mimeTypeMatch( $type, $avail ) {
- if( array_key_exists( $type, $avail ) ) {
+ if ( array_key_exists( $type, $avail ) ) {
return $type;
} else {
$parts = explode( '/', $type );
- if( array_key_exists( $parts[0] . '/*', $avail ) ) {
+ if ( array_key_exists( $parts[0] . '/*', $avail ) ) {
return $parts[0] . '/*';
- } elseif( array_key_exists( '*/*', $avail ) ) {
+ } elseif ( array_key_exists( '*/*', $avail ) ) {
return '*/*';
} else {
return null;
@@ -2267,8 +2264,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 $cprefs Array: client's acceptable type list
- * @param $sprefs Array: server's offered types
+ * @param array $cprefs client's acceptable type list
+ * @param array $sprefs server's offered types
* @return string
*
* @todo FIXME: Doesn't handle params like 'text/plain; charset=UTF-8'
@@ -2277,21 +2274,21 @@ function mimeTypeMatch( $type, $avail ) {
function wfNegotiateType( $cprefs, $sprefs ) {
$combine = array();
- foreach( array_keys( $sprefs ) as $type ) {
+ foreach ( array_keys( $sprefs ) as $type ) {
$parts = explode( '/', $type );
- if( $parts[1] != '*' ) {
+ if ( $parts[1] != '*' ) {
$ckey = mimeTypeMatch( $type, $cprefs );
- if( $ckey ) {
+ if ( $ckey ) {
$combine[$type] = $sprefs[$type] * $cprefs[$ckey];
}
}
}
- foreach( array_keys( $cprefs ) as $type ) {
+ foreach ( array_keys( $cprefs ) as $type ) {
$parts = explode( '/', $type );
- if( $parts[1] != '*' && !array_key_exists( $type, $sprefs ) ) {
+ if ( $parts[1] != '*' && !array_key_exists( $type, $sprefs ) ) {
$skey = mimeTypeMatch( $type, $sprefs );
- if( $skey ) {
+ if ( $skey ) {
$combine[$type] = $sprefs[$skey] * $cprefs[$type];
}
}
@@ -2300,8 +2297,8 @@ function wfNegotiateType( $cprefs, $sprefs ) {
$bestq = 0;
$besttype = null;
- foreach( array_keys( $combine ) as $type ) {
- if( $combine[$type] > $bestq ) {
+ foreach ( array_keys( $combine ) as $type ) {
+ if ( $combine[$type] > $bestq ) {
$besttype = $type;
$bestq = $combine[$type];
}
@@ -2390,11 +2387,6 @@ define( 'TS_ORACLE', 6 );
define( 'TS_POSTGRES', 7 );
/**
- * DB2 format time
- */
-define( 'TS_DB2', 8 );
-
-/**
* ISO 8601 basic format with no timezone: 19860209T200000Z. This is used by ResourceLoader
*/
define( 'TS_ISO_8601_BASIC', 9 );
@@ -2405,15 +2397,15 @@ define( 'TS_ISO_8601_BASIC', 9 );
* @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
+ * @param $ts Mixed: optional timestamp to convert, default 0 for the current time
* @return Mixed: String / false The same date in the format specified in $outputtype or false
*/
function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
try {
$timestamp = new MWTimestamp( $ts );
return $timestamp->getTimestamp( $outputtype );
- } catch( TimestampException $e ) {
- wfDebug("wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n");
+ } catch ( TimestampException $e ) {
+ wfDebug( "wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n" );
return false;
}
}
@@ -2427,7 +2419,7 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
* @return String
*/
function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
- if( is_null( $ts ) ) {
+ if ( is_null( $ts ) ) {
return null;
} else {
return wfTimestamp( $outputtype, $ts );
@@ -2463,7 +2455,7 @@ function wfIsWindows() {
* @return Bool
*/
function wfIsHipHop() {
- return function_exists( 'hphp_thread_set_warmup_enabled' );
+ return defined( 'HPHP_VERSION' );
}
/**
@@ -2498,8 +2490,8 @@ function wfTempDir() {
$tmpDir = array_map( "getenv", array( 'TMPDIR', 'TMP', 'TEMP' ) );
- foreach( $tmpDir as $tmp ) {
- if( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
+ foreach ( $tmpDir as $tmp ) {
+ if ( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
return $tmp;
}
}
@@ -2509,9 +2501,10 @@ function wfTempDir() {
/**
* Make directory, and make all parent directories if they don't exist
*
- * @param $dir String: full path to directory to create
+ * @param string $dir full path to directory to create
* @param $mode Integer: chmod value to use, default is $wgDirectoryMode
- * @param $caller String: optional caller param for debugging.
+ * @param string $caller optional caller param for debugging.
+ * @throws MWException
* @return bool
*/
function wfMkdirParents( $dir, $mode = null, $caller = null ) {
@@ -2525,7 +2518,7 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) {
wfDebug( "$caller: called wfMkdirParents($dir)\n" );
}
- if( strval( $dir ) === '' || file_exists( $dir ) ) {
+ if ( strval( $dir ) === '' || ( file_exists( $dir ) && is_dir( $dir ) ) ) {
return true;
}
@@ -2540,10 +2533,14 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) {
$ok = mkdir( $dir, $mode, true ); // PHP5 <3
wfRestoreWarnings();
- if( !$ok ) {
+ if ( !$ok ) {
+ //directory may have been created on another request since we last checked
+ if ( is_dir( $dir ) ) {
+ return true;
+ }
+
// PHP doesn't report the path in its warning message, so add our own to aid in diagnosis.
- trigger_error( sprintf( "%s: failed to mkdir \"%s\" mode 0%o", __FUNCTION__, $dir, $mode ),
- E_USER_WARNING );
+ wfLogWarning( sprintf( "failed to mkdir \"%s\" mode 0%o", $dir, $mode ) );
}
return $ok;
}
@@ -2585,14 +2582,18 @@ function wfPercent( $nr, $acc = 2, $round = true ) {
/**
* Find out whether or not a mixed variable exists in a string
*
+ * @deprecated Just use str(i)pos
* @param $needle String
* @param $str String
* @param $insensitive Boolean
* @return Boolean
*/
function in_string( $needle, $str, $insensitive = false ) {
+ wfDeprecated( __METHOD__, '1.21' );
$func = 'strpos';
- if( $insensitive ) $func = 'stripos';
+ if ( $insensitive ) {
+ $func = 'stripos';
+ }
return $func( $str, $needle ) !== false;
}
@@ -2621,48 +2622,15 @@ function in_string( $needle, $str, $insensitive = false ) {
* @return Bool
*/
function wfIniGetBool( $setting ) {
- $val = ini_get( $setting );
+ $val = strtolower( ini_get( $setting ) );
// 'on' and 'true' can't have whitespace around them, but '1' can.
- return strtolower( $val ) == 'on'
- || strtolower( $val ) == 'true'
- || strtolower( $val ) == 'yes'
+ return $val == 'on'
+ || $val == 'true'
+ || $val == 'yes'
|| preg_match( "/^\s*[+-]?0*[1-9]/", $val ); // approx C atoi() function
}
/**
- * Wrapper function for PHP's dl(). This doesn't work in most situations from
- * PHP 5.3 onward, and is usually disabled in shared environments anyway.
- *
- * @param $extension String A PHP extension. The file suffix (.so or .dll)
- * should be omitted
- * @param $fileName String Name of the library, if not $extension.suffix
- * @return Bool - Whether or not the extension is loaded
- */
-function wfDl( $extension, $fileName = null ) {
- if( extension_loaded( $extension ) ) {
- return true;
- }
-
- $canDl = false;
- $sapi = php_sapi_name();
- if( $sapi == 'cli' || $sapi == 'cgi' || $sapi == 'embed' ) {
- $canDl = ( function_exists( 'dl' ) && is_callable( 'dl' )
- && wfIniGetBool( 'enable_dl' ) && !wfIniGetBool( 'safe_mode' ) );
- }
-
- if( $canDl ) {
- $fileName = $fileName ? $fileName : $extension;
- if( wfIsWindows() ) {
- $fileName = 'php_' . $fileName;
- }
- wfSuppressWarnings();
- dl( $fileName . '.' . PHP_SHLIB_SUFFIX );
- wfRestoreWarnings();
- }
- return extension_loaded( $extension );
-}
-
-/**
* Windows-compatible version of escapeshellarg()
* Windows doesn't recognise single-quotes in the shell, but the escapeshellarg()
* function puts single quotes in regardless of OS.
@@ -2673,7 +2641,7 @@ function wfDl( $extension, $fileName = null ) {
* @param varargs
* @return String
*/
-function wfEscapeShellArg( ) {
+function wfEscapeShellArg() {
wfInitShellLocale();
$args = func_get_args();
@@ -2727,24 +2695,15 @@ function wfEscapeShellArg( ) {
}
/**
- * Execute a shell command, with time and memory limits mirrored from the PHP
- * configuration if supported.
- * @param $cmd String Command line, properly escaped for shell.
- * @param &$retval null|Mixed optional, will receive the program's exit code.
- * (non-zero is usually failure)
- * @param $environ Array optional environment variables which should be
- * added to the executed command environment.
- * @param $limits Array optional array with limits(filesize, memory, time)
- * this overwrites the global wgShellMax* limits.
- * @return string collected stdout as a string (trailing newlines stripped)
+ * Check if wfShellExec() is effectively disabled via php.ini config
+ * @return bool|string False or one of (safemode,disabled)
+ * @since 1.22
*/
-function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array() ) {
- global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime;
-
- static $disabled;
+function wfShellExecDisabled() {
+ static $disabled = null;
if ( is_null( $disabled ) ) {
$disabled = false;
- if( wfIniGetBool( 'safe_mode' ) ) {
+ if ( wfIniGetBool( 'safe_mode' ) ) {
wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" );
$disabled = 'safemode';
} else {
@@ -2757,6 +2716,28 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array
}
}
}
+ return $disabled;
+}
+
+/**
+ * Execute a shell command, with time and memory limits mirrored from the PHP
+ * configuration if supported.
+ * @param string $cmd Command line, properly escaped for shell.
+ * @param &$retval null|Mixed optional, will receive the program's exit code.
+ * (non-zero is usually failure)
+ * @param array $environ optional environment variables which should be
+ * added to the executed command environment.
+ * @param array $limits optional array with limits(filesize, memory, time, walltime)
+ * this overwrites the global wgShellMax* limits.
+ * @param array $options Array of options. Only one is "duplicateStderr" => true, which
+ * Which duplicates stderr to stdout, including errors from limit.sh
+ * @return string collected stdout as a string
+ */
+function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array(), $options = array() ) {
+ global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime,
+ $wgMaxShellWallClockTime, $wgShellCgroup;
+
+ $disabled = wfShellExecDisabled();
if ( $disabled ) {
$retval = 1;
return $disabled == 'safemode' ?
@@ -2764,10 +2745,12 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array
'Unable to run external programs, passthru() is disabled.';
}
+ $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr'];
+
wfInitShellLocale();
$envcmd = '';
- foreach( $environ as $k => $v ) {
+ foreach ( $environ as $k => $v ) {
if ( wfIsWindows() ) {
/* Surrounding a set in quotes (method used by wfEscapeShellArg) makes the quotes themselves
* appear in the environment variable, so we must use carat escaping as documented in
@@ -2786,20 +2769,40 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array
$cmd = $envcmd . $cmd;
if ( php_uname( 's' ) == 'Linux' ) {
- $time = intval ( isset($limits['time']) ? $limits['time'] : $wgMaxShellTime );
- $mem = intval ( isset($limits['memory']) ? $limits['memory'] : $wgMaxShellMemory );
- $filesize = intval ( isset($limits['filesize']) ? $limits['filesize'] : $wgMaxShellFileSize );
-
- if ( $time > 0 && $mem > 0 ) {
- $script = "$IP/bin/ulimit4.sh";
- if ( is_executable( $script ) ) {
- $cmd = '/bin/bash ' . escapeshellarg( $script ) . " $time $mem $filesize " . escapeshellarg( $cmd );
- }
- }
+ $time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime );
+ if ( isset( $limits['walltime'] ) ) {
+ $wallTime = intval( $limits['walltime'] );
+ } elseif ( isset( $limits['time'] ) ) {
+ $wallTime = $time;
+ } else {
+ $wallTime = intval( $wgMaxShellWallClockTime );
+ }
+ $mem = intval ( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory );
+ $filesize = intval ( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize );
+
+ if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) {
+ $cmd = '/bin/bash ' . escapeshellarg( "$IP/includes/limit.sh" ) . ' ' .
+ escapeshellarg( $cmd ) . ' ' .
+ escapeshellarg(
+ "MW_INCLUDE_STDERR=" . ( $includeStderr ? '1' : '' ) . ';' .
+ "MW_CPU_LIMIT=$time; " .
+ 'MW_CGROUP=' . escapeshellarg( $wgShellCgroup ) . '; ' .
+ "MW_MEM_LIMIT=$mem; " .
+ "MW_FILE_SIZE_LIMIT=$filesize; " .
+ "MW_WALL_CLOCK_LIMIT=$wallTime"
+ );
+ } elseif ( $includeStderr ) {
+ $cmd .= ' 2>&1';
+ }
+ } elseif ( $includeStderr ) {
+ $cmd .= ' 2>&1';
}
wfDebug( "wfShellExec: $cmd\n" );
- $retval = 1; // error by default?
+ // Default to an unusual value that shouldn't happen naturally,
+ // so in the unlikely event of a weird php bug, it would be
+ // more obvious what happened.
+ $retval = 200;
ob_start();
passthru( $cmd, $retval );
$output = ob_get_contents();
@@ -2812,6 +2815,24 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array
}
/**
+ * Execute a shell command, returning both stdout and stderr. Convenience
+ * function, as all the arguments to wfShellExec can become unwieldy.
+ *
+ * @note This also includes errors from limit.sh, e.g. if $wgMaxShellFileSize is exceeded.
+ * @param string $cmd Command line, properly escaped for shell.
+ * @param &$retval null|Mixed optional, will receive the program's exit code.
+ * (non-zero is usually failure)
+ * @param array $environ optional environment variables which should be
+ * added to the executed command environment.
+ * @param array $limits optional array with limits(filesize, memory, time, walltime)
+ * this overwrites the global wgShellMax* limits.
+ * @return string collected stdout and stderr as a string
+ */
+function wfShellExecWithStderr( $cmd, &$retval = null, $environ = array(), $limits = array() ) {
+ return wfShellExec( $cmd, $retval, $environ, $limits, array( 'duplicateStderr' => true ) );
+}
+
+/**
* Workaround for http://bugs.php.net/bug.php?id=45132
* escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale
*/
@@ -2840,9 +2861,9 @@ function wfShellMaintenanceCmd( $script, array $parameters = array(), array $opt
* Generate a shell-escaped command line string to run a MediaWiki cli script.
* Note that $parameters should be a flat array and an option with an argument
* should consist of two consecutive items in the array (do not use "--option value").
- * @param $script string MediaWiki cli script path
- * @param $parameters Array Arguments and options to the script
- * @param $options Array Associative array of options:
+ * @param string $script MediaWiki cli script path
+ * @param array $parameters Arguments and options to the script
+ * @param array $options Associative array of options:
* 'php': The path to the php executable
* 'wrapper': Path to a PHP wrapper to handle the maintenance script
* @return Array
@@ -2880,7 +2901,7 @@ function wfMerge( $old, $mine, $yours, &$result ) {
$haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
wfRestoreWarnings();
- if( !$haveDiff3 ) {
+ if ( !$haveDiff3 ) {
wfDebug( "diff3 not found\n" );
return false;
}
@@ -2891,21 +2912,25 @@ function wfMerge( $old, $mine, $yours, &$result ) {
$mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' );
$yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' );
- fwrite( $oldtextFile, $old );
+ # NOTE: diff3 issues a warning to stderr if any of the files does not end with
+ # a newline character. To avoid this, we normalize the trailing whitespace before
+ # creating the diff.
+
+ fwrite( $oldtextFile, rtrim( $old ) . "\n" );
fclose( $oldtextFile );
- fwrite( $mytextFile, $mine );
+ fwrite( $mytextFile, rtrim( $mine ) . "\n" );
fclose( $mytextFile );
- fwrite( $yourtextFile, $yours );
+ fwrite( $yourtextFile, rtrim( $yours ) . "\n" );
fclose( $yourtextFile );
# Check for a conflict
- $cmd = $wgDiff3 . ' -a --overlap-only ' .
+ $cmd = wfEscapeShellArg( $wgDiff3 ) . ' -a --overlap-only ' .
wfEscapeShellArg( $mytextName ) . ' ' .
wfEscapeShellArg( $oldtextName ) . ' ' .
wfEscapeShellArg( $yourtextName );
$handle = popen( $cmd, 'r' );
- if( fgets( $handle, 1024 ) ) {
+ if ( fgets( $handle, 1024 ) ) {
$conflict = true;
} else {
$conflict = false;
@@ -2913,7 +2938,7 @@ function wfMerge( $old, $mine, $yours, &$result ) {
pclose( $handle );
# Merge differences
- $cmd = $wgDiff3 . ' -a -e --merge ' .
+ $cmd = wfEscapeShellArg( $wgDiff3 ) . ' -a -e --merge ' .
wfEscapeShellArg( $mytextName, $oldtextName, $yourtextName );
$handle = popen( $cmd, 'r' );
$result = '';
@@ -2940,9 +2965,9 @@ 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.
+ * @param string $before the text before the changes.
+ * @param string $after the text after the changes.
+ * @param string $params command-line options for the diff command.
* @return String: unified diff of $before and $after
*/
function wfDiff( $before, $after, $params = '-u' ) {
@@ -2957,7 +2982,7 @@ function wfDiff( $before, $after, $params = '-u' ) {
# This check may also protect against code injection in
# case of broken installations.
- if( !$haveDiff ) {
+ if ( !$haveDiff ) {
wfDebug( "diff executable not found\n" );
$diffs = new Diff( explode( "\n", $before ), explode( "\n", $after ) );
$format = new UnifiedDiffFormatter();
@@ -3022,6 +3047,7 @@ function wfDiff( $before, $after, $params = '-u' ) {
*
* @param $req_ver Mixed: the version to check, can be a string, an integer, or
* a float
+ * @throws MWException
*/
function wfUsePHP( $req_ver ) {
$php_ver = PHP_VERSION;
@@ -3043,6 +3069,7 @@ function wfUsePHP( $req_ver ) {
*
* @param $req_ver Mixed: the version to check, can be a string, an integer, or
* a float
+ * @throws MWException
*/
function wfUseMW( $req_ver ) {
global $wgVersion;
@@ -3061,7 +3088,7 @@ function wfUseMW( $req_ver ) {
* We'll consider it so always, as we don't want '\s' in our Unix paths either.
*
* @param $path String
- * @param $suffix String: to remove if present
+ * @param string $suffix to remove if present
* @return String
*/
function wfBaseName( $path, $suffix = '' ) {
@@ -3069,7 +3096,7 @@ function wfBaseName( $path, $suffix = '' ) {
? ''
: ( '(?:' . preg_quote( $suffix, '#' ) . ')?' );
$matches = array();
- if( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) {
+ if ( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) {
return $matches[1];
} else {
return '';
@@ -3081,8 +3108,8 @@ function wfBaseName( $path, $suffix = '' ) {
* May explode on non-matching case-insensitive paths,
* funky symlinks, etc.
*
- * @param $path String: absolute destination path including target filename
- * @param $from String: Absolute source path, directory only
+ * @param string $path absolute destination path including target filename
+ * @param string $from Absolute source path, directory only
* @return String
*/
function wfRelativePath( $path, $from ) {
@@ -3097,21 +3124,21 @@ function wfRelativePath( $path, $from ) {
$pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) );
$against = explode( DIRECTORY_SEPARATOR, $from );
- if( $pieces[0] !== $against[0] ) {
+ if ( $pieces[0] !== $against[0] ) {
// Non-matching Windows drive letters?
// Return a full path.
return $path;
}
// Trim off common prefix
- while( count( $pieces ) && count( $against )
+ while ( count( $pieces ) && count( $against )
&& $pieces[0] == $against[0] ) {
array_shift( $pieces );
array_shift( $against );
}
// relative dots to bump us to the parent
- while( count( $against ) ) {
+ while ( count( $against ) ) {
array_unshift( $pieces, '..' );
array_shift( $against );
}
@@ -3140,91 +3167,105 @@ function wfDoUpdates( $commit = '' ) {
* 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 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 );
- if( $sourceBase < 2 ||
+ * @param string $input Input number
+ * @param int $sourceBase Base of the input number
+ * @param int $destBase Desired base of the output
+ * @param int $pad Minimum number of digits in the output (pad with zeroes)
+ * @param bool $lowercase Whether to output in lowercase or uppercase
+ * @param string $engine Either "gmp", "bcmath", or "php"
+ * @return string|bool The output number as a string, or false on error
+ */
+function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = true, $engine = 'auto' ) {
+ $input = (string)$input;
+ if (
+ $sourceBase < 2 ||
$sourceBase > 36 ||
$destBase < 2 ||
$destBase > 36 ||
- $pad < 1 ||
- $sourceBase != intval( $sourceBase ) ||
- $destBase != intval( $destBase ) ||
- $pad != intval( $pad ) ||
- !is_string( $input ) ||
- $input == '' ) {
+ $sourceBase != (int)$sourceBase ||
+ $destBase != (int)$destBase ||
+ $pad != (int)$pad ||
+ !preg_match( "/^[" . substr( '0123456789abcdefghijklmnopqrstuvwxyz', 0, $sourceBase ) . "]+$/i", $input )
+ ) {
return false;
}
- $digitChars = ( $lowercase ) ? '0123456789abcdefghijklmnopqrstuvwxyz' : '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
- $inDigits = array();
- $outChars = '';
- // Decode and validate input string
- $input = strtolower( $input );
- for( $i = 0; $i < strlen( $input ); $i++ ) {
- $n = strpos( $digitChars, $input[$i] );
- if( $n === false || $n > $sourceBase ) {
- return false;
+ static $baseChars = array(
+ 10 => 'a', 11 => 'b', 12 => 'c', 13 => 'd', 14 => 'e', 15 => 'f',
+ 16 => 'g', 17 => 'h', 18 => 'i', 19 => 'j', 20 => 'k', 21 => 'l',
+ 22 => 'm', 23 => 'n', 24 => 'o', 25 => 'p', 26 => 'q', 27 => 'r',
+ 28 => 's', 29 => 't', 30 => 'u', 31 => 'v', 32 => 'w', 33 => 'x',
+ 34 => 'y', 35 => 'z',
+
+ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5,
+ '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'a' => 10, 'b' => 11,
+ 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17,
+ 'i' => 18, 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23,
+ 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, 's' => 28, 't' => 29,
+ 'u' => 30, 'v' => 31, 'w' => 32, 'x' => 33, 'y' => 34, 'z' => 35
+ );
+
+ if ( extension_loaded( 'gmp' ) && ( $engine == 'auto' || $engine == 'gmp' ) ) {
+ $result = gmp_strval( gmp_init( $input, $sourceBase ), $destBase );
+ } elseif ( extension_loaded( 'bcmath' ) && ( $engine == 'auto' || $engine == 'bcmath' ) ) {
+ $decimal = '0';
+ foreach ( str_split( strtolower( $input ) ) as $char ) {
+ $decimal = bcmul( $decimal, $sourceBase );
+ $decimal = bcadd( $decimal, $baseChars[$char] );
}
- $inDigits[] = $n;
- }
- // Iterate over the input, modulo-ing out an output digit
- // at a time until input is gone.
- while( count( $inDigits ) ) {
- $work = 0;
- $workDigits = array();
+ for ( $result = ''; bccomp( $decimal, 0 ); $decimal = bcdiv( $decimal, $destBase, 0 ) ) {
+ $result .= $baseChars[bcmod( $decimal, $destBase )];
+ }
+
+ $result = strrev( $result );
+ } else {
+ $inDigits = array();
+ foreach ( str_split( strtolower( $input ) ) as $char ) {
+ $inDigits[] = $baseChars[$char];
+ }
- // Long division...
- foreach( $inDigits as $digit ) {
- $work *= $sourceBase;
- $work += $digit;
+ // Iterate over the input, modulo-ing out an output digit
+ // at a time until input is gone.
+ $result = '';
+ while ( $inDigits ) {
+ $work = 0;
+ $workDigits = array();
- if( $work < $destBase ) {
- // Gonna need to pull another digit.
- if( count( $workDigits ) ) {
- // Avoid zero-padding; this lets us find
- // the end of the input very easily when
- // length drops to zero.
- $workDigits[] = 0;
- }
- } else {
- // Finally! Actual division!
- $workDigits[] = intval( $work / $destBase );
+ // Long division...
+ foreach ( $inDigits as $digit ) {
+ $work *= $sourceBase;
+ $work += $digit;
- // Isn't it annoying that most programming languages
- // don't have a single divide-and-remainder operator,
- // even though the CPU implements it that way?
- $work = $work % $destBase;
+ if ( $workDigits || $work >= $destBase ) {
+ $workDigits[] = (int)( $work / $destBase );
+ }
+ $work %= $destBase;
}
- }
- // All that division leaves us with a remainder,
- // which is conveniently our next output digit.
- $outChars .= $digitChars[$work];
+ // All that division leaves us with a remainder,
+ // which is conveniently our next output digit.
+ $result .= $baseChars[$work];
- // And we continue!
- $inDigits = $workDigits;
+ // And we continue!
+ $inDigits = $workDigits;
+ }
+
+ $result = strrev( $result );
}
- while( strlen( $outChars ) < $pad ) {
- $outChars .= '0';
+ if ( !$lowercase ) {
+ $result = strtoupper( $result );
}
- return strrev( $outChars );
+ return str_pad( $result, $pad, '0', STR_PAD_LEFT );
}
/**
* Create an object with a given name and an array of construct parameters
*
* @param $name String
- * @param $p Array: parameters
+ * @param array $p parameters
* @return object
* @deprecated since 1.18, warnings in 1.18, removal in 1.20
*/
@@ -3239,9 +3280,9 @@ function wfCreateObject( $name, $p ) {
function wfHttpOnlySafe() {
global $wgHttpOnlyBlacklist;
- if( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
- foreach( $wgHttpOnlyBlacklist as $regex ) {
- if( preg_match( $regex, $_SERVER['HTTP_USER_AGENT'] ) ) {
+ if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
+ foreach ( $wgHttpOnlyBlacklist as $regex ) {
+ if ( preg_match( $regex, $_SERVER['HTTP_USER_AGENT'] ) ) {
return false;
}
}
@@ -3251,7 +3292,7 @@ function wfHttpOnlySafe() {
}
/**
- * Check if there is sufficent entropy in php's built-in session generation
+ * Check if there is sufficient entropy in php's built-in session generation
* @return bool true = there is sufficient entropy
*/
function wfCheckEntropy() {
@@ -3268,7 +3309,7 @@ function wfCheckEntropy() {
*/
function wfFixSessionID() {
// If the cookie or session id is already set we already have a session and should abort
- if ( isset( $_COOKIE[ session_name() ] ) || session_id() ) {
+ if ( isset( $_COOKIE[session_name()] ) || session_id() ) {
return;
}
@@ -3286,6 +3327,27 @@ function wfFixSessionID() {
}
/**
+ * Reset the session_id
+ * @since 1.22
+ */
+function wfResetSessionID() {
+ global $wgCookieSecure;
+ $oldSessionId = session_id();
+ $cookieParams = session_get_cookie_params();
+ if ( wfCheckEntropy() && $wgCookieSecure == $cookieParams['secure'] ) {
+ session_regenerate_id( false );
+ } else {
+ $tmp = $_SESSION;
+ session_destroy();
+ wfSetupSession( MWCryptRand::generateHex( 32 ) );
+ $_SESSION = $tmp;
+ }
+ $newSessionId = session_id();
+ wfRunHooks( 'ResetSessionID', array( $oldSessionId, $newSessionId ) );
+}
+
+
+/**
* Initialise php session
*
* @param $sessionId Bool
@@ -3293,9 +3355,9 @@ function wfFixSessionID() {
function wfSetupSession( $sessionId = false ) {
global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain,
$wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler;
- if( $wgSessionsInObjectCache || $wgSessionsInMemcached ) {
+ if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) {
ObjectCacheSessionHandler::install();
- } elseif( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) {
+ } 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 );
@@ -3370,7 +3432,7 @@ function wfForeignMemcKey( $db, $prefix /*, ... */ ) {
} else {
$key = $db . ':' . implode( ':', $args );
}
- return $key;
+ return str_replace( ' ', '_', $key );
}
/**
@@ -3414,7 +3476,7 @@ function wfSplitWikiID( $wiki ) {
* belongs to. May contain a single string if the query is only
* in one group.
*
- * @param $wiki String: the wiki ID, or false for the current wiki
+ * @param string $wiki the wiki ID, or false for the current wiki
*
* Note: multiple calls to wfGetDB(DB_SLAVE) during the course of one request
* will always return the same object, unless the underlying connection or load
@@ -3432,7 +3494,7 @@ function &wfGetDB( $db, $groups = array(), $wiki = false ) {
/**
* Get a load balancer object.
*
- * @param $wiki String: wiki ID, or false for the current wiki
+ * @param string $wiki wiki ID, or false for the current wiki
* @return LoadBalancer
*/
function wfGetLB( $wiki = false ) {
@@ -3452,8 +3514,8 @@ function &wfGetLBFactory() {
* Find a file.
* Shortcut for RepoGroup::singleton()->findFile()
*
- * @param $title String or Title object
- * @param $options array Associative array of options:
+ * @param string $title or Title object
+ * @param array $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.
@@ -3511,14 +3573,14 @@ function wfQueriesMustScale() {
* extensions; this is a wrapper around $wgScriptExtension etc.
* except for 'index' and 'load' which use $wgScript/$wgLoadScript
*
- * @param $script String: script filename, sans extension
+ * @param string $script script filename, sans extension
* @return String
*/
function wfScript( $script = 'index' ) {
global $wgScriptPath, $wgScriptExtension, $wgScript, $wgLoadScript;
if ( $script === 'index' ) {
return $wgScript;
- } else if ( $script === 'load' ) {
+ } elseif ( $script === 'load' ) {
return $wgLoadScript;
} else {
return "{$wgScriptPath}/{$script}{$wgScriptExtension}";
@@ -3531,7 +3593,7 @@ function wfScript( $script = 'index' ) {
* @return string script URL
*/
function wfGetScriptUrl() {
- if( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ if ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
#
# as it was called, minus the query string.
#
@@ -3560,16 +3622,6 @@ function wfBoolToStr( $value ) {
}
/**
- * Load an extension messages file
- *
- * @deprecated since 1.16, warnings in 1.18, remove in 1.20
- * @codeCoverageIgnore
- */
-function wfLoadExtensionMessages() {
- wfDeprecated( __FUNCTION__, '1.16' );
-}
-
-/**
* Get a platform-independent path to the null file, e.g. /dev/null
*
* @return string
@@ -3589,15 +3641,22 @@ function wfGetNull() {
*
* @param $maxLag Integer (deprecated)
* @param $wiki mixed Wiki identifier accepted by wfGetLB
+ * @param $cluster string cluster name accepted by LBFactory
*/
-function wfWaitForSlaves( $maxLag = false, $wiki = false ) {
- $lb = wfGetLB( $wiki );
+function wfWaitForSlaves( $maxLag = false, $wiki = false, $cluster = false ) {
+ $lb = ( $cluster !== false )
+ ? wfGetLBFactory()->getExternalLB( $cluster )
+ : wfGetLB( $wiki );
// bug 27975 - Don't try to wait for slaves if there are none
// Prevents permission error when getting master position
if ( $lb->getServerCount() > 1 ) {
- $dbw = $lb->getConnection( DB_MASTER );
+ $dbw = $lb->getConnection( DB_MASTER, array(), $wiki );
$pos = $dbw->getMasterPos();
- $lb->waitForAll( $pos );
+ // The DBMS may not support getMasterPos() or the whole
+ // load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
+ if ( $pos !== false ) {
+ $lb->waitForAll( $pos );
+ }
}
}
@@ -3642,8 +3701,8 @@ function wfCountDown( $n ) {
* characters before hashing.
* @return string
* @codeCoverageIgnore
- * @deprecated since 1.20; Please use MWCryptRand for security purposes and wfRandomString for pesudo-random strings
- * @warning This method is NOT secure. Additionally it has many callers that use it for pesudo-random purposes.
+ * @deprecated since 1.20; Please use MWCryptRand for security purposes and wfRandomString for pseudo-random strings
+ * @warning This method is NOT secure. Additionally it has many callers that use it for pseudo-random purposes.
*/
function wfGenerateToken( $salt = '' ) {
wfDeprecated( __METHOD__, '1.20' );
@@ -3679,9 +3738,9 @@ function wfStripIllegalFilenameChars( $name ) {
function wfMemoryLimit() {
global $wgMemoryLimit;
$memlimit = wfShorthandToInteger( ini_get( 'memory_limit' ) );
- if( $memlimit != -1 ) {
+ if ( $memlimit != -1 ) {
$conflimit = wfShorthandToInteger( $wgMemoryLimit );
- if( $conflimit == -1 ) {
+ if ( $conflimit == -1 ) {
wfDebug( "Removing PHP's memory limit\n" );
wfSuppressWarnings();
ini_set( 'memory_limit', $conflimit );
@@ -3706,12 +3765,12 @@ function wfMemoryLimit() {
*/
function wfShorthandToInteger( $string = '' ) {
$string = trim( $string );
- if( $string === '' ) {
+ if ( $string === '' ) {
return -1;
}
$last = $string[strlen( $string ) - 1];
$val = intval( $string );
- switch( $last ) {
+ switch ( $last ) {
case 'g':
case 'G':
$val *= 1024;
@@ -3732,29 +3791,24 @@ function wfShorthandToInteger( $string = '' ) {
* Get the normalised IETF language tag
* See unit test for examples.
*
- * @param $code String: The language code.
+ * @param string $code The language code.
* @return String: The language code which complying with BCP 47 standards.
*/
function wfBCP47( $code ) {
$codeSegment = explode( '-', $code );
$codeBCP = array();
foreach ( $codeSegment as $segNo => $seg ) {
- if ( count( $codeSegment ) > 0 ) {
- // when previous segment is x, it is a private segment and should be lc
- if( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) {
- $codeBCP[$segNo] = strtolower( $seg );
- // ISO 3166 country code
- } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) {
- $codeBCP[$segNo] = strtoupper( $seg );
- // ISO 15924 script code
- } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) {
- $codeBCP[$segNo] = ucfirst( strtolower( $seg ) );
- // Use lowercase for other cases
- } else {
- $codeBCP[$segNo] = strtolower( $seg );
- }
+ // when previous segment is x, it is a private segment and should be lc
+ if ( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) {
+ $codeBCP[$segNo] = strtolower( $seg );
+ // ISO 3166 country code
+ } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) {
+ $codeBCP[$segNo] = strtoupper( $seg );
+ // ISO 15924 script code
+ } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) {
+ $codeBCP[$segNo] = ucfirst( strtolower( $seg ) );
+ // Use lowercase for other cases
} else {
- // Use lowercase for single segment
$codeBCP[$segNo] = strtolower( $seg );
}
}
@@ -3815,20 +3869,20 @@ function wfGetLangConverterCacheStorage() {
/**
* Call hook functions defined in $wgHooks
*
- * @param $event String: event name
- * @param $args Array: parameters passed to hook functions
+ * @param string $event event name
+ * @param array $args parameters passed to hook functions
* @return Boolean True if no handler aborted the hook
*/
-function wfRunHooks( $event, $args = array() ) {
+function wfRunHooks( $event, array $args = array() ) {
return Hooks::run( $event, $args );
}
/**
* Wrapper around php's unpack.
*
- * @param $format String: The format string (See php's docs)
+ * @param string $format The format string (See php's docs)
* @param $data: A binary string of binary data
- * @param $length integer or false: The minimun length of $data. This is to
+ * @param $length integer or false: The minimum length of $data. This is to
* prevent reading beyond the end of $data. false to disable the check.
*
* Also be careful when using this function to read unsigned 32 bit integer
@@ -3837,7 +3891,7 @@ function wfRunHooks( $event, $args = array() ) {
* @throws MWException if $data not long enough, or if unpack fails
* @return array Associative array of the extracted data
*/
-function wfUnpack( $format, $data, $length=false ) {
+function wfUnpack( $format, $data, $length = false ) {
if ( $length !== false ) {
$realLen = strlen( $data );
if ( $realLen < $length ) {
@@ -3868,9 +3922,9 @@ function wfUnpack( $format, $data, $length=false ) {
* * Any subsequent links on the same line are considered to be exceptions,
* i.e. articles where the image may occur inline.
*
- * @param $name string the image name to check
+ * @param string $name the image name to check
* @param $contextTitle Title|bool the page on which the image occurs, if known
- * @param $blacklist string wikitext of a file blacklist
+ * @param string $blacklist wikitext of a file blacklist
* @return bool
*/
function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
@@ -3879,19 +3933,19 @@ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
# Handle redirects
$redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) );
- if( $redirectTitle ) {
- $name = $redirectTitle->getDbKey();
+ if ( $redirectTitle ) {
+ $name = $redirectTitle->getDBkey();
}
# Run the extension hook
$bad = false;
- if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
+ if ( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
wfProfileOut( __METHOD__ );
return $bad;
}
$cacheable = ( $blacklist === null );
- if( $cacheable && $badImageCache !== null ) {
+ if ( $cacheable && $badImageCache !== null ) {
$badImages = $badImageCache;
} else { // cache miss
if ( $blacklist === null ) {
@@ -3900,7 +3954,7 @@ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
# Build the list now
$badImages = array();
$lines = explode( "\n", $blacklist );
- foreach( $lines as $line ) {
+ foreach ( $lines as $line ) {
# List items only
if ( substr( $line, 0, 1 ) !== '*' ) {
continue;
@@ -3939,3 +3993,16 @@ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
wfProfileOut( __METHOD__ );
return $bad;
}
+
+/**
+ * Determine whether the client at a given source IP is likely to be able to
+ * access the wiki via HTTPS.
+ *
+ * @param string $ip The IPv4/6 address in the normal human-readable form
+ * @return boolean
+ */
+function wfCanIPUseHTTPS( $ip ) {
+ $canDo = true;
+ wfRunHooks( 'CanIPUseHTTPS', array( $ip, &$canDo ) );
+ return !!$canDo;
+}
diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php
index 6f89d5b8..d260862c 100644
--- a/includes/HTMLForm.php
+++ b/includes/HTMLForm.php
@@ -83,9 +83,8 @@
* $form = new HTMLForm( $someFields );
* $form->setMethod( 'get' )
* ->setWrapperLegendMsg( 'message-key' )
- * ->suppressReset()
* ->prepareForm()
- * ->displayForm();
+ * ->displayForm( '' );
* @endcode
* Note that you will have prepareForm and displayForm at the end. Other
* methods call done after that would simply not be part of the form :(
@@ -95,7 +94,7 @@
class HTMLForm extends ContextSource {
// A mapping of 'type' inputs onto standard HTMLFormField subclasses
- static $typeMappings = array(
+ public static $typeMappings = array(
'api' => 'HTMLApiField',
'text' => 'HTMLTextField',
'textarea' => 'HTMLTextAreaField',
@@ -112,6 +111,7 @@ class HTMLForm extends ContextSource {
'submit' => 'HTMLSubmitField',
'hidden' => 'HTMLHiddenField',
'edittools' => 'HTMLEditTools',
+ 'checkmatrix' => 'HTMLCheckMatrix',
// HTMLTextField will output the correct type="" attribute automagically.
// There are about four zillion other HTML5 input types, like url, but
@@ -127,6 +127,7 @@ class HTMLForm extends ContextSource {
protected $mFieldTree;
protected $mShowReset = false;
+ protected $mShowSubmit = true;
public $mFieldData;
protected $mSubmitCallback;
@@ -139,6 +140,7 @@ class HTMLForm extends ContextSource {
protected $mSectionFooters = array();
protected $mPost = '';
protected $mId;
+ protected $mTableId = '';
protected $mSubmitID;
protected $mSubmitName;
@@ -185,26 +187,27 @@ class HTMLForm extends ContextSource {
'table',
'div',
'raw',
+ 'vform',
);
/**
* Build a new HTMLForm from an array of field attributes
- * @param $descriptor Array of Field constructs, as described above
+ * @param array $descriptor of Field constructs, as described above
* @param $context IContextSource available since 1.18, will become compulsory in 1.18.
* Obviates the need to call $form->setTitle()
- * @param $messagePrefix String a prefix to go in front of default messages
+ * @param string $messagePrefix a prefix to go in front of default messages
*/
public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) {
if ( $context instanceof IContextSource ) {
$this->setContext( $context );
$this->mTitle = false; // We don't need them to set a title
$this->mMessagePrefix = $messagePrefix;
- } else {
+ } elseif ( is_null( $context ) && $messagePrefix !== '' ) {
+ $this->mMessagePrefix = $messagePrefix;
+ } elseif ( is_string( $context ) && $messagePrefix === '' ) {
// B/C since 1.18
- if ( is_string( $context ) && $messagePrefix === '' ) {
- // it's actually $messagePrefix
- $this->mMessagePrefix = $context;
- }
+ // it's actually $messagePrefix
+ $this->mMessagePrefix = $context;
}
// Expand out into a tree.
@@ -221,8 +224,15 @@ class HTMLForm extends ContextSource {
}
$field = self::loadInputFromParameters( $fieldname, $info );
+ // FIXME During field's construct, the parent form isn't available!
+ // could add a 'parent' name-value to $info, could add a third parameter.
$field->mParent = $this;
+ // vform gets too much space if empty labels generate HTML.
+ if ( $this->isVForm() ) {
+ $field->setShowEmptyLabel( false );
+ }
+
$setSection =& $loadedDescriptor;
if ( $section ) {
$sectionParts = explode( '/', $section );
@@ -247,14 +257,15 @@ class HTMLForm extends ContextSource {
/**
* Set format in which to display the form
- * @param $format String the name of the format to use, must be one of
+ * @param string $format the name of the format to use, must be one of
* $this->availableDisplayFormats
+ * @throws MWException
* @since 1.20
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setDisplayFormat( $format ) {
if ( !in_array( $format, $this->availableDisplayFormats ) ) {
- throw new MWException ( 'Display format must be one of ' . print_r( $this->availableDisplayFormats, true ) );
+ throw new MWException( 'Display format must be one of ' . print_r( $this->availableDisplayFormats, true ) );
}
$this->displayFormat = $format;
return $this;
@@ -270,16 +281,28 @@ class HTMLForm extends ContextSource {
}
/**
+ * Test if displayFormat is 'vform'
+ * @since 1.22
+ * @return Bool
+ */
+ public function isVForm() {
+ return $this->displayFormat === 'vform';
+ }
+
+ /**
* Add the HTMLForm-specific JavaScript, if it hasn't been
* done already.
* @deprecated since 1.18 load modules with ResourceLoader instead
*/
- static function addJS() { wfDeprecated( __METHOD__, '1.18' ); }
+ static function addJS() {
+ wfDeprecated( __METHOD__, '1.18' );
+ }
/**
* Initialise a new Object for the field
* @param $fieldname string
- * @param $descriptor string input Descriptor, as described above
+ * @param string $descriptor input Descriptor, as described above
+ * @throws MWException
* @return HTMLFormField subclass
*/
static function loadInputFromParameters( $fieldname, $descriptor ) {
@@ -313,6 +336,7 @@ class HTMLForm extends ContextSource {
* @attention When doing method chaining, that should be the very last
* method call before displayForm().
*
+ * @throws MWException
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function prepareForm() {
@@ -374,11 +398,12 @@ class HTMLForm extends ContextSource {
}
/**
- * Validate all the fields, and call the submision callback
+ * Validate all the fields, and call the submission callback
* function if everything is kosher.
+ * @throws MWException
* @return Mixed Bool true == Successful submission, Bool false
- * == No submission attempted, anything else == Error to
- * display.
+ * == No submission attempted, anything else == Error to
+ * display.
*/
function trySubmit() {
# Check for validation
@@ -412,7 +437,7 @@ class HTMLForm extends ContextSource {
/**
* Set a callback to a function to do something with the form
* once it's been successfully validated.
- * @param $cb String function name. The function will be passed
+ * @param string $cb function name. The function will be passed
* the output from HTMLForm::filterDataForSubmit, and must
* return Bool true on success, Bool false if no submission
* was attempted, or String HTML output to display on error.
@@ -436,7 +461,7 @@ class HTMLForm extends ContextSource {
/**
* Set the introductory message, overwriting any existing message.
- * @param $msg String complete text of message to display
+ * @param string $msg complete text of message to display
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function setIntro( $msg ) {
@@ -447,7 +472,7 @@ class HTMLForm extends ContextSource {
/**
* Set the introductory message, overwriting any existing message.
* @since 1.19
- * @param $msg String complete text of message to display
+ * @param string $msg complete text of message to display
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function setPreText( $msg ) {
@@ -457,7 +482,7 @@ class HTMLForm extends ContextSource {
/**
* Add introductory text.
- * @param $msg String complete text of message to display
+ * @param string $msg complete text of message to display
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function addPreText( $msg ) {
@@ -467,8 +492,8 @@ class HTMLForm extends ContextSource {
/**
* Add header text, inside the form.
- * @param $msg String complete text of message to display
- * @param $section string The section to add the header to
+ * @param string $msg complete text of message to display
+ * @param string $section The section to add the header to
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function addHeaderText( $msg, $section = null ) {
@@ -486,7 +511,7 @@ class HTMLForm extends ContextSource {
/**
* Set header text, inside the form.
* @since 1.19
- * @param $msg String complete text of message to display
+ * @param string $msg complete text of message to display
* @param $section The section to add the header to
* @return HTMLForm $this for chaining calls (since 1.20)
*/
@@ -501,8 +526,8 @@ class HTMLForm extends ContextSource {
/**
* Add footer text, inside the form.
- * @param $msg String complete text of message to display
- * @param $section string The section to add the footer text to
+ * @param string $msg complete text of message to display
+ * @param string $section The section to add the footer text to
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function addFooterText( $msg, $section = null ) {
@@ -520,8 +545,8 @@ class HTMLForm extends ContextSource {
/**
* Set footer text, inside the form.
* @since 1.19
- * @param $msg String complete text of message to display
- * @param $section string The section to add the footer text to
+ * @param string $msg complete text of message to display
+ * @param string $section The section to add the footer text to
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function setFooterText( $msg, $section = null ) {
@@ -535,7 +560,7 @@ class HTMLForm extends ContextSource {
/**
* Add text to the end of the display.
- * @param $msg String complete text of message to display
+ * @param string $msg complete text of message to display
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function addPostText( $msg ) {
@@ -545,7 +570,7 @@ class HTMLForm extends ContextSource {
/**
* Set text at the end of the display.
- * @param $msg String complete text of message to display
+ * @param string $msg complete text of message to display
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function setPostText( $msg ) {
@@ -555,8 +580,8 @@ class HTMLForm extends ContextSource {
/**
* Add a hidden field to the output
- * @param $name String field name. This will be used exactly as entered
- * @param $value String field value
+ * @param string $name field name. This will be used exactly as entered
+ * @param string $value field value
* @param $attribs Array
* @return HTMLForm $this for chaining calls (since 1.20)
*/
@@ -567,10 +592,25 @@ class HTMLForm extends ContextSource {
}
/**
+ * Add an array of hidden fields to the output
+ *
+ * @since 1.22
+ * @param array $fields Associative array of fields to add;
+ * mapping names to their values
+ * @return HTMLForm $this for chaining calls
+ */
+ public function addHiddenFields( array $fields ) {
+ foreach ( $fields as $name => $value ) {
+ $this->mHiddenFields[] = array( $value, array( 'name' => $name ) );
+ }
+ return $this;
+ }
+
+ /**
* Add a button to the form
- * @param $name String field name.
- * @param $value String field value
- * @param $id String DOM id for the button (default: null)
+ * @param string $name field name.
+ * @param string $value field value
+ * @param string $id DOM id for the button (default: null)
* @param $attribs Array
* @return HTMLForm $this for chaining calls (since 1.20)
*/
@@ -580,8 +620,8 @@ class HTMLForm extends ContextSource {
}
/**
- * Display the form (sending to $wgOut), with an appropriate error
- * message or stack of messages, and any validation errors, etc.
+ * Display the form (sending to the context's OutputPage object), with an
+ * appropriate error message or stack of messages, and any validation errors, etc.
*
* @attention You should call prepareForm() before calling this function.
* Moreover, when doing method chaining this should be the very last method
@@ -603,6 +643,11 @@ class HTMLForm extends ContextSource {
# For good measure (it is the default)
$this->getOutput()->preventClickjacking();
$this->getOutput()->addModules( 'mediawiki.htmlform' );
+ if ( $this->isVForm() ) {
+ $this->getOutput()->addModuleStyles( 'mediawiki.ui' );
+ // TODO should vertical form set setWrapperLegend( false )
+ // to hide ugly fieldsets?
+ }
$html = ''
. $this->getErrors( $submitResult )
@@ -620,7 +665,7 @@ class HTMLForm extends ContextSource {
/**
* Wrap the form innards in an actual "<form>" element
- * @param $html String HTML contents to wrap.
+ * @param string $html HTML contents to wrap.
* @return String wrapped HTML.
*/
function wrapForm( $html ) {
@@ -635,15 +680,18 @@ class HTMLForm extends ContextSource {
: 'application/x-www-form-urlencoded';
# Attributes
$attribs = array(
- 'action' => $this->mAction === false ? $this->getTitle()->getFullURL() : $this->mAction,
- 'method' => $this->mMethod,
- 'class' => 'visualClear',
+ 'action' => $this->getAction(),
+ 'method' => $this->getMethod(),
+ 'class' => array( 'visualClear' ),
'enctype' => $encType,
);
if ( !empty( $this->mId ) ) {
$attribs['id'] = $this->mId;
}
+ if ( $this->isVForm() ) {
+ array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' );
+ }
return Html::rawElement( 'form', $attribs, $html );
}
@@ -677,24 +725,40 @@ class HTMLForm extends ContextSource {
* @return String HTML.
*/
function getButtons() {
- $html = '';
- $attribs = array();
+ $html = '<span class="mw-htmlform-submit-buttons">';
- if ( isset( $this->mSubmitID ) ) {
- $attribs['id'] = $this->mSubmitID;
- }
+ if ( $this->mShowSubmit ) {
+ $attribs = array();
- if ( isset( $this->mSubmitName ) ) {
- $attribs['name'] = $this->mSubmitName;
- }
+ if ( isset( $this->mSubmitID ) ) {
+ $attribs['id'] = $this->mSubmitID;
+ }
- if ( isset( $this->mSubmitTooltip ) ) {
- $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
- }
+ if ( isset( $this->mSubmitName ) ) {
+ $attribs['name'] = $this->mSubmitName;
+ }
+
+ if ( isset( $this->mSubmitTooltip ) ) {
+ $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
+ }
+
+ $attribs['class'] = array( 'mw-htmlform-submit' );
- $attribs['class'] = 'mw-htmlform-submit';
+ if ( $this->isVForm() ) {
+ // mw-ui-block is necessary because the buttons aren't necessarily in an
+ // immediate child div of the vform.
+ array_push( $attribs['class'], 'mw-ui-button', 'mw-ui-big', 'mw-ui-primary', 'mw-ui-block' );
+ }
+
+ $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
- $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
+ // Buttons are top-level form elements in table and div layouts,
+ // but vform wants all elements inside divs to get spaced-out block
+ // styling.
+ if ( $this->isVForm() ) {
+ $html = Html::rawElement( 'div', null, "\n$html\n" );
+ }
+ }
if ( $this->mShowReset ) {
$html .= Html::element(
@@ -708,8 +772,8 @@ class HTMLForm extends ContextSource {
foreach ( $this->mButtons as $button ) {
$attrs = array(
- 'type' => 'submit',
- 'name' => $button['name'],
+ 'type' => 'submit',
+ 'name' => $button['name'],
'value' => $button['value']
);
@@ -724,6 +788,8 @@ class HTMLForm extends ContextSource {
$html .= Html::element( 'input', $attrs );
}
+ $html .= '</span>';
+
return $html;
}
@@ -732,7 +798,7 @@ class HTMLForm extends ContextSource {
* @return String
*/
function getBody() {
- return $this->displaySection( $this->mFieldTree );
+ return $this->displaySection( $this->mFieldTree, $this->mTableId );
}
/**
@@ -760,7 +826,7 @@ class HTMLForm extends ContextSource {
/**
* Format a stack of error messages into a single HTML string
- * @param $errors Array of message keys/values
+ * @param array $errors of message keys/values
* @return String HTML, a "<ul>" list of errors
*/
public static function formatErrors( $errors ) {
@@ -788,7 +854,7 @@ class HTMLForm extends ContextSource {
/**
* Set the text for the submit button
- * @param $t String plaintext.
+ * @param string $t plaintext.
* @return HTMLForm $this for chaining calls (since 1.20)
*/
function setSubmitText( $t ) {
@@ -799,7 +865,7 @@ class HTMLForm extends ContextSource {
/**
* Set the text for the submit button to a message
* @since 1.19
- * @param $msg String message key
+ * @param string $msg message key
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitTextMsg( $msg ) {
@@ -818,7 +884,7 @@ class HTMLForm extends ContextSource {
}
/**
- * @param $name String Submit button name
+ * @param string $name Submit button name
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitName( $name ) {
@@ -827,7 +893,7 @@ class HTMLForm extends ContextSource {
}
/**
- * @param $name String Tooltip for the submit button
+ * @param string $name Tooltip for the submit button
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitTooltip( $name ) {
@@ -847,17 +913,46 @@ class HTMLForm extends ContextSource {
}
/**
- * @param $id String DOM id for the form
+ * Stop a default submit button being shown for this form. This implies that an
+ * alternate submit method must be provided manually.
+ *
+ * @since 1.22
+ *
+ * @param bool $suppressSubmit Set to false to re-enable the button again
+ *
+ * @return HTMLForm $this for chaining calls
+ */
+ function suppressDefaultSubmit( $suppressSubmit = true ) {
+ $this->mShowSubmit = !$suppressSubmit;
+ return $this;
+ }
+
+ /**
+ * Set the id of the \<table\> or outermost \<div\> element.
+ *
+ * @since 1.22
+ * @param string $id new value of the id attribute, or "" to remove
+ * @return HTMLForm $this for chaining calls
+ */
+ public function setTableId( $id ) {
+ $this->mTableId = $id;
+ return $this;
+ }
+
+ /**
+ * @param string $id DOM id for the form
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setId( $id ) {
$this->mId = $id;
return $this;
}
+
/**
* Prompt the whole form to be wrapped in a "<fieldset>", with
* this text as its "<legend>" element.
- * @param $legend String HTML to go inside the "<legend>" element.
+ * @param string|false $legend HTML to go inside the "<legend>" element, or
+ * false for no <legend>
* Will be escaped
* @return HTMLForm $this for chaining calls (since 1.20)
*/
@@ -870,7 +965,7 @@ class HTMLForm extends ContextSource {
* Prompt the whole form to be wrapped in a "<fieldset>", with
* this message as its "<legend>" element.
* @since 1.19
- * @param $msg String message key
+ * @param string $msg message key
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setWrapperLegendMsg( $msg ) {
@@ -881,7 +976,7 @@ class HTMLForm extends ContextSource {
/**
* Set the prefix for various default messages
* @todo currently only used for the "<fieldset>" legend on forms
- * with multiple sections; should be used elsewhre?
+ * with multiple sections; should be used elsewhere?
* @param $p String
* @return HTMLForm $this for chaining calls (since 1.20)
*/
@@ -926,19 +1021,30 @@ class HTMLForm extends ContextSource {
/**
* @todo Document
- * @param $fields array[]|HTMLFormField[] array of fields (either arrays or objects)
- * @param $sectionName string ID attribute of the "<table>" tag for this section, ignored if empty
- * @param $fieldsetIDPrefix string ID prefix for the "<fieldset>" tag of each subsection, ignored if empty
+ * @param array[]|HTMLFormField[] $fields array of fields (either arrays or objects)
+ * @param string $sectionName ID attribute of the "<table>" tag for this section, ignored if empty
+ * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of each subsection, ignored if empty
+ * @param boolean &$hasUserVisibleFields Whether the section had user-visible fields
* @return String
*/
- public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) {
+ public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '', &$hasUserVisibleFields = false ) {
$displayFormat = $this->getDisplayFormat();
$html = '';
$subsectionHtml = '';
$hasLabel = false;
- $getFieldHtmlMethod = ( $displayFormat == 'table' ) ? 'getTableRow' : 'get' . ucfirst( $displayFormat );
+ switch( $displayFormat ) {
+ case 'table':
+ $getFieldHtmlMethod = 'getTableRow';
+ break;
+ case 'vform':
+ // Close enough to a div.
+ $getFieldHtmlMethod = 'getDiv';
+ break;
+ default:
+ $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
+ }
foreach ( $fields as $key => $value ) {
if ( $value instanceof HTMLFormField ) {
@@ -951,20 +1057,38 @@ class HTMLForm extends ContextSource {
if ( $labelValue != '&#160;' && $labelValue !== '' ) {
$hasLabel = true;
}
- } elseif ( is_array( $value ) ) {
- $section = $this->displaySection( $value, $key );
- $legend = $this->getLegend( $key );
- if ( isset( $this->mSectionHeaders[$key] ) ) {
- $section = $this->mSectionHeaders[$key] . $section;
- }
- if ( isset( $this->mSectionFooters[$key] ) ) {
- $section .= $this->mSectionFooters[$key];
+
+ if ( get_class( $value ) !== 'HTMLHiddenField' &&
+ get_class( $value ) !== 'HTMLApiField' ) {
+ $hasUserVisibleFields = true;
}
- $attributes = array();
- if ( $fieldsetIDPrefix ) {
- $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
+ } elseif ( is_array( $value ) ) {
+ $subsectionHasVisibleFields = false;
+ $section = $this->displaySection( $value, "mw-htmlform-$key", "$fieldsetIDPrefix$key-", $subsectionHasVisibleFields );
+ $legend = null;
+
+ if ( $subsectionHasVisibleFields === true ) {
+ // Display the section with various niceties.
+ $hasUserVisibleFields = true;
+
+ $legend = $this->getLegend( $key );
+
+ if ( isset( $this->mSectionHeaders[$key] ) ) {
+ $section = $this->mSectionHeaders[$key] . $section;
+ }
+ if ( isset( $this->mSectionFooters[$key] ) ) {
+ $section .= $this->mSectionFooters[$key];
+ }
+
+ $attributes = array();
+ if ( $fieldsetIDPrefix ) {
+ $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
+ }
+ $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n";
+ } else {
+ // Just return the inputs, nothing fancy.
+ $subsectionHtml .= $section;
}
- $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n";
}
}
@@ -980,13 +1104,13 @@ class HTMLForm extends ContextSource {
);
if ( $sectionName ) {
- $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" );
+ $attribs['id'] = Sanitizer::escapeId( $sectionName );
}
if ( $displayFormat === 'table' ) {
$html = Html::rawElement( 'table', $attribs,
Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
- } elseif ( $displayFormat === 'div' ) {
+ } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
$html = Html::rawElement( 'div', $attribs, "\n$html\n" );
}
}
@@ -1025,7 +1149,7 @@ class HTMLForm extends ContextSource {
/**
* Stop a reset button being shown for this form
- * @param $suppressReset Bool set to false to re-enable the
+ * @param bool $suppressReset set to false to re-enable the
* button again
* @return HTMLForm $this for chaining calls (since 1.20)
*/
@@ -1069,6 +1193,32 @@ class HTMLForm extends ContextSource {
return $this;
}
+ /**
+ * Get the value for the action attribute of the form.
+ *
+ * @since 1.22
+ *
+ * @return string
+ */
+ public function getAction() {
+ global $wgScript, $wgArticlePath;
+
+ // If an action is alredy provided, return it
+ if ( $this->mAction !== false ) {
+ return $this->mAction;
+ }
+
+ // Check whether we are in GET mode and $wgArticlePath contains a "?"
+ // meaning that getLocalURL() would return something like "index.php?title=...".
+ // As browser remove the query string before submitting GET forms,
+ // it means that the title would be lost. In such case use $wgScript instead
+ // and put title in an hidden field (see getHiddenFields()).
+ if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
+ return $wgScript;
+ }
+
+ return $this->getTitle()->getLocalURL();
+ }
}
/**
@@ -1087,6 +1237,12 @@ abstract class HTMLFormField {
protected $mDefault;
/**
+ * @var bool If true will generate an empty div element with no label
+ * @since 1.22
+ */
+ protected $mShowEmptyLabels = true;
+
+ /**
* @var HTMLForm
*/
public $mParent;
@@ -1095,7 +1251,7 @@ abstract class HTMLFormField {
* This function must be implemented to return the HTML to generate
* the input object itself. It should not implement the surrounding
* table cells/rows, or labels/help messages.
- * @param $value String the value to set the input to; eg a default
+ * @param string $value the value to set the input to; eg a default
* text for a text input.
* @return String valid HTML.
*/
@@ -1104,7 +1260,7 @@ abstract class HTMLFormField {
/**
* Get a translated interface message
*
- * This is a wrapper arround $this->mParent->msg() if $this->mParent is set
+ * This is a wrapper around $this->mParent->msg() if $this->mParent is set
* and wfMessage() otherwise.
*
* Parameters are the same as wfMessage().
@@ -1127,8 +1283,8 @@ abstract class HTMLFormField {
* Override this function to add specific validation checks on the
* field input. Don't forget to call parent::validate() to ensure
* that the user-defined callback mValidationCallback is still run
- * @param $value String the value the field was submitted with
- * @param $alldata Array the data collected from the form
+ * @param string $value the value the field was submitted with
+ * @param array $alldata the data collected from the form
* @return Mixed Bool true on success, or String error to display.
*/
function validate( $value, $alldata ) {
@@ -1162,6 +1318,18 @@ abstract class HTMLFormField {
}
/**
+ * Tell the field whether to generate a separate label element if its label
+ * is blank.
+ *
+ * @since 1.22
+ * @param bool $show Set to false to not generate a label.
+ * @return void
+ */
+ public function setShowEmptyLabel( $show ) {
+ $this->mShowEmptyLabels = $show;
+ }
+
+ /**
* Get the value that this input has been set to from a posted form,
* or the input's default value if it has not been set.
* @param $request WebRequest
@@ -1177,7 +1345,10 @@ abstract class HTMLFormField {
/**
* Initialise the object
- * @param $params array Associative Array. See HTMLForm doc for syntax.
+ * @param array $params Associative Array. See HTMLForm doc for syntax.
+ *
+ * @since 1.22 The 'label' attribute no longer accepts raw HTML, use 'label-raw' instead
+ * @throws MWException
*/
function __construct( $params ) {
$this->mParams = $params;
@@ -1195,7 +1366,14 @@ abstract class HTMLFormField {
$this->mLabel = wfMessage( $msg, $msgInfo )->parse();
} elseif ( isset( $params['label'] ) ) {
- $this->mLabel = $params['label'];
+ if ( $params['label'] === '&#160;' ) {
+ // Apparently some things set &nbsp directly and in an odd format
+ $this->mLabel = '&#160;';
+ } else {
+ $this->mLabel = htmlspecialchars( $params['label'] );
+ }
+ } elseif ( isset( $params['label-raw'] ) ) {
+ $this->mLabel = $params['label-raw'];
}
$this->mName = "wp{$params['fieldname']}";
@@ -1240,12 +1418,16 @@ abstract class HTMLFormField {
if ( isset( $params['flatlist'] ) ) {
$this->mClass .= ' mw-htmlform-flatlist';
}
+
+ if ( isset( $params['hidelabel'] ) ) {
+ $this->mShowEmptyLabels = false;
+ }
}
/**
* Get the complete table row for the input, including help text,
* labels, and whatever.
- * @param $value String the value to set the input to.
+ * @param string $value the value to set the input to.
* @return String complete HTML table row.
*/
function getTableRow( $value ) {
@@ -1289,7 +1471,7 @@ abstract class HTMLFormField {
* Get the complete div for the input, including help text,
* labels, and whatever.
* @since 1.20
- * @param $value String the value to set the input to.
+ * @param string $value the value to set the input to.
* @return String complete HTML table row.
*/
public function getDiv( $value ) {
@@ -1300,13 +1482,22 @@ abstract class HTMLFormField {
$cellAttributes = array();
$label = $this->getLabelHtml( $cellAttributes );
+ $outerDivClass = array(
+ 'mw-input',
+ 'mw-htmlform-nolabel' => ( $label === '' )
+ );
+
$field = Html::rawElement(
'div',
- array( 'class' => 'mw-input' ) + $cellAttributes,
+ array( 'class' => $outerDivClass ) + $cellAttributes,
$inputHtml . "\n$errors"
);
+ $divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass );
+ if ( $this->mParent->isVForm() ) {
+ $divCssClasses[] = 'mw-ui-vform-div';
+ }
$html = Html::rawElement( 'div',
- array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
+ array( 'class' => $divCssClasses ),
$label . $field );
$html .= $helptext;
return $html;
@@ -1316,13 +1507,12 @@ abstract class HTMLFormField {
* Get the complete raw fields for the input, including help text,
* labels, and whatever.
* @since 1.20
- * @param $value String the value to set the input to.
+ * @param string $value the value to set the input to.
* @return String complete HTML table row.
*/
public function getRaw( $value ) {
- list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
+ list( $errors, ) = $this->getErrorsAndErrorClass( $value );
$inputHtml = $this->getInputHTML( $value );
- $fieldType = get_class( $this );
$helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() );
$cellAttributes = array();
$label = $this->getLabelHtml( $cellAttributes );
@@ -1415,7 +1605,7 @@ abstract class HTMLFormField {
/**
* Determine form errors to display and their classes
* @since 1.20
- * @param $value String the value of the input
+ * @param string $value the value of the input
* @return Array
*/
public function getErrorsAndErrorClass( $value ) {
@@ -1432,7 +1622,7 @@ abstract class HTMLFormField {
}
function getLabel() {
- return $this->mLabel;
+ return is_null( $this->mLabel ) ? '' : $this->mLabel;
}
function getLabelHtml( $cellAttributes = array() ) {
@@ -1444,20 +1634,32 @@ abstract class HTMLFormField {
$for['for'] = $this->mID;
}
+ $labelValue = trim( $this->getLabel() );
+ $hasLabel = false;
+ if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
+ $hasLabel = true;
+ }
+
$displayFormat = $this->mParent->getDisplayFormat();
- $labelElement = Html::rawElement( 'label', $for, $this->getLabel() );
+ $html = '';
- if ( $displayFormat == 'table' ) {
- return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes,
- Html::rawElement( 'label', $for, $this->getLabel() )
+ if ( $displayFormat === 'table' ) {
+ $html = Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes,
+ Html::rawElement( 'label', $for, $labelValue )
);
- } elseif ( $displayFormat == 'div' ) {
- return Html::rawElement( 'div', array( 'class' => 'mw-label' ) + $cellAttributes,
- Html::rawElement( 'label', $for, $this->getLabel() )
- );
- } else {
- return $labelElement;
+ } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
+ if ( $displayFormat === 'div' ) {
+ $html = Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-label' ) + $cellAttributes,
+ Html::rawElement( 'label', $for, $labelValue )
+ );
+ } else {
+ $html = Html::rawElement( 'label', $for, $labelValue );
+ }
}
+
+ return $html;
}
function getDefault() {
@@ -1483,7 +1685,7 @@ abstract class HTMLFormField {
/**
* flatten an array of options to a single array, for instance,
* a set of "<options>" inside "<optgroups>".
- * @param $options array Associative Array with values either Strings
+ * @param array $options Associative Array with values either Strings
* or Arrays
* @return Array flattened input
*/
@@ -1597,16 +1799,19 @@ class HTMLTextField extends HTMLFormField {
}
}
class HTMLTextAreaField extends HTMLFormField {
+ const DEFAULT_COLS = 80;
+ const DEFAULT_ROWS = 25;
+
function getCols() {
return isset( $this->mParams['cols'] )
? $this->mParams['cols']
- : 80;
+ : static::DEFAULT_COLS;
}
function getRows() {
return isset( $this->mParams['rows'] )
? $this->mParams['rows']
- : 25;
+ : static::DEFAULT_ROWS;
}
function getInputHTML( $value ) {
@@ -1736,8 +1941,25 @@ class HTMLCheckField extends HTMLFormField {
$attr['class'] = $this->mClass;
}
- return Xml::check( $this->mName, $value, $attr ) . '&#160;' .
- Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
+ if ( $this->mParent->isVForm() ) {
+ // Nest checkbox inside label.
+ return Html::rawElement(
+ 'label',
+ array(
+ 'class' => 'mw-ui-checkbox-label'
+ ),
+ Xml::check(
+ $this->mName,
+ $value,
+ $attr
+ ) .
+ // Html:rawElement doesn't escape contents.
+ htmlspecialchars( $this->mLabel )
+ );
+ } else {
+ return Xml::check( $this->mName, $value, $attr ) . '&#160;' .
+ Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
+ }
}
/**
@@ -1750,6 +1972,13 @@ class HTMLCheckField extends HTMLFormField {
}
/**
+ * checkboxes don't need a label.
+ */
+ protected function needsLabel() {
+ return false;
+ }
+
+ /**
* @param $request WebRequest
* @return String
*/
@@ -1778,6 +2007,246 @@ class HTMLCheckField extends HTMLFormField {
}
/**
+ * A checkbox matrix
+ * Operates similarly to HTMLMultiSelectField, but instead of using an array of
+ * options, uses an array of rows and an array of columns to dynamically
+ * construct a matrix of options. The tags used to identify a particular cell
+ * are of the form "columnName-rowName"
+ *
+ * Options:
+ * - columns
+ * - Required list of columns in the matrix.
+ * - rows
+ * - Required list of rows in the matrix.
+ * - force-options-on
+ * - Accepts array of column-row tags to be displayed as enabled but unavailable to change
+ * - force-options-off
+ * - Accepts array of column-row tags to be displayed as disabled but unavailable to change.
+ * - tooltips
+ * - Optional array mapping row label to tooltip content
+ * - tooltip-class
+ * - Optional CSS class used on tooltip container span. Defaults to mw-icon-question.
+ */
+class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
+
+ static private $requiredParams = array(
+ // Required by underlying HTMLFormField
+ 'fieldname',
+ // Required by HTMLCheckMatrix
+ 'rows', 'columns'
+ );
+
+ public function __construct( $params ) {
+ $missing = array_diff( self::$requiredParams, array_keys( $params ) );
+ if ( $missing ) {
+ throw new HTMLFormFieldRequiredOptionsException( $this, $missing );
+ }
+ parent::__construct( $params );
+ }
+
+ function validate( $value, $alldata ) {
+ $rows = $this->mParams['rows'];
+ $columns = $this->mParams['columns'];
+
+ // Make sure user-defined validation callback is run
+ $p = parent::validate( $value, $alldata );
+ if ( $p !== true ) {
+ return $p;
+ }
+
+ // Make sure submitted value is an array
+ if ( !is_array( $value ) ) {
+ return false;
+ }
+
+ // If all options are valid, array_intersect of the valid options
+ // and the provided options will return the provided options.
+ $validOptions = array();
+ foreach ( $rows as $rowTag ) {
+ foreach ( $columns as $columnTag ) {
+ $validOptions[] = $columnTag . '-' . $rowTag;
+ }
+ }
+ $validValues = array_intersect( $value, $validOptions );
+ if ( count( $validValues ) == count( $value ) ) {
+ return true;
+ } else {
+ return $this->msg( 'htmlform-select-badoption' )->parse();
+ }
+ }
+
+ /**
+ * Build a table containing a matrix of checkbox options.
+ * The value of each option is a combination of the row tag and column tag.
+ * mParams['rows'] is an array with row labels as keys and row tags as values.
+ * mParams['columns'] is an array with column labels as keys and column tags as values.
+ * @param array $value of the options that should be checked
+ * @return String
+ */
+ function getInputHTML( $value ) {
+ $html = '';
+ $tableContents = '';
+ $attribs = array();
+ $rows = $this->mParams['rows'];
+ $columns = $this->mParams['columns'];
+
+ // If the disabled param is set, disable all the options
+ if ( !empty( $this->mParams['disabled'] ) ) {
+ $attribs['disabled'] = 'disabled';
+ }
+
+ // Build the column headers
+ $headerContents = Html::rawElement( 'td', array(), '&#160;' );
+ foreach ( $columns as $columnLabel => $columnTag ) {
+ $headerContents .= Html::rawElement( 'td', array(), $columnLabel );
+ }
+ $tableContents .= Html::rawElement( 'tr', array(), "\n$headerContents\n" );
+
+ $tooltipClass = 'mw-icon-question';
+ if ( isset( $this->mParams['tooltip-class'] ) ) {
+ $tooltipClass = $this->mParams['tooltip-class'];
+ }
+
+ // Build the options matrix
+ foreach ( $rows as $rowLabel => $rowTag ) {
+ // Append tooltip if configured
+ if ( isset( $this->mParams['tooltips'][$rowLabel] ) ) {
+ $tooltipAttribs = array(
+ 'class' => "mw-htmlform-tooltip $tooltipClass",
+ 'title' => $this->mParams['tooltips'][$rowLabel],
+ );
+ $rowLabel .= ' ' . Html::element( 'span', $tooltipAttribs, '' );
+ }
+ $rowContents = Html::rawElement( 'td', array(), $rowLabel );
+ foreach ( $columns as $columnTag ) {
+ $thisTag = "$columnTag-$rowTag";
+ // Construct the checkbox
+ $thisAttribs = array(
+ 'id' => "{$this->mID}-$thisTag",
+ 'value' => $thisTag,
+ );
+ $checked = in_array( $thisTag, (array)$value, true );
+ if ( $this->isTagForcedOff( $thisTag ) ) {
+ $checked = false;
+ $thisAttribs['disabled'] = 1;
+ } elseif ( $this->isTagForcedOn( $thisTag ) ) {
+ $checked = true;
+ $thisAttribs['disabled'] = 1;
+ }
+ $rowContents .= Html::rawElement(
+ 'td',
+ array(),
+ Xml::check( "{$this->mName}[]", $checked, $attribs + $thisAttribs )
+ );
+ }
+ $tableContents .= Html::rawElement( 'tr', array(), "\n$rowContents\n" );
+ }
+
+ // Put it all in a table
+ $html .= Html::rawElement( 'table', array( 'class' => 'mw-htmlform-matrix' ),
+ Html::rawElement( 'tbody', array(), "\n$tableContents\n" ) ) . "\n";
+
+ return $html;
+ }
+
+ protected function isTagForcedOff( $tag ) {
+ return isset( $this->mParams['force-options-off'] )
+ && in_array( $tag, $this->mParams['force-options-off'] );
+ }
+
+ protected function isTagForcedOn( $tag ) {
+ return isset( $this->mParams['force-options-on'] )
+ && in_array( $tag, $this->mParams['force-options-on'] );
+ }
+
+ /**
+ * Get the complete table row for the input, including help text,
+ * labels, and whatever.
+ * We override this function since the label should always be on a separate
+ * line above the options in the case of a checkbox matrix, i.e. it's always
+ * a "vertical-label".
+ * @param string $value the value to set the input to
+ * @return String complete HTML table row
+ */
+ function getTableRow( $value ) {
+ list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
+ $inputHtml = $this->getInputHTML( $value );
+ $fieldType = get_class( $this );
+ $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
+ $cellAttributes = array( 'colspan' => 2 );
+
+ $label = $this->getLabelHtml( $cellAttributes );
+
+ $field = Html::rawElement(
+ 'td',
+ array( 'class' => 'mw-input' ) + $cellAttributes,
+ $inputHtml . "\n$errors"
+ );
+
+ $html = Html::rawElement( 'tr',
+ array( 'class' => 'mw-htmlform-vertical-label' ), $label );
+ $html .= Html::rawElement( 'tr',
+ array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
+ $field );
+
+ return $html . $helptext;
+ }
+
+ /**
+ * @param $request WebRequest
+ * @return Array
+ */
+ function loadDataFromRequest( $request ) {
+ if ( $this->mParent->getMethod() == 'post' ) {
+ if ( $request->wasPosted() ) {
+ // Checkboxes are not added to the request arrays if they're not checked,
+ // so it's perfectly possible for there not to be an entry at all
+ return $request->getArray( $this->mName, array() );
+ } else {
+ // That's ok, the user has not yet submitted the form, so show the defaults
+ return $this->getDefault();
+ }
+ } else {
+ // This is the impossible case: if we look at $_GET and see no data for our
+ // field, is it because the user has not yet submitted the form, or that they
+ // have submitted it with all the options unchecked. We will have to assume the
+ // latter, which basically means that you can't specify 'positive' defaults
+ // for GET forms.
+ return $request->getArray( $this->mName, array() );
+ }
+ }
+
+ function getDefault() {
+ if ( isset( $this->mDefault ) ) {
+ return $this->mDefault;
+ } else {
+ return array();
+ }
+ }
+
+ function filterDataForSubmit( $data ) {
+ $columns = HTMLFormField::flattenOptions( $this->mParams['columns'] );
+ $rows = HTMLFormField::flattenOptions( $this->mParams['rows'] );
+ $res = array();
+ foreach ( $columns as $column ) {
+ foreach ( $rows as $row ) {
+ // Make sure option hasn't been forced
+ $thisTag = "$column-$row";
+ if ( $this->isTagForcedOff( $thisTag ) ) {
+ $res[$thisTag] = false;
+ } elseif ( $this->isTagForcedOn( $thisTag ) ) {
+ $res[$thisTag] = true;
+ } else {
+ $res[$thisTag] = in_array( $thisTag, $data );
+ }
+ }
+ }
+
+ return $res;
+ }
+}
+
+/**
* A select dropdown field. Basically a wrapper for Xmlselect class
*/
class HTMLSelectField extends HTMLFormField {
@@ -1790,10 +2259,11 @@ class HTMLSelectField extends HTMLFormField {
$validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
- if ( in_array( $value, $validOptions ) )
+ if ( in_array( $value, $validOptions ) ) {
return true;
- else
+ } else {
return $this->msg( 'htmlform-select-badoption' )->parse();
+ }
}
function getInputHTML( $value ) {
@@ -1827,7 +2297,6 @@ class HTMLSelectField extends HTMLFormField {
* Select dropdown field, with an additional "other" textbox.
*/
class HTMLSelectOrOtherField extends HTMLTextField {
- static $jsAdded = false;
function __construct( $params ) {
if ( !in_array( 'other', $params['options'], true ) ) {
@@ -1916,7 +2385,7 @@ class HTMLSelectOrOtherField extends HTMLTextField {
/**
* Multi-select field
*/
-class HTMLMultiSelectField extends HTMLFormField {
+class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable {
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
@@ -2009,6 +2478,17 @@ class HTMLMultiSelectField extends HTMLFormField {
}
}
+ function filterDataForSubmit( $data ) {
+ $options = HTMLFormField::flattenOptions( $this->mParams['options'] );
+
+ $res = array();
+ foreach ( $options as $opt ) {
+ $res["$opt"] = in_array( $opt, $data );
+ }
+
+ return $res;
+ }
+
protected function needsLabel() {
return false;
}
@@ -2053,8 +2533,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
/**
* Build a drop-down box from a textual list.
- * @param $string String message text
- * @param $otherName String name of "other reason" option
+ * @param string $string message text
+ * @param string $otherName name of "other reason" option
* @return Array
* TODO: this is copied from Xml::listDropDown(), deprecate/avoid duplication?
*/
@@ -2188,7 +2668,6 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
*/
class HTMLRadioField extends HTMLFormField {
-
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
@@ -2344,14 +2823,28 @@ class HTMLHiddenField extends HTMLFormField {
return $this->getTableRow( $value );
}
- public function getInputHTML( $value ) { return ''; }
+ public function getInputHTML( $value ) {
+ return '';
+ }
}
/**
* Add a submit button inline in the form (as opposed to
* HTMLForm::addButton(), which will add it at the end).
*/
-class HTMLSubmitField extends HTMLFormField {
+class HTMLSubmitField extends HTMLButtonField {
+ protected $buttonType = 'submit';
+}
+
+/**
+ * Adds a generic button inline to the form. Does not do anything, you must add
+ * click handling code in JavaScript. Use a HTMLSubmitField if you merely
+ * wish to add a submit button to a form.
+ *
+ * @since 1.22
+ */
+class HTMLButtonField extends HTMLFormField {
+ protected $buttonType = 'button';
public function __construct( $info ) {
$info['nodata'] = true;
@@ -2359,13 +2852,20 @@ class HTMLSubmitField extends HTMLFormField {
}
public function getInputHTML( $value ) {
- return Xml::submitButton(
+ $attr = array(
+ 'class' => 'mw-htmlform-submit ' . $this->mClass,
+ 'id' => $this->mID,
+ );
+
+ if ( !empty( $this->mParams['disabled'] ) ) {
+ $attr['disabled'] = 'disabled';
+ }
+
+ return Html::input(
+ $this->mName,
$value,
- array(
- 'class' => 'mw-htmlform-submit ' . $this->mClass,
- 'name' => $this->mName,
- 'id' => $this->mID,
- )
+ $this->buttonType,
+ $attr
);
}
@@ -2444,3 +2944,22 @@ class HTMLApiField extends HTMLFormField {
return '';
}
}
+
+interface HTMLNestedFilterable {
+ /**
+ * Support for seperating multi-option preferences into multiple preferences
+ * Due to lack of array support.
+ * @param $data array
+ */
+ function filterDataForSubmit( $data );
+}
+
+class HTMLFormFieldRequiredOptionsException extends MWException {
+ public function __construct( HTMLFormField $field, array $missing ) {
+ parent::__construct( sprintf(
+ "Form type `%s` expected the following parameters to be set: %s",
+ get_class( $field ),
+ implode( ', ', $missing )
+ ) );
+ }
+}
diff --git a/includes/HashRing.php b/includes/HashRing.php
new file mode 100644
index 00000000..930f8c0a
--- /dev/null
+++ b/includes/HashRing.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * Convenience class for weighted consistent hash rings.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Convenience class for weighted consistent hash rings
+ *
+ * @since 1.22
+ */
+class HashRing {
+ /** @var Array (location => weight) */
+ protected $sourceMap = array();
+ /** @var Array (location => (start, end)) */
+ protected $ring = array();
+
+ const RING_SIZE = 268435456; // 2^28
+
+ /**
+ * @param array $map (location => weight)
+ */
+ public function __construct( array $map ) {
+ $map = array_filter( $map, function( $w ) { return $w > 0; } );
+ if ( !count( $map ) ) {
+ throw new MWException( "Ring is empty or all weights are zero." );
+ }
+ $this->sourceMap = $map;
+ // Sort the locations based on the hash of their names
+ $hashes = array();
+ foreach ( $map as $location => $weight ) {
+ $hashes[$location] = sha1( $location );
+ }
+ uksort( $map, function ( $a, $b ) use ( $hashes ) {
+ return strcmp( $hashes[$a], $hashes[$b] );
+ } );
+ // Fit the map to weight-proportionate one with a space of size RING_SIZE
+ $sum = array_sum( $map );
+ $standardMap = array();
+ foreach ( $map as $location => $weight ) {
+ $standardMap[$location] = (int)floor( $weight / $sum * self::RING_SIZE );
+ }
+ // Build a ring of RING_SIZE spots, with each location at a spot in location hash order
+ $index = 0;
+ foreach ( $standardMap as $location => $weight ) {
+ // Location covers half-closed interval [$index,$index + $weight)
+ $this->ring[$location] = array( $index, $index + $weight );
+ $index += $weight;
+ }
+ // Make sure the last location covers what is left
+ end( $this->ring );
+ $this->ring[key( $this->ring )][1] = self::RING_SIZE;
+ }
+
+ /**
+ * Get the location of an item on the ring
+ *
+ * @param string $item
+ * @return string Location
+ */
+ public function getLocation( $item ) {
+ $locations = $this->getLocations( $item, 1 );
+ return $locations[0];
+ }
+
+ /**
+ * Get the location of an item on the ring, as well as the next clockwise locations
+ *
+ * @param string $item
+ * @param integer $limit Maximum number of locations to return
+ * @return array List of locations
+ */
+ public function getLocations( $item, $limit ) {
+ $locations = array();
+ $primaryLocation = null;
+ $spot = hexdec( substr( sha1( $item ), 0, 7 ) ); // first 28 bits
+ foreach ( $this->ring as $location => $range ) {
+ if ( count( $locations ) >= $limit ) {
+ break;
+ }
+ // The $primaryLocation is the location the item spot is in.
+ // After that is reached, keep appending the next locations.
+ if ( ( $range[0] <= $spot && $spot < $range[1] ) || $primaryLocation !== null ) {
+ if ( $primaryLocation === null ) {
+ $primaryLocation = $location;
+ }
+ $locations[] = $location;
+ }
+ }
+ // If more locations are requested, wrap-around and keep adding them
+ reset( $this->ring );
+ while ( count( $locations ) < $limit ) {
+ list( $location, ) = each( $this->ring );
+ if ( $location === $primaryLocation ) {
+ break; // don't go in circles
+ }
+ $locations[] = $location;
+ }
+ return $locations;
+ }
+
+ /**
+ * Get the map of locations to weight (ignores 0-weight items)
+ *
+ * @return array
+ */
+ public function getLocationWeights() {
+ return $this->sourceMap;
+ }
+
+ /**
+ * Get a new hash ring with a location removed from the ring
+ *
+ * @param string $location
+ * @return HashRing|bool Returns false if no non-zero weighted spots are left
+ */
+ public function newWithoutLocation( $location ) {
+ $map = $this->sourceMap;
+ unset( $map[$location] );
+ if ( count( $map ) ) {
+ return new self( $map );
+ }
+ return false;
+ }
+}
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
index bb8ec5e3..31aa0f87 100644
--- a/includes/HistoryBlob.php
+++ b/includes/HistoryBlob.php
@@ -19,10 +19,10 @@
*
* @file
*/
-
+
/**
- * Base class for general text storage via the "object" flag in old_flags, or
- * two-part external storage URLs. Used for represent efficient concatenated
+ * Base class for general text storage via the "object" flag in old_flags, or
+ * two-part external storage URLs. Used for represent efficient concatenated
* storage, and migration-related pointer objects.
*/
interface HistoryBlob
@@ -143,7 +143,7 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob
* Compress the bulk data in the object
*/
public function compress() {
- if ( !$this->mCompressed ) {
+ if ( !$this->mCompressed ) {
$this->mItems = gzdeflate( serialize( $this->mItems ) );
$this->mCompressed = true;
}
@@ -178,12 +178,11 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob
* @return bool
*/
public function isHappy() {
- return $this->mSize < $this->mMaxSize
+ return $this->mSize < $this->mMaxSize
&& count( $this->mItems ) < $this->mMaxCount;
}
}
-
/**
* Pointer object for an item within a CGZ blob stored in the text table.
*/
@@ -199,7 +198,7 @@ class HistoryBlobStub {
var $mOldId, $mHash, $mRef;
/**
- * @param $hash string the content hash of the text
+ * @param string $hash the content hash of the text
* @param $oldid Integer the old_id for the CGZ object
*/
function __construct( $hash = '', $oldid = 0 ) {
@@ -232,32 +231,29 @@ class HistoryBlobStub {
* @return string
*/
function getText() {
- $fname = 'HistoryBlobStub::getText';
-
- if( isset( self::$blobCache[$this->mOldId] ) ) {
+ if ( isset( self::$blobCache[$this->mOldId] ) ) {
$obj = self::$blobCache[$this->mOldId];
} else {
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'text', array( 'old_flags', 'old_text' ), array( 'old_id' => $this->mOldId ) );
- if( !$row ) {
+ if ( !$row ) {
return false;
}
$flags = explode( ',', $row->old_flags );
- if( in_array( 'external', $flags ) ) {
- $url=$row->old_text;
+ if ( in_array( 'external', $flags ) ) {
+ $url = $row->old_text;
$parts = explode( '://', $url, 2 );
if ( !isset( $parts[1] ) || $parts[1] == '' ) {
- wfProfileOut( $fname );
return false;
}
- $row->old_text = ExternalStore::fetchFromUrl($url);
+ $row->old_text = ExternalStore::fetchFromUrl( $url );
}
- if( !in_array( 'object', $flags ) ) {
+ if ( !in_array( 'object', $flags ) ) {
return false;
}
- if( in_array( 'gzip', $flags ) ) {
+ if ( in_array( 'gzip', $flags ) ) {
// This shouldn't happen, but a bug in the compress script
// may at times gzip-compress a HistoryBlob object row.
$obj = unserialize( gzinflate( $row->old_text ) );
@@ -265,7 +261,7 @@ class HistoryBlobStub {
$obj = unserialize( $row->old_text );
}
- if( !is_object( $obj ) ) {
+ if ( !is_object( $obj ) ) {
// Correct for old double-serialization bug.
$obj = unserialize( $obj );
}
@@ -288,7 +284,6 @@ class HistoryBlobStub {
}
}
-
/**
* To speed up conversion from 1.4 to 1.5 schema, text rows can refer to the
* leftover cur table as the backend. This avoids expensively copying hundreds
@@ -323,7 +318,7 @@ class HistoryBlobCurStub {
function getText() {
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'cur', array( 'cur_text' ), array( 'cur_id' => $this->mCurId ) );
- if( !$row ) {
+ if ( !$row ) {
return false;
}
return $row->cur_text;
@@ -341,12 +336,12 @@ class DiffHistoryBlob implements HistoryBlob {
/** Total uncompressed size */
var $mSize = 0;
- /**
- * Array of diffs. If a diff D from A to B is notated D = B - A, and Z is
+ /**
+ * Array of diffs. If a diff D from A to B is notated D = B - A, and Z is
* an empty string:
*
* { item[map[i]] - item[map[i-1]] where i > 0
- * diff[i] = {
+ * diff[i] = {
* { item[map[i]] - Z where i = 0
*/
var $mDiffs;
@@ -379,7 +374,7 @@ class DiffHistoryBlob implements HistoryBlob {
* The maximum number of text items before the object becomes sad
*/
var $mMaxCount = 100;
-
+
/** Constants from xdiff.h */
const XDL_BDOP_INS = 1;
const XDL_BDOP_CPY = 2;
@@ -398,7 +393,7 @@ class DiffHistoryBlob implements HistoryBlob {
*/
function addItem( $text ) {
if ( $this->mFrozen ) {
- throw new MWException( __METHOD__.": Cannot add more items after sleep/wakeup" );
+ throw new MWException( __METHOD__ . ": Cannot add more items after sleep/wakeup" );
}
$this->mItems[] = $text;
@@ -433,7 +428,7 @@ class DiffHistoryBlob implements HistoryBlob {
* @throws MWException
*/
function compress() {
- if ( !function_exists( 'xdiff_string_rabdiff' ) ){
+ if ( !function_exists( 'xdiff_string_rabdiff' ) ) {
throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" );
}
if ( isset( $this->mDiffs ) ) {
@@ -534,18 +529,18 @@ class DiffHistoryBlob implements HistoryBlob {
# Pure PHP implementation
$header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) );
-
- # Check the checksum if hash/mhash is available
+
+ # Check the checksum if hash extension is available
$ofp = $this->xdiffAdler32( $base );
if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) {
- wfDebug( __METHOD__. ": incorrect base checksum\n" );
+ wfDebug( __METHOD__ . ": incorrect base checksum\n" );
return false;
}
if ( $header['csize'] != strlen( $base ) ) {
- wfDebug( __METHOD__. ": incorrect base length\n" );
+ wfDebug( __METHOD__ . ": incorrect base length\n" );
return false;
}
-
+
$p = 8;
$out = '';
while ( $p < strlen( $diff ) ) {
@@ -571,7 +566,7 @@ class DiffHistoryBlob implements HistoryBlob {
$out .= substr( $base, $x['off'], $x['csize'] );
break;
default:
- wfDebug( __METHOD__.": invalid op\n" );
+ wfDebug( __METHOD__ . ": invalid op\n" );
return false;
}
}
@@ -579,27 +574,26 @@ class DiffHistoryBlob implements HistoryBlob {
}
/**
- * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with
+ * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with
* the bytes backwards and initialised with 0 instead of 1. See bug 34428.
*
- * Returns false if no hashing library is available
+ * @param string $s
+ * @return string|bool: false if the hash extension is not available
*/
function xdiffAdler32( $s ) {
+ if ( !function_exists( 'hash' ) ) {
+ return false;
+ }
+
static $init;
if ( $init === null ) {
$init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02";
}
- // The real Adler-32 checksum of $init is zero, so it initialises the
- // state to zero, as it is at the start of LibXDiff's checksum
+
+ // The real Adler-32 checksum of $init is zero, so it initialises the
+ // state to zero, as it is at the start of LibXDiff's checksum
// algorithm. Appending the subject string then simulates LibXDiff.
- if ( function_exists( 'hash' ) ) {
- $hash = hash( 'adler32', $init . $s, true );
- } elseif ( function_exists( 'mhash' ) ) {
- $hash = mhash( MHASH_ADLER32, $init . $s );
- } else {
- return false;
- }
- return strrev( $hash );
+ return strrev( hash( 'adler32', $init . $s, true ) );
}
function uncompress() {
@@ -664,7 +658,7 @@ class DiffHistoryBlob implements HistoryBlob {
if ( isset( $info['base'] ) ) {
// Old format
$this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 );
- array_unshift( $this->mDiffs,
+ array_unshift( $this->mDiffs,
pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) .
$info['base'] );
} else {
@@ -687,7 +681,7 @@ class DiffHistoryBlob implements HistoryBlob {
* @return bool
*/
function isHappy() {
- return $this->mSize < $this->mMaxSize
+ return $this->mSize < $this->mMaxSize
&& count( $this->mItems ) < $this->mMaxCount;
}
diff --git a/includes/Hooks.php b/includes/Hooks.php
index bc39f2fc..396e360d 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -1,4 +1,5 @@
<?php
+
/**
* A tool for running hook functions.
*
@@ -23,26 +24,36 @@
* @file
*/
+/**
+ * @since 1.18
+ */
class MWHookException extends MWException {}
/**
* Hooks class.
*
* Used to supersede $wgHooks, because globals are EVIL.
+ *
+ * @since 1.18
*/
class Hooks {
+ /**
+ * Array of events mapped to an array of callbacks to be run
+ * when that event is triggered.
+ */
protected static $handlers = array();
/**
- * Attach an event handler to a given hook
+ * Attach an event handler to a given hook.
*
- * @param $name Mixed: name of hook
- * @param $callback Mixed: callback function to attach
- * @return void
+ * @param string $name Name of hook
+ * @param mixed $callback Callback function to attach
+ *
+ * @since 1.18
*/
public static function register( $name, $callback ) {
- if( !isset( self::$handlers[$name] ) ) {
+ if ( !isset( self::$handlers[$name] ) ) {
self::$handlers[$name] = array();
}
@@ -50,222 +61,185 @@ class Hooks {
}
/**
- * Returns true if a hook has a function registered to it.
+ * Clears hooks registered via Hooks::register(). Does not touch $wgHooks.
+ * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
*
- * @param $name Mixed: name of hook
- * @return Boolean: true if a hook has a function registered to it
+ * @param string $name the name of the hook to clear.
+ *
+ * @since 1.21
+ * @throws MWException if not in testing mode.
*/
- public static function isRegistered( $name ) {
- if( !isset( self::$handlers[$name] ) ) {
- self::$handlers[$name] = array();
+ public static function clear( $name ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException( 'Cannot reset hooks in operation.' );
}
- return ( count( self::$handlers[$name] ) != 0 );
+ unset( self::$handlers[$name] );
+ }
+
+ /**
+ * Returns true if a hook has a function registered to it.
+ * The function may have been registered either via Hooks::register or in $wgHooks.
+ *
+ * @since 1.18
+ *
+ * @param string $name Name of hook
+ * @return bool True if the hook has a function registered to it
+ */
+ public static function isRegistered( $name ) {
+ global $wgHooks;
+ return !empty( $wgHooks[$name] ) || !empty( self::$handlers[$name] );
}
/**
* Returns an array of all the event functions attached to a hook
+ * This combines functions registered via Hooks::register and with $wgHooks.
+ *
+ * @since 1.18
*
- * @param $name Mixed: name of the hook
+ * @param string $name Name of the hook
* @return array
*/
public static function getHandlers( $name ) {
- if( !isset( self::$handlers[$name] ) ) {
+ global $wgHooks;
+
+ if ( !self::isRegistered( $name ) ) {
return array();
+ } elseif ( !isset( self::$handlers[$name] ) ) {
+ return $wgHooks[$name];
+ } elseif ( !isset( $wgHooks[$name] ) ) {
+ return self::$handlers[$name];
+ } else {
+ return array_merge( self::$handlers[$name], $wgHooks[$name] );
}
-
- return self::$handlers[$name];
}
/**
- * Call hook functions defined in Hooks::register
+ * Call hook functions defined in Hooks::register and $wgHooks.
+ *
+ * For a certain hook event, fetch the array of hook events and
+ * process them. Determine the proper callback for each hook and
+ * then call the actual hook using the appropriate arguments.
+ * Finally, process the return value and return/throw accordingly.
*
- * Because programmers assign to $wgHooks, we need to be very
- * careful about its contents. So, there's a lot more error-checking
- * in here than would normally be necessary.
+ * @param string $event Event name
+ * @param array $args Array of parameters passed to hook functions
+ * @return bool True if no handler aborted the hook
*
- * @param $event String: event name
- * @param $args Array: parameters passed to hook functions
- * @return Boolean True if no handler aborted the hook
+ * @since 1.22 A hook function is not required to return a value for
+ * processing to continue. Not returning a value (or explicitly
+ * returning null) is equivalent to returning true.
+ * @throws MWException
+ * @throws FatalError
*/
- public static function run( $event, $args = array() ) {
- global $wgHooks;
-
- // Return quickly in the most common case
- if ( !isset( self::$handlers[$event] ) && !isset( $wgHooks[$event] ) ) {
- return true;
- }
-
- if ( !is_array( self::$handlers ) ) {
- throw new MWException( "Local hooks array is not an array!\n" );
- }
-
- if ( !is_array( $wgHooks ) ) {
- throw new MWException( "Global hooks array is not an array!\n" );
- }
-
- $new_handlers = (array) self::$handlers;
- $old_handlers = (array) $wgHooks;
-
- $hook_array = array_merge( $new_handlers, $old_handlers );
-
- if ( !is_array( $hook_array[$event] ) ) {
- throw new MWException( "Hooks array for event '$event' is not an array!\n" );
- }
+ public static function run( $event, array $args = array() ) {
+ wfProfileIn( 'hook: ' . $event );
+ foreach ( self::getHandlers( $event ) as $hook ) {
+ // Turn non-array values into an array. (Can't use casting because of objects.)
+ if ( !is_array( $hook ) ) {
+ $hook = array( $hook );
+ }
- foreach ( $hook_array[$event] as $index => $hook ) {
- $object = null;
- $method = null;
- $func = null;
- $data = null;
- $have_data = false;
- $closure = false;
- $badhookmsg = false;
+ if ( !array_filter( $hook ) ) {
+ // Either array is empty or it's an array filled with null/false/empty.
+ continue;
+ } elseif ( is_array( $hook[0] ) ) {
+ // First element is an array, meaning the developer intended
+ // the first element to be a callback. Merge it in so that
+ // processing can be uniform.
+ $hook = array_merge( $hook[0], array_slice( $hook, 1 ) );
+ }
/**
* $hook can be: a function, an object, an array of $function and
* $data, an array of just a function, an array of object and
* method, or an array of object, method, and data.
*/
- if ( is_array( $hook ) ) {
- if ( count( $hook ) < 1 ) {
- throw new MWException( 'Empty array in hooks for ' . $event . "\n" );
- } elseif ( is_object( $hook[0] ) ) {
- $object = $hook_array[$event][$index][0];
- if ( $object instanceof Closure ) {
- $closure = true;
- if ( count( $hook ) > 1 ) {
- $data = $hook[1];
- $have_data = true;
- }
- } else {
- if ( count( $hook ) < 2 ) {
- $method = 'on' . $event;
- } else {
- $method = $hook[1];
- if ( count( $hook ) > 2 ) {
- $data = $hook[2];
- $have_data = true;
- }
- }
- }
- } elseif ( is_string( $hook[0] ) ) {
- $func = $hook[0];
- if ( count( $hook ) > 1) {
- $data = $hook[1];
- $have_data = true;
- }
- } else {
- throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" );
- }
- } elseif ( is_string( $hook ) ) { # functions look like strings, too
- $func = $hook;
- } elseif ( is_object( $hook ) ) {
- $object = $hook_array[$event][$index];
- if ( $object instanceof Closure ) {
- $closure = true;
- } else {
- $method = "on" . $event;
+ if ( $hook[0] instanceof Closure ) {
+ $func = "hook-$event-closure";
+ $callback = array_shift( $hook );
+ } elseif ( is_object( $hook[0] ) ) {
+ $object = array_shift( $hook );
+ $method = array_shift( $hook );
+
+ // If no method was specified, default to on$event.
+ if ( $method === null ) {
+ $method = "on$event";
}
- } else {
- throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" );
- }
- /* We put the first data element on, if needed. */
- if ( $have_data ) {
- $hook_args = array_merge( array( $data ), $args );
- } else {
- $hook_args = $args;
- }
-
- if ( $closure ) {
- $callback = $object;
- $func = "hook-$event-closure";
- } elseif ( isset( $object ) ) {
$func = get_class( $object ) . '::' . $method;
$callback = array( $object, $method );
+ } elseif ( is_string( $hook[0] ) ) {
+ $func = $callback = array_shift( $hook );
} else {
- $callback = $func;
+ throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" );
}
// Run autoloader (workaround for call_user_func_array bug)
- is_callable( $callback );
+ // and throw error if not callable.
+ if ( !is_callable( $callback ) ) {
+ throw new MWException( 'Invalid callback in hooks for ' . $event . "\n" );
+ }
- /**
- * Call the hook. The documentation of call_user_func_array clearly
- * states that FALSE is returned on failure. However this is not
- * case always. In some version of PHP if the function signature
- * does not match the call signature, PHP will issue an warning:
- * Param y in x expected to be a reference, value given.
- *
- * In that case the call will also return null. The following code
- * catches that warning and provides better error message. The
- * function documentation also says that:
- * In other words, it does not depend on the function signature
- * whether the parameter is passed by a value or by a reference.
- * There is also PHP bug http://bugs.php.net/bug.php?id=47554 which
- * is unsurprisingly marked as bogus. In short handling of failures
- * with call_user_func_array is a failure, the documentation for that
- * function is wrong and misleading and PHP developers don't see any
- * problem here.
+ /*
+ * Call the hook. The documentation of call_user_func_array says
+ * false is returned on failure. However, if the function signature
+ * does not match the call signature, PHP will issue an warning and
+ * return null instead. The following code catches that warning and
+ * provides better error message.
*/
$retval = null;
- set_error_handler( 'Hooks::hookErrorHandler' );
+ $badhookmsg = null;
+ $hook_args = array_merge( $hook, $args );
+
+ // Profile first in case the Profiler causes errors.
wfProfileIn( $func );
+ set_error_handler( 'Hooks::hookErrorHandler' );
try {
$retval = call_user_func_array( $callback, $hook_args );
} catch ( MWHookException $e ) {
$badhookmsg = $e->getMessage();
}
- wfProfileOut( $func );
restore_error_handler();
+ wfProfileOut( $func );
- /* String return is an error; false return means stop processing. */
+ // Process the return value.
if ( is_string( $retval ) ) {
+ // String returned means error.
throw new FatalError( $retval );
- } elseif( $retval === null ) {
- if ( $closure ) {
- $prettyFunc = "$event closure";
- } elseif( is_array( $callback ) ) {
- if( is_object( $callback[0] ) ) {
- $prettyClass = get_class( $callback[0] );
- } else {
- $prettyClass = strval( $callback[0] );
- }
- $prettyFunc = $prettyClass . '::' . strval( $callback[1] );
- } else {
- $prettyFunc = strval( $callback );
- }
- if ( $badhookmsg ) {
- throw new MWException(
- 'Detected bug in an extension! ' .
- "Hook $prettyFunc has invalid call signature; " . $badhookmsg
- );
- } else {
- throw new MWException(
- 'Detected bug in an extension! ' .
- "Hook $prettyFunc failed to return a value; " .
- 'should return true to continue hook processing or false to abort.'
- );
- }
- } elseif ( !$retval ) {
+ } elseif ( $badhookmsg !== null ) {
+ // Exception was thrown from Hooks::hookErrorHandler.
+ throw new MWException(
+ 'Detected bug in an extension! ' .
+ "Hook $func has invalid call signature; " . $badhookmsg
+ );
+ } elseif ( $retval === false ) {
+ wfProfileOut( 'hook: ' . $event );
+ // False was returned. Stop processing, but no error.
return false;
}
}
+ wfProfileOut( 'hook: ' . $event );
return true;
}
/**
+ * Handle PHP errors issued inside a hook. Catch errors that have to do with
+ * a function expecting a reference, and let all others pass through.
+ *
* This REALLY should be protected... but it's public for compatibility
*
- * @param $errno int Unused
- * @param $errstr String: error message
- * @return Boolean: false
+ * @since 1.18
+ *
+ * @param int $errno Error number (unused)
+ * @param string $errstr Error message
+ * @throws MWHookException If the error has to do with the function signature
+ * @return bool Always returns false
*/
public static function hookErrorHandler( $errno, $errstr ) {
if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) {
- throw new MWHookException( $errstr );
+ throw new MWHookException( $errstr, $errno );
}
return false;
}
diff --git a/includes/Html.php b/includes/Html.php
index b33d6fbb..932f753e 100644
--- a/includes/Html.php
+++ b/includes/Html.php
@@ -36,8 +36,8 @@
*
* There are two important configuration options this class uses:
*
- * $wgHtml5: If this is set to false, then all output should be valid XHTML 1.0
- * Transitional.
+ * $wgMimeType: If this is set to an xml mimetype then output should be
+ * valid XHTML5.
* $wgWellFormedXml: If this is set to true, then all output should be
* well-formed XML (quotes on attributes, self-closing tags, etc.).
*
@@ -48,7 +48,7 @@
* @since 1.16
*/
class Html {
- # List of void elements from HTML5, section 8.1.2 as of 2011-08-12
+ // List of void elements from HTML5, section 8.1.2 as of 2011-08-12
private static $voidElements = array(
'area',
'base',
@@ -68,8 +68,8 @@ class Html {
'wbr',
);
- # Boolean attributes, which may have the value omitted entirely. Manually
- # collected from the HTML5 spec as of 2011-08-12.
+ // Boolean attributes, which may have the value omitted entirely. Manually
+ // collected from the HTML5 spec as of 2011-08-12.
private static $boolAttribs = array(
'async',
'autofocus',
@@ -97,23 +97,10 @@ class Html {
'selected',
'truespeed',
'typemustmatch',
- # HTML5 Microdata
+ // HTML5 Microdata
'itemscope',
);
- private static $HTMLFiveOnlyAttribs = array(
- 'autocomplete',
- 'autofocus',
- 'max',
- 'min',
- 'multiple',
- 'pattern',
- 'placeholder',
- 'required',
- 'step',
- 'spellcheck',
- );
-
/**
* Returns an HTML element in a string. The major advantage here over
* manually typing out the HTML is that it will escape all attribute
@@ -126,11 +113,11 @@ class Html {
* content model. If $wgWellFormedXml is false, then a few bytes will be
* shaved off the HTML output as well.
*
- * @param $element string The element's name, e.g., 'a'
- * @param $attribs array Associative array of attributes, e.g., array(
+ * @param string $element The element's name, e.g., 'a'
+ * @param array $attribs Associative array of attributes, e.g., array(
* 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
* further documentation.
- * @param $contents string The raw HTML contents of the element: *not*
+ * @param string $contents The raw HTML contents of the element: *not*
* escaped!
* @return string Raw HTML
*/
@@ -139,7 +126,7 @@ class Html {
$start = self::openElement( $element, $attribs );
if ( in_array( $element, self::$voidElements ) ) {
if ( $wgWellFormedXml ) {
- # Silly XML.
+ // Silly XML.
return substr( $start, 0, -1 ) . ' />';
}
return $start;
@@ -160,8 +147,8 @@ class Html {
*/
public static function element( $element, $attribs = array(), $contents = '' ) {
return self::rawElement( $element, $attribs, strtr( $contents, array(
- # There's no point in escaping quotes, >, etc. in the contents of
- # elements.
+ // There's no point in escaping quotes, >, etc. in the contents of
+ // elements.
'&' => '&amp;',
'<' => '&lt;'
) ) );
@@ -177,26 +164,22 @@ class Html {
* @return string
*/
public static function openElement( $element, $attribs = array() ) {
- global $wgHtml5, $wgWellFormedXml;
+ global $wgWellFormedXml;
$attribs = (array)$attribs;
- # This is not required in HTML5, but let's do it anyway, for
- # consistency and better compression.
+ // This is not required in HTML5, but let's do it anyway, for
+ // consistency and better compression.
$element = strtolower( $element );
- # In text/html, initial <html> and <head> tags can be omitted under
- # pretty much any sane circumstances, if they have no attributes. See:
- # <http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags>
+ // In text/html, initial <html> and <head> tags can be omitted under
+ // pretty much any sane circumstances, if they have no attributes. See:
+ // <http://www.whatwg.org/html/syntax.html#optional-tags>
if ( !$wgWellFormedXml && !$attribs
&& in_array( $element, array( 'html', 'head' ) ) ) {
return '';
}
- # Remove HTML5-only attributes if we aren't doing HTML5, and disable
- # form validation regardless (see bug 23769 and the more detailed
- # comment in expandAttributes())
+ // Remove invalid input types
if ( $element == 'input' ) {
- # Whitelist of types that don't cause validation. All except
- # 'search' are valid in XHTML1.
$validTypes = array(
'hidden',
'text',
@@ -208,39 +191,33 @@ class Html {
'image',
'reset',
'button',
+
+ // HTML input types
+ 'datetime',
+ 'datetime-local',
+ 'date',
+ 'month',
+ 'time',
+ 'week',
+ 'number',
+ 'range',
+ 'email',
+ 'url',
'search',
+ 'tel',
+ 'color',
);
-
- if( $wgHtml5 ) {
- $validTypes = array_merge( $validTypes, array(
- 'datetime',
- 'datetime-local',
- 'date',
- 'month',
- 'time',
- 'week',
- 'number',
- 'range',
- 'email',
- 'url',
- 'search',
- 'tel',
- 'color',
- ) );
- }
if ( isset( $attribs['type'] )
&& !in_array( $attribs['type'], $validTypes ) ) {
unset( $attribs['type'] );
}
-
- if ( isset( $attribs['type'] ) && $attribs['type'] == 'search'
- && !$wgHtml5 ) {
- unset( $attribs['type'] );
- }
}
- if ( !$wgHtml5 && $element == 'textarea' && isset( $attribs['maxlength'] ) ) {
- unset( $attribs['maxlength'] );
+ // According to standard the default type for <button> elements is "submit".
+ // Depending on compatibility mode IE might use "button", instead.
+ // We enforce the standard "submit".
+ if ( $element == 'button' && !isset( $attribs['type'] ) ) {
+ $attribs['type'] = 'submit';
}
return "<$element" . self::expandAttributes(
@@ -252,7 +229,7 @@ class Html {
* it returns the empty string when that's guaranteed to be safe.
*
* @since 1.17
- * @param $element string Name of the element, e.g., 'a'
+ * @param string $element Name of the element, e.g., 'a'
* @return string A closing tag, if required
*/
public static function closeElement( $element ) {
@@ -260,8 +237,8 @@ class Html {
$element = strtolower( $element );
- # Reference:
- # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags
+ // Reference:
+ // http://www.whatwg.org/html/syntax.html#optional-tags
if ( !$wgWellFormedXml && in_array( $element, array(
'html',
'head',
@@ -289,28 +266,21 @@ class Html {
* only guarantees that the output array should be functionally identical
* to the input array (currently per the HTML 5 draft as of 2009-09-06).
*
- * @param $element string Name of the element, e.g., 'a'
- * @param $attribs array Associative array of attributes, e.g., array(
+ * @param string $element Name of the element, e.g., 'a'
+ * @param array $attribs Associative array of attributes, e.g., array(
* 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
* further documentation.
* @return array An array of attributes functionally identical to $attribs
*/
private static function dropDefaults( $element, $attribs ) {
- # Don't bother doing anything if we aren't outputting HTML5; it's too
- # much of a pain to maintain two sets of defaults.
- global $wgHtml5;
- if ( !$wgHtml5 ) {
- return $attribs;
- }
- # Whenever altering this array, please provide a covering test case
- # in HtmlTest::provideElementsWithAttributesHavingDefaultValues
+ // Whenever altering this array, please provide a covering test case
+ // in HtmlTest::provideElementsWithAttributesHavingDefaultValues
static $attribDefaults = array(
'area' => array( 'shape' => 'rect' ),
'button' => array(
'formaction' => 'GET',
'formenctype' => 'application/x-www-form-urlencoded',
- 'type' => 'submit',
),
'canvas' => array(
'height' => '150',
@@ -329,8 +299,8 @@ class Html {
'keygen' => array( 'keytype' => 'rsa' ),
'link' => array( 'media' => 'all' ),
'menu' => array( 'type' => 'list' ),
- # Note: the use of text/javascript here instead of other JavaScript
- # MIME types follows the HTML5 spec.
+ // Note: the use of text/javascript here instead of other JavaScript
+ // MIME types follows the HTML5 spec.
'script' => array( 'type' => 'text/javascript' ),
'style' => array(
'media' => 'all',
@@ -343,13 +313,13 @@ class Html {
foreach ( $attribs as $attrib => $value ) {
$lcattrib = strtolower( $attrib );
- if( is_array( $value ) ) {
+ if ( is_array( $value ) ) {
$value = implode( ' ', $value );
} else {
$value = strval( $value );
}
- # Simple checks using $attribDefaults
+ // Simple checks using $attribDefaults
if ( isset( $attribDefaults[$element][$lcattrib] ) &&
$attribDefaults[$element][$lcattrib] == $value ) {
unset( $attribs[$attrib] );
@@ -360,7 +330,7 @@ class Html {
}
}
- # More subtle checks
+ // More subtle checks
if ( $element === 'link' && isset( $attribs['type'] )
&& strval( $attribs['type'] ) == 'text/css' ) {
unset( $attribs['type'] );
@@ -392,12 +362,12 @@ class Html {
if ( in_array( 'multiple', $attribs )
|| ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
) {
- # A multi-select
+ // A multi-select
if ( strval( $attribs['size'] ) == '4' ) {
unset( $attribs['size'] );
}
} else {
- # Single select
+ // Single select
if ( strval( $attribs['size'] ) == '1' ) {
unset( $attribs['size'] );
}
@@ -438,7 +408,7 @@ class Html {
* // gives '<em class="bar quux"></em>'
* @endcode
*
- * @param $attribs array Associative array of attributes, e.g., array(
+ * @param array $attribs Associative array of attributes, e.g., array(
* 'href' => 'http://www.mediawiki.org/' ). Values will be HTML-escaped.
* A value of false means to omit the attribute. For boolean attributes,
* you can omit the key, e.g., array( 'checked' ) instead of
@@ -447,42 +417,37 @@ class Html {
* (starting with a space if at least one attribute is output)
*/
public static function expandAttributes( $attribs ) {
- global $wgHtml5, $wgWellFormedXml;
+ global $wgWellFormedXml;
$ret = '';
$attribs = (array)$attribs;
foreach ( $attribs as $key => $value ) {
+ // Support intuitive array( 'checked' => true/false ) form
if ( $value === false || is_null( $value ) ) {
continue;
}
- # For boolean attributes, support array( 'foo' ) instead of
- # requiring array( 'foo' => 'meaningless' ).
+ // For boolean attributes, support array( 'foo' ) instead of
+ // requiring array( 'foo' => 'meaningless' ).
if ( is_int( $key )
&& in_array( strtolower( $value ), self::$boolAttribs ) ) {
$key = $value;
}
- # Not technically required in HTML5, but required in XHTML 1.0,
- # and we'd like consistency and better compression anyway.
+ // Not technically required in HTML5 but we'd like consistency
+ // and better compression anyway.
$key = strtolower( $key );
- # Here we're blacklisting some HTML5-only attributes...
- if ( !$wgHtml5 && in_array( $key, self::$HTMLFiveOnlyAttribs )
- ) {
- continue;
- }
+ // Bug 23769: Blacklist all form validation attributes for now. Current
+ // (June 2010) WebKit has no UI, so the form just refuses to submit
+ // without telling the user why, which is much worse than failing
+ // server-side validation. Opera is the only other implementation at
+ // this time, and has ugly UI, so just kill the feature entirely until
+ // we have at least one good implementation.
- # Bug 23769: Blacklist all form validation attributes for now. Current
- # (June 2010) WebKit has no UI, so the form just refuses to submit
- # without telling the user why, which is much worse than failing
- # server-side validation. Opera is the only other implementation at
- # this time, and has ugly UI, so just kill the feature entirely until
- # we have at least one good implementation.
-
- # As the default value of "1" for "step" rejects decimal
- # numbers to be entered in 'type="number"' fields, allow
- # the special case 'step="any"'.
+ // As the default value of "1" for "step" rejects decimal
+ // numbers to be entered in 'type="number"' fields, allow
+ // the special case 'step="any"'.
if ( in_array( $key, array( 'max', 'min', 'pattern', 'required' ) ) ||
$key === 'step' && $value !== 'any' ) {
@@ -495,20 +460,19 @@ class Html {
'class', // html4, html5
'accesskey', // as of html5, multiple space-separated values allowed
// html4-spec doesn't document rel= as space-separated
- // but has been used like that and is now documented as such
+ // but has been used like that and is now documented as such
// in the html5-spec.
'rel',
);
- # Specific features for attributes that allow a list of space-separated values
+ // Specific features for attributes that allow a list of space-separated values
if ( in_array( $key, $spaceSeparatedListAttributes ) ) {
// Apply some normalization and remove duplicates
- // Convert into correct array. Array can contain space-seperated
+ // Convert into correct array. Array can contain space-separated
// values. Implode/explode to get those into the main array as well.
if ( is_array( $value ) ) {
// If input wasn't an array, we can skip this step
-
$newValue = array();
foreach ( $value as $k => $v ) {
if ( is_string( $v ) ) {
@@ -517,7 +481,7 @@ class Html {
if ( !isset( $value[$v] ) ) {
// As a special case don't set 'foo' if a
// separate 'foo' => true/false exists in the array
- // keys should be authoritive
+ // keys should be authoritative
$newValue[] = $v;
}
} elseif ( $v ) {
@@ -538,14 +502,14 @@ class Html {
$value = implode( ' ', array_unique( $value ) );
}
- # See the "Attributes" section in the HTML syntax part of HTML5,
- # 9.1.2.3 as of 2009-08-10. Most attributes can have quotation
- # marks omitted, but not all. (Although a literal " is not
- # permitted, we don't check for that, since it will be escaped
- # anyway.)
+ // See the "Attributes" section in the HTML syntax part of HTML5,
+ // 9.1.2.3 as of 2009-08-10. Most attributes can have quotation
+ // marks omitted, but not all. (Although a literal " is not
+ // permitted, we don't check for that, since it will be escaped
+ // anyway.)
#
- # See also research done on further characters that need to be
- # escaped: http://code.google.com/p/html5lib/issues/detail?id=93
+ // See also research done on further characters that need to be
+ // escaped: http://code.google.com/p/html5lib/issues/detail?id=93
$badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}"
. "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}"
. "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}";
@@ -557,27 +521,24 @@ class Html {
}
if ( in_array( $key, self::$boolAttribs ) ) {
- # In XHTML 1.0 Transitional, the value needs to be equal to the
- # key. In HTML5, we can leave the value empty instead. If we
- # don't need well-formed XML, we can omit the = entirely.
+ // In HTML5, we can leave the value empty. If we don't need
+ // well-formed XML, we can omit the = entirely.
if ( !$wgWellFormedXml ) {
$ret .= " $key";
- } elseif ( $wgHtml5 ) {
- $ret .= " $key=\"\"";
} else {
- $ret .= " $key=\"$key\"";
+ $ret .= " $key=\"\"";
}
} else {
- # Apparently we need to entity-encode \n, \r, \t, although the
- # spec doesn't mention that. Since we're doing strtr() anyway,
- # and we don't need <> escaped here, we may as well not call
- # htmlspecialchars().
- # @todo FIXME: Verify that we actually need to
- # escape \n\r\t here, and explain why, exactly.
+ // Apparently we need to entity-encode \n, \r, \t, although the
+ // spec doesn't mention that. Since we're doing strtr() anyway,
+ // and we don't need <> escaped here, we may as well not call
+ // htmlspecialchars().
+ // @todo FIXME: Verify that we actually need to
+ // escape \n\r\t here, and explain why, exactly.
#
- # We could call Sanitizer::encodeAttribute() for this, but we
- # don't because we're stubborn and like our marginal savings on
- # byte size from not having to encode unnecessary quotes.
+ // We could call Sanitizer::encodeAttribute() for this, but we
+ // don't because we're stubborn and like our marginal savings on
+ // byte size from not having to encode unnecessary quotes.
$map = array(
'&' => '&amp;',
'"' => '&quot;',
@@ -586,12 +547,11 @@ class Html {
"\t" => '&#9;'
);
if ( $wgWellFormedXml ) {
- # This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
- # But reportedly it breaks some XML tools?
- # @todo FIXME: Is this really true?
+ // This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
+ // But reportedly it breaks some XML tools?
+ // @todo FIXME: Is this really true?
$map['<'] = '&lt;';
}
-
$ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
}
}
@@ -604,18 +564,14 @@ class Html {
* @todo do some useful escaping as well, like if $contents contains
* literal "</script>" or (for XML) literal "]]>".
*
- * @param $contents string JavaScript
+ * @param string $contents JavaScript
* @return string Raw HTML
*/
public static function inlineScript( $contents ) {
- global $wgHtml5, $wgJsMimeType, $wgWellFormedXml;
+ global $wgWellFormedXml;
$attrs = array();
- if ( !$wgHtml5 ) {
- $attrs['type'] = $wgJsMimeType;
- }
-
if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
$contents = "/*<![CDATA[*/$contents/*]]>*/";
}
@@ -631,14 +587,8 @@ class Html {
* @return string Raw HTML
*/
public static function linkedScript( $url ) {
- global $wgHtml5, $wgJsMimeType;
-
$attrs = array( 'src' => $url );
- if ( !$wgHtml5 ) {
- $attrs['type'] = $wgJsMimeType;
- }
-
return self::element( 'script', $attrs );
}
@@ -647,7 +597,7 @@ class Html {
* (if any). TODO: do some useful escaping as well, like if $contents
* contains literal "</style>" (admittedly unlikely).
*
- * @param $contents string CSS
+ * @param string $contents CSS
* @param $media mixed A media type string, like 'screen'
* @return string Raw HTML
*/
@@ -683,13 +633,12 @@ class Html {
/**
* Convenience function to produce an "<input>" element. This supports the
- * new HTML5 input types and attributes, and will silently strip them if
- * $wgHtml5 is false.
+ * new HTML5 input types and attributes.
*
* @param $name string name attribute
* @param $value mixed value attribute
* @param $type string type attribute
- * @param $attribs array Associative array of miscellaneous extra
+ * @param array $attribs Associative array of miscellaneous extra
* attributes, passed to Html::element()
* @return string Raw HTML
*/
@@ -706,7 +655,7 @@ class Html {
*
* @param $name string name attribute
* @param $value string value attribute
- * @param $attribs array Associative array of miscellaneous extra
+ * @param array $attribs Associative array of miscellaneous extra
* attributes, passed to Html::element()
* @return string Raw HTML
*/
@@ -718,32 +667,18 @@ class Html {
* Convenience function to produce an "<input>" element.
*
* This supports leaving out the cols= and rows= which Xml requires and are
- * required by HTML4/XHTML but not required by HTML5 and will silently set
- * cols="" and rows="" if $wgHtml5 is false and cols and rows are omitted
- * (HTML4 validates present but empty cols="" and rows="" as valid).
+ * required by HTML4/XHTML but not required by HTML5.
*
* @param $name string name attribute
* @param $value string value attribute
- * @param $attribs array Associative array of miscellaneous extra
+ * @param array $attribs 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'] = "";
- }
- }
-
- if (substr($value, 0, 1) == "\n") {
+ if ( substr( $value, 0, 1 ) == "\n" ) {
// Workaround for bug 12130: browsers eat the initial newline
// assuming that it's just for show, but they do keep the later
// newlines, which we may want to preserve during editing.
@@ -763,12 +698,12 @@ class Html {
* - label: text for label to add before the field
* - exclude: [optional] Array of namespace ids to exclude
* - disable: [optional] Array of namespace ids for which the option should be disabled in the selector
- * @param $selectAttribs array HTML attributes for the generated select element.
+ * @param array $selectAttribs HTML attributes for the generated select element.
* - id: [optional], default: 'namespace'
* - name: [optional], default: 'namespace'
* @return string HTML code to select a namespace.
*/
- public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) {
+ public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) {
global $wgContLang;
ksort( $selectAttribs );
@@ -802,7 +737,7 @@ class Html {
// Value is provided by user, the name shown is localized for the user.
$options[$params['all']] = wfMessage( 'namespacesall' )->text();
}
- // Add all namespaces as options (in the content langauge)
+ // Add all namespaces as options (in the content language)
$options += $wgContLang->getFormattedNamespaces();
// Convert $options to HTML and filter out namespaces below 0
@@ -811,10 +746,12 @@ class Html {
if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) {
continue;
}
- if ( $nsId === 0 ) {
+ if ( $nsId === NS_MAIN ) {
// For other namespaces use use the namespace prefix as label, but for
- // main we don't use "" but the user message descripting it (e.g. "(Main)" or "(Article)")
+ // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)")
$nsName = wfMessage( 'blanknamespace' )->text();
+ } elseif ( is_int( $nsId ) ) {
+ $nsName = $wgContLang->convertNamespace( $nsId );
}
$optionsHtml[] = Html::element(
'option', array(
@@ -825,6 +762,14 @@ class Html {
);
}
+ if ( !array_key_exists( 'id', $selectAttribs ) ) {
+ $selectAttribs['id'] = 'namespace';
+ }
+
+ if ( !array_key_exists( 'name', $selectAttribs ) ) {
+ $selectAttribs['name'] = 'namespace';
+ }
+
$ret = '';
if ( isset( $params['label'] ) ) {
$ret .= Html::element(
@@ -848,35 +793,36 @@ class Html {
* Constructs the opening html-tag with necessary doctypes depending on
* global variables.
*
- * @param $attribs array Associative array of miscellaneous extra
+ * @param array $attribs Associative array of miscellaneous extra
* attributes, passed to Html::element() of html tag.
* @return string Raw HTML
*/
public static function htmlHeader( $attribs = array() ) {
$ret = '';
- global $wgMimeType;
-
- if ( self::isXmlMimeType( $wgMimeType ) ) {
- $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n";
- }
+ global $wgHtml5Version, $wgMimeType, $wgXhtmlNamespaces;
- global $wgHtml5, $wgHtml5Version, $wgDocType, $wgDTD;
- global $wgXhtmlNamespaces, $wgXhtmlDefaultNamespace;
+ $isXHTML = self::isXmlMimeType( $wgMimeType );
- if ( $wgHtml5 ) {
- $ret .= "<!DOCTYPE html>\n";
+ if ( $isXHTML ) { // XHTML5
+ // XML mimetyped markup should have an xml header.
+ // However a DOCTYPE is not needed.
+ $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n";
- if ( $wgHtml5Version ) {
- $attribs['version'] = $wgHtml5Version;
- }
- } else {
- $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\" \"$wgDTD\">\n";
- $attribs['xmlns'] = $wgXhtmlDefaultNamespace;
+ // Add the standard xmlns
+ $attribs['xmlns'] = 'http://www.w3.org/1999/xhtml';
+ // And support custom namespaces
foreach ( $wgXhtmlNamespaces as $tag => $ns ) {
$attribs["xmlns:$tag"] = $ns;
}
+ } else { // HTML5
+ // DOCTYPE
+ $ret .= "<!DOCTYPE html>\n";
+ }
+
+ if ( $wgHtml5Version ) {
+ $attribs['version'] = $wgHtml5Version;
}
$html = Html::openElement( 'html', $attribs );
@@ -897,23 +843,20 @@ class Html {
* @return Boolean
*/
public static function isXmlMimeType( $mimetype ) {
- switch ( $mimetype ) {
- case 'text/xml':
- case 'application/xhtml+xml':
- case 'application/xml':
- return true;
- default:
- return false;
- }
+ # http://www.whatwg.org/html/infrastructure.html#xml-mime-type
+ # * text/xml
+ # * application/xml
+ # * Any mimetype with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
+ return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
}
/**
* Get HTML for an info box with an icon.
*
- * @param $text String: wikitext, get this with wfMessage()->plain()
- * @param $icon String: icon name, file in skins/common/images
- * @param $alt String: alternate text for the icon
- * @param $class String: additional class name to add to the wrapper div
+ * @param string $text wikitext, get this with wfMessage()->plain()
+ * @param string $icon icon name, file in skins/common/images
+ * @param string $alt alternate text for the icon
+ * @param string $class additional class name to add to the wrapper div
* @param $useStylePath
*
* @return string
@@ -922,22 +865,22 @@ class Html {
global $wgStylePath;
if ( $useStylePath ) {
- $icon = $wgStylePath.'/common/images/'.$icon;
+ $icon = $wgStylePath . '/common/images/' . $icon;
}
- $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class") );
+ $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class" ) );
- $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ).
+ $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ) .
Html::element( 'img',
array(
'src' => $icon,
'alt' => $alt,
)
- ).
+ ) .
Html::closeElement( 'div' );
- $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ).
- $text.
+ $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ) .
+ $text .
Html::closeElement( 'div' );
$s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' );
@@ -947,4 +890,22 @@ class Html {
return $s;
}
+
+ /**
+ * Generate a srcset attribute value from an array mapping pixel densities
+ * to URLs. Note that srcset supports width and height values as well, which
+ * are not used here.
+ *
+ * @param array $urls
+ * @return string
+ */
+ static function srcSet( $urls ) {
+ $candidates = array();
+ foreach ( $urls as $density => $url ) {
+ // Image candidate syntax per current whatwg live spec, 2012-09-23:
+ // http://www.whatwg.org/html/embedded-content-1.html#attr-img-srcset
+ $candidates[] = "{$url} {$density}x";
+ }
+ return implode( ", ", $candidates );
+ }
}
diff --git a/includes/HtmlFormatter.php b/includes/HtmlFormatter.php
new file mode 100644
index 00000000..248a76fe
--- /dev/null
+++ b/includes/HtmlFormatter.php
@@ -0,0 +1,356 @@
+<?php
+/**
+ * Performs transformations of HTML by wrapping around libxml2 and working
+ * around its countless bugs.
+ *
+ * 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
+ */
+class HtmlFormatter {
+ /**
+ * @var DOMDocument
+ */
+ private $doc;
+
+ private $html;
+ private $itemsToRemove = array();
+ private $elementsToFlatten = array();
+ protected $removeMedia = false;
+
+ /**
+ * Constructor
+ *
+ * @param string $html: Text to process
+ */
+ public function __construct( $html ) {
+ $this->html = $html;
+ }
+
+ /**
+ * Turns a chunk of HTML into a proper document
+ * @param string $html
+ * @return string
+ */
+ public static function wrapHTML( $html ) {
+ return '<!doctype html><html><head></head><body>' . $html . '</body></html>';
+ }
+
+ /**
+ * Override this in descendant class to modify HTML after it has been converted from DOM tree
+ * @param string $html: HTML to process
+ * @return string: Processed HTML
+ */
+ protected function onHtmlReady( $html ) {
+ return $html;
+ }
+
+ /**
+ * @return DOMDocument: DOM to manipulate
+ */
+ public function getDoc() {
+ if ( !$this->doc ) {
+ $html = mb_convert_encoding( $this->html, 'HTML-ENTITIES', 'UTF-8' );
+
+ // Workaround for bug that caused spaces before references
+ // to disappear during processing:
+ // https://bugzilla.wikimedia.org/show_bug.cgi?id=53086
+ //
+ // Please replace with a better fix if one can be found.
+ $html = str_replace( ' <', '&#32;<', $html );
+
+ libxml_use_internal_errors( true );
+ $loader = libxml_disable_entity_loader();
+ $this->doc = new DOMDocument();
+ $this->doc->strictErrorChecking = false;
+ $this->doc->loadHTML( $html );
+ libxml_disable_entity_loader( $loader );
+ libxml_use_internal_errors( false );
+ $this->doc->encoding = 'UTF-8';
+ }
+ return $this->doc;
+ }
+
+ /**
+ * Sets whether images/videos/sounds should be removed from output
+ * @param bool $flag
+ */
+ public function setRemoveMedia( $flag = true ) {
+ $this->removeMedia = $flag;
+ }
+
+ /**
+ * Adds one or more selector of content to remove. A subset of CSS selector
+ * syntax is supported:
+ *
+ * <tag>
+ * <tag>.class
+ * .<class>
+ * #<id>
+ *
+ * @param Array|string $selectors: Selector(s) of stuff to remove
+ */
+ public function remove( $selectors ) {
+ $this->itemsToRemove = array_merge( $this->itemsToRemove, (array)$selectors );
+ }
+
+ /**
+ * Adds one or more element name to the list to flatten (remove tag, but not its content)
+ * Can accept undelimited regexes
+ *
+ * Note this interface may fail in surprising unexpected ways due to usage of regexes,
+ * so should not be relied on for HTML markup security measures.
+ *
+ * @param Array|string $elements: Name(s) of tag(s) to flatten
+ */
+ public function flatten( $elements ) {
+ $this->elementsToFlatten = array_merge( $this->elementsToFlatten, (array)$elements );
+ }
+
+ /**
+ * Instructs the formatter to flatten all tags
+ */
+ public function flattenAllTags() {
+ $this->flatten( '[?!]?[a-z0-9]+' );
+ }
+
+ /**
+ * Removes content we've chosen to remove
+ */
+ public function filterContent() {
+ wfProfileIn( __METHOD__ );
+ $removals = $this->parseItemsToRemove();
+
+ if ( !$removals ) {
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $doc = $this->getDoc();
+
+ // Remove tags
+
+ // You can't remove DOMNodes from a DOMNodeList as you're iterating
+ // over them in a foreach loop. It will seemingly leave the internal
+ // iterator on the foreach out of wack and results will be quite
+ // strange. Though, making a queue of items to remove seems to work.
+ $domElemsToRemove = array();
+ foreach ( $removals['TAG'] as $tagToRemove ) {
+ $tagToRemoveNodes = $doc->getElementsByTagName( $tagToRemove );
+ foreach ( $tagToRemoveNodes as $tagToRemoveNode ) {
+ if ( $tagToRemoveNode ) {
+ $domElemsToRemove[] = $tagToRemoveNode;
+ }
+ }
+ }
+
+ $this->removeElements( $domElemsToRemove );
+
+ // Elements with named IDs
+ $domElemsToRemove = array();
+ foreach ( $removals['ID'] as $itemToRemove ) {
+ $itemToRemoveNode = $doc->getElementById( $itemToRemove );
+ if ( $itemToRemoveNode ) {
+ $domElemsToRemove[] = $itemToRemoveNode;
+ }
+ }
+ $this->removeElements( $domElemsToRemove );
+
+ // CSS Classes
+ $domElemsToRemove = array();
+ $xpath = new DOMXpath( $doc );
+ foreach ( $removals['CLASS'] as $classToRemove ) {
+ $elements = $xpath->query( '//*[contains(@class, "' . $classToRemove . '")]' );
+
+ /** @var $element DOMElement */
+ foreach ( $elements as $element ) {
+ $classes = $element->getAttribute( 'class' );
+ if ( preg_match( "/\b$classToRemove\b/", $classes ) && $element->parentNode ) {
+ $domElemsToRemove[] = $element;
+ }
+ }
+ }
+ $this->removeElements( $domElemsToRemove );
+
+ // Tags with CSS Classes
+ foreach ( $removals['TAG_CLASS'] as $classToRemove ) {
+ $parts = explode( '.', $classToRemove );
+
+ $elements = $xpath->query(
+ '//' . $parts[0] . '[@class="' . $parts[1] . '"]'
+ );
+
+ $this->removeElements( $elements );
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Removes a list of elelments from DOMDocument
+ * @param array|DOMNodeList $elements
+ */
+ private function removeElements( $elements ) {
+ $list = $elements;
+ if ( $elements instanceof DOMNodeList ) {
+ $list = array();
+ foreach ( $elements as $element ) {
+ $list[] = $element;
+ }
+ }
+ /** @var $element DOMElement */
+ foreach ( $list as $element ) {
+ if ( $element->parentNode ) {
+ $element->parentNode->removeChild( $element );
+ }
+ }
+ }
+
+ /**
+ * libxml in its usual pointlessness converts many chars to entities - this function
+ * perfoms a reverse conversion
+ * @param string $html
+ * @return string
+ */
+ private function fixLibXML( $html ) {
+ wfProfileIn( __METHOD__ );
+ static $replacements;
+ if ( ! $replacements ) {
+ // We don't include rules like '&#34;' => '&amp;quot;' because entities had already been
+ // normalized by libxml. Using this function with input not sanitized by libxml is UNSAFE!
+ $replacements = new ReplacementArray( array(
+ '&quot;' => '&amp;quot;',
+ '&amp;' => '&amp;amp;',
+ '&lt;' => '&amp;lt;',
+ '&gt;' => '&amp;gt;',
+ ) );
+ }
+ $html = $replacements->replace( $html );
+ $html = mb_convert_encoding( $html, 'UTF-8', 'HTML-ENTITIES' );
+ wfProfileOut( __METHOD__ );
+ return $html;
+ }
+
+ /**
+ * Performs final transformations and returns resulting HTML
+ *
+ * @param DOMElement|string|null $element: ID of element to get HTML from or false to get it from the whole tree
+ * @return string: Processed HTML
+ */
+ public function getText( $element = null ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( $this->doc ) {
+ if ( $element !== null && !( $element instanceof DOMElement ) ) {
+ $element = $this->doc->getElementById( $element );
+ }
+ if ( $element ) {
+ $body = $this->doc->getElementsByTagName( 'body' )->item( 0 );
+ $nodesArray = array();
+ foreach ( $body->childNodes as $node ) {
+ $nodesArray[] = $node;
+ }
+ foreach ( $nodesArray as $nodeArray ) {
+ $body->removeChild( $nodeArray );
+ }
+ $body->appendChild( $element );
+ }
+ $html = $this->doc->saveHTML();
+ $html = $this->fixLibXml( $html );
+ } else {
+ $html = $this->html;
+ }
+ if ( wfIsWindows() ) {
+ // Appears to be cleanup for CRLF misprocessing of unknown origin
+ // when running server on Windows platform.
+ //
+ // If this error continues in the future, please track it down in the
+ // XML code paths if possible and fix there.
+ $html = str_replace( '&#13;', '', $html );
+ }
+ $html = preg_replace( '/<!--.*?-->|^.*?<body>|<\/body>.*$/s', '', $html );
+ $html = $this->onHtmlReady( $html );
+
+ if ( $this->elementsToFlatten ) {
+ $elements = implode( '|', $this->elementsToFlatten );
+ $html = preg_replace( "#</?($elements)\\b[^>]*>#is", '', $html );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $html;
+ }
+
+ /**
+ * @param $selector: CSS selector to parse
+ * @param $type
+ * @param $rawName
+ * @return bool: Whether the selector was successfully recognised
+ */
+ protected function parseSelector( $selector, &$type, &$rawName ) {
+ if ( strpos( $selector, '.' ) === 0 ) {
+ $type = 'CLASS';
+ $rawName = substr( $selector, 1 );
+ } elseif ( strpos( $selector, '#' ) === 0 ) {
+ $type = 'ID';
+ $rawName = substr( $selector, 1 );
+ } elseif ( strpos( $selector, '.' ) !== 0 &&
+ strpos( $selector, '.' ) !== false )
+ {
+ $type = 'TAG_CLASS';
+ $rawName = $selector;
+ } elseif ( strpos( $selector, '[' ) === false
+ && strpos( $selector, ']' ) === false )
+ {
+ $type = 'TAG';
+ $rawName = $selector;
+ } else {
+ throw new MWException( __METHOD__ . "(): unrecognized selector '$selector'" );
+ }
+
+ return true;
+ }
+
+ /**
+ * Transforms CSS selectors into an internal representation suitable for processing
+ * @return array
+ */
+ protected function parseItemsToRemove() {
+ wfProfileIn( __METHOD__ );
+ $removals = array(
+ 'ID' => array(),
+ 'TAG' => array(),
+ 'CLASS' => array(),
+ 'TAG_CLASS' => array(),
+ );
+
+ foreach ( $this->itemsToRemove as $itemToRemove ) {
+ $type = '';
+ $rawName = '';
+ if ( $this->parseSelector( $itemToRemove, $type, $rawName ) ) {
+ $removals[$type][] = $rawName;
+ }
+ }
+
+ if ( $this->removeMedia ) {
+ $removals['TAG'][] = 'img';
+ $removals['TAG'][] = 'audio';
+ $removals['TAG'][] = 'video';
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $removals;
+ }
+}
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 8e48da46..fa2fc12b 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -35,19 +35,20 @@ class Http {
/**
* Perform an HTTP request
*
- * @param $method String: HTTP method. Usually GET/POST
- * @param $url String: full URL to act on. If protocol-relative, will be expanded to an http:// URL
- * @param $options Array: options to pass to MWHttpRequest object.
+ * @param string $method HTTP method. Usually GET/POST
+ * @param string $url full URL to act on. If protocol-relative, will be expanded to an http:// URL
+ * @param array $options options to pass to MWHttpRequest object.
* Possible keys for the array:
* - timeout Timeout length in seconds
+ * - connectTimeout Timeout for connection, in seconds (curl only)
* - postData An array of key-value pairs or a url-encoded form data
* - proxy The proxy to use.
* Otherwise it will use $wgHTTPProxy (if set)
* Otherwise it will use the environment variable "http_proxy" (if set)
* - noProxy Don't use any proxy at all. Takes precedence over proxy value(s).
- * - sslVerifyHost (curl only) Verify hostname against certificate
- * - sslVerifyCert (curl only) Verify SSL certificate
- * - caInfo (curl only) Provide CA information
+ * - sslVerifyHost Verify hostname against certificate
+ * - sslVerifyCert Verify SSL certificate
+ * - caInfo Provide CA information
* - maxRedirects Maximum number of redirects to follow (defaults to 5)
* - followRedirects Whether to follow redirects (defaults to false).
* Note: this should only be used when the target URL is trusted,
@@ -58,20 +59,26 @@ class Http {
*/
public static function request( $method, $url, $options = array() ) {
wfDebug( "HTTP: $method: $url\n" );
+ wfProfileIn( __METHOD__ . "-$method" );
+
$options['method'] = strtoupper( $method );
if ( !isset( $options['timeout'] ) ) {
$options['timeout'] = 'default';
}
+ if ( !isset( $options['connectTimeout'] ) ) {
+ $options['connectTimeout'] = 'default';
+ }
$req = MWHttpRequest::factory( $url, $options );
$status = $req->execute();
+ $content = false;
if ( $status->isOK() ) {
- return $req->getContent();
- } else {
- return false;
+ $content = $req->getContent();
}
+ wfProfileOut( __METHOD__ . "-$method" );
+ return $content;
}
/**
@@ -103,7 +110,7 @@ class Http {
/**
* Check if the URL can be served by localhost
*
- * @param $url String: full url to check
+ * @param string $url full url to check
* @return Boolean
*/
public static function isLocalURL( $url ) {
@@ -209,11 +216,11 @@ class MWHttpRequest {
public $status;
/**
- * @param $url String: url to use. If protocol-relative, will be expanded to an http:// URL
- * @param $options Array: (optional) extra params to pass (see Http::request())
+ * @param string $url url to use. If protocol-relative, will be expanded to an http:// URL
+ * @param array $options (optional) extra params to pass (see Http::request())
*/
protected function __construct( $url, $options = array() ) {
- global $wgHTTPTimeout;
+ global $wgHTTPTimeout, $wgHTTPConnectTimeout;
$this->url = wfExpandUrl( $url, PROTO_HTTP );
$this->parsedUrl = wfParseUrl( $this->url );
@@ -229,12 +236,17 @@ class MWHttpRequest {
} else {
$this->timeout = $wgHTTPTimeout;
}
- if( isset( $options['userAgent'] ) ) {
+ if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
+ $this->connectTimeout = $options['connectTimeout'];
+ } else {
+ $this->connectTimeout = $wgHTTPConnectTimeout;
+ }
+ if ( isset( $options['userAgent'] ) ) {
$this->setUserAgent( $options['userAgent'] );
}
$members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
- "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" );
+ "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" );
foreach ( $members as $o ) {
if ( isset( $options[$o] ) ) {
@@ -263,8 +275,9 @@ class MWHttpRequest {
/**
* Generate a new request object
- * @param $url String: url to use
- * @param $options Array: (optional) extra params to pass (see Http::request())
+ * @param string $url url to use
+ * @param array $options (optional) extra params to pass (see Http::request())
+ * @throws MWException
* @return CurlHttpRequest|PhpHttpRequest
* @see MWHttpRequest::__construct
*/
@@ -273,10 +286,10 @@ class MWHttpRequest {
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"' );
+ ' Http::$httpEngine is set to "curl"' );
}
- switch( Http::$httpEngine ) {
+ switch ( Http::$httpEngine ) {
case 'curl':
return new CurlHttpRequest( $url, $options );
case 'php':
@@ -301,7 +314,7 @@ class MWHttpRequest {
/**
* Set the parameters of the request
-
+ *
* @param $args Array
* @todo overload the args param
*/
@@ -317,21 +330,24 @@ class MWHttpRequest {
public function proxySetup() {
global $wgHTTPProxy;
- if ( $this->proxy || !$this->noProxy ) {
+ // If there is an explicit proxy set and proxies are not disabled, then use it
+ if ( $this->proxy && !$this->noProxy ) {
return;
}
+ // Otherwise, fallback to $wgHTTPProxy/http_proxy (when set) if this is not a machine
+ // local URL and proxies are not disabled
if ( Http::isLocalURL( $this->url ) || $this->noProxy ) {
$this->proxy = '';
} elseif ( $wgHTTPProxy ) {
- $this->proxy = $wgHTTPProxy ;
+ $this->proxy = $wgHTTPProxy;
} elseif ( getenv( "http_proxy" ) ) {
$this->proxy = getenv( "http_proxy" );
}
}
/**
- * Set the refererer header
+ * Set the referrer header
*/
public function setReferer( $url ) {
$this->setHeader( 'Referer', $url );
@@ -393,6 +409,7 @@ class MWHttpRequest {
* will be aborted.
*
* @param $callback Callback
+ * @throws MWException
*/
public function setCallback( $callback ) {
if ( !is_callable( $callback ) ) {
@@ -422,6 +439,8 @@ class MWHttpRequest {
public function execute() {
global $wgTitle;
+ wfProfileIn( __METHOD__ );
+
$this->content = "";
if ( strtoupper( $this->method ) == "HEAD" ) {
@@ -441,14 +460,18 @@ class MWHttpRequest {
if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
$this->setUserAgent( Http::userAgent() );
}
+
+ wfProfileOut( __METHOD__ );
}
/**
* Parses the headers, including the HTTP status code and any
- * Set-Cookie headers. This function expectes the headers to be
+ * Set-Cookie headers. This function expects the headers to be
* found in an array in the member variable headerList.
*/
protected function parseHeader() {
+ wfProfileIn( __METHOD__ );
+
$lastname = "";
foreach ( $this->headerList as $header ) {
@@ -465,6 +488,8 @@ class MWHttpRequest {
}
$this->parseCookies();
+
+ wfProfileOut( __METHOD__ );
}
/**
@@ -501,7 +526,6 @@ class MWHttpRequest {
return (int)$this->respStatus;
}
-
/**
* Returns true if the last status code was a redirect.
*
@@ -548,8 +572,8 @@ class MWHttpRequest {
$this->parseHeader();
}
- if ( isset( $this->respHeaders[strtolower ( $header ) ] ) ) {
- $v = $this->respHeaders[strtolower ( $header ) ];
+ if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
+ $v = $this->respHeaders[strtolower( $header )];
return $v[count( $v ) - 1];
}
@@ -579,8 +603,8 @@ class MWHttpRequest {
}
/**
- * Sets a cookie. Used before a request to set up any individual
- * cookies. Used internally after a request to parse the
+ * 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
* @param $name
@@ -599,6 +623,8 @@ class MWHttpRequest {
* Parse the cookies in the response headers and store them in the cookie jar.
*/
protected function parseCookies() {
+ wfProfileIn( __METHOD__ );
+
if ( !$this->cookieJar ) {
$this->cookieJar = new CookieJar;
}
@@ -609,6 +635,8 @@ class MWHttpRequest {
$this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
}
}
+
+ wfProfileOut( __METHOD__ );
}
/**
@@ -627,18 +655,18 @@ class MWHttpRequest {
$headers = $this->getResponseHeaders();
//return full url (fix for incorrect but handled relative location)
- if ( isset( $headers[ 'location' ] ) ) {
- $locations = $headers[ 'location' ];
+ if ( isset( $headers['location'] ) ) {
+ $locations = $headers['location'];
$domain = '';
$foundRelativeURI = false;
- $countLocations = count($locations);
+ $countLocations = count( $locations );
for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
- $url = parse_url( $locations[ $i ] );
+ $url = parse_url( $locations[$i] );
- if ( isset($url[ 'host' ]) ) {
- $domain = $url[ 'scheme' ] . '://' . $url[ 'host' ];
- break; //found correct URI (with host)
+ if ( isset( $url['host'] ) ) {
+ $domain = $url['scheme'] . '://' . $url['host'];
+ break; //found correct URI (with host)
} else {
$foundRelativeURI = true;
}
@@ -646,15 +674,15 @@ class MWHttpRequest {
if ( $foundRelativeURI ) {
if ( $domain ) {
- return $domain . $locations[ $countLocations - 1 ];
+ return $domain . $locations[$countLocations - 1];
} else {
$url = parse_url( $this->url );
- if ( isset($url[ 'host' ]) ) {
- return $url[ 'scheme' ] . '://' . $url[ 'host' ] . $locations[ $countLocations - 1 ];
+ if ( isset( $url['host'] ) ) {
+ return $url['scheme'] . '://' . $url['host'] . $locations[$countLocations - 1];
}
}
} else {
- return $locations[ $countLocations - 1 ];
+ return $locations[$countLocations - 1];
}
}
@@ -677,11 +705,6 @@ class MWHttpRequest {
class CurlHttpRequest extends MWHttpRequest {
const SUPPORTS_FILE_POSTS = true;
- static $curlMessageMap = array(
- 6 => 'http-host-unreachable',
- 28 => 'http-timed-out'
- );
-
protected $curlOptions = array();
protected $headerText = "";
@@ -696,14 +719,23 @@ class CurlHttpRequest extends MWHttpRequest {
}
public function execute() {
+ wfProfileIn( __METHOD__ );
+
parent::execute();
if ( !$this->status->isOK() ) {
+ wfProfileOut( __METHOD__ );
return $this->status;
}
$this->curlOptions[CURLOPT_PROXY] = $this->proxy;
$this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
+
+ // Only supported in curl >= 7.16.2
+ if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) {
+ $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
+ }
+
$this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
$this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
$this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" );
@@ -742,6 +774,7 @@ class CurlHttpRequest extends MWHttpRequest {
$curlHandle = curl_init( $this->url );
if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Error setting curl options." );
}
@@ -756,14 +789,11 @@ class CurlHttpRequest extends MWHttpRequest {
wfRestoreWarnings();
}
- 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 ) );
- }
+ $curlRes = curl_exec( $curlHandle );
+ if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
+ $this->status->fatal( 'http-timed-out', $this->url );
+ } elseif ( $curlRes === false ) {
+ $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
} else {
$this->headerList = explode( "\r\n", $this->headerText );
}
@@ -773,6 +803,8 @@ class CurlHttpRequest extends MWHttpRequest {
$this->parseHeader();
$this->setStatus();
+ wfProfileOut( __METHOD__ );
+
return $this->status;
}
@@ -807,10 +839,12 @@ class PhpHttpRequest extends MWHttpRequest {
}
public function execute() {
+ wfProfileIn( __METHOD__ );
+
parent::execute();
if ( is_array( $this->postData ) ) {
- $this->postData = wfArrayToCGI( $this->postData );
+ $this->postData = wfArrayToCgi( $this->postData );
}
if ( $this->parsedUrl['scheme'] != 'http' &&
@@ -822,7 +856,7 @@ class PhpHttpRequest extends MWHttpRequest {
if ( $this->method == 'POST' ) {
// Required for HTTP 1.0 POSTs
$this->reqHeaders['Content-Length'] = strlen( $this->postData );
- if( !isset( $this->reqHeaders['Content-Type'] ) ) {
+ if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
$this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
}
}
@@ -856,7 +890,23 @@ class PhpHttpRequest extends MWHttpRequest {
$options['timeout'] = $this->timeout;
- $context = stream_context_create( array( 'http' => $options ) );
+ if ( $this->sslVerifyHost ) {
+ $options['CN_match'] = $this->parsedUrl['host'];
+ }
+ if ( $this->sslVerifyCert ) {
+ $options['verify_peer'] = true;
+ }
+
+ if ( is_dir( $this->caInfo ) ) {
+ $options['capath'] = $this->caInfo;
+ } elseif ( is_file( $this->caInfo ) ) {
+ $options['cafile'] = $this->caInfo;
+ } elseif ( $this->caInfo ) {
+ throw new MWException( "Invalid CA info passed: {$this->caInfo}" );
+ }
+
+ $scheme = $this->parsedUrl['scheme'];
+ $context = stream_context_create( array( "$scheme" => $options ) );
$this->headerList = array();
$reqCount = 0;
@@ -899,18 +949,19 @@ class PhpHttpRequest extends MWHttpRequest {
if ( $fh === false ) {
$this->status->fatal( 'http-request-error' );
+ wfProfileOut( __METHOD__ );
return $this->status;
}
if ( $result['timed_out'] ) {
$this->status->fatal( 'http-timed-out', $this->url );
+ wfProfileOut( __METHOD__ );
return $this->status;
}
// If everything went OK, or we received some error code
// get the response body content.
- if ( $this->status->isOK()
- || (int)$this->respStatus >= 300) {
+ if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
while ( !feof( $fh ) ) {
$buf = fread( $fh, 8192 );
@@ -926,6 +977,8 @@ class PhpHttpRequest extends MWHttpRequest {
}
fclose( $fh );
+ wfProfileOut( __METHOD__ );
+
return $this->status;
}
}
diff --git a/includes/IP.php b/includes/IP.php
index 10c707e7..73834a59 100644
--- a/includes/IP.php
+++ b/includes/IP.php
@@ -18,7 +18,7 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @author Antoine Musso <hashar at free dot fr>, Aaron Schulz
+ * @author Antoine Musso "<hashar at free dot fr>", Aaron Schulz
*/
// Some regex definition to "play" with IP address and IP address blocks
@@ -33,24 +33,17 @@ define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX );
// An IPv6 address is made up of 8 words (each x0000 to xFFFF).
// However, the "::" abbreviation can be used on consecutive x0000 words.
define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
-define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)');
+define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' );
define( 'RE_IPV6_ADD',
'(?:' . // starts with "::" (including "::")
':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
'|' . // ends with "::" (except "::")
RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
- '|' . // contains one "::" in the middle, ending in "::WORD"
- RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,5}' . '::' . RE_IPV6_WORD .
- '|' . // contains one "::" in the middle, not ending in "::WORD" (regex for PCRE 4.0+)
- RE_IPV6_WORD . '(?::(?P<abn>:(?P<iabn>))?' . RE_IPV6_WORD . '(?!:(?P=abn))){1,5}' .
- ':' . RE_IPV6_WORD . '(?P=iabn)' .
- // NOTE: (?!(?P=abn)) fails iff "::" used twice; (?P=iabn) passes iff a "::" was found.
+ '|' . // contains one "::" in the middle (the ^ makes the test fail if none found)
+ RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
'|' . // contains no "::"
RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
')'
- // NOTE: With PCRE 7.2+, we can combine the two '"::" in the middle' cases into:
- // RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)'
- // This also improves regex concatenation by using relative references.
);
// An IPv6 block is an IP address and a prefix (d1 to d128)
define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
@@ -77,7 +70,7 @@ class IP {
* SIIT IPv4-translated addresses are rejected.
* Note: canonicalize() tries to convert translated addresses to IPv4.
*
- * @param $ip String: possible IP address
+ * @param string $ip possible IP address
* @return Boolean
*/
public static function isIPAddress( $ip ) {
@@ -88,7 +81,7 @@ class IP {
* Given a string, determine if it as valid IP in IPv6 only.
* Note: Unlike isValid(), this looks for networks too.
*
- * @param $ip String: possible IP address
+ * @param string $ip possible IP address
* @return Boolean
*/
public static function isIPv6( $ip ) {
@@ -99,7 +92,7 @@ class IP {
* Given a string, determine if it as valid IP in IPv4 only.
* Note: Unlike isValid(), this looks for networks too.
*
- * @param $ip String: possible IP address
+ * @param string $ip possible IP address
* @return Boolean
*/
public static function isIPv4( $ip ) {
@@ -137,7 +130,7 @@ class IP {
* IPv6 addresses in octet notation are expanded to 8 words.
* IPv4 addresses are just trimmed.
*
- * @param $ip String: IP address in quad or octet form (CIDR or not).
+ * @param string $ip IP address in quad or octet form (CIDR or not).
* @return String
*/
public static function sanitizeIP( $ip ) {
@@ -180,7 +173,7 @@ class IP {
$ip
);
}
- // Remove leading zereos from each bloc as needed
+ // Remove leading zeros from each bloc as needed
$ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
return $ip;
}
@@ -212,7 +205,7 @@ class IP {
$longest = $match;
$longestPos = $pos;
}
- $offset += ( $pos + strlen( $match ) ); // advance
+ $offset = ( $pos + strlen( $match ) ); // advance
}
if ( $longest !== false ) {
// Replace this portion of the string with the '::' abbreviation
@@ -241,7 +234,7 @@ class IP {
*
* A bare IPv6 address is accepted despite the lack of square brackets.
*
- * @param $both string The string with the host and port
+ * @param string $both The string with the host and port
* @return array
*/
public static function splitHostAndPort( $both ) {
@@ -316,7 +309,7 @@ class IP {
/**
* Convert an IPv4 or IPv6 hexadecimal representation back to readable format
*
- * @param $hex String: number, with "v6-" prefix if it is IPv6
+ * @param string $hex number, with "v6-" prefix if it is IPv6
* @return String: quad-dotted (IPv4) or octet notation (IPv6)
*/
public static function formatHex( $hex ) {
@@ -392,11 +385,11 @@ class IP {
static $privateRanges = false;
if ( !$privateRanges ) {
$privateRanges = array(
- array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private)
- array( '172.16.0.0', '172.31.255.255' ), # "
- array( '192.168.0.0', '192.168.255.255' ), # "
- array( '0.0.0.0', '0.255.255.255' ), # this network
- array( '127.0.0.0', '127.255.255.255' ), # loopback
+ array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private)
+ array( '172.16.0.0', '172.31.255.255' ), # RFC 1918 (private)
+ array( '192.168.0.0', '192.168.255.255' ), # RFC 1918 (private)
+ array( '0.0.0.0', '0.255.255.255' ), # this network
+ array( '127.0.0.0', '127.255.255.255' ), # loopback
);
}
@@ -444,7 +437,7 @@ class IP {
* function for an IPv6 address will be prefixed with "v6-", a non-
* hexadecimal string which sorts after the IPv4 addresses.
*
- * @param $ip String: quad dotted/octet IP address.
+ * @param string $ip quad dotted/octet IP address.
* @return String
*/
public static function toHex( $ip ) {
@@ -462,7 +455,7 @@ class IP {
/**
* Given an IPv6 address in octet notation, returns a pure hex string.
*
- * @param $ip String: octet ipv6 IP address.
+ * @param string $ip octet ipv6 IP address.
* @return String: pure hex (uppercase)
*/
private static function IPv6ToRawHex( $ip ) {
@@ -482,7 +475,7 @@ class IP {
* Like ip2long() except that it actually works and has a consistent error return value.
* Comes from ProxyTools.php
*
- * @param $ip String: quad dotted IP address.
+ * @param string $ip quad dotted IP address.
* @return Mixed: string/int/false
*/
public static function toUnsigned( $ip ) {
@@ -492,6 +485,11 @@ class IP {
$n = ip2long( $ip );
if ( $n < 0 ) {
$n += pow( 2, 32 );
+ # On 32-bit platforms (and on Windows), 2^32 does not fit into an int,
+ # so $n becomes a float. We convert it to string instead.
+ if ( is_float( $n ) ) {
+ $n = (string)$n;
+ }
}
}
return $n;
@@ -509,7 +507,7 @@ class IP {
* Convert a network specification in CIDR notation
* to an integer network and a number of bits
*
- * @param $range String: IP with CIDR prefix
+ * @param string $range IP with CIDR prefix
* @return array(int or string, int)
*/
public static function parseCIDR( $range ) {
@@ -526,7 +524,7 @@ class IP {
if ( $bits == 0 ) {
$network = 0;
} else {
- $network &= ~( ( 1 << ( 32 - $bits ) ) - 1);
+ $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 );
}
# Convert to unsigned
if ( $network < 0 ) {
@@ -551,7 +549,7 @@ class IP {
* 2001:0db8:85a3::7344/96 CIDR
* 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
* 2001:0db8:85a3::7344 Single IP
- * @param $range String: IP range
+ * @param string $range IP range
* @return array(string, string)
*/
public static function parseRange( $range ) {
@@ -692,8 +690,8 @@ class IP {
/**
* Determine if a given IPv4/IPv6 address is in a given CIDR network
*
- * @param $addr String: the address to check against the given range.
- * @param $range String: the range to check the given address against.
+ * @param string $addr the address to check against the given range.
+ * @param string $range the range to check the given address against.
* @return Boolean: whether or not the given address is in the given range.
*/
public static function isInRange( $addr, $range ) {
@@ -710,11 +708,13 @@ class IP {
* This currently only checks a few IPV4-to-IPv6 related cases. More
* unusual representations may be added later.
*
- * @param $addr String: something that might be an IP address
+ * @param string $addr something that might be an IP address
* @return String: valid dotted quad IPv4 address or null
*/
public static function canonicalize( $addr ) {
- $addr = preg_replace( '/\%.*/','', $addr ); // remove zone info (bug 35738)
+ // remove zone info (bug 35738)
+ $addr = preg_replace( '/\%.*/', '', $addr );
+
if ( self::isValid( $addr ) ) {
return $addr;
}
@@ -740,13 +740,13 @@ class IP {
return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
}
- return null; // give up
+ return null; // give up
}
/**
- * Gets rid of uneeded numbers in quad-dotted/octet IP strings
+ * Gets rid of unneeded numbers in quad-dotted/octet IP strings
* For example, 127.111.113.151/24 -> 127.111.113.0/24
- * @param $range String: IP address to normalize
+ * @param string $range IP address to normalize
* @return string
*/
public static function sanitizeRange( $range ) {
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
deleted file mode 100644
index 91c3190f..00000000
--- a/includes/ImageGallery.php
+++ /dev/null
@@ -1,406 +0,0 @@
-<?php
-/**
- * Image gallery.
- *
- * 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
- */
-
-/**
- * Image gallery
- *
- * Add images to the gallery using add(), then render that list to HTML using toHTML().
- *
- * @ingroup Media
- */
-class ImageGallery {
- var $mImages, $mShowBytes, $mShowFilename;
- var $mCaption = false;
-
- /**
- * Hide blacklisted images?
- */
- var $mHideBadImages;
-
- /**
- * Registered parser object for output callbacks
- * @var Parser
- */
- var $mParser;
-
- /**
- * Contextual title, used when images are being screened
- * against the bad image list
- */
- protected $contextTitle = false;
-
- protected $mAttribs = array();
-
- /**
- * Fixed margins
- */
- const THUMB_PADDING = 30;
- const GB_PADDING = 5;
- // 2px borders on each side + 2px implied padding on each side
- const GB_BORDERS = 8;
-
- /**
- * Create a new image gallery object.
- */
- function __construct() {
- global $wgGalleryOptions;
- $this->mImages = array();
- $this->mShowBytes = $wgGalleryOptions['showBytes'];
- $this->mShowFilename = true;
- $this->mParser = false;
- $this->mHideBadImages = false;
- $this->mPerRow = $wgGalleryOptions['imagesPerRow'];
- $this->mWidths = $wgGalleryOptions['imageWidth'];
- $this->mHeights = $wgGalleryOptions['imageHeight'];
- $this->mCaptionLength = $wgGalleryOptions['captionLength'];
- }
-
- /**
- * Register a parser object
- *
- * @param $parser Parser
- */
- function setParser( $parser ) {
- $this->mParser = $parser;
- }
-
- /**
- * Set bad image flag
- */
- function setHideBadImages( $flag = true ) {
- $this->mHideBadImages = $flag;
- }
-
- /**
- * Set the caption (as plain text)
- *
- * @param $caption string Caption
- */
- function setCaption( $caption ) {
- $this->mCaption = htmlspecialchars( $caption );
- }
-
- /**
- * Set the caption (as HTML)
- *
- * @param $caption String: Caption
- */
- public function setCaptionHtml( $caption ) {
- $this->mCaption = $caption;
- }
-
- /**
- * Set how many images will be displayed per row.
- *
- * @param $num Integer >= 0; If perrow=0 the gallery layout will adapt to screensize
- * invalid numbers will be rejected
- */
- public function setPerRow( $num ) {
- if ( $num >= 0 ) {
- $this->mPerRow = (int)$num;
- }
- }
-
- /**
- * Set how wide each image will be, in pixels.
- *
- * @param $num Integer > 0; invalid numbers will be ignored
- */
- public function setWidths( $num ) {
- if ( $num > 0 ) {
- $this->mWidths = (int)$num;
- }
- }
-
- /**
- * Set how high each image will be, in pixels.
- *
- * @param $num Integer > 0; invalid numbers will be ignored
- */
- public function setHeights( $num ) {
- if ( $num > 0 ) {
- $this->mHeights = (int)$num;
- }
- }
-
- /**
- * Instruct the class to use a specific skin for rendering
- *
- * @param $skin Skin object
- * @deprecated since 1.18 Not used anymore
- */
- function useSkin( $skin ) {
- wfDeprecated( __METHOD__, '1.18' );
- /* no op */
- }
-
- /**
- * Add an image to the gallery.
- *
- * @param $title Title object of the image that is added to the gallery
- * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown.
- * @param $alt String: Alt text for the image
- * @param $link String: Override image link (optional)
- */
- function add( $title, $html = '', $alt = '', $link = '') {
- if ( $title instanceof File ) {
- // Old calling convention
- $title = $title->getTitle();
- }
- $this->mImages[] = array( $title, $html, $alt, $link );
- wfDebug( 'ImageGallery::add ' . $title->getText() . "\n" );
- }
-
- /**
- * Add an image at the beginning of the gallery.
- *
- * @param $title Title object of the image that is added to the gallery
- * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown.
- * @param $alt String: Alt text for the image
- */
- function insert( $title, $html = '', $alt = '' ) {
- if ( $title instanceof File ) {
- // Old calling convention
- $title = $title->getTitle();
- }
- array_unshift( $this->mImages, array( &$title, $html, $alt ) );
- }
-
- /**
- * isEmpty() returns true if the gallery contains no images
- * @return bool
- */
- function isEmpty() {
- return empty( $this->mImages );
- }
-
- /**
- * Enable/Disable showing of the file size of an image in the gallery.
- * Enabled by default.
- *
- * @param $f Boolean: set to false to disable.
- */
- function setShowBytes( $f ) {
- $this->mShowBytes = (bool)$f;
- }
-
- /**
- * Enable/Disable showing of the filename of an image in the gallery.
- * Enabled by default.
- *
- * @param $f Boolean: set to false to disable.
- */
- function setShowFilename( $f ) {
- $this->mShowFilename = (bool)$f;
- }
-
- /**
- * Set arbitrary attributes to go on the HTML gallery output element.
- * Should be suitable for a <ul> element.
- *
- * Note -- if taking from user input, you should probably run through
- * Sanitizer::validateAttributes() first.
- *
- * @param $attribs Array of HTML attribute pairs
- */
- function setAttributes( $attribs ) {
- $this->mAttribs = $attribs;
- }
-
- /**
- * Return a HTML representation of the image gallery
- *
- * For each image in the gallery, display
- * - a thumbnail
- * - the image name
- * - the additional text provided when adding the image
- * - the size of the image
- *
- * @return string
- */
- function toHTML() {
- global $wgLang;
-
- if ( $this->mPerRow > 0 ) {
- $maxwidth = $this->mPerRow * ( $this->mWidths + self::THUMB_PADDING + self::GB_PADDING + self::GB_BORDERS );
- $oldStyle = isset( $this->mAttribs['style'] ) ? $this->mAttribs['style'] : '';
- # _width is ignored by any sane browser. IE6 doesn't know max-width so it uses _width instead
- $this->mAttribs['style'] = "max-width: {$maxwidth}px;_width: {$maxwidth}px;" . $oldStyle;
- }
-
- $attribs = Sanitizer::mergeAttributes(
- array( 'class' => 'gallery' ), $this->mAttribs );
-
- $output = Xml::openElement( 'ul', $attribs );
- if ( $this->mCaption ) {
- $output .= "\n\t<li class='gallerycaption'>{$this->mCaption}</li>";
- }
-
- $params = array(
- 'width' => $this->mWidths,
- 'height' => $this->mHeights
- );
- # Output each image...
- foreach ( $this->mImages as $pair ) {
- $nt = $pair[0];
- $text = $pair[1]; # "text" means "caption" here
- $alt = $pair[2];
- $link = $pair[3];
-
- $descQuery = false;
- if ( $nt->getNamespace() == NS_FILE ) {
- # Get the file...
- if ( $this->mParser instanceof Parser ) {
- # Give extensions a chance to select the file revision for us
- $options = array();
- wfRunHooks( 'BeforeParserFetchFileAndTitle',
- array( $this->mParser, $nt, &$options, &$descQuery ) );
- # Fetch and register the file (file title may be different via hooks)
- list( $img, $nt ) = $this->mParser->fetchFileAndTitle( $nt, $options );
- } else {
- $img = wfFindFile( $nt );
- }
- } else {
- $img = false;
- }
-
- if( !$img ) {
- # We're dealing with a non-image, spit out the name and be done with it.
- $thumbhtml = "\n\t\t\t" . '<div style="height: ' . ( self::THUMB_PADDING + $this->mHeights ) . 'px;">'
- . htmlspecialchars( $nt->getText() ) . '</div>';
- } elseif( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) {
- # The image is blacklisted, just show it as a text link.
- $thumbhtml = "\n\t\t\t" . '<div style="height: ' . ( self::THUMB_PADDING + $this->mHeights ) . 'px;">' .
- Linker::link(
- $nt,
- htmlspecialchars( $nt->getText() ),
- array(),
- array(),
- array( 'known', 'noclasses' )
- ) .
- '</div>';
- } elseif( !( $thumb = $img->transform( $params ) ) ) {
- # Error generating thumbnail.
- $thumbhtml = "\n\t\t\t" . '<div style="height: ' . ( self::THUMB_PADDING + $this->mHeights ) . 'px;">'
- . htmlspecialchars( $img->getLastError() ) . '</div>';
- } else {
- $vpad = ( self::THUMB_PADDING + $this->mHeights - $thumb->height ) /2;
-
- $imageParameters = array(
- 'desc-link' => true,
- 'desc-query' => $descQuery,
- 'alt' => $alt,
- 'custom-url-link' => $link
- );
- # In the absence of both alt text and caption, fall back on providing screen readers with the filename as alt text
- if ( $alt == '' && $text == '' ) {
- $imageParameters['alt'] = $nt->getText();
- }
-
- # Set both fixed width and min-height.
- $thumbhtml = "\n\t\t\t" .
- '<div class="thumb" style="width: ' . ( $this->mWidths + self::THUMB_PADDING ) . 'px;">'
- # Auto-margin centering for block-level elements. Needed now that we have video
- # handlers since they may emit block-level elements as opposed to simple <img> tags.
- # ref http://css-discuss.incutio.com/?page=CenteringBlockElement
- . '<div style="margin:' . $vpad . 'px auto;">'
- . $thumb->toHtml( $imageParameters ) . '</div></div>';
-
- // Call parser transform hook
- if ( $this->mParser && $img->getHandler() ) {
- $img->getHandler()->parserTransformHook( $this->mParser, $img );
- }
- }
-
- //TODO
- // $linkTarget = Title::newFromText( $wgContLang->getNsText( MWNamespace::getUser() ) . ":{$ut}" );
- // $ul = Linker::link( $linkTarget, $ut );
-
- if( $this->mShowBytes ) {
- if( $img ) {
- $fileSize = htmlspecialchars( $wgLang->formatSize( $img->getSize() ) );
- } else {
- $fileSize = wfMessage( 'filemissing' )->escaped();
- }
- $fileSize = "$fileSize<br />\n";
- } else {
- $fileSize = '';
- }
-
- $textlink = $this->mShowFilename ?
- Linker::link(
- $nt,
- htmlspecialchars( $wgLang->truncate( $nt->getText(), $this->mCaptionLength ) ),
- array(),
- array(),
- array( 'known', 'noclasses' )
- ) . "<br />\n" :
- '' ;
-
- # ATTENTION: The newline after <div class="gallerytext"> is needed to accommodate htmltidy which
- # in version 4.8.6 generated crackpot html in its absence, see:
- # http://bugzilla.wikimedia.org/show_bug.cgi?id=1765 -Ævar
-
- # Weird double wrapping (the extra div inside the li) needed due to FF2 bug
- # Can be safely removed if FF2 falls completely out of existance
- $output .=
- "\n\t\t" . '<li class="gallerybox" style="width: ' . ( $this->mWidths + self::THUMB_PADDING + self::GB_PADDING ) . 'px">'
- . '<div style="width: ' . ( $this->mWidths + self::THUMB_PADDING + self::GB_PADDING ) . 'px">'
- . $thumbhtml
- . "\n\t\t\t" . '<div class="gallerytext">' . "\n"
- . $textlink . $text . $fileSize
- . "\n\t\t\t</div>"
- . "\n\t\t</div></li>";
- }
- $output .= "\n</ul>";
-
- return $output;
- }
-
- /**
- * @return Integer: number of images in the gallery
- */
- public function count() {
- return count( $this->mImages );
- }
-
- /**
- * Set the contextual title
- *
- * @param $title Title: contextual title
- */
- public function setContextTitle( $title ) {
- $this->contextTitle = $title;
- }
-
- /**
- * Get the contextual title, if applicable
- *
- * @return mixed Title or false
- */
- public function getContextTitle() {
- return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title
- ? $this->contextTitle
- : false;
- }
-
-} //class
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index 6f1b1a15..7ea06b0e 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -50,7 +50,7 @@ class ImagePage extends Article {
/**
* Constructor from a page id
- * @param $id Int article ID to load
+ * @param int $id article ID to load
* @return ImagePage|null
*/
public static function newFromID( $id ) {
@@ -108,7 +108,7 @@ class ImagePage extends Article {
$diff = $request->getVal( 'diff' );
$diffOnly = $request->getBool( 'diffonly', $this->getContext()->getUser()->getOption( 'diffonly' ) );
- if ( $this->getTitle()->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) ) {
+ if ( $this->getTitle()->getNamespace() != NS_FILE || ( $diff !== null && $diffOnly ) ) {
parent::view();
return;
}
@@ -116,7 +116,7 @@ class ImagePage extends Article {
$this->loadFile();
if ( $this->getTitle()->getNamespace() == NS_FILE && $this->mPage->getFile()->getRedirected() ) {
- if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || isset( $diff ) ) {
+ if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || $diff !== null ) {
// mTitle is the same as the redirect target so ask Article
// to perform the redirect for us.
$request->setVal( 'diffonly', 'true' );
@@ -156,8 +156,10 @@ class ImagePage extends Article {
$pageLang = $this->getTitle()->getPageViewLanguage();
$out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
- 'class' => 'mw-content-'.$pageLang->getDir() ) ) );
+ 'class' => 'mw-content-' . $pageLang->getDir() ) ) );
+
parent::view();
+
$out->addHTML( Xml::closeElement( 'div' ) );
} else {
# Just need to set the right headers
@@ -204,7 +206,7 @@ class ImagePage extends Article {
}
// Add remote Filepage.css
- if( !$this->repo->isLocal() ) {
+ if ( !$this->repo->isLocal() ) {
$css = $this->repo->getDescriptionStylesheetUrl();
if ( $css ) {
$out->addStyle( $css );
@@ -248,7 +250,7 @@ class ImagePage extends Article {
*
* @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata().
*
- * @param $metadata Array: the array containing the EXIF data
+ * @param array $metadata the array containing the Exif data
* @return String The metadata table. This is treated as Wikitext (!)
*/
protected function makeMetadataTable( $metadata ) {
@@ -260,7 +262,7 @@ class ImagePage extends Article {
# @todo FIXME: Why is this using escapeId for a class?!
$class = Sanitizer::escapeId( $v['id'] );
if ( $type == 'collapsed' ) {
- $class .= ' collapsable';
+ $class .= ' collapsable'; // sic
}
$r .= "<tr class=\"$class\">\n";
$r .= "<th>{$v['name']}</th>\n";
@@ -272,18 +274,18 @@ class ImagePage extends Article {
}
/**
- * Overloading Article's getContent method.
+ * Overloading Article's getContentObject method.
*
* Omit noarticletext if sharedupload; text will be fetched from the
* shared upload server if possible.
* @return string
*/
- public function getContent() {
+ public function getContentObject() {
$this->loadFile();
if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
- return '';
+ return null;
}
- return parent::getContent();
+ return parent::getContentObject();
}
protected function openShowImage() {
@@ -296,18 +298,7 @@ class ImagePage extends Article {
$dirmark = $lang->getDirMarkEntity();
$request = $this->getContext()->getRequest();
- $sizeSel = intval( $user->getOption( 'imagesize' ) );
- if ( !isset( $wgImageLimits[$sizeSel] ) ) {
- $sizeSel = User::getDefaultOption( 'imagesize' );
-
- // The user offset might still be incorrect, specially if
- // $wgImageLimits got changed (see bug #8858).
- if ( !isset( $wgImageLimits[$sizeSel] ) ) {
- // Default to the first offset in $wgImageLimits
- $sizeSel = 0;
- }
- }
- $max = $wgImageLimits[$sizeSel];
+ $max = $this->getImageLimitsFromOption( $user, 'imagesize' );
$maxWidth = $max[0];
$maxHeight = $max[1];
@@ -320,12 +311,19 @@ class ImagePage extends Article {
} else {
$params = array( 'page' => $page );
}
+
+ $renderLang = $request->getVal( 'lang' );
+ if ( !is_null( $renderLang ) ) {
+ $params['lang'] = $renderLang;
+ }
+
$width_orig = $this->displayImg->getWidth( $page );
$width = $width_orig;
$height_orig = $this->displayImg->getHeight( $page );
$height = $height_orig;
- $longDesc = wfMessage( 'parentheses', $this->displayImg->getLongDesc() )->text();
+ $filename = wfEscapeWikiText( $this->displayImg->getName() );
+ $linktext = $filename;
wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$out ) );
@@ -338,27 +336,24 @@ class ImagePage extends Article {
if ( $width > $maxWidth || $height > $maxHeight ) {
# Calculate the thumbnail size.
# First case, the limiting factor is the width, not the height.
- if ( $width / $height >= $maxWidth / $maxHeight ) {
- $height = round( $height * $maxWidth / $width );
+ if ( $width / $height >= $maxWidth / $maxHeight ) { // FIXME: Possible division by 0. bug 36911
+ $height = round( $height * $maxWidth / $width ); // FIXME: Possible division by 0. bug 36911
$width = $maxWidth;
# Note that $height <= $maxHeight now.
} else {
- $newwidth = floor( $width * $maxHeight / $height );
- $height = round( $height * $newwidth / $width );
+ $newwidth = floor( $width * $maxHeight / $height ); // FIXME: Possible division by 0. bug 36911
+ $height = round( $height * $newwidth / $width ); // FIXME: Possible division by 0. bug 36911
$width = $newwidth;
# Note that $height <= $maxHeight now, but might not be identical
# because of rounding.
}
- $msgbig = wfMessage( 'show-big-image' )->escaped();
+ $linktext = wfMessage( 'show-big-image' )->escaped();
if ( $this->displayImg->getRepo()->canTransformVia404() ) {
$thumbSizes = $wgImageLimits;
} else {
# Creating thumb links triggers thumbnail generation.
# Just generate the thumb for the current users prefs.
- $thumbOption = $user->getOption( 'thumbsize' );
- $thumbSizes = array( isset( $wgImageLimits[$thumbOption] )
- ? $wgImageLimits[$thumbOption]
- : $wgImageLimits[User::getDefaultOption( 'thumbsize' )] );
+ $thumbSizes = array( $this->getImageLimitsFromOption( $user, 'thumbsize' ) );
}
# Generate thumbnails or thumbnail links as needed...
$otherSizes = array();
@@ -366,12 +361,19 @@ class ImagePage extends Article {
if ( $size[0] < $width_orig && $size[1] < $height_orig
&& $size[0] != $width && $size[1] != $height )
{
- $otherSizes[] = $this->makeSizeLink( $params, $size[0], $size[1] );
+ $sizeLink = $this->makeSizeLink( $params, $size[0], $size[1] );
+ if ( $sizeLink ) {
+ $otherSizes[] = $sizeLink;
+ }
}
}
- $msgsmall = wfMessage( 'show-big-image-preview' )->
- rawParams( $this->makeSizeLink( $params, $width, $height ) )->
- parse();
+ $msgsmall = '';
+ $sizeLinkBigImagePreview = $this->makeSizeLink( $params, $width, $height );
+ if ( $sizeLinkBigImagePreview ) {
+ $msgsmall .= wfMessage( 'show-big-image-preview' )->
+ rawParams( $sizeLinkBigImagePreview )->
+ parse();
+ }
if ( count( $otherSizes ) ) {
$msgsmall .= ' ' .
Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ),
@@ -379,7 +381,7 @@ class ImagePage extends Article {
params( count( $otherSizes ) )->parse()
);
}
- } elseif ( $width == 0 && $height == 0 ){
+ } elseif ( $width == 0 && $height == 0 ) {
# Some sort of audio file that doesn't have dimensions
# Don't output a no hi res message for such a file
$msgsmall = '';
@@ -395,11 +397,11 @@ class ImagePage extends Article {
$params['height'] = $height;
$thumbnail = $this->displayImg->transform( $params );
- $showLink = true;
$anchorclose = Html::rawElement( 'div', array( 'class' => 'mw-filepage-resolutioninfo' ), $msgsmall );
$isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1;
if ( $isMulti ) {
+ $out->addModules( 'mediawiki.page.image.pagination' );
$out->addHTML( '<table class="multipageimage"><tr><td>' );
}
@@ -449,7 +451,6 @@ class ImagePage extends Article {
$formParams = array(
'name' => 'pageselector',
'action' => $wgScript,
- 'onchange' => 'document.pageselector.submit();',
);
$options = array();
for ( $i = 1; $i <= $count; $i++ ) {
@@ -469,48 +470,39 @@ class ImagePage extends Article {
"<hr />$thumb1\n$thumb2<br style=\"clear: both\" /></div></td></tr></table>"
);
}
- } else {
+ } elseif ( $this->displayImg->isSafeFile() ) {
# if direct link is allowed but it's not a renderable image, show an icon.
- if ( $this->displayImg->isSafeFile() ) {
- $icon = $this->displayImg->iconThumb();
-
- $out->addHTML( '<div class="fullImageLink" id="file">' .
- $icon->toHtml( array( 'file-link' => true ) ) .
- "</div>\n" );
- }
+ $icon = $this->displayImg->iconThumb();
- $showLink = true;
+ $out->addHTML( '<div class="fullImageLink" id="file">' .
+ $icon->toHtml( array( 'file-link' => true ) ) .
+ "</div>\n" );
}
- if ( $showLink ) {
- $filename = wfEscapeWikiText( $this->displayImg->getName() );
- $linktext = $filename;
- if ( isset( $msgbig ) ) {
- $linktext = wfEscapeWikiText( $msgbig );
- }
- $medialink = "[[Media:$filename|$linktext]]";
-
- if ( !$this->displayImg->isSafeFile() ) {
- $warning = wfMessage( 'mediawarning' )->plain();
- // dirmark is needed here to separate the file name, which
- // most likely ends in Latin characters, from the description,
- // which may begin with the file type. In RTL environment
- // this will get messy.
- // The dirmark, however, must not be immediately adjacent
- // to the filename, because it can get copied with it.
- // See bug 25277.
- $out->addWikiText( <<<EOT
+ $longDesc = wfMessage( 'parentheses', $this->displayImg->getLongDesc() )->text();
+
+ $medialink = "[[Media:$filename|$linktext]]";
+
+ if ( !$this->displayImg->isSafeFile() ) {
+ $warning = wfMessage( 'mediawarning' )->plain();
+ // dirmark is needed here to separate the file name, which
+ // most likely ends in Latin characters, from the description,
+ // which may begin with the file type. In RTL environment
+ // this will get messy.
+ // The dirmark, however, must not be immediately adjacent
+ // to the filename, because it can get copied with it.
+ // See bug 25277.
+ $out->addWikiText( <<<EOT
<div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div>
<div class="mediaWarning">$warning</div>
EOT
- );
- } else {
- $out->addWikiText( <<<EOT
+ );
+ } else {
+ $out->addWikiText( <<<EOT
<div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span>
</div>
EOT
- );
- }
+ );
}
// Add cannot animate thumbnail warning
@@ -545,7 +537,7 @@ EOT
array( 'delete', 'move' ),
$this->getTitle()->getPrefixedText(),
'',
- array( 'lim' => 10,
+ array( 'lim' => 10,
'conds' => array( "log_action != 'revision'" ),
'showIfEmpty' => false,
'msgKey' => array( 'moveddeleted-notice' )
@@ -564,7 +556,7 @@ EOT
$nofile = 'filepage-nofile';
}
// Note, if there is an image description page, but
- // no image, then this setRobotPolicy is overriden
+ // no image, then this setRobotPolicy is overridden
// by Article::View().
$out->setRobotPolicy( 'noindex,nofollow' );
$out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
@@ -579,7 +571,7 @@ EOT
/**
* Creates an thumbnail of specified size and returns an HTML link to it
- * @param $params array Scaler parameters
+ * @param array $params Scaler parameters
* @param $width int
* @param $height int
* @return string
@@ -611,14 +603,14 @@ EOT
$descText = $this->mPage->getFile()->getDescriptionText();
/* Add canonical to head if there is no local page for this shared file */
- if( $descUrl && $this->mPage->getID() == 0 ) {
- $out->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) );
+ if ( $descUrl && $this->mPage->getID() == 0 ) {
+ $out->setCanonicalUrl( $descUrl );
}
$wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n";
$repo = $this->mPage->getFile()->getRepo()->getDisplayName();
- if ( $descUrl && $descText && wfMessage( 'sharedupload-desc-here' )->plain() !== '-' ) {
+ if ( $descUrl && $descText && wfMessage( 'sharedupload-desc-here' )->plain() !== '-' ) {
$out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) );
} elseif ( $descUrl && wfMessage( 'sharedupload-desc-there' )->plain() !== '-' ) {
$out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) );
@@ -637,7 +629,7 @@ EOT
return $uploadTitle->getFullURL( array(
'wpDestFile' => $this->mPage->getFile()->getName(),
'wpForReUpload' => 1
- ) );
+ ) );
}
/**
@@ -645,7 +637,7 @@ EOT
* external editing (and instructions link) etc.
*/
protected function uploadLinksBox() {
- global $wgEnableUploads, $wgUseExternalEditor;
+ global $wgEnableUploads;
if ( !$wgEnableUploads ) {
return;
@@ -668,25 +660,6 @@ EOT
$out->addHTML( "<li id=\"mw-imagepage-upload-disallowed\">" . $this->getContext()->msg( 'upload-disallowed-here' )->escaped() . "</li>\n" );
}
- # External editing link
- if ( $wgUseExternalEditor ) {
- $elink = Linker::linkKnown(
- $this->getTitle(),
- wfMessage( 'edit-externally' )->escaped(),
- array(),
- array(
- 'action' => 'edit',
- 'externaledit' => 'true',
- 'mode' => 'file'
- )
- );
- $out->addHTML(
- '<li id="mw-imagepage-edit-external">' . $elink . ' <small>' .
- wfMessage( 'edit-externally-help' )->parse() .
- "</small></li>\n"
- );
- }
-
$out->addHTML( "</ul>\n" );
}
@@ -733,7 +706,7 @@ EOT
$limit = 100;
$out = $this->getContext()->getOutput();
- $res = $this->queryImageLinks( $this->getTitle()->getDbKey(), $limit + 1);
+ $res = $this->queryImageLinks( $this->getTitle()->getDBkey(), $limit + 1 );
$rows = array();
$redirects = array();
foreach ( $res as $row ) {
@@ -786,17 +759,30 @@ EOT
// Create links for every element
$currentCount = 0;
- foreach( $rows as $element ) {
+ foreach ( $rows as $element ) {
$currentCount++;
if ( $currentCount > $limit ) {
break;
}
- $link = Linker::linkKnown( Title::makeTitle( $element->page_namespace, $element->page_title ) );
+ $query = array();
+ # Add a redirect=no to make redirect pages reachable
+ if ( isset( $redirects[$element->page_title] ) ) {
+ $query['redirect'] = 'no';
+ }
+ $link = Linker::linkKnown(
+ Title::makeTitle( $element->page_namespace, $element->page_title ),
+ null, array(), $query
+ );
if ( !isset( $redirects[$element->page_title] ) ) {
+ # No redirects
$liContents = $link;
+ } elseif ( count( $redirects[$element->page_title] ) === 0 ) {
+ # Redirect without usages
+ $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams( $link, '' )->parse();
} else {
- $ul = "<ul class='mw-imagepage-redirectstofile'>\n";
+ # Redirect with usages
+ $li = '';
foreach ( $redirects[$element->page_title] as $row ) {
$currentCount++;
if ( $currentCount > $limit ) {
@@ -804,13 +790,18 @@ EOT
}
$link2 = Linker::linkKnown( Title::makeTitle( $row->page_namespace, $row->page_title ) );
- $ul .= Html::rawElement(
+ $li .= Html::rawElement(
'li',
array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
$link2
) . "\n";
}
- $ul .= '</ul>';
+
+ $ul = Html::rawElement(
+ 'ul',
+ array( 'class' => 'mw-imagepage-redirectstofile' ),
+ $li
+ ) . "\n";
$liContents = wfMessage( 'linkstoimage-redirect' )->rawParams(
$link, $ul )->parse();
}
@@ -908,6 +899,34 @@ EOT
return $a->page_namespace - $b->page_namespace;
}
}
+
+ /**
+ * Returns the corresponding $wgImageLimits entry for the selected user option
+ *
+ * @param $user User
+ * @param string $optionName Name of a option to check, typically imagesize or thumbsize
+ * @return array
+ * @since 1.21
+ */
+ public function getImageLimitsFromOption( $user, $optionName ) {
+ global $wgImageLimits;
+
+ $option = $user->getIntOption( $optionName );
+ if ( !isset( $wgImageLimits[$option] ) ) {
+ $option = User::getDefaultOption( $optionName );
+ }
+
+ // The user offset might still be incorrect, specially if
+ // $wgImageLimits got changed (see bug #8858).
+ if ( !isset( $wgImageLimits[$option] ) ) {
+ // Default to the first offset in $wgImageLimits
+ $option = 0;
+ }
+
+ return isset( $wgImageLimits[$option] )
+ ? $wgImageLimits[$option]
+ : array( 800, 600 ); // if nothing is set, fallback to a hardcoded default
+ }
}
/**
@@ -1041,9 +1060,9 @@ class ImageHistoryList extends ContextSource {
} else {
list( $ts, ) = explode( '!', $img, 2 );
$query = array(
- 'type' => 'oldimage',
+ 'type' => 'oldimage',
'target' => $this->title->getPrefixedText(),
- 'ids' => $ts,
+ 'ids' => $ts,
);
$del = Linker::revDeleteLink( $query,
$file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
@@ -1163,7 +1182,7 @@ class ImageHistoryList extends ContextSource {
protected function getThumbForLine( $file ) {
$lang = $this->getLanguage();
$user = $this->getUser();
- if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE,$user )
+ if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE, $user )
&& !$file->isDeleted( File::DELETED_FILE ) )
{
$params = array(
@@ -1223,7 +1242,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager {
* @param ImagePage $imagePage
*/
function __construct( $imagePage ) {
- parent::__construct();
+ parent::__construct( $imagePage->getContext() );
$this->mImagePage = $imagePage;
$this->mTitle = clone ( $imagePage->getTitle() );
$this->mTitle->setFragment( '#filehistory' );
diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php
index f9f6ceed..75f7ba64 100644
--- a/includes/ImageQueryPage.php
+++ b/includes/ImageQueryPage.php
@@ -29,25 +29,25 @@
* @author Rob Church <robchur@gmail.com>
*/
abstract class ImageQueryPage extends QueryPage {
-
/**
* Format and output report results using the given information plus
* OutputPage
*
- * @param $out OutputPage to print to
- * @param $skin Skin: user skin to use [unused]
- * @param $dbr DatabaseBase (read) connection to use
- * @param $res Integer: result pointer
- * @param $num Integer: number of available result rows
- * @param $offset Integer: paging offset
+ * @param OutputPage $out OutputPage to print to
+ * @param Skin $skin User skin to use [unused]
+ * @param DatabaseBase $dbr (read) connection to use
+ * @param int $res Result pointer
+ * @param int $num Number of available result rows
+ * @param int $offset Paging offset
*/
protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
- if( $num > 0 ) {
- $gallery = new ImageGallery();
+ if ( $num > 0 ) {
+ $gallery = ImageGalleryBase::factory();
+ $gallery->setContext( $this->getContext() );
# $res might contain the whole 1,000 rows, so we read up to
# $num [should update this to use a Pager]
- for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
+ for ( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
$namespace = isset( $row->namespace ) ? $row->namespace : NS_FILE;
$title = Title::makeTitleSafe( $namespace, $row->title );
if ( $title instanceof Title && $title->getNamespace() == NS_FILE ) {
@@ -65,11 +65,10 @@ abstract class ImageQueryPage extends QueryPage {
/**
* Get additional HTML to be shown in a results' cell
*
- * @param $row Object: result row
- * @return String
+ * @param object $row Result row
+ * @return string
*/
protected function getCellHtml( $row ) {
return '';
}
-
}
diff --git a/includes/Import.php b/includes/Import.php
index 480239fe..8b7af02a 100644
--- a/includes/Import.php
+++ b/includes/Import.php
@@ -47,7 +47,7 @@ class WikiImporter {
stream_wrapper_register( 'uploadsource', 'UploadSourceAdapter' );
$id = UploadSourceAdapter::registerSource( $source );
- if (defined( 'LIBXML_PARSEHUGE' ) ) {
+ if ( defined( 'LIBXML_PARSEHUGE' ) ) {
$this->reader->open( "uploadsource://$id", null, LIBXML_PARSEHUGE );
} else {
$this->reader->open( "uploadsource://$id" );
@@ -66,7 +66,7 @@ class WikiImporter {
}
private function debug( $data ) {
- if( $this->mDebug ) {
+ if ( $this->mDebug ) {
wfDebug( "IMPORT: $data\n" );
}
}
@@ -188,10 +188,10 @@ class WikiImporter {
* @return bool
*/
public function setTargetNamespace( $namespace ) {
- if( is_null( $namespace ) ) {
+ if ( is_null( $namespace ) ) {
// Don't override namespaces
$this->mTargetNamespace = null;
- } elseif( $namespace >= 0 ) {
+ } elseif ( $namespace >= 0 ) {
// @todo FIXME: Check for validity
$this->mTargetNamespace = intval( $namespace );
} else {
@@ -206,16 +206,16 @@ class WikiImporter {
*/
public function setTargetRootPage( $rootpage ) {
$status = Status::newGood();
- if( is_null( $rootpage ) ) {
+ if ( is_null( $rootpage ) ) {
// No rootpage
$this->mTargetRootPage = null;
- } elseif( $rootpage !== '' ) {
+ } elseif ( $rootpage !== '' ) {
$rootpage = rtrim( $rootpage, '/' ); //avoid double slashes
$title = Title::newFromText( $rootpage, !is_null( $this->mTargetNamespace ) ? $this->mTargetNamespace : NS_MAIN );
- if( !$title || $title->isExternal() ) {
+ if ( !$title || $title->isExternal() ) {
$status->fatal( 'import-rootpage-invalid' );
} else {
- if( !MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ if ( !MWNamespace::hasSubpages( $title->getNamespace() ) ) {
global $wgContLang;
$displayNSText = $title->getNamespace() == NS_MAIN
@@ -225,7 +225,7 @@ class WikiImporter {
} else {
// set namespace to 'all', so the namespace check in processTitle() can passed
$this->setTargetNamespace( null );
- $this->mTargetRootPage = $title->getPrefixedDBKey();
+ $this->mTargetRootPage = $title->getPrefixedDBkey();
}
}
}
@@ -252,8 +252,16 @@ class WikiImporter {
* @return bool
*/
public function importRevision( $revision ) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
+ try {
+ $dbw = wfGetDB( DB_MASTER );
+ return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
+ } catch ( MWContentSerializationException $ex ) {
+ $this->notice( 'import-error-unserialize',
+ $revision->getTitle()->getPrefixedText(),
+ $revision->getID(),
+ $revision->getModel(),
+ $revision->getFormat() );
+ }
}
/**
@@ -296,7 +304,7 @@ class WikiImporter {
*/
public function debugRevisionHandler( &$revision ) {
$this->debug( "Got revision:" );
- if( is_object( $revision->title ) ) {
+ if ( is_object( $revision->title ) ) {
$this->debug( "-- Title: " . $revision->title->getPrefixedText() );
} else {
$this->debug( "-- Title: <invalid>" );
@@ -312,7 +320,7 @@ class WikiImporter {
* @param $title Title
*/
function pageCallback( $title ) {
- if( isset( $this->mPageCallback ) ) {
+ if ( isset( $this->mPageCallback ) ) {
call_user_func( $this->mPageCallback, $title );
}
}
@@ -322,11 +330,11 @@ class WikiImporter {
* @param $title Title
* @param $origTitle Title
* @param $revCount Integer
- * @param $sucCount Int: number of revisions for which callback returned true
- * @param $pageInfo Array: associative array of page information
+ * @param int $sucCount number of revisions for which callback returned true
+ * @param array $pageInfo associative array of page information
*/
private function pageOutCallback( $title, $origTitle, $revCount, $sucCount, $pageInfo ) {
- if( isset( $this->mPageOutCallback ) ) {
+ if ( isset( $this->mPageOutCallback ) ) {
$args = func_get_args();
call_user_func_array( $this->mPageOutCallback, $args );
}
@@ -368,12 +376,12 @@ class WikiImporter {
* @access private
*/
private function nodeContents() {
- if( $this->reader->isEmptyElement ) {
+ if ( $this->reader->isEmptyElement ) {
return "";
}
$buffer = "";
- while( $this->reader->read() ) {
- switch( $this->reader->nodeType ) {
+ while ( $this->reader->read() ) {
+ switch ( $this->reader->nodeType ) {
case XmlReader::TEXT:
case XmlReader::SIGNIFICANT_WHITESPACE:
$buffer .= $this->reader->value;
@@ -392,7 +400,7 @@ class WikiImporter {
/** Left in for debugging */
private function dumpElement() {
static $lookup = null;
- if (!$lookup) {
+ if ( !$lookup ) {
$xmlReaderConstants = array(
"NONE",
"ELEMENT",
@@ -412,23 +420,24 @@ class WikiImporter {
"END_ELEMENT",
"END_ENTITY",
"XML_DECLARATION",
- );
+ );
$lookup = array();
- foreach( $xmlReaderConstants as $name ) {
- $lookup[constant("XmlReader::$name")] = $name;
+ foreach ( $xmlReaderConstants as $name ) {
+ $lookup[constant( "XmlReader::$name" )] = $name;
}
}
- print( var_dump(
+ print var_dump(
$lookup[$this->reader->nodeType],
$this->reader->name,
$this->reader->value
- )."\n\n" );
+ ) . "\n\n";
}
/**
* Primary entry point
+ * @throws MWException
* @return bool
*/
public function doImport() {
@@ -454,7 +463,7 @@ class WikiImporter {
$tag = $this->reader->name;
$type = $this->reader->nodeType;
- if ( !wfRunHooks( 'ImportHandleToplevelXMLTag', $this ) ) {
+ if ( !wfRunHooks( 'ImportHandleToplevelXMLTag', array( $this ) ) ) {
// Do nothing
} elseif ( $tag == 'mediawiki' && $type == XmlReader::END_ELEMENT ) {
break;
@@ -470,7 +479,7 @@ class WikiImporter {
$skip = true;
}
- if ($skip) {
+ if ( $skip ) {
$keepReading = $this->reader->next();
$skip = false;
$this->debug( "Skip" );
@@ -507,14 +516,15 @@ class WikiImporter {
while ( $this->reader->read() ) {
if ( $this->reader->nodeType == XmlReader::END_ELEMENT &&
- $this->reader->name == 'logitem') {
+ $this->reader->name == 'logitem' ) {
break;
}
$tag = $this->reader->name;
- if ( !wfRunHooks( 'ImportHandleLogItemXMLTag',
- $this, $logInfo ) ) {
+ if ( !wfRunHooks( 'ImportHandleLogItemXMLTag', array(
+ $this, $logInfo
+ ) ) ) {
// Do nothing
} elseif ( in_array( $tag, $normalFields ) ) {
$logInfo[$tag] = $this->nodeContents();
@@ -570,7 +580,7 @@ class WikiImporter {
while ( $skip ? $this->reader->next() : $this->reader->read() ) {
if ( $this->reader->nodeType == XmlReader::END_ELEMENT &&
- $this->reader->name == 'page') {
+ $this->reader->name == 'page' ) {
break;
}
@@ -618,20 +628,21 @@ class WikiImporter {
$this->debug( "Enter revision handler" );
$revisionInfo = array();
- $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'text' );
+ $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'model', 'format', 'text' );
$skip = false;
while ( $skip ? $this->reader->next() : $this->reader->read() ) {
if ( $this->reader->nodeType == XmlReader::END_ELEMENT &&
- $this->reader->name == 'revision') {
+ $this->reader->name == 'revision' ) {
break;
}
$tag = $this->reader->name;
- if ( !wfRunHooks( 'ImportHandleRevisionXMLTag', $this,
- $pageInfo, $revisionInfo ) ) {
+ if ( !wfRunHooks( 'ImportHandleRevisionXMLTag', array(
+ $this, $pageInfo, $revisionInfo
+ ) ) ) {
// Do nothing
} elseif ( in_array( $tag, $normalFields ) ) {
$revisionInfo[$tag] = $this->nodeContents();
@@ -657,12 +668,18 @@ class WikiImporter {
private function processRevision( $pageInfo, $revisionInfo ) {
$revision = new WikiRevision;
- if( isset( $revisionInfo['id'] ) ) {
+ if ( isset( $revisionInfo['id'] ) ) {
$revision->setID( $revisionInfo['id'] );
}
if ( isset( $revisionInfo['text'] ) ) {
$revision->setText( $revisionInfo['text'] );
}
+ if ( isset( $revisionInfo['model'] ) ) {
+ $revision->setModel( $revisionInfo['model'] );
+ }
+ if ( isset( $revisionInfo['format'] ) ) {
+ $revision->setFormat( $revisionInfo['format'] );
+ }
$revision->setTitle( $pageInfo['_title'] );
if ( isset( $revisionInfo['timestamp'] ) ) {
@@ -704,14 +721,15 @@ class WikiImporter {
while ( $skip ? $this->reader->next() : $this->reader->read() ) {
if ( $this->reader->nodeType == XmlReader::END_ELEMENT &&
- $this->reader->name == 'upload') {
+ $this->reader->name == 'upload' ) {
break;
}
$tag = $this->reader->name;
- if ( !wfRunHooks( 'ImportHandleUploadXMLTag', $this,
- $pageInfo ) ) {
+ if ( !wfRunHooks( 'ImportHandleUploadXMLTag', array(
+ $this, $pageInfo
+ ) ) ) {
// Do nothing
} elseif ( in_array( $tag, $normalFields ) ) {
$uploadInfo[$tag] = $this->nodeContents();
@@ -801,7 +819,7 @@ class WikiImporter {
while ( $this->reader->read() ) {
if ( $this->reader->nodeType == XmlReader::END_ELEMENT &&
- $this->reader->name == 'contributor') {
+ $this->reader->name == 'contributor' ) {
break;
}
@@ -825,33 +843,33 @@ class WikiImporter {
$workTitle = $text;
$origTitle = Title::newFromText( $workTitle );
- if( !is_null( $this->mTargetNamespace ) && !is_null( $origTitle ) ) {
+ if ( !is_null( $this->mTargetNamespace ) && !is_null( $origTitle ) ) {
# makeTitleSafe, because $origTitle can have a interwiki (different setting of interwiki map)
# and than dbKey can begin with a lowercase char
$title = Title::makeTitleSafe( $this->mTargetNamespace,
$origTitle->getDBkey() );
} else {
- if( !is_null( $this->mTargetRootPage ) ) {
+ if ( !is_null( $this->mTargetRootPage ) ) {
$workTitle = $this->mTargetRootPage . '/' . $workTitle;
}
$title = Title::newFromText( $workTitle );
}
- if( is_null( $title ) ) {
+ if ( is_null( $title ) ) {
# Invalid page title? Ignore the page
$this->notice( 'import-error-invalid', $workTitle );
return false;
- } elseif( $title->isExternal() ) {
+ } elseif ( $title->isExternal() ) {
$this->notice( 'import-error-interwiki', $title->getPrefixedText() );
return false;
- } elseif( !$title->canExist() ) {
+ } elseif ( !$title->canExist() ) {
$this->notice( 'import-error-special', $title->getPrefixedText() );
return false;
- } elseif( !$title->userCan( 'edit' ) && !$wgCommandLineMode ) {
+ } elseif ( !$title->userCan( 'edit' ) && !$wgCommandLineMode ) {
# Do not import if the importing wiki user cannot edit this page
$this->notice( 'import-error-edit', $title->getPrefixedText() );
return false;
- } elseif( !$title->exists() && !$title->userCan( 'create' ) && !$wgCommandLineMode ) {
+ } elseif ( !$title->exists() && !$title->userCan( 'create' ) && !$wgCommandLineMode ) {
# Do not import if the importing wiki user cannot create this page
$this->notice( 'import-error-create', $title->getPrefixedText() );
return false;
@@ -889,7 +907,7 @@ class UploadSourceAdapter {
* @return bool
*/
function stream_open( $path, $mode, $options, &$opened_path ) {
- $url = parse_url($path);
+ $url = parse_url( $path );
$id = $url['host'];
if ( !isset( self::$sourceRegistrations[$id] ) ) {
@@ -910,22 +928,22 @@ class UploadSourceAdapter {
$leave = false;
while ( !$leave && !$this->mSource->atEnd() &&
- strlen($this->mBuffer) < $count ) {
+ strlen( $this->mBuffer ) < $count ) {
$read = $this->mSource->readChunk();
- if ( !strlen($read) ) {
+ if ( !strlen( $read ) ) {
$leave = true;
}
$this->mBuffer .= $read;
}
- if ( strlen($this->mBuffer) ) {
+ if ( strlen( $this->mBuffer ) ) {
$return = substr( $this->mBuffer, 0, $count );
$this->mBuffer = substr( $this->mBuffer, $count );
}
- $this->mPosition += strlen($return);
+ $this->mPosition += strlen( $return );
return $return;
}
@@ -982,12 +1000,12 @@ class XMLReader2 extends XMLReader {
* @return bool|string
*/
function nodeContents() {
- if( $this->isEmptyElement ) {
+ if ( $this->isEmptyElement ) {
return "";
}
$buffer = "";
- while( $this->read() ) {
- switch( $this->nodeType ) {
+ while ( $this->read() ) {
+ switch ( $this->nodeType ) {
case XmlReader::TEXT:
case XmlReader::SIGNIFICANT_WHITESPACE:
$buffer .= $this->value;
@@ -1015,7 +1033,10 @@ class WikiRevision {
var $timestamp = "20010115000000";
var $user = 0;
var $user_text = "";
+ var $model = null;
+ var $format = null;
var $text = "";
+ var $content = null;
var $comment = "";
var $minor = false;
var $type = "";
@@ -1033,9 +1054,9 @@ class WikiRevision {
* @throws MWException
*/
function setTitle( $title ) {
- if( is_object( $title ) ) {
+ if ( is_object( $title ) ) {
$this->title = $title;
- } elseif( is_null( $title ) ) {
+ } elseif ( is_null( $title ) ) {
throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." );
} else {
throw new MWException( "WikiRevision given non-object title in import." );
@@ -1072,6 +1093,20 @@ class WikiRevision {
}
/**
+ * @param $model
+ */
+ function setModel( $model ) {
+ $this->model = $model;
+ }
+
+ /**
+ * @param $format
+ */
+ function setFormat( $format ) {
+ $this->format = $format;
+ }
+
+ /**
* @param $text
*/
function setText( $text ) {
@@ -1194,12 +1229,55 @@ class WikiRevision {
/**
* @return string
+ *
+ * @deprecated Since 1.21, use getContent() instead.
*/
function getText() {
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
return $this->text;
}
/**
+ * @return Content
+ */
+ function getContent() {
+ if ( is_null( $this->content ) ) {
+ $this->content =
+ ContentHandler::makeContent(
+ $this->text,
+ $this->getTitle(),
+ $this->getModel(),
+ $this->getFormat()
+ );
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * @return String
+ */
+ function getModel() {
+ if ( is_null( $this->model ) ) {
+ $this->model = $this->getTitle()->getContentModel();
+ }
+
+ return $this->model;
+ }
+
+ /**
+ * @return String
+ */
+ function getFormat() {
+ if ( is_null( $this->model ) ) {
+ $this->format = ContentHandler::getForTitle( $this->getTitle() )->getDefaultFormat();
+ }
+
+ return $this->format;
+ }
+
+ /**
* @return string
*/
function getComment() {
@@ -1294,7 +1372,7 @@ class WikiRevision {
# Sneak a single revision into place
$user = User::newFromName( $this->getUser() );
- if( $user ) {
+ if ( $user ) {
$userId = intval( $user->getId() );
$userText = $user->getName();
$userObj = $user;
@@ -1309,7 +1387,7 @@ class WikiRevision {
$linkCache->clear();
$page = WikiPage::factory( $this->title );
- if( !$page->exists() ) {
+ if ( !$page->exists() ) {
# must create the page...
$pageId = $page->insertOn( $dbw );
$created = true;
@@ -1322,10 +1400,10 @@ class WikiRevision {
array( 'rev_page' => $pageId,
'rev_timestamp' => $dbw->timestamp( $this->timestamp ),
'rev_user_text' => $userText,
- 'rev_comment' => $this->getComment() ),
+ 'rev_comment' => $this->getComment() ),
__METHOD__
);
- if( $prior ) {
+ if ( $prior ) {
// @todo FIXME: This could fail slightly for multiple matches :P
wfDebug( __METHOD__ . ": skipping existing revision for [[" .
$this->title->getPrefixedText() . "]], timestamp " . $this->timestamp . "\n" );
@@ -1337,12 +1415,15 @@ class WikiRevision {
# @todo FIXME: Use original rev_id optionally (better for backups)
# Insert the row
$revision = new Revision( array(
- 'page' => $pageId,
- 'text' => $this->getText(),
- 'comment' => $this->getComment(),
- 'user' => $userId,
- 'user_text' => $userText,
- 'timestamp' => $this->timestamp,
+ 'title' => $this->title,
+ 'page' => $pageId,
+ 'content_model' => $this->getModel(),
+ 'content_format' => $this->getFormat(),
+ 'text' => $this->getContent()->serialize( $this->getFormat() ), //XXX: just set 'content' => $this->getContent()?
+ 'comment' => $this->getComment(),
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => $this->timestamp,
'minor_edit' => $this->minor,
) );
$revision->insertOn( $dbw );
@@ -1362,7 +1443,7 @@ class WikiRevision {
function importLogItem() {
$dbw = wfGetDB( DB_MASTER );
# @todo FIXME: This will not record autoblocks
- if( !$this->getTitle() ) {
+ if ( !$this->getTitle() ) {
wfDebug( __METHOD__ . ": skipping invalid {$this->type}/{$this->action} log time, timestamp " .
$this->timestamp . "\n" );
return;
@@ -1371,17 +1452,17 @@ class WikiRevision {
// @todo FIXME: Use original log ID (better for backups)
$prior = $dbw->selectField( 'logging', '1',
array( 'log_type' => $this->getType(),
- 'log_action' => $this->getAction(),
+ 'log_action' => $this->getAction(),
'log_timestamp' => $dbw->timestamp( $this->timestamp ),
'log_namespace' => $this->getTitle()->getNamespace(),
- 'log_title' => $this->getTitle()->getDBkey(),
- 'log_comment' => $this->getComment(),
+ 'log_title' => $this->getTitle()->getDBkey(),
+ 'log_comment' => $this->getComment(),
#'log_user_text' => $this->user_text,
- 'log_params' => $this->params ),
+ 'log_params' => $this->params ),
__METHOD__
);
// @todo FIXME: This could fail slightly for multiple matches :P
- if( $prior ) {
+ if ( $prior ) {
wfDebug( __METHOD__ . ": skipping existing item for Log:{$this->type}/{$this->action}, timestamp " .
$this->timestamp . "\n" );
return;
@@ -1422,7 +1503,7 @@ class WikiRevision {
wfDebug( __METHOD__ . "File already exists; importing as $archiveName\n" );
}
}
- if( !$file ) {
+ if ( !$file ) {
wfDebug( __METHOD__ . ': Bad file for ' . $this->getTitle() . "\n" );
return false;
}
@@ -1434,7 +1515,7 @@ class WikiRevision {
$source = $this->downloadSource();
$flags |= File::DELETE_SOURCE;
}
- if( !$source ) {
+ if ( !$source ) {
wfDebug( __METHOD__ . ": Could not fetch remote file.\n" );
return false;
}
@@ -1460,7 +1541,7 @@ class WikiRevision {
}
if ( $status->isGood() ) {
- wfDebug( __METHOD__ . ": Succesful\n" );
+ wfDebug( __METHOD__ . ": Successful\n" );
return true;
} else {
wfDebug( __METHOD__ . ': failed: ' . $status->getXml() . "\n" );
@@ -1473,13 +1554,13 @@ class WikiRevision {
*/
function downloadSource() {
global $wgEnableUploads;
- if( !$wgEnableUploads ) {
+ if ( !$wgEnableUploads ) {
return false;
}
$tempo = tempnam( wfTempDir(), 'download' );
$f = fopen( $tempo, 'wb' );
- if( !$f ) {
+ if ( !$f ) {
wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
return false;
}
@@ -1487,7 +1568,7 @@ class WikiRevision {
// @todo FIXME!
$src = $this->getSrc();
$data = Http::get( $src );
- if( !$data ) {
+ if ( !$data ) {
wfDebug( "IMPORT: couldn't fetch source $src\n" );
fclose( $f );
unlink( $tempo );
@@ -1523,7 +1604,7 @@ class ImportStringSource {
* @return bool|string
*/
function readChunk() {
- if( $this->atEnd() ) {
+ if ( $this->atEnd() ) {
return false;
}
$this->mRead = true;
@@ -1562,7 +1643,7 @@ class ImportStreamSource {
wfSuppressWarnings();
$file = fopen( $filename, 'rt' );
wfRestoreWarnings();
- if( !$file ) {
+ if ( !$file ) {
return Status::newFatal( "importcantopen" );
}
return Status::newGood( new ImportStreamSource( $file ) );
@@ -1575,11 +1656,11 @@ class ImportStreamSource {
static function newFromUpload( $fieldname = "xmlimport" ) {
$upload =& $_FILES[$fieldname];
- if( !isset( $upload ) || !$upload['name'] ) {
+ if ( $upload === null || !$upload['name'] ) {
return Status::newFatal( 'importnofile' );
}
- if( !empty( $upload['error'] ) ) {
- switch($upload['error']){
+ if ( !empty( $upload['error'] ) ) {
+ switch ( $upload['error'] ) {
case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
return Status::newFatal( 'importuploaderrorsize' );
case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
@@ -1593,7 +1674,7 @@ class ImportStreamSource {
}
$fname = $upload['tmp_name'];
- if( is_uploaded_file( $fname ) ) {
+ if ( is_uploaded_file( $fname ) ) {
return ImportStreamSource::newFromFile( $fname );
} else {
return Status::newFatal( 'importnofile' );
@@ -1612,7 +1693,7 @@ class ImportStreamSource {
# otherwise prevent importing from large sites, such
# as the Wikimedia cluster, etc.
$data = Http::request( $method, $url, array( 'followRedirects' => true ) );
- if( $data !== false ) {
+ if ( $data !== false ) {
$file = tmpfile();
fwrite( $file, $data );
fflush( $file );
@@ -1632,18 +1713,24 @@ class ImportStreamSource {
* @return Status
*/
public static function newFromInterwiki( $interwiki, $page, $history = false, $templates = false, $pageLinkDepth = 0 ) {
- if( $page == '' ) {
+ if ( $page == '' ) {
return Status::newFatal( 'import-noarticle' );
}
$link = Title::newFromText( "$interwiki:Special:Export/$page" );
- if( is_null( $link ) || $link->getInterwiki() == '' ) {
+ if ( is_null( $link ) || $link->getInterwiki() == '' ) {
return Status::newFatal( 'importbadinterwiki' );
} else {
$params = array();
- if ( $history ) $params['history'] = 1;
- if ( $templates ) $params['templates'] = 1;
- if ( $pageLinkDepth ) $params['pagelink-depth'] = $pageLinkDepth;
- $url = $link->getFullUrl( $params );
+ if ( $history ) {
+ $params['history'] = 1;
+ }
+ if ( $templates ) {
+ $params['templates'] = 1;
+ }
+ if ( $pageLinkDepth ) {
+ $params['pagelink-depth'] = $pageLinkDepth;
+ }
+ $url = $link->getFullURL( $params );
# For interwikis, use POST to avoid redirects.
return ImportStreamSource::newFromURL( $url, "POST" );
}
diff --git a/includes/Init.php b/includes/Init.php
index a8540f2c..64431f09 100644
--- a/includes/Init.php
+++ b/includes/Init.php
@@ -2,6 +2,9 @@
/**
* Some functions that are useful during startup.
*
+ * This class previously contained some functionality related to a PHP compiler
+ * called hphpc. That compiler has now been discontinued.
+ *
* 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
@@ -22,40 +25,35 @@
/**
* Some functions that are useful during startup.
+ *
+ * This class previously contained some functionality related to a PHP compiler
+ * called hphpc. That compiler has now been discontinued. All methods are now
+ * deprecated.
*/
class MWInit {
static $compilerVersion;
/**
- * Get the version of HipHop used to compile, or false if MediaWiki was not
- * compiled. This works by having our build script insert a special function
- * into the compiled code.
+ * @deprecated since 1.22
*/
static function getCompilerVersion() {
- if ( self::$compilerVersion === null ) {
- if ( self::functionExists( 'wfHipHopCompilerVersion' ) ) {
- self::$compilerVersion = wfHipHopCompilerVersion();
- } else {
- self::$compilerVersion = false;
- }
- }
- return self::$compilerVersion;
+ return false;
}
/**
* Returns true if we are running under HipHop, whether in compiled or
* interpreted mode.
*
+ * @deprecated since 1.22
* @return bool
*/
static function isHipHop() {
- return function_exists( 'hphp_thread_set_warmup_enabled' );
+ return defined( 'HPHP_VERSION' );
}
/**
- * Get a fully-qualified path for a source file relative to $IP. Including
- * such a path under HipHop will force the file to be interpreted. This is
- * useful for configuration files.
+ * Get a fully-qualified path for a source file relative to $IP.
+ * @deprecated since 1.22
*
* @param $file string
*
@@ -67,117 +65,39 @@ class MWInit {
}
/**
- * If we are running code compiled by HipHop, this will pass through the
- * input path, assumed to be relative to $IP. If the code is interpreted,
- * it will converted to a fully qualified path. It is necessary to use a
- * path which is relative to $IP in order to make HipHop use its compiled
- * code.
- *
+ * @deprecated since 1.22
* @param $file string
- *
* @return string
*/
static function compiledPath( $file ) {
global $IP;
-
- if ( defined( 'MW_COMPILED' ) ) {
- return "phase3/$file";
- } else {
- return "$IP/$file";
- }
- }
-
- /**
- * The equivalent of MWInit::interpretedPath() but for files relative to the
- * extensions directory.
- *
- * @param $file string
- * @return string
- */
- static function extInterpretedPath( $file ) {
- return self::getExtensionsDirectory() . '/' . $file;
+ return "$IP/$file";
}
/**
- * The equivalent of MWInit::compiledPath() but for files relative to the
- * extensions directory. Any files referenced in this way must be registered
- * for compilation by including them in $wgCompiledFiles.
+ * @deprecated since 1.22
* @param $file string
* @return string
*/
static function extCompiledPath( $file ) {
- if ( defined( 'MW_COMPILED' ) ) {
- return "extensions/$file";
- } else {
- return self::getExtensionsDirectory() . '/' . $file;
- }
+ return false;
}
/**
- * Register an extension setup file and return its path for compiled
- * inclusion. Use this function in LocalSettings.php to add extensions
- * to the build. For example:
- *
- * require( MWInit::extSetupPath( 'ParserFunctions/ParserFunctions.php' ) );
- *
- * @param $extRel string The path relative to the extensions directory, as defined by
- * $wgExtensionsDirectory.
- *
- * @return string
- */
- static function extSetupPath( $extRel ) {
- $baseRel = "extensions/$extRel";
- if ( defined( 'MW_COMPILED' ) ) {
- return $baseRel;
- } else {
- global $wgCompiledFiles;
- $wgCompiledFiles[] = $baseRel;
- return self::getExtensionsDirectory() . '/' . $extRel;
- }
- }
-
- /**
- * @return bool|string
- */
- static function getExtensionsDirectory() {
- global $wgExtensionsDirectory, $IP;
- if ( $wgExtensionsDirectory === false ) {
- $wgExtensionsDirectory = "$IP/../extensions";
- }
- return $wgExtensionsDirectory;
- }
-
- /**
- * Determine whether a class exists, using a method which works under HipHop.
- *
- * Note that it's not possible to implement this with any variant of
- * class_exists(), because class_exists() returns false for classes which
- * are compiled in.
- *
- * Calling class_exists() on a literal string causes the class to be made
- * "volatile", which means (as of March 2011) that the class is broken and
- * can't be used at all. So don't do that. See
- * https://github.com/facebook/hiphop-php/issues/314
+ * Deprecated wrapper for class_exists()
+ * @deprecated since 1.22
*
* @param $class string
*
* @return bool
*/
static function classExists( $class ) {
- try {
- $r = new ReflectionClass( $class );
- } catch( ReflectionException $r ) {
- $r = false;
- }
- return $r !== false;
+ return class_exists( $class );
}
/**
- * Determine wether a method exists within a class, using a method which works
- * under HipHop.
- *
- * Note that under HipHop when method_exists is given a string for it's class
- * such as to test for a static method has the same issues as class_exists does.
+ * Deprecated wrapper for method_exists()
+ * @deprecated since 1.22
*
* @param $class string
* @param $method string
@@ -185,34 +105,25 @@ class MWInit {
* @return bool
*/
static function methodExists( $class, $method ) {
- try {
- $r = new ReflectionMethod( $class, $method );
- } catch( ReflectionException $r ) {
- $r = false;
- }
- return $r !== false;
+ return method_exists( $class, $method );
}
/**
- * Determine whether a function exists, using a method which works under
- * HipHop.
+ * Deprecated wrapper for function_exists()
+ * @deprecated since 1.22
*
* @param $function string
*
* @return bool
*/
static function functionExists( $function ) {
- try {
- $r = new ReflectionFunction( $function );
- } catch( ReflectionException $r ) {
- $r = false;
- }
- return $r !== false;
+ return function_exists( $function );
}
/**
- * Call a static method of a class with variable arguments without causing
- * it to become volatile.
+ * Deprecated wrapper for call_user_func_array()
+ * @deprecated since 1.22
+ *
* @param $className string
* @param $methodName string
* @param $args array
@@ -220,7 +131,6 @@ class MWInit {
* @return mixed
*/
static function callStaticMethod( $className, $methodName, $args ) {
- $r = new ReflectionMethod( $className, $methodName );
- return $r->invokeArgs( null, $args );
+ return call_user_func_array( array( $className, $methodName ), $args );
}
}
diff --git a/includes/Licenses.php b/includes/Licenses.php
index ba504a99..6c582aaf 100644
--- a/includes/Licenses.php
+++ b/includes/Licenses.php
@@ -105,7 +105,7 @@ class Licenses extends HTMLFormField {
protected function stackItem( &$list, $path, $item ) {
$position =& $list;
if ( $path ) {
- foreach( $path as $key ) {
+ foreach ( $path as $key ) {
$position =& $position[$key];
}
}
@@ -117,7 +117,7 @@ class Licenses extends HTMLFormField {
* @param $depth int
*/
protected function makeHtml( $tagset, $depth = 0 ) {
- foreach ( $tagset as $key => $val )
+ foreach ( $tagset as $key => $val ) {
if ( is_array( $val ) ) {
$this->html .= $this->outputOption(
$key, '',
@@ -135,6 +135,7 @@ class Licenses extends HTMLFormField {
$depth
);
}
+ }
}
/**
@@ -148,8 +149,10 @@ class Licenses extends HTMLFormField {
$msgObj = $this->msg( $message );
$text = $msgObj->exists() ? $msgObj->text() : $message;
$attribs['value'] = $value;
- if ( $value === $this->selected )
+ if ( $value === $this->selected ) {
$attribs['selected'] = 'selected';
+ }
+
$val = str_repeat( /* &nbsp */ "\xc2\xa0", $depth * 2 ) . $text;
return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n";
}
@@ -208,7 +211,7 @@ class License {
/**
* Constructor
*
- * @param $str String: license name??
+ * @param string $str license name??
*/
function __construct( $str ) {
list( $text, $template ) = explode( '|', strrev( $str ), 2 );
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
index 214f4959..d552c696 100644
--- a/includes/LinkFilter.php
+++ b/includes/LinkFilter.php
@@ -20,10 +20,9 @@
* @file
*/
-
/**
* Some functions to help implement an external link filter for spam control.
- *
+ *
* @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
@@ -34,13 +33,22 @@
class LinkFilter {
/**
- * Check whether $text contains a link to $filterEntry
+ * Check whether $content contains a link to $filterEntry
*
- * @param $text String: text to check
- * @param $filterEntry String: domainparts, see makeRegex() for more details
+ * @param $content Content: content to check
+ * @param string $filterEntry 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 ) {
+ static function matchEntry( Content $content, $filterEntry ) {
+ if ( !( $content instanceof TextContent ) ) {
+ //TODO: handle other types of content too.
+ // Maybe create ContentHandler::matchFilter( LinkFilter ).
+ // Think about a common base class for LinkFilter and MagicWord.
+ return 0;
+ }
+
+ $text = $content->getNativeData();
+
$regex = LinkFilter::makeRegex( $filterEntry );
return preg_match( $regex, $text );
}
@@ -48,7 +56,7 @@ class LinkFilter {
/**
* Builds a regex pattern for $filterEntry.
*
- * @param $filterEntry String: URL, if it begins with "*.", it'll be
+ * @param string $filterEntry URL, if it begins with "*.", it'll be
* replaced to match any subdomain
* @return String: regex pattern, for preg_match()
*/
@@ -76,11 +84,11 @@ class LinkFilter {
*
* Asterisks in any other location are considered invalid.
*
- * @param $filterEntry String: domainparts
+ * @param string $filterEntry domainparts
* @param $prot String: protocol
* @return Array to be passed to DatabaseBase::buildLike() or false on error
*/
- public static function makeLikeArray( $filterEntry , $prot = 'http://' ) {
+ public static function makeLikeArray( $filterEntry, $prot = 'http://' ) {
$db = wfGetDB( DB_MASTER );
if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
$subdomains = true;
@@ -109,18 +117,18 @@ class LinkFilter {
}
// Reverse the labels in the hostname, convert to lower case
// For emails reverse domainpart only
- if ( $prot == 'mailto:' && strpos($host, '@') ) {
- // complete email adress
+ if ( $prot == 'mailto:' && strpos( $host, '@' ) ) {
+ // complete email address
$mailparts = explode( '@', $host );
$domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) );
$host = $domainpart . '@' . $mailparts[0];
$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 = array( "$prot$host", $db->anyString() );
+ // domainpart of email address only. do not add '.'
+ $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
+ $like = array( "$prot$host", $db->anyString() );
} else {
- $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
+ $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
if ( substr( $host, -1, 1 ) !== '.' ) {
$host .= '.';
}
@@ -140,15 +148,15 @@ class LinkFilter {
/**
* Filters an array returned by makeLikeArray(), removing everything past first pattern placeholder.
*
- * @param $arr array: array to filter
+ * @param array $arr array to filter
* @return array filtered array
*/
public static function keepOneWildcard( $arr ) {
- if( !is_array( $arr ) ) {
+ if ( !is_array( $arr ) ) {
return $arr;
}
- foreach( $arr as $key => $value ) {
+ foreach ( $arr as $key => $value ) {
if ( $value instanceof LikeMatch ) {
return array_slice( $arr, 0, $key + 1 );
}
diff --git a/includes/Linker.php b/includes/Linker.php
index 56626bd7..23ece751 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -33,13 +33,13 @@ class Linker {
* Flags for userToolLinks()
*/
const TOOL_LINKS_NOBLOCK = 1;
- const TOOL_LINKS_EMAIL = 2;
+ const TOOL_LINKS_EMAIL = 2;
/**
- * Get the appropriate HTML attributes to add to the "a" element of an ex-
- * ternal link, as created by [wikisyntax].
+ * Get the appropriate HTML attributes to add to the "a" element of an
+ * external link, as created by [wikisyntax].
*
- * @param $class String: the contents of the class attribute; if an empty
+ * @param string $class the contents of the class attribute; if an empty
* string is passed, which is the default value, defaults to 'external'.
* @return string
* @deprecated since 1.18 Just pass the external class directly to something using Html::expandAttributes
@@ -50,13 +50,12 @@ class Linker {
}
/**
- * Get the appropriate HTML attributes to add to the "a" element of an in-
- * terwiki link.
+ * Get the appropriate HTML attributes to add to the "a" element of an interwiki link.
*
- * @param $title String: the title text for the link, URL-encoded (???) but
+ * @param string $title the title text for the link, URL-encoded (???) but
* not HTML-escaped
- * @param $unused String: unused
- * @param $class String: the contents of the class attribute; if an empty
+ * @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'.
* @return string
*/
@@ -73,13 +72,12 @@ class Linker {
}
/**
- * Get the appropriate HTML attributes to add to the "a" element of an in-
- * ternal link.
+ * Get the appropriate HTML attributes to add to the "a" element of an internal link.
*
- * @param $title String: the title text for the link, URL-encoded (???) but
+ * @param string $title the title text for the link, URL-encoded (???) but
* not HTML-escaped
- * @param $unused String: unused
- * @param $class String: the contents of the class attribute, default none
+ * @param string $unused unused
+ * @param string $class the contents of the class attribute, default none
* @return string
*/
static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) {
@@ -89,12 +87,12 @@ class Linker {
}
/**
- * Get the appropriate HTML attributes to add to the "a" element of an in-
- * ternal link, given the Title object for the page we want to link to.
+ * Get the appropriate HTML attributes to add to the "a" element of an internal
+ * link, given the Title object for the page we want to link to.
*
* @param $nt Title
- * @param $unused String: unused
- * @param $class String: the contents of the class attribute, default none
+ * @param string $unused unused
+ * @param string $class the contents of the class attribute, default none
* @param $title Mixed: optional (unescaped) string to use in the title
* attribute; if false, default to the name of the page we're linking to
* @return string
@@ -171,15 +169,15 @@ class Linker {
* the link text. This is raw HTML and will not be escaped. If null,
* defaults to the prefixed text of the Title; or if the Title is just a
* fragment, the contents of the fragment.
- * @param $customAttribs array A key => value array of extra HTML attri-
- * butes, such as title and class. (href is ignored.) Classes will be
+ * @param array $customAttribs A key => value array of extra HTML attributes,
+ * such as title and class. (href is ignored.) Classes will be
* merged with the default classes, while other attributes will replace
* default attributes. All passed attribute values will be HTML-escaped.
* A false attribute value means to suppress that attribute.
* @param $query array The query string to append to the URL
* you're linking to, in key => value array form. Query keys and values
* will be URL-encoded.
- * @param $options string|array String or array of strings:
+ * @param string|array $options String or array of strings:
* 'known': Page is known to exist, so don't check if it does.
* 'broken': Page is known not to exist, so don't check if it does.
* 'noclasses': Don't add any classes automatically (includes "new",
@@ -188,6 +186,8 @@ class Linker {
* cons.
* 'forcearticlepath': Use the article path always, even with a querystring.
* Has compatibility issues on some setups, so avoid wherever possible.
+ * 'http': Force a full URL with http:// as the scheme.
+ * 'https': Force a full URL with https:// as the scheme.
* @return string HTML <a> attribute
*/
public static function link(
@@ -199,7 +199,7 @@ class Linker {
return "<!-- ERROR -->$html";
}
- if( is_string( $query ) ) {
+ if ( is_string( $query ) ) {
// some functions withing core using this still hand over query strings
wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
$query = wfCgiToArray( $query );
@@ -238,7 +238,7 @@ class Linker {
# Note: we want the href attribute first, for prettiness.
$attribs = array( 'href' => self::linkUrl( $target, $query, $options ) );
if ( in_array( 'forcearticlepath', $options ) && $oldquery ) {
- $attribs['href'] = wfAppendQuery( $attribs['href'], wfArrayToCgi( $oldquery ) );
+ $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
}
$attribs = array_merge(
@@ -273,7 +273,7 @@ class Linker {
* Returns the Url used to link to a Title
*
* @param $target Title
- * @param $query Array: query parameters
+ * @param array $query query parameters
* @param $options Array
* @return String
*/
@@ -294,7 +294,16 @@ class Linker {
$query['action'] = 'edit';
$query['redlink'] = '1';
}
- $ret = $target->getLinkURL( $query );
+
+ if ( in_array( 'http', $options ) ) {
+ $proto = PROTO_HTTP;
+ } elseif ( in_array( 'https', $options ) ) {
+ $proto = PROTO_HTTPS;
+ } else {
+ $proto = PROTO_RELATIVE;
+ }
+
+ $ret = $target->getLinkURL( $query, false, $proto );
wfProfileOut( __METHOD__ );
return $ret;
}
@@ -371,13 +380,13 @@ class Linker {
* @return string
*/
private static function linkText( $target ) {
- # We might be passed a non-Title by make*LinkObj(). Fail gracefully.
+ // We might be passed a non-Title by make*LinkObj(). Fail gracefully.
if ( !$target instanceof Title ) {
return '';
}
- # If the target is just a fragment, with no title, we return the frag-
- # ment text. Otherwise, we return the title text itself.
+ // If the target is just a fragment, with no title, we return the fragment
+ // text. Otherwise, we return the title text itself.
if ( $target->getPrefixedText() === '' && $target->getFragment() !== '' ) {
return htmlspecialchars( $target->getFragment() );
}
@@ -413,10 +422,10 @@ class Linker {
* despite $query not being used.
*
* @param $nt Title
- * @param $html String [optional]
- * @param $query String [optional]
- * @param $trail String [optional]
- * @param $prefix String [optional]
+ * @param string $html [optional]
+ * @param string $query [optional]
+ * @param string $trail [optional]
+ * @param string $prefix [optional]
*
*
* @return string
@@ -435,8 +444,9 @@ class Linker {
* a value indicating that the title object is invalid.
*
* @param $context IContextSource context to use to get the messages
- * @param $namespace int Namespace number
- * @param $title string Text of the title, without the namespace part
+ * @param int $namespace Namespace number
+ * @param string $title Text of the title, without the namespace part
+ * @return string
*/
public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
global $wgContLang;
@@ -522,7 +532,7 @@ class Linker {
* @param $parser Parser object
* @param $title Title object of the file (not the currently viewed page)
* @param $file File object, or false if it doesn't exist
- * @param $frameParams Array: associative array of parameters external to the media handler.
+ * @param array $frameParams associative array of parameters external to the media handler.
* Boolean parameters are indicated by presence or absence, the value is arbitrary and
* will often be false.
* thumbnail If present, downscale and frame
@@ -540,13 +550,13 @@ class Linker {
* caption HTML for image caption.
* link-url URL to link to
* link-title Title object to link to
- * link-target Value for the target attribue, only with link-url
+ * link-target Value for the target attribute, only with link-url
* no-link Boolean, suppress description link
*
- * @param $handlerParams Array: associative array of media handler parameters, to be passed
+ * @param array $handlerParams associative array of media handler parameters, to be passed
* to transform(). Typical keys are "width" and "page".
- * @param $time String: timestamp of the file, set as false for current
- * @param $query String: query params for desc url
+ * @param string $time timestamp of the file, set as false for current
+ * @param string $query query params for desc url
* @param $widthOption: Used by the parser to remember the user preference thumbnailsize
* @since 1.20
* @return String: HTML for an image, with links, wrappers, etc.
@@ -588,9 +598,9 @@ class Linker {
$prefix = $postfix = '';
if ( 'center' == $fp['align'] ) {
- $prefix = '<div class="center">';
+ $prefix = '<div class="center">';
$postfix = '</div>';
- $fp['align'] = 'none';
+ $fp['align'] = 'none';
}
if ( $file && !isset( $hp['width'] ) ) {
if ( isset( $hp['height'] ) && $file->isVectorized() ) {
@@ -602,9 +612,9 @@ class Linker {
$hp['width'] = $file->getWidth( $page );
}
- if ( isset( $fp['thumbnail'] ) || isset( $fp['framed'] ) || isset( $fp['frameless'] ) || !$hp['width'] ) {
+ if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) || isset( $fp['frameless'] ) || !$hp['width'] ) {
global $wgThumbLimits, $wgThumbUpright;
- if ( !isset( $widthOption ) || !isset( $wgThumbLimits[$widthOption] ) ) {
+ if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
$widthOption = User::getDefaultOption( 'thumbsize' );
}
@@ -634,7 +644,7 @@ class Linker {
# If a thumbnail width has not been provided, it is set
# to the default user option as specified in Language*.php
if ( $fp['align'] == '' ) {
- if( $parser instanceof Parser ) {
+ if ( $parser instanceof Parser ) {
$fp['align'] = $parser->getTargetLanguage()->alignEnd();
} else {
# backwards compatibility, remove with makeImageLink2()
@@ -648,7 +658,7 @@ class Linker {
if ( $file && isset( $fp['frameless'] ) ) {
$srcWidth = $file->getWidth( $page );
# For "frameless" option: do not present an image bigger than the source (for bitmap-style images)
- # This is the same behaviour as the "thumb" option does it already.
+ # This is the same behavior as the "thumb" option does it already.
if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
$hp['width'] = $srcWidth;
}
@@ -664,12 +674,14 @@ class Linker {
if ( !$thumb ) {
$s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
} else {
+ self::processResponsiveImages( $file, $thumb, $hp );
$params = array(
'alt' => $fp['alt'],
'title' => $fp['title'],
'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
'img-class' => $fp['class'] );
if ( isset( $fp['border'] ) ) {
+ // TODO: BUG? Both values are identical
$params['img-class'] .= ( $params['img-class'] !== '' ) ? ' thumbborder' : 'thumbborder';
}
$params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
@@ -696,8 +708,8 @@ class Linker {
/**
* Get the link parameters for MediaTransformOutput::toHtml() from given
* frame parameters supplied by the Parser.
- * @param $frameParams array The frame parameters
- * @param $query string An optional query string to add to description page links
+ * @param array $frameParams The frame parameters
+ * @param string $query An optional query string to add to description page links
* @return array
*/
private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
@@ -711,7 +723,7 @@ class Linker {
$extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
foreach ( $extLinkAttrs as $name => $val ) {
// Currently could include 'rel' and 'target'
- $mtoParams['parser-extlink-'.$name] = $val;
+ $mtoParams['parser-extlink-' . $name] = $val;
}
}
} elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
@@ -738,7 +750,7 @@ class Linker {
* @return mixed
*/
public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
- $align = 'right', $params = array(), $framed = false , $manualthumb = "" )
+ $align = 'right', $params = array(), $framed = false, $manualthumb = "" )
{
$frameParams = array(
'alt' => $alt,
@@ -774,16 +786,26 @@ class Linker {
$hp =& $handlerParams;
$page = isset( $hp['page'] ) ? $hp['page'] : false;
- if ( !isset( $fp['align'] ) ) $fp['align'] = 'right';
- if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
- if ( !isset( $fp['title'] ) ) $fp['title'] = '';
- if ( !isset( $fp['caption'] ) ) $fp['caption'] = '';
+ if ( !isset( $fp['align'] ) ) {
+ $fp['align'] = 'right';
+ }
+ if ( !isset( $fp['alt'] ) ) {
+ $fp['alt'] = '';
+ }
+ if ( !isset( $fp['title'] ) ) {
+ $fp['title'] = '';
+ }
+ if ( !isset( $fp['caption'] ) ) {
+ $fp['caption'] = '';
+ }
if ( empty( $hp['width'] ) ) {
// Reduce width for upright images when parameter 'upright' is used
$hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
}
$thumb = false;
+ $noscale = false;
+ $manualthumb = false;
if ( !$exists ) {
$outerWidth = $hp['width'] + 2;
@@ -795,6 +817,7 @@ class Linker {
$manual_img = wfFindFile( $manual_title );
if ( $manual_img ) {
$thumb = $manual_img->getUnscaledThumb( $hp );
+ $manualthumb = true;
} else {
$exists = false;
}
@@ -802,9 +825,10 @@ class Linker {
} elseif ( isset( $fp['framed'] ) ) {
// Use image dimensions, don't scale
$thumb = $file->getUnscaledThumb( $hp );
+ $noscale = true;
} else {
# Do not present an image bigger than the source, for bitmap-style images
- # This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour
+ # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
$srcWidth = $file->getWidth( $page );
if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
$hp['width'] = $srcWidth;
@@ -824,7 +848,13 @@ class Linker {
# zoom icon still needs it, so we make a unique query for it. See bug 14771
$url = $title->getLocalURL( $query );
if ( $page ) {
- $url = wfAppendQuery( $url, 'page=' . urlencode( $page ) );
+ $url = wfAppendQuery( $url, array( 'page' => $page ) );
+ }
+ if ( $manualthumb &&
+ !isset( $fp['link-title'] ) &&
+ !isset( $fp['link-url'] ) &&
+ !isset( $fp['no-link'] ) ) {
+ $fp['link-url'] = $url;
}
$s = "<div class=\"thumb t{$fp['align']}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
@@ -835,6 +865,9 @@ class Linker {
$s .= wfMessage( 'thumbnail_error', '' )->escaped();
$zoomIcon = '';
} else {
+ if ( !$noscale && !$manualthumb ) {
+ self::processResponsiveImages( $file, $thumb, $hp );
+ }
$params = array(
'alt' => $fp['alt'],
'title' => $fp['title'],
@@ -862,11 +895,42 @@ class Linker {
}
/**
+ * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
+ * applicable.
+ *
+ * @param File $file
+ * @param MediaOutput $thumb
+ * @param array $hp image parameters
+ */
+ protected static function processResponsiveImages( $file, $thumb, $hp ) {
+ global $wgResponsiveImages;
+ if ( $wgResponsiveImages ) {
+ $hp15 = $hp;
+ $hp15['width'] = round( $hp['width'] * 1.5 );
+ $hp20 = $hp;
+ $hp20['width'] = $hp['width'] * 2;
+ if ( isset( $hp['height'] ) ) {
+ $hp15['height'] = round( $hp['height'] * 1.5 );
+ $hp20['height'] = $hp['height'] * 2;
+ }
+
+ $thumb15 = $file->transform( $hp15 );
+ $thumb20 = $file->transform( $hp20 );
+ if ( $thumb15->url !== $thumb->url ) {
+ $thumb->responsiveUrls['1.5'] = $thumb15->url;
+ }
+ if ( $thumb20->url !== $thumb->url ) {
+ $thumb->responsiveUrls['2'] = $thumb20->url;
+ }
+ }
+ }
+
+ /**
* Make a "broken" link to an image
*
* @param $title Title object
- * @param $label String: link label (plain text)
- * @param $query String: query string
+ * @param string $label link label (plain text)
+ * @param string $query query string
* @param $unused1 Unused parameter kept for b/c
* @param $unused2 Unused parameter kept for b/c
* @param $time Boolean: a file of a certain timestamp was requested
@@ -898,32 +962,33 @@ class Linker {
return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
$encLabel . '</a>';
- } else {
- wfProfileOut( __METHOD__ );
- return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) );
}
+
+ wfProfileOut( __METHOD__ );
+ return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) );
}
/**
* Get the URL to upload a certain file
*
* @param $destFile Title object of the file to upload
- * @param $query String: urlencoded query string to prepend
+ * @param string $query urlencoded query string to prepend
* @return String: urlencoded URL
*/
protected static function getUploadUrl( $destFile, $query = '' ) {
global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
- $q = 'wpDestFile=' . $destFile->getPartialUrl();
- if ( $query != '' )
+ $q = 'wpDestFile=' . $destFile->getPartialURL();
+ if ( $query != '' ) {
$q .= '&' . $query;
+ }
if ( $wgUploadMissingFileUrl ) {
return wfAppendQuery( $wgUploadMissingFileUrl, $q );
- } elseif( $wgUploadNavigationUrl ) {
+ } elseif ( $wgUploadNavigationUrl ) {
return wfAppendQuery( $wgUploadNavigationUrl, $q );
} else {
$upload = SpecialPage::getTitleFor( 'Upload' );
- return $upload->getLocalUrl( $q );
+ return $upload->getLocalURL( $q );
}
}
@@ -931,8 +996,8 @@ class Linker {
* Create a direct link to a given uploaded file.
*
* @param $title Title object.
- * @param $html String: pre-sanitized HTML
- * @param $time string: MW timestamp of file creation time
+ * @param string $html pre-sanitized HTML
+ * @param string $time MW timestamp of file creation time
* @return String: HTML
*/
public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
@@ -946,7 +1011,7 @@ class Linker {
*
* @param $title Title object.
* @param $file File|bool mixed File object or false
- * @param $html String: pre-sanitized HTML
+ * @param string $html pre-sanitized HTML
* @return String: HTML
*
* @todo Handle invalid or missing images better.
@@ -979,19 +1044,21 @@ class Linker {
$key = strtolower( $name );
}
- return self::linkKnown( SpecialPage::getTitleFor( $name ) , wfMessage( $key )->text() );
+ return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
}
/**
* Make an external link
- * @param $url String: URL to link to
- * @param $text String: text of link
+ * @param string $url URL to link to
+ * @param string $text text of link
* @param $escape Boolean: do we escape the link text?
- * @param $linktype String: type of external link. Gets added to the classes
- * @param $attribs Array of extra attributes to <a>
+ * @param string $linktype type of external link. Gets added to the classes
+ * @param array $attribs of extra attributes to <a>
+ * @param $title Title|null Title object used for title specific link attributes
* @return string
*/
- public static function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) {
+ public static function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array(), $title = null ) {
+ global $wgTitle;
$class = "external";
if ( $linktype ) {
$class .= " $linktype";
@@ -1004,6 +1071,11 @@ class Linker {
if ( $escape ) {
$text = htmlspecialchars( $text );
}
+
+ if ( !$title ) {
+ $title = $wgTitle;
+ }
+ $attribs['rel'] = Parser::getExternalLinkRel( $url, $title );
$link = '';
$success = wfRunHooks( 'LinkerMakeExternalLink',
array( &$url, &$text, &$link, &$attribs, $linktype ) );
@@ -1018,14 +1090,17 @@ class Linker {
/**
* Make user link (or user contributions for unregistered users)
* @param $userId Integer: user id in database.
- * @param $userName String: user name in database.
- * @param $altUserName String: text to display instead of the user name (optional)
+ * @param string $userName user name in database.
+ * @param string $altUserName text to display instead of the user name (optional)
* @return String: HTML fragment
* @since 1.19 Method exists for a long time. $altUserName was added in 1.19.
*/
public static function userLink( $userId, $userName, $altUserName = false ) {
if ( $userId == 0 ) {
$page = SpecialPage::getTitleFor( 'Contributions', $userName );
+ if ( $altUserName === false ) {
+ $altUserName = IP::prettifyIP( $userName );
+ }
} else {
$page = Title::makeTitle( NS_USER, $userName );
}
@@ -1041,7 +1116,7 @@ class Linker {
* Generate standard user tool links (talk, contributions, block link, etc.)
*
* @param $userId Integer: user identifier
- * @param $userText String: user name or IP address
+ * @param string $userText user name or IP address
* @param $redContribsWhenNoEdits Boolean: should the contributions link be
* red if the user has no edits?
* @param $flags Integer: customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK and Linker::TOOL_LINKS_EMAIL)
@@ -1064,8 +1139,11 @@ class Linker {
// check if the user has an edit
$attribs = array();
if ( $redContribsWhenNoEdits ) {
- $count = !is_null( $edits ) ? $edits : User::edits( $userId );
- if ( $count == 0 ) {
+ if ( intval( $edits ) === 0 && $edits !== 0 ) {
+ $user = User::newFromId( $userId );
+ $edits = $user->getEditCount();
+ }
+ if ( $edits === 0 ) {
$attribs['class'] = 'new';
}
}
@@ -1096,7 +1174,7 @@ class Linker {
/**
* Alias for userToolLinks( $userId, $userText, true );
* @param $userId Integer: user identifier
- * @param $userText String: user name or IP address
+ * @param string $userText user name or IP address
* @param $edits Integer: user edit count (optional, for performance)
* @return String
*/
@@ -1104,10 +1182,9 @@ class Linker {
return self::userToolLinks( $userId, $userText, true, 0, $edits );
}
-
/**
* @param $userId Integer: user id in database.
- * @param $userText String: user name in database.
+ * @param string $userText user name in database.
* @return String: HTML fragment with user talk link
*/
public static function userTalkLink( $userId, $userText ) {
@@ -1118,7 +1195,7 @@ class Linker {
/**
* @param $userId Integer: userid
- * @param $userText String: user name in database.
+ * @param string $userText user name in database.
* @return String: HTML fragment with block link
*/
public static function blockLink( $userId, $userText ) {
@@ -1129,7 +1206,7 @@ class Linker {
/**
* @param $userId Integer: userid
- * @param $userText String: user name in database.
+ * @param string $userText user name in database.
* @return String: HTML fragment with e-mail user link
*/
public static function emailLink( $userId, $userText ) {
@@ -1186,7 +1263,7 @@ class Linker {
/**
* This function is called by all recent changes variants, by the page history,
* and by the user contributions list. It is responsible for formatting edit
- * comments. It escapes any HTML in the comment, but adds some CSS to format
+ * summaries. It escapes any HTML in the summary, but adds some CSS to format
* auto-generated comments (from section editing) and formats [[wikilinks]].
*
* @author Erik Moeller <moeller@scireview.de>
@@ -1223,13 +1300,14 @@ class Linker {
static $autocommentLocal;
/**
+ * Converts autogenerated comments in edit summaries into section links.
* The pattern for autogen comments is / * foo * /, which makes for
* some nasty regex.
* We look for all comments, match any text before and after the comment,
* add a separator where needed and format the comment itself with CSS
* Called by Linker::formatComment.
*
- * @param $comment String: comment text
+ * @param string $comment comment text
* @param $title Title|null An optional title object used to links to sections
* @param $local Boolean: whether section links should refer to local page
* @return String: formatted comment
@@ -1248,6 +1326,7 @@ class Linker {
}
/**
+ * Helper function for Linker::formatAutocomments
* @param $match
* @return string
*/
@@ -1312,7 +1391,7 @@ class Linker {
* is ignored
*
* @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
- * @param $comment String: text to format links in
+ * @param string $comment text to format links in
* @param $title Title|null An optional title object used to links to sections
* @param $local Boolean: whether section links should refer to local page
* @return String
@@ -1380,8 +1459,9 @@ class Linker {
$trail = "";
}
$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
- if ( isset( $match[1][0] ) && $match[1][0] == ':' )
+ if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
$match[1] = substr( $match[1], 1 );
+ }
list( $inside, $trail ) = self::splitTrail( $trail );
$linkText = $text;
@@ -1465,7 +1545,7 @@ class Linker {
$nodotdot = substr( $nodotdot, 3 );
}
if ( $dotdotcount > 0 ) {
- $exploded = explode( '/', $contextTitle->GetPrefixedText() );
+ $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
@@ -1502,7 +1582,7 @@ class Linker {
public static 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
+ // compatibility, acc. to brion -ævar
if ( $comment == '' || $comment == '*' ) {
return '';
} else {
@@ -1600,7 +1680,7 @@ class Linker {
/**
* Wraps the TOC in a table and provides the hide/collapse javascript.
*
- * @param $toc String: html of the Table Of Contents
+ * @param string $toc html of the Table Of Contents
* @param $lang String|Language|false: Language for the toc title, defaults to user language
* @return String: full html of the TOC
*/
@@ -1608,31 +1688,31 @@ class Linker {
$lang = wfGetLangObj( $lang );
$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
- return
- '<table id="toc" class="toc"><tr><td>'
- . '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
- . $toc
- . "</ul>\n</td></tr></table>\n";
+ return '<div id="toc" class="toc">'
+ . '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
+ . $toc
+ . "</ul>\n</div>\n";
}
/**
* Generate a table of contents from a section tree
* Currently unused.
*
- * @param $tree array Return value of ParserOutput::getSections()
+ * @param array $tree Return value of ParserOutput::getSections()
* @return String: HTML fragment
*/
public static function generateTOC( $tree ) {
$toc = '';
$lastLevel = 0;
foreach ( $tree as $section ) {
- if ( $section['toclevel'] > $lastLevel )
+ if ( $section['toclevel'] > $lastLevel ) {
$toc .= self::tocIndent();
- elseif ( $section['toclevel'] < $lastLevel )
+ } elseif ( $section['toclevel'] < $lastLevel ) {
$toc .= self::tocUnindent(
$lastLevel - $section['toclevel'] );
- else
+ } else {
$toc .= self::tocLineEnd();
+ }
$toc .= self::tocLine( $section['anchor'],
$section['line'], $section['number'],
@@ -1647,12 +1727,12 @@ class Linker {
* Create a headline for content
*
* @param $level Integer: the level of the headline (1-6)
- * @param $attribs String: any attributes for the headline, starting with
+ * @param string $attribs any attributes for the headline, starting with
* a space and ending with '>'
* This *must* be at least '>' for no attribs
- * @param $anchor String: the anchor to give the headline (the bit after the #)
- * @param $html String: html for the text of the header
- * @param $link String: HTML to add for the section edit link
+ * @param string $anchor the anchor to give the headline (the bit after the #)
+ * @param string $html html for the text of the header
+ * @param string $link HTML to add for the section edit link
* @param $legacyAnchor Mixed: a second, optional anchor to give for
* backward compatibility (false to omit)
*
@@ -1660,8 +1740,8 @@ class Linker {
*/
public static function makeHeadline( $level, $attribs, $anchor, $html, $link, $legacyAnchor = false ) {
$ret = "<h$level$attribs"
+ . "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
. $link
- . " <span class=\"mw-headline\" id=\"$anchor\">$html</span>"
. "</h$level>";
if ( $legacyAnchor !== false ) {
$ret = "<div id=\"$legacyAnchor\"></div>$ret";
@@ -1699,19 +1779,101 @@ class Linker {
* changes, so this allows sysops to combat a busy vandal without bothering
* other users.
*
+ * If the option verify is set this function will return the link only in case the
+ * revision can be reverted. Please note that due to performance limitations
+ * it might be assumed that a user isn't the only contributor of a page while
+ * (s)he is, which will lead to useless rollback links. Furthermore this wont
+ * work if $wgShowRollbackEditCount is disabled, so this can only function
+ * as an additional check.
+ *
+ * If the option noBrackets is set the rollback link wont be enclosed in []
+ *
* @param $rev Revision object
* @param $context IContextSource context to use or null for the main context.
+ * @param $options array
* @return string
*/
- public static function generateRollback( $rev, IContextSource $context = null ) {
+ public static function generateRollback( $rev, IContextSource $context = null, $options = array( 'verify' ) ) {
if ( $context === null ) {
$context = RequestContext::getMain();
}
+ $editCount = false;
+ if ( in_array( 'verify', $options ) ) {
+ $editCount = self::getRollbackEditCount( $rev, true );
+ if ( $editCount === false ) {
+ return '';
+ }
+ }
+
+ $inner = self::buildRollbackLink( $rev, $context, $editCount );
+
+ if ( !in_array( 'noBrackets', $options ) ) {
+ $inner = $context->msg( 'brackets' )->rawParams( $inner )->plain();
+ }
- return '<span class="mw-rollback-link">'
- . $context->msg( 'brackets' )->rawParams(
- self::buildRollbackLink( $rev, $context ) )->plain()
- . '</span>';
+ return '<span class="mw-rollback-link">' . $inner . '</span>';
+ }
+
+ /**
+ * This function will return the number of revisions which a rollback
+ * would revert and, if $verify is set it will verify that a revision
+ * can be reverted (that the user isn't the only contributor and the
+ * revision we might rollback to isn't deleted). These checks can only
+ * function as an additional check as this function only checks against
+ * the last $wgShowRollbackEditCount edits.
+ *
+ * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
+ * is set and the user is the only contributor of the page.
+ *
+ * @param $rev Revision object
+ * @param bool $verify Try to verify that this revision can really be rolled back
+ * @return integer|bool|null
+ */
+ public static function getRollbackEditCount( $rev, $verify ) {
+ global $wgShowRollbackEditCount;
+ if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
+ // Nothing has happened, indicate this by returning 'null'
+ return null;
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ // Up to the value of $wgShowRollbackEditCount revisions are counted
+ $res = $dbr->select(
+ 'revision',
+ array( 'rev_user_text', 'rev_deleted' ),
+ // $rev->getPage() returns null sometimes
+ array( 'rev_page' => $rev->getTitle()->getArticleID() ),
+ __METHOD__,
+ array(
+ 'USE INDEX' => array( 'revision' => 'page_timestamp' ),
+ 'ORDER BY' => 'rev_timestamp DESC',
+ 'LIMIT' => $wgShowRollbackEditCount + 1
+ )
+ );
+
+ $editCount = 0;
+ $moreRevs = false;
+ foreach ( $res as $row ) {
+ if ( $rev->getRawUserText() != $row->rev_user_text ) {
+ if ( $verify && ( $row->rev_deleted & Revision::DELETED_TEXT || $row->rev_deleted & Revision::DELETED_USER ) ) {
+ // If the user or the text of the revision we might rollback to is deleted in some way we can't rollback
+ // Similar to the sanity checks in WikiPage::commitRollback
+ return false;
+ }
+ $moreRevs = true;
+ break;
+ }
+ $editCount++;
+ }
+
+ if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
+ // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
+ // and there weren't any other revisions. That means that the current user is the only
+ // editor, so we can't rollback
+ return false;
+ }
+ return $editCount;
}
/**
@@ -1719,11 +1881,12 @@ class Linker {
*
* @param $rev Revision object
* @param $context IContextSource context to use or null for the main context.
+ * @param $editCount integer Number of edits that would be reverted
* @return String: HTML fragment
*/
- public static function buildRollbackLink( $rev, IContextSource $context = null ) {
+ public static function buildRollbackLink( $rev, IContextSource $context = null, $editCount = false ) {
global $wgShowRollbackEditCount, $wgMiserMode;
-
+
// To config which pages are effected by miser mode
$disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' );
@@ -1743,38 +1906,21 @@ class Linker {
}
$disableRollbackEditCount = false;
- if( $wgMiserMode ) {
- foreach( $disableRollbackEditCountSpecialPage as $specialPage ) {
- if( $context->getTitle()->isSpecial( $specialPage ) ) {
+ if ( $wgMiserMode ) {
+ foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
+ if ( $context->getTitle()->isSpecial( $specialPage ) ) {
$disableRollbackEditCount = true;
break;
}
}
}
- if( !$disableRollbackEditCount && is_int( $wgShowRollbackEditCount ) && $wgShowRollbackEditCount > 0 ) {
- $dbr = wfGetDB( DB_SLAVE );
-
- // Up to the value of $wgShowRollbackEditCount revisions are counted
- $res = $dbr->select( 'revision',
- array( 'rev_id', 'rev_user_text' ),
- // $rev->getPage() returns null sometimes
- array( 'rev_page' => $rev->getTitle()->getArticleID() ),
- __METHOD__,
- array( 'USE INDEX' => 'page_timestamp',
- 'ORDER BY' => 'rev_timestamp DESC',
- 'LIMIT' => $wgShowRollbackEditCount + 1 )
- );
-
- $editCount = 0;
- while( $row = $dbr->fetchObject( $res ) ) {
- if( $rev->getUserText() != $row->rev_user_text ) {
- break;
- }
- $editCount++;
+ if ( !$disableRollbackEditCount && is_int( $wgShowRollbackEditCount ) && $wgShowRollbackEditCount > 0 ) {
+ if ( !is_numeric( $editCount ) ) {
+ $editCount = self::getRollbackEditCount( $rev, false );
}
- if( $editCount > $wgShowRollbackEditCount ) {
+ if ( $editCount > $wgShowRollbackEditCount ) {
$editCount_output = $context->msg( 'rollbacklinkcount-morethan' )->numParams( $wgShowRollbackEditCount )->parse();
} else {
$editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
@@ -1801,13 +1947,20 @@ class Linker {
/**
* Returns HTML for the "templates used on this page" list.
*
- * @param $templates Array of templates from Article::getUsedTemplate
- * or similar
- * @param $preview Boolean: whether this is for a preview
- * @param $section Boolean: whether this is for a section edit
+ * Make an HTML list of templates, and then add a "More..." link at
+ * the bottom. If $more is null, do not add a "More..." link. If $more
+ * is a Title, make a link to that title and use it. If $more is a string,
+ * directly paste it in as the link (escaping needs to be done manually).
+ * Finally, if $more is a Message, call toString().
+ *
+ * @param array $templates Array of templates from Article::getUsedTemplate or similar
+ * @param bool $preview Whether this is for a preview
+ * @param bool $section Whether this is for a section edit
+ * @param Title|Message|string|null $more An escaped link for "More..." of the templates
* @return String: HTML output
*/
- public static function formatTemplates( $templates, $preview = false, $section = false ) {
+ public static function formatTemplates( $templates, $preview = false, $section = false, $more = null ) {
+ global $wgLang;
wfProfileIn( __METHOD__ );
$outText = '';
@@ -1835,13 +1988,28 @@ class Linker {
usort( $templates, 'Title::compare' );
foreach ( $templates as $titleObj ) {
- $r = $titleObj->getRestrictions( 'edit' );
- if ( in_array( 'sysop', $r ) ) {
- $protected = wfMessage( 'template-protected' )->parse();
- } elseif ( in_array( 'autoconfirmed', $r ) ) {
- $protected = wfMessage( 'template-semiprotected' )->parse();
- } else {
- $protected = '';
+ $protected = '';
+ $restrictions = $titleObj->getRestrictions( 'edit' );
+ if ( $restrictions ) {
+ // Check backwards-compatible messages
+ $msg = null;
+ if ( $restrictions === array( 'sysop' ) ) {
+ $msg = wfMessage( 'template-protected' );
+ } elseif ( $restrictions === array( 'autoconfirmed' ) ) {
+ $msg = wfMessage( 'template-semiprotected' );
+ }
+ if ( $msg && !$msg->isDisabled() ) {
+ $protected = $msg->parse();
+ } else {
+ // Construct the message from restriction-level-*
+ // e.g. restriction-level-sysop, restriction-level-autoconfirmed
+ $msgs = array();
+ foreach ( $restrictions as $r ) {
+ $msgs[] = wfMessage( "restriction-level-$r" )->parse();
+ }
+ $protected = wfMessage( 'parentheses' )
+ ->rawParams( $wgLang->commaList( $msgs ) )->escaped();
+ }
}
if ( $titleObj->quickUserCan( 'edit' ) ) {
$editLink = self::link(
@@ -1858,18 +2026,29 @@ class Linker {
array( 'action' => 'edit' )
);
}
- $outText .= '<li>' . self::link( $titleObj ) . ' (' . $editLink . ') ' . $protected . '</li>';
+ $outText .= '<li>' . self::link( $titleObj )
+ . wfMessage( 'word-separator' )->escaped()
+ . wfMessage( 'parentheses' )->rawParams( $editLink )->escaped()
+ . wfMessage( 'word-separator' )->escaped()
+ . $protected . '</li>';
+ }
+
+ if ( $more instanceof Title ) {
+ $outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>';
+ } elseif ( $more ) {
+ $outText .= "<li>$more</li>";
}
+
$outText .= '</ul>';
}
- wfProfileOut( __METHOD__ );
+ wfProfileOut( __METHOD__ );
return $outText;
}
/**
* Returns HTML for the "hidden categories on this page" list.
*
- * @param $hiddencats Array of hidden categories from Article::getHiddenCategories
+ * @param array $hiddencats of hidden categories from Article::getHiddenCategories
* or similar
* @return String: HTML output
*/
@@ -1888,7 +2067,7 @@ class Linker {
}
$outText .= '</ul>';
}
- wfProfileOut( __METHOD__ );
+ wfProfileOut( __METHOD__ );
return $outText;
}
@@ -1896,7 +2075,7 @@ class Linker {
* Format a size in bytes for output, using an appropriate
* unit (B, KB, MB or GB) according to the magnitude in question
*
- * @param $size int Size to format
+ * @param int $size Size to format
* @return String
*/
public static function formatSize( $size ) {
@@ -1910,7 +2089,7 @@ class Linker {
* isn't always, because sometimes the accesskey needs to go on a different
* element than the id, for reverse-compatibility, etc.)
*
- * @param $name String: id of the element, minus prefixes.
+ * @param string $name id of the element, minus prefixes.
* @param $options Mixed: null or the string 'withaccess' to add an access-
* key hint
* @return String: contents of the title attribute (which you must HTML-
@@ -1928,7 +2107,7 @@ class Linker {
# Compatibility: formerly some tooltips had [alt-.] hardcoded
$tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
# Message equal to '-' means suppress it.
- if ( $tooltip == '-' ) {
+ if ( $tooltip == '-' ) {
$tooltip = false;
}
}
@@ -1956,7 +2135,7 @@ class Linker {
* the id but isn't always, because sometimes the accesskey needs to go on
* a different element than the id, for reverse-compatibility, etc.)
*
- * @param $name String: id of the element, minus prefixes.
+ * @param string $name id of the element, minus prefixes.
* @return String: contents of the accesskey attribute (which you must HTML-
* escape), or false for no accesskey attribute
*/
@@ -2010,17 +2189,17 @@ class Linker {
// RevDelete links using revision ID are stable across
// page deletion and undeletion; use when possible.
$query = array(
- 'type' => 'revision',
+ 'type' => 'revision',
'target' => $title->getPrefixedDBkey(),
- 'ids' => $rev->getId()
+ 'ids' => $rev->getId()
);
} else {
// Older deleted entries didn't save a revision ID.
// We have to refer to these by timestamp, ick!
$query = array(
- 'type' => 'archive',
+ 'type' => 'archive',
'target' => $title->getPrefixedDBkey(),
- 'ids' => $rev->getTimestamp()
+ 'ids' => $rev->getTimestamp()
);
}
return Linker::revDeleteLink( $query,
@@ -2031,7 +2210,7 @@ class Linker {
/**
* Creates a (show/hide) link for deleting revisions/log entries
*
- * @param $query Array: query parameters to be passed to link()
+ * @param array $query query parameters to be passed to link()
* @param $restricted Boolean: set to true to use a "<strong>" instead of a "<span>"
* @param $delete Boolean: set to true to use (show/hide) rather than (show)
*
@@ -2070,17 +2249,17 @@ class Linker {
* This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call
* it if you already have a title object handy. See makeBrokenLinkObj for further documentation.
*
- * @param $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
+ * @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.
* @return string
*/
static function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) {
wfDeprecated( __METHOD__, '1.16' );
-
+
$nt = Title::newFromText( $title );
if ( $nt instanceof Title ) {
return self::makeBrokenLinkObj( $nt, $text, $query, $trail );
@@ -2091,7 +2270,7 @@ class Linker {
}
/**
- * @deprecated since 1.16 Use link()
+ * @deprecated since 1.16 Use link(); warnings since 1.21
*
* 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
@@ -2100,16 +2279,16 @@ class Linker {
* @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
+ * @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.
- * @param $prefix String: optional prefix. As trail, only before instead of after.
+ * @param string $prefix optional prefix. As trail, only before instead of after.
* @return string
*/
static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- # wfDeprecated( __METHOD__, '1.16' ); // See r105985 and it's revert. Somewhere still used.
-
+ wfDeprecated( __METHOD__, '1.21' );
+
wfProfileIn( __METHOD__ );
$query = wfCgiToArray( $query );
list( $inside, $trail ) = self::splitTrail( $trail );
@@ -2124,7 +2303,7 @@ class Linker {
}
/**
- * @deprecated since 1.16 Use link()
+ * @deprecated since 1.16 Use link(); warnings since 1.21
*
* 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
@@ -2134,16 +2313,16 @@ class Linker {
* @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 string $prefix text before link text
+ * @param string $aprops extra attributes to the a-element
* @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead
* @return string the a-element
*/
static function makeKnownLinkObj(
- $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = ''
+ $title, $text = '', $query = '', $trail = '', $prefix = '', $aprops = '', $style = ''
) {
- # wfDeprecated( __METHOD__, '1.16' ); // See r105985 and it's revert. Somewhere still used.
-
+ wfDeprecated( __METHOD__, '1.21' );
+
wfProfileIn( __METHOD__ );
if ( $text == '' ) {
@@ -2170,16 +2349,16 @@ class Linker {
*
* @param $title 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
+ * @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.
- * @param $prefix String: Optional prefix
+ * @param string $prefix Optional prefix
* @return string
*/
static function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
wfDeprecated( __METHOD__, '1.16' );
-
+
wfProfileIn( __METHOD__ );
list( $inside, $trail ) = self::splitTrail( $trail );
@@ -2206,12 +2385,12 @@ class Linker {
* @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
+ * @param string $prefix Optional prefix
* @return string
*/
static function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
wfDeprecated( __METHOD__, '1.16' );
-
+
if ( $colour != '' ) {
$style = self::getInternalLinkAttributesObj( $nt, $text, $colour );
} else {
@@ -2268,8 +2447,8 @@ class DummyLinker {
* Use PHP's magic __call handler to transform instance calls to a dummy instance
* into static calls to the new Linker for backwards compatibility.
*
- * @param $fname String Name of called method
- * @param $args Array Arguments to the method
+ * @param string $fname Name of called method
+ * @param array $args Arguments to the method
* @return mixed
*/
public function __call( $fname, $args ) {
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index 87db4d60..ed52eb9c 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -27,7 +27,7 @@
*/
class LinksUpdate extends SqlDataUpdate {
- // @todo: make members protected, but make sure extensions don't break
+ // @todo make members protected, but make sure extensions don't break
public $mId, //!< Page ID of the article linked from
$mTitle, //!< Title object of the article linked from
@@ -44,11 +44,22 @@ class LinksUpdate extends SqlDataUpdate {
$mRecursive; //!< Whether to queue jobs for recursive updates
/**
+ * @var null|array Added links if calculated.
+ */
+ private $linkInsertions = null;
+
+ /**
+ * @var null|array Deleted links if calculated.
+ */
+ private $linkDeletions = null;
+
+ /**
* Constructor
*
* @param $title Title of the page we're updating
* @param $parserOutput ParserOutput: output from a full parse of this page
* @param $recursive Boolean: queue jobs for recursive updates?
+ * @throws MWException
*/
function __construct( $title, $parserOutput, $recursive = true ) {
parent::__construct( false ); // no implicit transaction
@@ -71,6 +82,7 @@ class LinksUpdate extends SqlDataUpdate {
}
$this->mParserOutput = $parserOutput;
+
$this->mLinks = $parserOutput->getLinks();
$this->mImages = $parserOutput->getImages();
$this->mTemplates = $parserOutput->getTemplates();
@@ -110,14 +122,8 @@ class LinksUpdate extends SqlDataUpdate {
* Update link tables with outgoing links from an updated article
*/
public function doUpdate() {
- global $wgUseDumbLinkUpdate;
-
wfRunHooks( 'LinksUpdate', array( &$this ) );
- if ( $wgUseDumbLinkUpdate ) {
- $this->doDumbUpdate();
- } else {
- $this->doIncrementalUpdate();
- }
+ $this->doIncrementalUpdate();
wfRunHooks( 'LinksUpdateComplete', array( &$this ) );
}
@@ -126,8 +132,9 @@ class LinksUpdate extends SqlDataUpdate {
# Page links
$existing = $this->getExistingLinks();
- $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ),
- $this->getLinkInsertions( $existing ) );
+ $this->linkDeletions = $this->getLinkDeletions( $existing );
+ $this->linkInsertions = $this->getLinkInsertions( $existing );
+ $this->incrTableUpdate( 'pagelinks', 'pl', $this->linkDeletions, $this->linkInsertions );
# Image links
$existing = $this->getExistingImages();
@@ -196,67 +203,35 @@ class LinksUpdate extends SqlDataUpdate {
}
/**
- * Link update which clears the previous entries and inserts new ones
- * May be slower or faster depending on level of lock contention and write speed of DB
- * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php
+ * Queue recursive jobs for this page
+ *
+ * Which means do LinksUpdate on all templates
+ * that include the current page, using the job queue.
*/
- protected function doDumbUpdate() {
- wfProfileIn( __METHOD__ );
-
- # Refresh category pages and image description pages
- $existing = $this->getExistingCategories();
- $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
- $categoryDeletes = array_diff_assoc( $existing, $this->mCategories );
- $categoryUpdates = $categoryInserts + $categoryDeletes;
- $existing = $this->getExistingImages();
- $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
-
- $this->dumbTableUpdate( 'pagelinks', $this->getLinkInsertions(), 'pl_from' );
- $this->dumbTableUpdate( 'imagelinks', $this->getImageInsertions(), 'il_from' );
- $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' );
- $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
- $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
- $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(),'ll_from' );
- $this->dumbTableUpdate( 'iwlinks', $this->getInterwikiInsertions(),'iwl_from' );
- $this->dumbTableUpdate( 'page_props', $this->getPropertyInsertions(), 'pp_page' );
-
- # Update the cache of all the category pages and image description
- # pages which were changed, and fix the category table count
- $this->invalidateCategories( $categoryUpdates );
- $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
- $this->invalidateImageDescriptions( $imageUpdates );
-
- # Refresh links of all pages including this page
- # This will be in a separate transaction
- if ( $this->mRecursive ) {
- $this->queueRecursiveJobs();
- }
-
- wfProfileOut( __METHOD__ );
+ function queueRecursiveJobs() {
+ self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
}
- function queueRecursiveJobs() {
- global $wgUpdateRowsPerJob;
+ /**
+ * Queue a RefreshLinks job for any table.
+ *
+ * @param Title $title Title to do job for
+ * @param String $table Table to use (e.g. 'templatelinks')
+ */
+ public static function queueRecursiveJobsForTable( Title $title, $table ) {
wfProfileIn( __METHOD__ );
-
- $cache = $this->mTitle->getBacklinkCache();
- $batches = $cache->partition( 'templatelinks', $wgUpdateRowsPerJob );
- if ( !$batches ) {
- wfProfileOut( __METHOD__ );
- return;
- }
- $jobs = array();
- foreach ( $batches as $batch ) {
- list( $start, $end ) = $batch;
- $params = array(
- 'table' => 'templatelinks',
- 'start' => $start,
- 'end' => $end,
+ if ( $title->getBacklinkCache()->hasLinks( $table ) ) {
+ $job = new RefreshLinksJob2(
+ $title,
+ array(
+ 'table' => $table,
+ ) + Job::newRootJobParams( // "overall" refresh links job info
+ "refreshlinks:{$table}:{$title->getPrefixedText()}"
+ )
);
- $jobs[] = new RefreshLinksJob2( $this->mTitle, $params );
+ JobQueueGroup::singleton()->push( $job );
+ JobQueueGroup::singleton()->deduplicateRootJob( $job );
}
- Job::batchInsert( $jobs );
-
wfProfileOut( __METHOD__ );
}
@@ -269,8 +244,8 @@ class LinksUpdate extends SqlDataUpdate {
/**
* Update all the appropriate counts in the category table.
- * @param $added array associative array of category name => sort key
- * @param $deleted array associative array of category name => sort key
+ * @param array $added associative array of category name => sort key
+ * @param array $deleted associative array of category name => sort key
*/
function updateCategoryCounts( $added, $deleted ) {
$a = WikiPage::factory( $this->mTitle );
@@ -287,21 +262,6 @@ class LinksUpdate extends SqlDataUpdate {
}
/**
- * @param $table
- * @param $insertions
- * @param $fromField
- */
- private function dumbTableUpdate( $table, $insertions, $fromField ) {
- $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
- if ( count( $insertions ) ) {
- # The link array was constructed without FOR UPDATE, so there may
- # be collisions. This may cause minor link table inconsistencies,
- # which is better than crippling the site with lock contention.
- $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
- }
- }
-
- /**
* Update a table by doing a delete query then an insert query
* @param $table
* @param $prefix
@@ -346,6 +306,7 @@ class LinksUpdate extends SqlDataUpdate {
}
if ( count( $insertions ) ) {
$this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
+ wfRunHooks( 'LinksUpdateAfterInsert', array( $this, $table, $insertions ) );
}
}
@@ -357,15 +318,15 @@ class LinksUpdate extends SqlDataUpdate {
*/
private function getLinkInsertions( $existing = array() ) {
$arr = array();
- foreach( $this->mLinks as $ns => $dbkeys ) {
+ foreach ( $this->mLinks as $ns => $dbkeys ) {
$diffs = isset( $existing[$ns] )
? array_diff_key( $dbkeys, $existing[$ns] )
: $dbkeys;
foreach ( $diffs as $dbk => $id ) {
$arr[] = array(
- 'pl_from' => $this->mId,
+ 'pl_from' => $this->mId,
'pl_namespace' => $ns,
- 'pl_title' => $dbk
+ 'pl_title' => $dbk
);
}
}
@@ -379,13 +340,13 @@ class LinksUpdate extends SqlDataUpdate {
*/
private function getTemplateInsertions( $existing = array() ) {
$arr = array();
- foreach( $this->mTemplates as $ns => $dbkeys ) {
+ foreach ( $this->mTemplates as $ns => $dbkeys ) {
$diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
foreach ( $diffs as $dbk => $id ) {
$arr[] = array(
- 'tl_from' => $this->mId,
+ 'tl_from' => $this->mId,
'tl_namespace' => $ns,
- 'tl_title' => $dbk
+ 'tl_title' => $dbk
);
}
}
@@ -401,10 +362,10 @@ class LinksUpdate extends SqlDataUpdate {
private function getImageInsertions( $existing = array() ) {
$arr = array();
$diffs = array_diff_key( $this->mImages, $existing );
- foreach( $diffs as $iname => $dummy ) {
+ foreach ( $diffs as $iname => $dummy ) {
$arr[] = array(
'il_from' => $this->mId,
- 'il_to' => $iname
+ 'il_to' => $iname
);
}
return $arr;
@@ -418,12 +379,13 @@ class LinksUpdate extends SqlDataUpdate {
private function getExternalInsertions( $existing = array() ) {
$arr = array();
$diffs = array_diff_key( $this->mExternals, $existing );
- foreach( $diffs as $url => $dummy ) {
- foreach( wfMakeUrlIndexes( $url ) as $index ) {
+ foreach ( $diffs as $url => $dummy ) {
+ foreach ( wfMakeUrlIndexes( $url ) as $index ) {
$arr[] = array(
- 'el_from' => $this->mId,
- 'el_to' => $url,
- 'el_index' => $index,
+ 'el_id' => $this->mDb->nextSequenceValue( 'externallinks_el_id_seq' ),
+ 'el_from' => $this->mId,
+ 'el_to' => $url,
+ 'el_index' => $index,
);
}
}
@@ -433,7 +395,7 @@ class LinksUpdate extends SqlDataUpdate {
/**
* Get an array of category insertions
*
- * @param $existing array mapping existing category names to sort keys. If both
+ * @param array $existing mapping existing category names to sort keys. If both
* match a link in $this, the link will be omitted from the output
*
* @return array
@@ -462,8 +424,8 @@ class LinksUpdate extends SqlDataUpdate {
$this->mTitle->getCategorySortkey( $prefix ) );
$arr[] = array(
- 'cl_from' => $this->mId,
- 'cl_to' => $name,
+ 'cl_from' => $this->mId,
+ 'cl_to' => $name,
'cl_sortkey' => $sortkey,
'cl_timestamp' => $this->mDb->timestamp(),
'cl_sortkey_prefix' => $prefix,
@@ -477,17 +439,17 @@ class LinksUpdate extends SqlDataUpdate {
/**
* Get an array of interlanguage link insertions
*
- * @param $existing Array mapping existing language codes to titles
+ * @param array $existing mapping existing language codes to titles
*
* @return array
*/
private function getInterlangInsertions( $existing = array() ) {
$diffs = array_diff_assoc( $this->mInterlangs, $existing );
$arr = array();
- foreach( $diffs as $lang => $title ) {
+ foreach ( $diffs as $lang => $title ) {
$arr[] = array(
- 'll_from' => $this->mId,
- 'll_lang' => $lang,
+ 'll_from' => $this->mId,
+ 'll_lang' => $lang,
'll_title' => $title
);
}
@@ -504,9 +466,9 @@ class LinksUpdate extends SqlDataUpdate {
$arr = array();
foreach ( $diffs as $name => $value ) {
$arr[] = array(
- 'pp_page' => $this->mId,
- 'pp_propname' => $name,
- 'pp_value' => $value,
+ 'pp_page' => $this->mId,
+ 'pp_propname' => $name,
+ 'pp_value' => $value,
);
}
return $arr;
@@ -520,13 +482,13 @@ class LinksUpdate extends SqlDataUpdate {
*/
private function getInterwikiInsertions( $existing = array() ) {
$arr = array();
- foreach( $this->mInterwikis as $prefix => $dbkeys ) {
+ foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
$diffs = isset( $existing[$prefix] ) ? array_diff_key( $dbkeys, $existing[$prefix] ) : $dbkeys;
foreach ( $diffs as $dbk => $id ) {
$arr[] = array(
- 'iwl_from' => $this->mId,
+ 'iwl_from' => $this->mId,
'iwl_prefix' => $prefix,
- 'iwl_title' => $dbk
+ 'iwl_title' => $dbk
);
}
}
@@ -810,6 +772,40 @@ class LinksUpdate extends SqlDataUpdate {
}
}
}
+
+ /**
+ * Fetch page links added by this LinksUpdate. Only available after the update is complete.
+ * @since 1.22
+ * @return null|array of Titles
+ */
+ public function getAddedLinks() {
+ if ( $this->linkInsertions === null ) {
+ return null;
+ }
+ $result = array();
+ foreach ( $this->linkInsertions as $insertion ) {
+ $result[] = Title::makeTitle( $insertion[ 'pl_namespace' ], $insertion[ 'pl_title' ] );
+ }
+ return $result;
+ }
+
+ /**
+ * Fetch page links removed by this LinksUpdate. Only available after the update is complete.
+ * @since 1.22
+ * @return null|array of Titles
+ */
+ public function getRemovedLinks() {
+ if ( $this->linkDeletions === null ) {
+ return null;
+ }
+ $result = array();
+ foreach ( $this->linkDeletions as $ns => $titles ) {
+ foreach ( $titles as $title => $unused ) {
+ $result[] = Title::makeTitle( $ns, $title );
+ }
+ }
+ return $result;
+ }
}
/**
@@ -823,11 +819,16 @@ class LinksDeletionUpdate extends SqlDataUpdate {
* Constructor
*
* @param $page WikiPage Page we are updating
+ * @throws MWException
*/
function __construct( WikiPage $page ) {
parent::__construct( false ); // no implicit transaction
$this->mPage = $page;
+
+ if ( !$page->exists() ) {
+ throw new MWException( "Page ID not known, perhaps the page doesn't exist?" );
+ }
}
/**
@@ -838,22 +839,20 @@ class LinksDeletionUpdate extends SqlDataUpdate {
$id = $this->mPage->getId();
# Delete restrictions for it
- $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
+ $this->mDb->delete( 'page_restrictions', array( 'pr_page' => $id ), __METHOD__ );
# Fix category table counts
$cats = array();
$res = $this->mDb->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
foreach ( $res as $row ) {
- $cats [] = $row->cl_to;
+ $cats[] = $row->cl_to;
}
$this->mPage->updateCategoryCounts( array(), $cats );
# If using cascading deletes, we can skip some explicit deletes
if ( !$this->mDb->cascadingDeletes() ) {
- $this->mDb->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
-
# Delete outgoing links
$this->mDb->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
$this->mDb->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
@@ -879,4 +878,16 @@ class LinksDeletionUpdate extends SqlDataUpdate {
__METHOD__ );
}
}
+
+ /**
+ * Update all the appropriate counts in the category table.
+ * @param array $added associative array of category name => sort key
+ * @param array $deleted associative array of category name => sort key
+ */
+ function updateCategoryCounts( $added, $deleted ) {
+ $a = WikiPage::factory( $this->mTitle );
+ $a->updateCategoryCounts(
+ array_keys( $added ), array_keys( $deleted )
+ );
+ }
}
diff --git a/includes/CryptRand.php b/includes/MWCryptRand.php
index 858eebf2..bac018e8 100644
--- a/includes/CryptRand.php
+++ b/includes/MWCryptRand.php
@@ -79,7 +79,7 @@ class MWCryptRand {
// Include some information about the filesystem's current state in the random state
$files = array();
- // We know this file is here so grab some info about ourself
+ // We know this file is here so grab some info about ourselves
$files[] = __FILE__;
// We must also have a parent folder, and with the usual file structure, a grandparent
@@ -106,7 +106,11 @@ class MWCryptRand {
}
}
// The absolute filename itself will differ from install to install so don't leave it out
- $state .= realpath( $file );
+ if ( ( $path = realpath( $file ) ) !== false ) {
+ $state .= $path;
+ } else {
+ $state .= $file;
+ }
$state .= implode( '', $stat );
} else {
// The fact that the file isn't there is worth at least a
@@ -144,7 +148,7 @@ class MWCryptRand {
/**
* Randomly hash data while mixing in clock drift data for randomness
*
- * @param $data string The data to randomly hash.
+ * @param string $data The data to randomly hash.
* @return String The hashed bytes
* @author Tim Starling
*/
@@ -158,7 +162,7 @@ class MWCryptRand {
$buffer = str_repeat( ' ', $bufLength );
$bufPos = 0;
- // Iterate for $duration seconds or at least $minIerations number of iterations
+ // Iterate for $duration seconds or at least $minIterations number of iterations
$iterations = 0;
$startTime = microtime( true );
$currentTime = $startTime;
@@ -391,7 +395,7 @@ class MWCryptRand {
// We hash the random state with more salt to avoid the state from leaking
// out and being used to predict the /randomness/ that follows.
if ( strlen( $buffer ) < $bytes ) {
- wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" );
+ wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" );
}
while ( strlen( $buffer ) < $bytes ) {
wfProfileIn( __METHOD__ . '-fallback' );
@@ -464,8 +468,8 @@ class MWCryptRand {
* You can use MWCryptRand::wasStrong() if you wish to know if the source used
* was cryptographically strong.
*
- * @param $bytes int the number of bytes of random data to generate
- * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
+ * @param int $bytes the number of bytes of random data to generate
+ * @param bool $forceStrong Pass true if you want generate to prefer cryptographically
* strong sources of entropy even if reading from them may steal
* more entropy from the system than optimal.
* @return String Raw binary random data
@@ -480,8 +484,8 @@ class MWCryptRand {
* You can use MWCryptRand::wasStrong() if you wish to know if the source used
* was cryptographically strong.
*
- * @param $chars int the number of hex chars of random data to generate
- * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
+ * @param int $chars the number of hex chars of random data to generate
+ * @param bool $forceStrong Pass true if you want generate to prefer cryptographically
* strong sources of entropy even if reading from them may steal
* more entropy from the system than optimal.
* @return String Hexadecimal random data
diff --git a/includes/MWFunction.php b/includes/MWFunction.php
index 36fcc30b..6d11d178 100644
--- a/includes/MWFunction.php
+++ b/includes/MWFunction.php
@@ -23,48 +23,24 @@
class MWFunction {
/**
- * @param $callback
- * @return array
- * @throws MWException
- */
- protected static function cleanCallback( $callback ) {
- if( is_string( $callback ) ) {
- if ( strpos( $callback, '::' ) !== false ) {
- // PHP 5.1 cannot use call_user_func( 'Class::Method' )
- // It can only handle only call_user_func( array( 'Class', 'Method' ) )
- $callback = explode( '::', $callback, 2);
- }
- }
-
- if( count( $callback ) == 2 && $callback[0] == 'self' || $callback[0] == 'parent' ) {
- throw new MWException( 'MWFunction cannot call self::method() or parent::method()' );
- }
-
- // Run autoloader (workaround for call_user_func_array bug: http://bugs.php.net/bug.php?id=51329)
- is_callable( $callback );
-
- return $callback;
- }
-
- /**
+ * @deprecated since 1.22; use call_user_func()
* @param $callback
* @return mixed
*/
public static function call( $callback ) {
- $callback = self::cleanCallback( $callback );
-
+ wfDeprecated( __METHOD__, '1.22' );
$args = func_get_args();
-
return call_user_func_array( 'call_user_func', $args );
}
/**
+ * @deprecated since 1.22; use call_user_func_array()
* @param $callback
* @param $argsarams
* @return mixed
*/
public static function callArray( $callback, $argsarams ) {
- $callback = self::cleanCallback( $callback );
+ wfDeprecated( __METHOD__, '1.22' );
return call_user_func_array( $callback, $argsarams );
}
@@ -74,7 +50,7 @@ class MWFunction {
* @return object
*/
public static function newObj( $class, $args = array() ) {
- if( !count( $args ) ) {
+ if ( !count( $args ) ) {
return new $class;
}
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index 42791f57..232f43e8 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -124,6 +124,7 @@ class MagicWord {
'revisionyear',
'revisiontimestamp',
'revisionuser',
+ 'revisionsize',
'subpagename',
'subpagenamee',
'talkspace',
@@ -138,6 +139,8 @@ class MagicWord {
'numberofactiveusers',
'numberofpages',
'currentversion',
+ 'rootpagename',
+ 'rootpagenamee',
'basepagename',
'basepagenamee',
'currenttimestamp',
@@ -149,7 +152,7 @@ class MagicWord {
);
/* Array of caching hints for ParserCache */
- static public $mCacheTTLs = array (
+ static public $mCacheTTLs = array(
'currentmonth' => 86400,
'currentmonth1' => 86400,
'currentmonthname' => 86400,
@@ -217,7 +220,7 @@ class MagicWord {
/**#@-*/
- function __construct($id = 0, $syn = array(), $cs = false) {
+ function __construct( $id = 0, $syn = array(), $cs = false ) {
$this->mId = $id;
$this->mSynonyms = (array)$syn;
$this->mCaseSensitive = $cs;
@@ -282,6 +285,7 @@ class MagicWord {
*/
static function getDoubleUnderscoreArray() {
if ( is_null( self::$mDoubleUnderscoreArray ) ) {
+ wfRunHooks( 'GetDoubleUnderscoreIDs', array( &self::$mDoubleUnderscoreIDs ) );
self::$mDoubleUnderscoreArray = new MagicWordArray( self::$mDoubleUnderscoreIDs );
}
return self::$mDoubleUnderscoreArray;
@@ -307,9 +311,9 @@ class MagicWord {
$this->mId = $id;
$wgContLang->getMagic( $this );
if ( !$this->mSynonyms ) {
- $this->mSynonyms = array( 'dkjsagfjsgashfajsh' );
+ $this->mSynonyms = array( 'brionmademeputthishere' );
+ wfProfileOut( __METHOD__ );
throw new MWException( "Error: invalid magic word '$id'" );
- #wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" );
}
wfProfileOut( __METHOD__ );
}
@@ -325,9 +329,10 @@ class MagicWord {
usort( $synonyms, array( $this, 'compareStringLength' ) );
$escSyn = array();
- foreach ( $synonyms as $synonym )
+ foreach ( $synonyms as $synonym ) {
// In case a magic word contains /, like that's going to happen;)
$escSyn[] = preg_quote( $synonym, '/' );
+ }
$this->mBaseRegex = implode( '|', $escSyn );
$case = $this->mCaseSensitive ? '' : 'iu';
@@ -366,7 +371,7 @@ class MagicWord {
* @return string
*/
function getRegex() {
- if ($this->mRegex == '' ) {
+ if ( $this->mRegex == '' ) {
$this->initRegex();
}
return $this->mRegex;
@@ -380,8 +385,9 @@ class MagicWord {
* @return string
*/
function getRegexCase() {
- if ( $this->mRegex === '' )
+ if ( $this->mRegex === '' ) {
$this->initRegex();
+ }
return $this->mCaseSensitive ? '' : 'iu';
}
@@ -392,7 +398,7 @@ class MagicWord {
* @return string
*/
function getRegexStart() {
- if ($this->mRegex == '' ) {
+ if ( $this->mRegex == '' ) {
$this->initRegex();
}
return $this->mRegexStart;
@@ -404,7 +410,7 @@ class MagicWord {
* @return string
*/
function getBaseRegex() {
- if ($this->mRegex == '') {
+ if ( $this->mRegex == '' ) {
$this->initRegex();
}
return $this->mBaseRegex;
@@ -453,9 +459,9 @@ class MagicWord {
# blank elements and re-sort the indices.
# See also bug 6526
- $matches = array_values(array_filter($matches));
+ $matches = array_values( array_filter( $matches ) );
- if ( count($matches) == 1 ) {
+ if ( count( $matches ) == 1 ) {
return $matches[0];
} else {
return $matches[1];
@@ -463,7 +469,6 @@ class MagicWord {
}
}
-
/**
* Returns true if the text matches the word, and alters the
* input string, removing all instances of the word
@@ -509,7 +514,7 @@ class MagicWord {
*/
function replace( $replacement, $subject, $limit = -1 ) {
$res = preg_replace( $this->getRegex(), StringUtils::escapeRegexReplacement( $replacement ), $subject, $limit );
- $this->mModified = !($res === $subject);
+ $this->mModified = $res !== $subject;
return $res;
}
@@ -525,7 +530,7 @@ class MagicWord {
*/
function substituteCallback( $text, $callback ) {
$res = preg_replace_callback( $this->getVariableRegex(), $callback, $text );
- $this->mModified = !($res === $text);
+ $this->mModified = $res !== $text;
return $res;
}
@@ -534,7 +539,7 @@ class MagicWord {
*
* @return string
*/
- function getVariableRegex() {
+ function getVariableRegex() {
if ( $this->mVariableRegex == '' ) {
$this->initRegex();
}
@@ -577,7 +582,7 @@ class MagicWord {
*
* @return bool
*/
- function getWasModified(){
+ function getWasModified() {
return $this->mModified;
}
@@ -594,17 +599,17 @@ class MagicWord {
*
* @return bool
*/
- function replaceMultiple( $magicarr, $subject, &$result ){
+ function replaceMultiple( $magicarr, $subject, &$result ) {
$search = array();
$replace = array();
- foreach( $magicarr as $id => $replacement ){
+ foreach ( $magicarr as $id => $replacement ) {
$mw = MagicWord::get( $id );
$search[] = $mw->getRegex();
$replace[] = $replacement;
}
$result = preg_replace( $search, $replace, $subject );
- return !($result === $subject);
+ return $result !== $subject;
}
/**
@@ -617,7 +622,7 @@ class MagicWord {
function addToArray( &$array, $value ) {
global $wgContLang;
foreach ( $this->mSynonyms as $syn ) {
- $array[$wgContLang->lc($syn)] = $value;
+ $array[$wgContLang->lc( $syn )] = $value;
}
}
@@ -704,7 +709,9 @@ class MagicWordArray {
$magic = MagicWord::get( $name );
$case = intval( $magic->isCaseSensitive() );
foreach ( $magic->getSynonyms() as $i => $syn ) {
- $group = "(?P<{$i}_{$name}>" . preg_quote( $syn, '/' ) . ')';
+ // Group name must start with a non-digit in PCRE 8.34+
+ $it = strtr( $i, '0123456789', 'abcdefghij' );
+ $group = "(?P<{$it}_{$name}>" . preg_quote( $syn, '/' ) . ')';
if ( $this->baseRegex[$case] === '' ) {
$this->baseRegex[$case] = $group;
} else {
@@ -811,7 +818,7 @@ class MagicWordArray {
return array( $magicName, $paramValue );
}
// This shouldn't happen either
- throw new MWException( __METHOD__.': parameter not found' );
+ throw new MWException( __METHOD__ . ': parameter not found' );
}
/**
diff --git a/includes/MappedIterator.php b/includes/MappedIterator.php
new file mode 100644
index 00000000..70d20327
--- /dev/null
+++ b/includes/MappedIterator.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Convenience class for generating iterators from iterators.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Convenience class for generating iterators from iterators.
+ *
+ * @since 1.21
+ */
+class MappedIterator extends FilterIterator {
+ /** @var callable */
+ protected $vCallback;
+ /** @var callable */
+ protected $aCallback;
+ /** @var array */
+ protected $cache = array();
+
+ protected $rewound = false; // boolean; whether rewind() has been called
+
+ /**
+ * Build an new iterator from a base iterator by having the former wrap the
+ * later, returning the result of "value" callback for each current() invocation.
+ * The callback takes the result of current() on the base iterator as an argument.
+ * The keys of the base iterator are reused verbatim.
+ *
+ * An "accept" callback can also be provided which will be called for each value in
+ * the base iterator (post-callback) and will return true if that value should be
+ * included in iteration of the MappedIterator (otherwise it will be filtered out).
+ *
+ * @param Iterator|Array $iter
+ * @param callable $vCallback Value transformation callback
+ * @param array $options Options map (includes "accept") (since 1.22)
+ * @throws MWException
+ */
+ public function __construct( $iter, $vCallback, array $options = array() ) {
+ if ( is_array( $iter ) ) {
+ $baseIterator = new ArrayIterator( $iter );
+ } elseif ( $iter instanceof Iterator ) {
+ $baseIterator = $iter;
+ } else {
+ throw new MWException( "Invalid base iterator provided." );
+ }
+ parent::__construct( $baseIterator );
+ $this->vCallback = $vCallback;
+ $this->aCallback = isset( $options['accept'] ) ? $options['accept'] : null;
+ }
+
+ public function next() {
+ $this->cache = array();
+ parent::next();
+ }
+
+ public function rewind() {
+ $this->rewound = true;
+ $this->cache = array();
+ parent::rewind();
+ }
+
+ public function accept() {
+ $value = call_user_func( $this->vCallback, $this->getInnerIterator()->current() );
+ $ok = ( $this->aCallback ) ? call_user_func( $this->aCallback, $value ) : true;
+ if ( $ok ) {
+ $this->cache['current'] = $value;
+ }
+ return $ok;
+ }
+
+ public function key() {
+ $this->init();
+ return parent::key();
+ }
+
+ public function valid() {
+ $this->init();
+ return parent::valid();
+ }
+
+ public function current() {
+ $this->init();
+ if ( parent::valid() ) {
+ return $this->cache['current'];
+ } else {
+ return null; // out of range
+ }
+ }
+
+ /**
+ * Obviate the usual need for rewind() before using a FilterIterator in a manual loop
+ */
+ protected function init() {
+ if ( !$this->rewound ) {
+ $this->rewind();
+ }
+ }
+}
diff --git a/includes/Message.php b/includes/Message.php
index b0147b9a..57c6264d 100644
--- a/includes/Message.php
+++ b/includes/Message.php
@@ -22,11 +22,11 @@
*/
/**
- * The Message class provides methods which fullfil two basic services:
+ * The Message class provides methods which fulfil two basic services:
* - fetching interface messages
* - processing messages into a variety of formats
*
- * First implemented with MediaWiki 1.17, the Message class is intented to
+ * First implemented with MediaWiki 1.17, the Message class is intended to
* replace the old wfMsg* functions that over time grew unusable.
* @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences
* between old and new functions.
@@ -203,6 +203,11 @@ class Message {
protected $title = null;
/**
+ * Content object representing the message
+ */
+ protected $content = null;
+
+ /**
* @var string
*/
protected $message;
@@ -211,7 +216,7 @@ class Message {
* Constructor.
* @since 1.17
* @param $key: message key, or array of message keys to try and use the first non-empty message for
- * @param $params Array message parameters
+ * @param array $params message parameters
* @return Message: $this
*/
public function __construct( $key, $params = array() ) {
@@ -222,11 +227,50 @@ class Message {
}
/**
+ * Returns the message key
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getKey() {
+ if ( is_array( $this->key ) ) {
+ // May happen if some kind of fallback is applied.
+ // For now, just use the first key. We really need a better solution.
+ return $this->key[0];
+ } else {
+ return $this->key;
+ }
+ }
+
+ /**
+ * Returns the message parameters
+ *
+ * @since 1.21
+ *
+ * @return string[]
+ */
+ public function getParams() {
+ return $this->parameters;
+ }
+
+ /**
+ * Returns the message format
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getFormat() {
+ return $this->format;
+ }
+
+ /**
* Factory function that is just wrapper for the real constructor. It is
- * intented to be used instead of the real constructor, because it allows
+ * intended to be used instead of the real constructor, because it allows
* chaining method calls, while new objects don't.
* @since 1.17
- * @param $key String: message key
+ * @param string $key message key
* @param Varargs: parameters as Strings
* @return Message: $this
*/
@@ -247,9 +291,9 @@ class Message {
public static function newFallbackSequence( /*...*/ ) {
$keys = func_get_args();
if ( func_num_args() == 1 ) {
- if ( is_array($keys[0]) ) {
+ if ( is_array( $keys[0] ) ) {
// Allow an array to be passed as the first argument instead
- $keys = array_values($keys[0]);
+ $keys = array_values( $keys[0] );
} else {
// Optimize a single string to not need special fallback handling
$keys = $keys[0];
@@ -288,7 +332,7 @@ class Message {
if ( isset( $params[0] ) && is_array( $params[0] ) ) {
$params = $params[0];
}
- foreach( $params as $param ) {
+ foreach ( $params as $param ) {
$this->parameters[] = self::rawParam( $param );
}
return $this;
@@ -306,13 +350,103 @@ class Message {
if ( isset( $params[0] ) && is_array( $params[0] ) ) {
$params = $params[0];
}
- foreach( $params as $param ) {
+ foreach ( $params as $param ) {
$this->parameters[] = self::numParam( $param );
}
return $this;
}
/**
+ * Add parameters that are durations of time and will be passed through
+ * Language::formatDuration before substitution
+ * @since 1.22
+ * @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
+ * @return Message: $this
+ */
+ public function durationParams( /*...*/ ) {
+ $params = func_get_args();
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ foreach ( $params as $param ) {
+ $this->parameters[] = self::durationParam( $param );
+ }
+ return $this;
+ }
+
+ /**
+ * Add parameters that are expiration times and will be passed through
+ * Language::formatExpiry before substitution
+ * @since 1.22
+ * @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
+ * @return Message: $this
+ */
+ public function expiryParams( /*...*/ ) {
+ $params = func_get_args();
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ foreach ( $params as $param ) {
+ $this->parameters[] = self::expiryParam( $param );
+ }
+ return $this;
+ }
+
+ /**
+ * Add parameters that are time periods and will be passed through
+ * Language::formatTimePeriod before substitution
+ * @since 1.22
+ * @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
+ * @return Message: $this
+ */
+ public function timeperiodParams( /*...*/ ) {
+ $params = func_get_args();
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ foreach ( $params as $param ) {
+ $this->parameters[] = self::timeperiodParam( $param );
+ }
+ return $this;
+ }
+
+ /**
+ * Add parameters that are file sizes and will be passed through
+ * Language::formatSize before substitution
+ * @since 1.22
+ * @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
+ * @return Message: $this
+ */
+ public function sizeParams( /*...*/ ) {
+ $params = func_get_args();
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ foreach ( $params as $param ) {
+ $this->parameters[] = self::sizeParam( $param );
+ }
+ return $this;
+ }
+
+ /**
+ * Add parameters that are bitrates and will be passed through
+ * Language::formatBitrate before substitution
+ * @since 1.22
+ * @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
+ * @return Message: $this
+ */
+ public function bitrateParams( /*...*/ ) {
+ $params = func_get_args();
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ foreach ( $params as $param ) {
+ $this->parameters[] = self::bitrateParam( $param );
+ }
+ return $this;
+ }
+
+ /**
* Set the language and the title from a context object
* @since 1.19
* @param $context IContextSource
@@ -332,13 +466,14 @@ class Message {
* turned off.
* @since 1.17
* @param $lang Mixed: language code or Language object.
+ * @throws MWException
* @return Message: $this
*/
public function inLanguage( $lang ) {
if ( $lang instanceof Language || $lang instanceof StubUserLang ) {
$this->language = $lang;
} elseif ( is_string( $lang ) ) {
- if( $this->language->getCode() != $lang ) {
+ if ( $this->language->getCode() != $lang ) {
$this->language = Language::factory( $lang );
}
} else {
@@ -378,7 +513,7 @@ class Message {
* @since 1.20
*/
public function setInterfaceMessageFlag( $value ) {
- $this->interface = (bool) $value;
+ $this->interface = (bool)$value;
return $this;
}
@@ -389,7 +524,7 @@ class Message {
* @return Message: $this
*/
public function useDatabase( $value ) {
- $this->useDatabase = (bool) $value;
+ $this->useDatabase = (bool)$value;
return $this;
}
@@ -405,6 +540,18 @@ class Message {
}
/**
+ * Returns the message as a Content object.
+ * @return Content
+ */
+ public function content() {
+ if ( !$this->content ) {
+ $this->content = new MessageContent( $this );
+ }
+
+ return $this->content;
+ }
+
+ /**
* Returns the message parsed from wikitext to HTML.
* @since 1.17
* @return String: HTML
@@ -413,28 +560,37 @@ class Message {
$string = $this->fetchMessage();
if ( $string === false ) {
- $key = htmlspecialchars( is_array( $this->key ) ? $this->key[0] : $this->key );
+ $key = htmlspecialchars( is_array( $this->key ) ? $this->key[0] : $this->key );
if ( $this->format === 'plain' ) {
return '<' . $key . '>';
}
return '&lt;' . $key . '&gt;';
}
+ # Replace $* with a list of parameters for &uselang=qqx.
+ if ( strpos( $string, '$*' ) !== false ) {
+ $paramlist = '';
+ if ( $this->parameters !== array() ) {
+ $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
+ }
+ $string = str_replace( '$*', $paramlist, $string );
+ }
+
# Replace parameters before text parsing
$string = $this->replaceParameters( $string, 'before' );
# Maybe transform using the full parser
- if( $this->format === 'parse' ) {
+ if ( $this->format === 'parse' ) {
$string = $this->parseText( $string );
$m = array();
- if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
+ if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
$string = $m[1];
}
- } elseif( $this->format === 'block-parse' ){
+ } elseif ( $this->format === 'block-parse' ) {
$string = $this->parseText( $string );
- } elseif( $this->format === 'text' ){
+ } elseif ( $this->format === 'text' ) {
$string = $this->transformText( $string );
- } elseif( $this->format === 'escaped' ){
+ } elseif ( $this->format === 'escaped' ) {
$string = $this->transformText( $string );
$string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
}
@@ -447,13 +603,30 @@ class Message {
/**
* Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
- * $foo = Message::get($key);
+ * $foo = Message::get( $key );
* $string = "<abbr>$foo</abbr>";
* @since 1.18
* @return String
*/
public function __toString() {
- return $this->toString();
+ // PHP doesn't allow __toString to throw exceptions and will
+ // trigger a fatal error if it does. So, catch any exceptions.
+
+ try {
+ return $this->toString();
+ } catch ( Exception $ex ) {
+ try {
+ trigger_error( "Exception caught in " . __METHOD__ . " (message " . $this->key . "): "
+ . $ex, E_USER_WARNING );
+ } catch ( Exception $ex ) {
+ // Doh! Cause a fatal error after all?
+ }
+
+ if ( $this->format === 'plain' ) {
+ return '<' . $this->key . '>';
+ }
+ return '&lt;' . $this->key . '&gt;';
+ }
}
/**
@@ -477,7 +650,7 @@ class Message {
}
/**
- * Returns the message text as-is, only parameters are subsituted.
+ * Returns the message text as-is, only parameters are substituted.
* @since 1.17
* @return String: Unescaped untransformed message text.
*/
@@ -530,7 +703,7 @@ class Message {
/**
* Check whether a message does not exist, is an empty string, or is "-"
* @since 1.18
- * @return Bool: true if is is and false if not
+ * @return Bool: true if it is and false if not
*/
public function isDisabled() {
$message = $this->fetchMessage();
@@ -556,18 +729,63 @@ class Message {
}
/**
- * Substitutes any paramaters into the message text.
+ * @since 1.22
+ * @param $value
+ * @return array
+ */
+ public static function durationParam( $value ) {
+ return array( 'duration' => $value );
+ }
+
+ /**
+ * @since 1.22
+ * @param $value
+ * @return array
+ */
+ public static function expiryParam( $value ) {
+ return array( 'expiry' => $value );
+ }
+
+ /**
+ * @since 1.22
+ * @param $value
+ * @return array
+ */
+ public static function timeperiodParam( $value ) {
+ return array( 'period' => $value );
+ }
+
+ /**
+ * @since 1.22
+ * @param $value
+ * @return array
+ */
+ public static function sizeParam( $value ) {
+ return array( 'size' => $value );
+ }
+
+ /**
+ * @since 1.22
+ * @param $value
+ * @return array
+ */
+ public static function bitrateParam( $value ) {
+ return array( 'bitrate' => $value );
+ }
+
+ /**
+ * Substitutes any parameters into the message text.
* @since 1.17
- * @param $message String: the message text
- * @param $type String: either before or after
+ * @param string $message the message text
+ * @param string $type either before or after
* @return String
*/
protected function replaceParameters( $message, $type = 'before' ) {
$replacementKeys = array();
- foreach( $this->parameters as $n => $param ) {
+ foreach ( $this->parameters as $n => $param ) {
list( $paramType, $value ) = $this->extractParam( $param );
if ( $type === $paramType ) {
- $replacementKeys['$' . ($n + 1)] = $value;
+ $replacementKeys['$' . ( $n + 1 )] = $value;
}
}
$message = strtr( $message, $replacementKeys );
@@ -577,41 +795,59 @@ class Message {
/**
* Extracts the parameter type and preprocessed the value if needed.
* @since 1.18
- * @param $param String|Array: Parameter as defined in this class.
+ * @param string|array $param Parameter as defined in this class.
* @return Tuple(type, value)
*/
protected function extractParam( $param ) {
- if ( is_array( $param ) && isset( $param['raw'] ) ) {
- return array( 'after', $param['raw'] );
- } elseif ( is_array( $param ) && isset( $param['num'] ) ) {
- // Replace number params always in before step for now.
- // No support for combined raw and num params
- return array( 'before', $this->language->formatNum( $param['num'] ) );
- } elseif ( !is_array( $param ) ) {
- return array( 'before', $param );
+ if ( is_array( $param ) ) {
+ if ( isset( $param['raw'] ) ) {
+ return array( 'after', $param['raw'] );
+ } elseif ( isset( $param['num'] ) ) {
+ // Replace number params always in before step for now.
+ // No support for combined raw and num params
+ return array( 'before', $this->language->formatNum( $param['num'] ) );
+ } elseif ( isset( $param['duration'] ) ) {
+ return array( 'before', $this->language->formatDuration( $param['duration'] ) );
+ } elseif ( isset( $param['expiry'] ) ) {
+ return array( 'before', $this->language->formatExpiry( $param['expiry'] ) );
+ } elseif ( isset( $param['period'] ) ) {
+ return array( 'before', $this->language->formatTimePeriod( $param['period'] ) );
+ } elseif ( isset( $param['size'] ) ) {
+ return array( 'before', $this->language->formatSize( $param['size'] ) );
+ } elseif ( isset( $param['bitrate'] ) ) {
+ return array( 'before', $this->language->formatBitrate( $param['bitrate'] ) );
+ } else {
+ trigger_error(
+ "Invalid message parameter: " . htmlspecialchars( serialize( $param ) ),
+ E_USER_WARNING
+ );
+ return array( 'before', '[INVALID]' );
+ }
+ } elseif ( $param instanceof Message ) {
+ // Message objects should not be before parameters because
+ // then they'll get double escaped. If the message needs to be
+ // escaped, it'll happen right here when we call toString().
+ return array( 'after', $param->toString() );
} else {
- trigger_error(
- "Invalid message parameter: " . htmlspecialchars( serialize( $param ) ),
- E_USER_WARNING
- );
- return array( 'before', '[INVALID]' );
+ return array( 'before', $param );
}
}
/**
* Wrapper for what ever method we use to parse wikitext.
* @since 1.17
- * @param $string String: Wikitext message contents
+ * @param string $string Wikitext message contents
* @return string Wikitext parsed into HTML
*/
protected function parseText( $string ) {
- return MessageCache::singleton()->parse( $string, $this->title, /*linestart*/true, $this->interface, $this->language )->getText();
+ $out = MessageCache::singleton()->parse( $string, $this->title, /*linestart*/true, $this->interface, $this->language );
+ return is_object( $out ) ? $out->getText() : $out;
}
/**
* Wrapper for what ever method we use to {{-transform wikitext.
* @since 1.17
- * @param $string String: Wikitext message contents
+ * @param string $string Wikitext message contents
* @return string Wikitext with {{-constructs replaced with their values.
*/
protected function transformText( $string ) {
@@ -621,6 +857,7 @@ class Message {
/**
* Wrapper for what ever method we use to get message contents
* @since 1.17
+ * @throws MWException
* @return string
*/
protected function fetchMessage() {
@@ -645,3 +882,45 @@ class Message {
}
}
+
+/**
+ * Variant of the Message class.
+ *
+ * Rather than treating the message key as a lookup
+ * value (which is passed to the MessageCache and
+ * translated as necessary), a RawMessage key is
+ * treated as the actual message.
+ *
+ * All other functionality (parsing, escaping, etc.)
+ * is preserved.
+ *
+ * @since 1.21
+ */
+class RawMessage extends Message {
+ /**
+ * Call the parent constructor, then store the key as
+ * the message.
+ *
+ * @param string $key Message to use
+ * @param array $params Parameters for the message
+ * @see Message::__construct
+ */
+ public function __construct( $key, $params = array() ) {
+ parent::__construct( $key, $params );
+ // The key is the message.
+ $this->message = $key;
+ }
+
+ /**
+ * Fetch the message (in this case, the key).
+ *
+ * @return string
+ */
+ public function fetchMessage() {
+ // Just in case the message is unset somewhere.
+ if ( !isset( $this->message ) ) {
+ $this->message = $this->key;
+ }
+ return $this->message;
+ }
+}
diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php
index c96ea56e..8a8142b7 100644
--- a/includes/MessageBlobStore.php
+++ b/includes/MessageBlobStore.php
@@ -29,7 +29,7 @@
* A message blob is a JSON object containing the interface messages for a
* certain resource in a certain language. These message blobs are cached
* in the msg_resource table and automatically invalidated when one of their
- * consistuent messages or the resource itself is changed.
+ * constituent messages or the resource itself is changed.
*/
class MessageBlobStore {
@@ -37,8 +37,8 @@ class MessageBlobStore {
* Get the message blobs for a set of modules
*
* @param $resourceLoader ResourceLoader object
- * @param $modules array Array of module objects keyed by module name
- * @param $lang string Language code
+ * @param array $modules Array of module objects keyed by module name
+ * @param string $lang Language code
* @return array An array mapping module names to message blobs
*/
public static function get( ResourceLoader $resourceLoader, $modules, $lang ) {
@@ -68,9 +68,9 @@ class MessageBlobStore {
* present, it is not regenerated; instead, the preexisting blob
* is fetched and returned.
*
- * @param $name String: module name
+ * @param string $name module name
* @param $module ResourceLoaderModule object
- * @param $lang String: language code
+ * @param string $lang language code
* @return mixed Message blob or false if the module has no messages
*/
public static function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
@@ -125,9 +125,9 @@ class MessageBlobStore {
/**
* Update the message blob for a given module in a given language
*
- * @param $name String: module name
+ * @param string $name module name
* @param $module ResourceLoaderModule object
- * @param $lang String: language code
+ * @param string $lang language code
* @return String Regenerated message blob, or null if there was no blob for the given module/language pair
*/
public static function updateModule( $name, ResourceLoaderModule $module, $lang ) {
@@ -195,7 +195,7 @@ class MessageBlobStore {
/**
* Update a single message in all message blobs it occurs in.
*
- * @param $key String: message key
+ * @param string $key message key
*/
public static function updateMessage( $key ) {
try {
@@ -255,8 +255,8 @@ class MessageBlobStore {
/**
* Create an update queue for updateMessage()
*
- * @param $key String: message key
- * @param $prevUpdates Array: updates queue to refresh or null to build a fresh update queue
+ * @param string $key message key
+ * @param array $prevUpdates updates queue to refresh or null to build a fresh update queue
* @return Array: updates queue
*/
private static function getUpdatesForMessage( $key, $prevUpdates = null ) {
@@ -306,9 +306,9 @@ class MessageBlobStore {
/**
* Reencode a message blob with the updated value for a message
*
- * @param $blob String: message blob (JSON object)
- * @param $key String: message key
- * @param $lang String: language code
+ * @param string $blob message blob (JSON object)
+ * @param string $key message key
+ * @param string $lang language code
* @return Message blob with $key replaced with its new value
*/
private static function reencodeBlob( $blob, $key, $lang ) {
@@ -323,8 +323,9 @@ class MessageBlobStore {
* Modules whose blobs are not in the database are silently dropped.
*
* @param $resourceLoader ResourceLoader object
- * @param $modules Array of module names
- * @param $lang String: language code
+ * @param array $modules of module names
+ * @param string $lang language code
+ * @throws MWException
* @return array Array mapping module names to blobs
*/
private static function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) {
@@ -360,7 +361,7 @@ class MessageBlobStore {
* Generate the message blob for a given module in a given language.
*
* @param $module ResourceLoaderModule object
- * @param $lang String: language code
+ * @param string $lang language code
* @return String: JSON object
*/
private static function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
diff --git a/includes/Metadata.php b/includes/Metadata.php
index 0ca15393..37df489c 100644
--- a/includes/Metadata.php
+++ b/includes/Metadata.php
@@ -34,7 +34,7 @@ abstract class RdfMetaData {
$this->mArticle = $article;
}
- public abstract function show();
+ abstract public function show();
protected function setup() {
global $wgOut, $wgRequest;
@@ -42,7 +42,7 @@ abstract class RdfMetaData {
$httpaccept = isset( $_SERVER['HTTP_ACCEPT'] ) ? $_SERVER['HTTP_ACCEPT'] : null;
$rdftype = wfNegotiateType( wfAcceptToPrefs( $httpaccept ), wfAcceptToPrefs( self::RDF_TYPE_PREFS ) );
- if( !$rdftype ){
+ if ( !$rdftype ) {
throw new HttpError( 406, wfMessage( 'notacceptable' ) );
}
@@ -70,7 +70,7 @@ abstract class RdfMetaData {
$lastEditor = User::newFromId( $this->mArticle->getUser() );
$this->person( 'creator', $lastEditor );
- foreach( $this->mArticle->getContributors() as $user ){
+ foreach ( $this->mArticle->getContributors() as $user ) {
$this->person( 'contributor', $user );
}
@@ -82,20 +82,20 @@ abstract class RdfMetaData {
print "\t\t<dc:{$name}>{$value}</dc:{$name}>\n";
}
- protected function date($timestamp) {
- return substr($timestamp, 0, 4) . '-'
- . substr($timestamp, 4, 2) . '-'
- . substr($timestamp, 6, 2);
+ protected function date( $timestamp ) {
+ return substr( $timestamp, 0, 4 ) . '-'
+ . substr( $timestamp, 4, 2 ) . '-'
+ . substr( $timestamp, 6, 2 );
}
protected function pageOrString( $name, $page, $str ) {
- if( $page instanceof Title ) {
+ if ( $page instanceof Title ) {
$nt = $page;
} else {
$nt = Title::newFromText( $page );
}
- if( !$nt || $nt->getArticleID() == 0 ){
+ if ( !$nt || $nt->getArticleID() == 0 ) {
$this->element( $name, $str );
} else {
$this->page( $name, $nt );
@@ -107,20 +107,22 @@ abstract class RdfMetaData {
* @param $title Title
*/
protected function page( $name, $title ) {
- $this->url( $name, $title->getFullUrl() );
+ $this->url( $name, $title->getFullURL() );
}
- protected function url($name, $url) {
+ protected function url( $name, $url ) {
$url = htmlspecialchars( $url );
print "\t\t<dc:{$name} rdf:resource=\"{$url}\" />\n";
}
protected function person( $name, User $user ) {
- if( $user->isAnon() ){
+ global $wgHiddenPrefs;
+
+ if ( $user->isAnon() ) {
$this->element( $name, wfMessage( 'anonymous' )->numParams( 1 )->text() );
} else {
$real = $user->getRealName();
- if( $real ) {
+ if ( $real && !in_array( 'realname', $wgHiddenPrefs ) ) {
$this->element( $name, $real );
} else {
$userName = $user->getName();
@@ -140,24 +142,24 @@ abstract class RdfMetaData {
protected function rights() {
global $wgRightsPage, $wgRightsUrl, $wgRightsText;
- if( $wgRightsPage && ( $nt = Title::newFromText( $wgRightsPage ) )
- && ($nt->getArticleID() != 0)) {
- $this->page('rights', $nt);
- } elseif( $wgRightsUrl ){
- $this->url('rights', $wgRightsUrl);
- } elseif( $wgRightsText ){
+ if ( $wgRightsPage && ( $nt = Title::newFromText( $wgRightsPage ) )
+ && ( $nt->getArticleID() != 0 ) ) {
+ $this->page( 'rights', $nt );
+ } elseif ( $wgRightsUrl ) {
+ $this->url( 'rights', $wgRightsUrl );
+ } elseif ( $wgRightsText ) {
$this->element( 'rights', $wgRightsText );
}
}
- protected function getTerms( $url ){
+ protected function getTerms( $url ) {
global $wgLicenseTerms;
- if( $wgLicenseTerms ){
+ if ( $wgLicenseTerms ) {
return $wgLicenseTerms;
} else {
$known = $this->getKnownLicenses();
- if( isset( $known[$url] ) ) {
+ if ( isset( $known[$url] ) ) {
return $known[$url];
} else {
return array();
@@ -166,23 +168,23 @@ abstract class RdfMetaData {
}
protected function getKnownLicenses() {
- $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc',
- 'by-nc-sa', 'by-sa');
- $ccVersions = array('1.0', '2.0');
+ $ccLicenses = array( 'by', 'by-nd', 'by-nd-nc', 'by-nc',
+ 'by-nc-sa', 'by-sa' );
+ $ccVersions = array( '1.0', '2.0' );
$knownLicenses = array();
- foreach ($ccVersions as $version) {
- foreach ($ccLicenses as $license) {
- if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) {
+ foreach ( $ccVersions as $version ) {
+ foreach ( $ccLicenses as $license ) {
+ if ( $version == '2.0' && substr( $license, 0, 2 ) != 'by' ) {
# 2.0 dropped the non-attribs licenses
continue;
}
$lurl = "http://creativecommons.org/licenses/{$license}/{$version}/";
- $knownLicenses[$lurl] = explode('-', $license);
+ $knownLicenses[$lurl] = explode( '-', $license );
$knownLicenses[$lurl][] = 're';
$knownLicenses[$lurl][] = 'di';
$knownLicenses[$lurl][] = 'no';
- if (!in_array('nd', $knownLicenses[$lurl])) {
+ if ( !in_array( 'nd', $knownLicenses[$lurl] ) ) {
$knownLicenses[$lurl][] = 'de';
}
}
@@ -191,13 +193,12 @@ abstract class RdfMetaData {
/* Handle the GPL and LGPL, too. */
$knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] =
- array('de', 're', 'di', 'no', 'sa', 'sc');
+ array( 'de', 're', 'di', 'no', 'sa', 'sc' );
$knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] =
- array('de', 're', 'di', 'no', 'sa', 'sc');
+ array( 'de', 're', 'di', 'no', 'sa', 'sc' );
$knownLicenses['http://www.gnu.org/copyleft/fdl.html'] =
- array('de', 're', 'di', 'no', 'sa', 'sc');
+ array( 'de', 're', 'di', 'no', 'sa', 'sc' );
return $knownLicenses;
}
}
-
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index 1873e7bf..8220e92f 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -25,21 +25,21 @@
* This is used as a fallback to mime.types files.
* An extensive list of well known mime types is provided by
* the file mime.types in the includes directory.
- *
+ *
* This list concatenated with mime.types is used to create a mime <-> ext
* map. Each line contains a mime type followed by a space separated list of
- * extensions. If multiple extensions for a single mime type exist or if
+ * extensions. If multiple extensions for a single mime type exist or if
* multiple mime types exist for a single extension then in most cases
* MediaWiki assumes that the first extension following the mime type is the
* canonical extension, and the first time a mime type appears for a certain
* extension is considered the canonical mime type.
- *
+ *
* (Note that appending $wgMimeTypeFile to the end of MM_WELL_KNOWN_MIME_TYPES
- * sucks because you can't redefine canonical types. This could be fixed by
+ * sucks because you can't redefine canonical types. This could be fixed by
* appending MM_WELL_KNOWN_MIME_TYPES behind $wgMimeTypeFile, but who knows
* what will break? In practice this probably isn't a problem anyway -- Bryan)
*/
-define('MM_WELL_KNOWN_MIME_TYPES',<<<END_STRING
+define( 'MM_WELL_KNOWN_MIME_TYPES', <<<END_STRING
application/ogg ogx ogg ogm ogv oga spx
application/pdf pdf
application/vnd.oasis.opendocument.chart odc
@@ -70,7 +70,7 @@ image/x-bmp bmp
image/gif gif
image/jpeg jpeg jpg jpe
image/png png
-image/svg+xml svg
+image/svg+xml svg
image/svg svg
image/tiff tiff tif
image/vnd.djvu djvu
@@ -91,7 +91,7 @@ END_STRING
* An extensive list of well known mime types is provided by
* the file mime.info in the includes directory.
*/
-define('MM_WELL_KNOWN_MIME_INFO', <<<END_STRING
+define( 'MM_WELL_KNOWN_MIME_INFO', <<<END_STRING
application/pdf [OFFICE]
application/vnd.oasis.opendocument.chart [OFFICE]
application/vnd.oasis.opendocument.chart-template [OFFICE]
@@ -144,21 +144,21 @@ END_STRING
class MimeMagic {
/**
- * Mapping of media types to arrays of mime types.
- * This is used by findMediaType and getMediaType, respectively
- */
+ * Mapping of media types to arrays of mime types.
+ * This is used by findMediaType and getMediaType, respectively
+ */
var $mMediaTypes = null;
/** Map of mime type aliases
- */
+ */
var $mMimeTypeAliases = null;
- /** map of mime types to file extensions (as a space seprarated list)
- */
+ /** map of mime types to file extensions (as a space separated list)
+ */
var $mMimeToExt = null;
- /** map of file extensions types to mime types (as a space seprarated list)
- */
+ /** map of file extensions types to mime types (as a space separated list)
+ */
var $mExtToMime = null;
/** IEContentAnalyzer instance
@@ -169,20 +169,16 @@ class MimeMagic {
*/
private static $instance;
- /** True if the fileinfo extension has been loaded
- */
- private static $extensionLoaded = false;
-
/** Initializes the MimeMagic object. This is called by MimeMagic::singleton().
*
* This constructor parses the mime.types and mime.info files and build internal mappings.
*/
function __construct() {
/**
- * --- load mime.types ---
- */
+ * --- load mime.types ---
+ */
- global $wgMimeTypeFile, $IP, $wgLoadFileinfoExtension;
+ global $wgMimeTypeFile, $IP;
$types = MM_WELL_KNOWN_MIME_TYPES;
@@ -190,21 +186,16 @@ class MimeMagic {
$wgMimeTypeFile = "$IP/$wgMimeTypeFile";
}
- if ( $wgLoadFileinfoExtension && !self::$extensionLoaded ) {
- self::$extensionLoaded = true;
- wfDl( 'fileinfo' );
- }
-
if ( $wgMimeTypeFile ) {
if ( is_file( $wgMimeTypeFile ) and is_readable( $wgMimeTypeFile ) ) {
- wfDebug( __METHOD__.": loading mime types from $wgMimeTypeFile\n" );
+ wfDebug( __METHOD__ . ": loading mime types from $wgMimeTypeFile\n" );
$types .= "\n";
$types .= file_get_contents( $wgMimeTypeFile );
} else {
- wfDebug( __METHOD__.": can't load mime types from $wgMimeTypeFile\n" );
+ wfDebug( __METHOD__ . ": can't load mime types from $wgMimeTypeFile\n" );
}
} else {
- wfDebug( __METHOD__.": no mime types file defined, using build-ins only.\n" );
+ wfDebug( __METHOD__ . ": no mime types file defined, using build-ins only.\n" );
}
$types = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $types );
@@ -213,7 +204,7 @@ class MimeMagic {
$this->mMimeToExt = array();
$this->mToMime = array();
- $lines = explode( "\n",$types );
+ $lines = explode( "\n", $types );
foreach ( $lines as $s ) {
$s = trim( $s );
if ( empty( $s ) ) {
@@ -231,7 +222,7 @@ class MimeMagic {
}
$mime = substr( $s, 0, $i );
- $ext = trim( substr($s, $i+1 ) );
+ $ext = trim( substr( $s, $i + 1 ) );
if ( empty( $ext ) ) {
continue;
@@ -272,17 +263,17 @@ class MimeMagic {
if ( $wgMimeInfoFile ) {
if ( is_file( $wgMimeInfoFile ) and is_readable( $wgMimeInfoFile ) ) {
- wfDebug( __METHOD__.": loading mime info from $wgMimeInfoFile\n" );
+ wfDebug( __METHOD__ . ": loading mime info from $wgMimeInfoFile\n" );
$info .= "\n";
$info .= file_get_contents( $wgMimeInfoFile );
} else {
- wfDebug(__METHOD__.": can't load mime info from $wgMimeInfoFile\n");
+ wfDebug( __METHOD__ . ": can't load mime info from $wgMimeInfoFile\n" );
}
} else {
- wfDebug(__METHOD__.": no mime info file defined, using build-ins only.\n");
+ wfDebug( __METHOD__ . ": no mime info file defined, using build-ins only.\n" );
}
- $info = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $info);
+ $info = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $info );
$info = str_replace( "\t", " ", $info );
$this->mMimeTypeAliases = array();
@@ -330,9 +321,9 @@ class MimeMagic {
$this->mMediaTypes[$mtype][] = $mime;
}
- if ( sizeof( $m ) > 1 ) {
+ if ( count( $m ) > 1 ) {
$main = $m[0];
- for ( $i=1; $i<sizeof($m); $i += 1 ) {
+ for ( $i = 1; $i < count( $m ); $i += 1 ) {
$mime = $m[$i];
$this->mMimeTypeAliases[$mime] = $main;
}
@@ -346,17 +337,17 @@ class MimeMagic {
* @return MimeMagic
*/
public static function &singleton() {
- if ( !isset( self::$instance ) ) {
+ if ( self::$instance === null ) {
self::$instance = new MimeMagic;
}
return self::$instance;
}
- /**
- * Returns a list of file extensions for a given mime type as a space
+ /**
+ * Returns a list of file extensions for a given mime type as a space
* separated string or null if the mime type was unrecognized. Resolves
* mime type aliases.
- *
+ *
* @param $mime string
* @return string|null
*/
@@ -379,10 +370,10 @@ class MimeMagic {
return null;
}
- /**
- * Returns a list of mime types for a given file extension as a space
+ /**
+ * Returns a list of mime types for a given file extension as a space
* separated string or null if the extension was unrecognized.
- *
+ *
* @param $ext string
* @return string|null
*/
@@ -393,10 +384,10 @@ class MimeMagic {
return $r;
}
- /**
+ /**
* Returns a single mime type for a given file extension or null if unknown.
* This is always the first type from the list returned by getTypesForExtension($ext).
- *
+ *
* @param $ext string
* @return string|null
*/
@@ -413,12 +404,11 @@ class MimeMagic {
return $m;
}
-
- /**
- * Tests if the extension matches the given mime type. Returns true if a
- * match was found, null if the mime type is unknown, and false if the
+ /**
+ * Tests if the extension matches the given mime type. Returns true if a
+ * match was found, null if the mime type is unknown, and false if the
* mime type is known but no matches where found.
- *
+ *
* @param $extension string
* @param $mime string
* @return bool|null
@@ -427,21 +417,21 @@ class MimeMagic {
$ext = $this->getExtensionsForType( $mime );
if ( !$ext ) {
- return null; // Unknown mime type
+ return null; // Unknown mime type
}
$ext = explode( ' ', $ext );
$extension = strtolower( $extension );
- return in_array( $extension, $ext );
+ return in_array( $extension, $ext );
}
- /**
- * Returns true if the mime type is known to represent an image format
+ /**
+ * Returns true if the mime type is known to represent an image format
* supported by the PHP GD library.
*
* @param $mime string
- *
+ *
* @return bool
*/
public function isPHPImageType( $mime ) {
@@ -489,32 +479,32 @@ class MimeMagic {
return in_array( strtolower( $extension ), $types );
}
- /**
+ /**
* Improves a mime type using the file extension. Some file formats are very generic,
- * so their mime type is not very meaningful. A more useful mime type can be derived
- * by looking at the file extension. Typically, this method would be called on the
+ * so their mime type is not very meaningful. A more useful mime type can be derived
+ * by looking at the file extension. Typically, this method would be called on the
* result of guessMimeType().
- *
+ *
* Currently, this method does the following:
*
* If $mime is "unknown/unknown" and isRecognizableExtension( $ext ) returns false,
- * return the result of guessTypesForExtension($ext).
+ * return the result of guessTypesForExtension($ext).
*
* If $mime is "application/x-opc+zip" and isMatchingExtension( $ext, $mime )
- * gives true, return the result of guessTypesForExtension($ext).
+ * gives true, return the result of guessTypesForExtension($ext).
*
- * @param $mime String: the mime type, typically guessed from a file's content.
- * @param $ext String: the file extension, as taken from the file name
+ * @param string $mime the mime type, typically guessed from a file's content.
+ * @param string $ext the file extension, as taken from the file name
*
* @return string the mime type
*/
public function improveTypeFromExtension( $mime, $ext ) {
if ( $mime === 'unknown/unknown' ) {
if ( $this->isRecognizableExtension( $ext ) ) {
- wfDebug( __METHOD__. ': refusing to guess mime type for .' .
+ wfDebug( __METHOD__ . ': refusing to guess mime type for .' .
"$ext file, we should have recognized it\n" );
} else {
- // Not something we can detect, so simply
+ // Not something we can detect, so simply
// trust the file extension
$mime = $this->guessTypesForExtension( $ext );
}
@@ -525,7 +515,7 @@ class MimeMagic {
// find the proper mime type for that file extension
$mime = $this->guessTypesForExtension( $ext );
} else {
- wfDebug( __METHOD__. ": refusing to guess better type for $mime file, " .
+ wfDebug( __METHOD__ . ": refusing to guess better type for $mime file, " .
".$ext is not a known OPC extension.\n" );
$mime = 'application/zip';
}
@@ -535,34 +525,34 @@ class MimeMagic {
$mime = $this->mMimeTypeAliases[$mime];
}
- wfDebug(__METHOD__.": improved mime type for .$ext: $mime\n");
+ wfDebug( __METHOD__ . ": improved mime type for .$ext: $mime\n" );
return $mime;
}
- /**
- * Mime type detection. This uses detectMimeType to detect the mime type
- * of the file, but applies additional checks to determine some well known
- * file formats that may be missed or misinterpreter by the default mime
- * detection (namely XML based formats like XHTML or SVG, as well as ZIP
+ /**
+ * Mime type detection. This uses detectMimeType to detect the mime type
+ * of the file, but applies additional checks to determine some well known
+ * file formats that may be missed or misinterpreted by the default mime
+ * detection (namely XML based formats like XHTML or SVG, as well as ZIP
* based formats like OPC/ODF files).
*
- * @param $file String: the file to check
+ * @param string $file the file to check
* @param $ext Mixed: the file extension, or true (default) to extract it from the filename.
- * Set it to false to ignore the extension. DEPRECATED! Set to false, use
+ * Set it to false to ignore the extension. DEPRECATED! Set to false, use
* improveTypeFromExtension($mime, $ext) later to improve mime type.
*
* @return string the mime type of $file
*/
public function guessMimeType( $file, $ext = true ) {
if ( $ext ) { // TODO: make $ext default to false. Or better, remove it.
- wfDebug( __METHOD__.": WARNING: use of the \$ext parameter is deprecated. " .
+ wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. " .
"Use improveTypeFromExtension(\$mime, \$ext) instead.\n" );
}
$mime = $this->doGuessMimeType( $file, $ext );
- if( !$mime ) {
- wfDebug( __METHOD__.": internal type detection failed for $file (.$ext)...\n" );
+ if ( !$mime ) {
+ wfDebug( __METHOD__ . ": internal type detection failed for $file (.$ext)...\n" );
$mime = $this->detectMimeType( $file, $ext );
}
@@ -570,7 +560,7 @@ class MimeMagic {
$mime = $this->mMimeTypeAliases[$mime];
}
- wfDebug(__METHOD__.": guessed mime type of $file: $mime\n");
+ wfDebug( __METHOD__ . ": guessed mime type of $file: $mime\n" );
return $mime;
}
@@ -587,8 +577,8 @@ class MimeMagic {
// @todo FIXME: Shouldn't this be rb?
$f = fopen( $file, 'rt' );
wfRestoreWarnings();
-
- if( !$f ) {
+
+ if ( !$f ) {
return 'unknown/unknown';
}
$head = fread( $f, 1024 );
@@ -629,7 +619,7 @@ class MimeMagic {
$doctype = strpos( $head, "\x42\x82" );
if ( $doctype ) {
// Next byte is datasize, then data (sizes larger than 1 byte are very stupid muxers)
- $data = substr($head, $doctype+3, 8);
+ $data = substr( $head, $doctype + 3, 8 );
if ( strncmp( $data, "matroska", 8 ) == 0 ) {
wfDebug( __METHOD__ . ": recognized file as video/x-matroska\n" );
return "video/x-matroska";
@@ -643,7 +633,7 @@ class MimeMagic {
}
/* Look for WebP */
- if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 8), "WEBPVP8 ", 8 ) == 0 ) {
+ if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 8 ), "WEBPVP8 ", 8 ) == 0 ) {
wfDebug( __METHOD__ . ": recognized file as image/webp\n" );
return "image/webp";
}
@@ -661,12 +651,11 @@ class MimeMagic {
* strings like "<? ", but should it be axed completely?
*/
if ( ( strpos( $head, '<?php' ) !== false ) ||
-
- ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) ||
- ( strpos( $head, "<\x00?\x00 " ) !== false ) ||
- ( strpos( $head, "<\x00?\x00\n" ) !== false ) ||
- ( strpos( $head, "<\x00?\x00\t" ) !== false ) ||
- ( strpos( $head, "<\x00?\x00=" ) !== false ) ) {
+ ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00 " ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00\n" ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00\t" ) !== false ) ||
+ ( strpos( $head, "<\x00?\x00=" ) !== false ) ) {
wfDebug( __METHOD__ . ": recognized $file as application/x-php\n" );
return 'application/x-php';
@@ -691,24 +680,24 @@ class MimeMagic {
$script_type = null;
# detect by shebang
- if ( substr( $head, 0, 2) == "#!" ) {
+ if ( substr( $head, 0, 2 ) == "#!" ) {
$script_type = "ASCII";
- } elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) {
+ } elseif ( substr( $head, 0, 5 ) == "\xef\xbb\xbf#!" ) {
$script_type = "UTF-8";
- } elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) {
+ } elseif ( substr( $head, 0, 7 ) == "\xfe\xff\x00#\x00!" ) {
$script_type = "UTF-16BE";
} elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) {
- $script_type= "UTF-16LE";
+ $script_type = "UTF-16LE";
}
if ( $script_type ) {
- if ( $script_type !== "UTF-8" && $script_type !== "ASCII") {
+ if ( $script_type !== "UTF-8" && $script_type !== "ASCII" ) {
// Quick and dirty fold down to ASCII!
$pack = array( 'UTF-16BE' => 'n*', 'UTF-16LE' => 'v*' );
$chars = unpack( $pack[$script_type], substr( $head, 2 ) );
$head = '';
- foreach( $chars as $codepoint ) {
- if( $codepoint < 128 ) {
+ foreach ( $chars as $codepoint ) {
+ if ( $codepoint < 128 ) {
$head .= chr( $codepoint );
} else {
$head .= '?';
@@ -720,14 +709,14 @@ class MimeMagic {
if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) {
$mime = "application/x-{$match[2]}";
- wfDebug( __METHOD__.": shell script recognized as $mime\n" );
+ wfDebug( __METHOD__ . ": shell script recognized as $mime\n" );
return $mime;
}
}
// Check for ZIP variants (before getimagesize)
if ( strpos( $tail, "PK\x05\x06" ) !== false ) {
- wfDebug( __METHOD__.": ZIP header present in $file\n" );
+ wfDebug( __METHOD__ . ": ZIP header present in $file\n" );
return $this->detectZipType( $head, $tail, $ext );
}
@@ -735,38 +724,38 @@ class MimeMagic {
$gis = getimagesize( $file );
wfRestoreWarnings();
- if( $gis && isset( $gis['mime'] ) ) {
+ if ( $gis && isset( $gis['mime'] ) ) {
$mime = $gis['mime'];
- wfDebug( __METHOD__.": getimagesize detected $file as $mime\n" );
+ wfDebug( __METHOD__ . ": getimagesize detected $file as $mime\n" );
return $mime;
}
// Also test DjVu
$deja = new DjVuImage( $file );
- if( $deja->isValid() ) {
- wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" );
+ if ( $deja->isValid() ) {
+ wfDebug( __METHOD__ . ": detected $file as image/vnd.djvu\n" );
return 'image/vnd.djvu';
}
return false;
}
-
+
/**
* Detect application-specific file type of a given ZIP file from its
* header data. Currently works for OpenDocument and OpenXML types...
* If can't tell, returns 'application/zip'.
*
- * @param $header String: some reasonably-sized chunk of file header
+ * @param string $header some reasonably-sized chunk of file header
* @param $tail String: the tail of the file
* @param $ext Mixed: the file extension, or true to extract it from the filename.
- * Set it to false (default) to ignore the extension. DEPRECATED! Set to false,
+ * Set it to false (default) to ignore the extension. DEPRECATED! Set to false,
* use improveTypeFromExtension($mime, $ext) later to improve mime type.
*
* @return string
*/
function detectZipType( $header, $tail = null, $ext = false ) {
- if( $ext ) { # TODO: remove $ext param
- wfDebug( __METHOD__.": WARNING: use of the \$ext parameter is deprecated. " .
+ if ( $ext ) { # TODO: remove $ext param
+ wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. " .
"Use improveTypeFromExtension(\$mime, \$ext) instead.\n" );
}
@@ -797,31 +786,32 @@ class MimeMagic {
if ( preg_match( $opendocRegex, substr( $header, 30 ), $matches ) ) {
$mime = $matches[1];
- wfDebug( __METHOD__.": detected $mime from ZIP archive\n" );
+ wfDebug( __METHOD__ . ": detected $mime from ZIP archive\n" );
} elseif ( preg_match( $openxmlRegex, substr( $header, 30 ) ) ) {
$mime = "application/x-opc+zip";
- # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere
- if ( $ext !== true && $ext !== false ) {
+ # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere
+ if ( $ext !== true && $ext !== false ) {
/** This is the mode used by getPropsFromPath
- * These mime's are stored in the database, where we don't really want
- * x-opc+zip, because we use it only for internal purposes
- */
- if ( $this->isMatchingExtension( $ext, $mime) ) {
+ * These mime's are stored in the database, where we don't really want
+ * x-opc+zip, because we use it only for internal purposes
+ */
+ if ( $this->isMatchingExtension( $ext, $mime ) ) {
/* A known file extension for an OPC file,
- * find the proper mime type for that file extension */
+ * find the proper mime type for that file extension
+ */
$mime = $this->guessTypesForExtension( $ext );
} else {
$mime = "application/zip";
}
}
- wfDebug( __METHOD__.": detected an Open Packaging Conventions archive: $mime\n" );
- } elseif ( substr( $header, 0, 8 ) == "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" &&
- ($headerpos = strpos( $tail, "PK\x03\x04" ) ) !== false &&
+ wfDebug( __METHOD__ . ": detected an Open Packaging Conventions archive: $mime\n" );
+ } elseif ( substr( $header, 0, 8 ) == "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" &&
+ ( $headerpos = strpos( $tail, "PK\x03\x04" ) ) !== false &&
preg_match( $openxmlRegex, substr( $tail, $headerpos + 30 ) ) ) {
- if ( substr( $header, 512, 4) == "\xEC\xA5\xC1\x00" ) {
+ if ( substr( $header, 512, 4 ) == "\xEC\xA5\xC1\x00" ) {
$mime = "application/msword";
- }
- switch( substr( $header, 512, 6) ) {
+ }
+ switch ( substr( $header, 512, 6 ) ) {
case "\xEC\xA5\xC1\x00\x0E\x00":
case "\xEC\xA5\xC1\x00\x1C\x00":
case "\xEC\xA5\xC1\x00\x43\x00":
@@ -843,27 +833,27 @@ class MimeMagic {
break;
}
- wfDebug( __METHOD__.": detected a MS Office document with OPC trailer\n");
+ wfDebug( __METHOD__ . ": detected a MS Office document with OPC trailer\n" );
} else {
- wfDebug( __METHOD__.": unable to identify type of ZIP archive\n" );
+ wfDebug( __METHOD__ . ": unable to identify type of ZIP archive\n" );
}
return $mime;
}
- /**
- * Internal mime type detection. Detection is done using an external
- * program, if $wgMimeDetectorCommand is set. Otherwise, the fileinfo
- * extension and mime_content_type are tried (in this order), if they
- * are available. If the dections fails and $ext is not false, the mime
+ /**
+ * Internal mime type detection. Detection is done using an external
+ * program, if $wgMimeDetectorCommand is set. Otherwise, the fileinfo
+ * extension and mime_content_type are tried (in this order), if they
+ * are available. If the detections fails and $ext is not false, the mime
* type is guessed from the file extension, using guessTypesForExtension.
- *
- * If the mime type is still unknown, getimagesize is used to detect the
- * mime type if the file is an image. If no mime type can be determined,
+ *
+ * If the mime type is still unknown, getimagesize is used to detect the
+ * mime type if the file is an image. If no mime type can be determined,
* this function returns 'unknown/unknown'.
*
- * @param $file String: the file to check
+ * @param string $file the file to check
* @param $ext Mixed: the file extension, or true (default) to extract it from the filename.
- * Set it to false to ignore the extension. DEPRECATED! Set to false, use
+ * Set it to false to ignore the extension. DEPRECATED! Set to false, use
* improveTypeFromExtension($mime, $ext) later to improve mime type.
*
* @return string the mime type of $file
@@ -872,14 +862,13 @@ class MimeMagic {
global $wgMimeDetectorCommand;
if ( $ext ) { # TODO: make $ext default to false. Or better, remove it.
- wfDebug( __METHOD__.": WARNING: use of the \$ext parameter is deprecated. Use improveTypeFromExtension(\$mime, \$ext) instead.\n" );
+ wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. Use improveTypeFromExtension(\$mime, \$ext) instead.\n" );
}
$m = null;
if ( $wgMimeDetectorCommand ) {
- // @todo FIXME: Use wfShellExec
- $fn = wfEscapeShellArg( $file );
- $m = `$wgMimeDetectorCommand $fn`;
+ $args = wfEscapeShellArg( $file );
+ $m = wfShellExec( "$wgMimeDetectorCommand $args" );
} elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) {
# This required the fileinfo extension by PECL,
@@ -898,22 +887,22 @@ class MimeMagic {
$m = finfo_file( $mime_magic_resource, $file );
finfo_close( $mime_magic_resource );
} else {
- wfDebug( __METHOD__.": finfo_open failed on ".FILEINFO_MIME."!\n" );
+ wfDebug( __METHOD__ . ": finfo_open failed on " . FILEINFO_MIME . "!\n" );
}
} elseif ( function_exists( "mime_content_type" ) ) {
# NOTE: this function is available since PHP 4.3.0, but only if
# PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic.
#
- # On Windows, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP;
+ # On Windows, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundled with PHP;
# sometimes, this may even be needed under linus/unix.
#
# Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above.
# see http://www.php.net/manual/en/ref.mime-magic.php for details.
- $m = mime_content_type($file);
+ $m = mime_content_type( $file );
} else {
- wfDebug( __METHOD__.": no magic mime detector found!\n" );
+ wfDebug( __METHOD__ . ": no magic mime detector found!\n" );
}
if ( $m ) {
@@ -925,7 +914,7 @@ class MimeMagic {
if ( strpos( $m, 'unknown' ) !== false ) {
$m = null;
} else {
- wfDebug( __METHOD__.": magic mime type of $file: $m\n" );
+ wfDebug( __METHOD__ . ": magic mime type of $file: $m\n" );
return $m;
}
}
@@ -936,12 +925,12 @@ class MimeMagic {
$ext = strtolower( $i ? substr( $file, $i + 1 ) : '' );
}
if ( $ext ) {
- if( $this->isRecognizableExtension( $ext ) ) {
- wfDebug( __METHOD__. ": refusing to guess mime type for .$ext file, we should have recognized it\n" );
+ if ( $this->isRecognizableExtension( $ext ) ) {
+ wfDebug( __METHOD__ . ": refusing to guess mime type for .$ext file, we should have recognized it\n" );
} else {
$m = $this->guessTypesForExtension( $ext );
if ( $m ) {
- wfDebug( __METHOD__.": extension mime type of $file: $m\n" );
+ wfDebug( __METHOD__ . ": extension mime type of $file: $m\n" );
return $m;
}
}
@@ -962,19 +951,19 @@ class MimeMagic {
* @todo analyse file if need be
* @todo look at multiple extension, separately and together.
*
- * @param $path String: full path to the image file, in case we have to look at the contents
+ * @param string $path full path to the image file, in case we have to look at the contents
* (if null, only the mime type is used to determine the media type code).
- * @param $mime String: mime type. If null it will be guessed using guessMimeType.
+ * @param string $mime mime type. If null it will be guessed using guessMimeType.
*
* @return (int?string?) a value to be used with the MEDIATYPE_xxx constants.
*/
function getMediaType( $path = null, $mime = null ) {
- if( !$mime && !$path ) {
+ if ( !$mime && !$path ) {
return MEDIATYPE_UNKNOWN;
}
// If mime type is unknown, guess it
- if( !$mime ) {
+ if ( !$mime ) {
$mime = $this->guessMimeType( $path, false );
}
@@ -984,22 +973,30 @@ class MimeMagic {
// Read a chunk of the file
$f = fopen( $path, "rt" );
- if ( !$f ) return MEDIATYPE_UNKNOWN;
+ if ( !$f ) {
+ return MEDIATYPE_UNKNOWN;
+ }
$head = fread( $f, 256 );
fclose( $f );
$head = strtolower( $head );
// This is an UGLY HACK, file should be parsed correctly
- if ( strpos( $head, 'theora' ) !== false ) return MEDIATYPE_VIDEO;
- elseif ( strpos( $head, 'vorbis' ) !== false ) return MEDIATYPE_AUDIO;
- elseif ( strpos( $head, 'flac' ) !== false ) return MEDIATYPE_AUDIO;
- elseif ( strpos( $head, 'speex' ) !== false ) return MEDIATYPE_AUDIO;
- else return MEDIATYPE_MULTIMEDIA;
+ if ( strpos( $head, 'theora' ) !== false ) {
+ return MEDIATYPE_VIDEO;
+ } elseif ( strpos( $head, 'vorbis' ) !== false ) {
+ return MEDIATYPE_AUDIO;
+ } elseif ( strpos( $head, 'flac' ) !== false ) {
+ return MEDIATYPE_AUDIO;
+ } elseif ( strpos( $head, 'speex' ) !== false ) {
+ return MEDIATYPE_AUDIO;
+ } else {
+ return MEDIATYPE_MULTIMEDIA;
+ }
}
// Check for entry for full mime type
- if( $mime ) {
+ if ( $mime ) {
$type = $this->findMediaType( $mime );
if ( $type !== MEDIATYPE_UNKNOWN ) {
return $type;
@@ -1030,24 +1027,24 @@ class MimeMagic {
}
}
- if( !$type ) {
+ if ( !$type ) {
$type = MEDIATYPE_UNKNOWN;
}
return $type;
}
- /**
+ /**
* Returns a media code matching the given mime type or file extension.
* File extensions are represented by a string starting with a dot (.) to
* distinguish them from mime types.
*
- * This funktion relies on the mapping defined by $this->mMediaTypes
+ * This function relies on the mapping defined by $this->mMediaTypes
* @access private
* @return int|string
*/
function findMediaType( $extMime ) {
- if ( strpos( $extMime, '.' ) === 0 ) {
+ if ( strpos( $extMime, '.' ) === 0 ) {
// If it's an extension, look up the mime types
$m = $this->getTypesForExtension( substr( $extMime, 1 ) );
if ( !$m ) {
@@ -1066,7 +1063,7 @@ class MimeMagic {
foreach ( $m as $mime ) {
foreach ( $this->mMediaTypes as $type => $codes ) {
- if ( in_array($mime, $codes, true ) ) {
+ if ( in_array( $mime, $codes, true ) ) {
return $type;
}
}
@@ -1076,12 +1073,12 @@ class MimeMagic {
}
/**
- * Get the MIME types that various versions of Internet Explorer would
+ * Get the MIME types that various versions of Internet Explorer would
* detect from a chunk of the content.
*
- * @param $fileName String: the file name (unused at present)
- * @param $chunk String: the first 256 bytes of the file
- * @param $proposed String: the MIME type proposed by the server
+ * @param string $fileName the file name (unused at present)
+ * @param string $chunk the first 256 bytes of the file
+ * @param string $proposed the MIME type proposed by the server
* @return Array
*/
public function getIEMimeTypes( $fileName, $chunk, $proposed ) {
diff --git a/includes/Namespace.php b/includes/Namespace.php
index 2e2b8d61..5c8e63b7 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -48,6 +48,7 @@ class MWNamespace {
* @param $index
* @param $method
*
+ * @throws MWException
* @return bool
*/
private static function isMethodValidFor( $index, $method ) {
@@ -60,13 +61,13 @@ class MWNamespace {
/**
* Can pages in the given namespace be moved?
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return bool
*/
public static function isMovable( $index ) {
global $wgAllowImageMoving;
- $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) || $index == NS_CATEGORY );
+ $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) || $index == NS_CATEGORY );
/**
* @since 1.20
@@ -79,7 +80,7 @@ class MWNamespace {
/**
* Is the given namespace is a subject (non-talk) namespace?
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return bool
* @since 1.19
*/
@@ -100,7 +101,7 @@ class MWNamespace {
/**
* Is the given namespace a talk namespace?
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return bool
*/
public static function isTalk( $index ) {
@@ -111,7 +112,7 @@ class MWNamespace {
/**
* Get the talk namespace index for a given namespace
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return int
*/
public static function getTalk( $index ) {
@@ -125,7 +126,7 @@ class MWNamespace {
* Get the subject namespace index for a given namespace
* Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
*
- * @param $index Int: Namespace index
+ * @param int $index Namespace index
* @return int
*/
public static function getSubject( $index ) {
@@ -144,7 +145,7 @@ class MWNamespace {
* For talk namespaces, returns the subject (non-talk) namespace
* For subject (non-talk) namespaces, returns the talk namespace
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return int or null if no associated namespace could be found
*/
public static function getAssociated( $index ) {
@@ -180,8 +181,8 @@ class MWNamespace {
* of this function rather than directly doing comparison will make
* sure that code will not potentially break.
*
- * @param $ns1 int The first namespace index
- * @param $ns2 int The second namespae index
+ * @param int $ns1 The first namespace index
+ * @param int $ns2 The second namespace index
*
* @return bool
* @since 1.19
@@ -195,8 +196,8 @@ class MWNamespace {
* eg: NS_USER and NS_USER wil return true, as well
* NS_USER and NS_USER_TALK will return true.
*
- * @param $ns1 int The first namespace index
- * @param $ns2 int The second namespae index
+ * @param int $ns1 The first namespace index
+ * @param int $ns2 The second namespace index
*
* @return bool
* @since 1.19
@@ -209,12 +210,14 @@ class MWNamespace {
* Returns array of all defined namespaces with their canonical
* (English) names.
*
+ * @param bool $rebuild rebuild namespace list (default = false). Used for testing.
+ *
* @return array
* @since 1.17
*/
- public static function getCanonicalNamespaces() {
+ public static function getCanonicalNamespaces( $rebuild = false ) {
static $namespaces = null;
- if ( $namespaces === null ) {
+ if ( $namespaces === null || $rebuild ) {
global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
$namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
if ( is_array( $wgExtraNamespaces ) ) {
@@ -228,7 +231,7 @@ class MWNamespace {
/**
* Returns the canonical (English) name for a given index
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return string or false if no canonical definition.
*/
public static function getCanonicalName( $index ) {
@@ -244,7 +247,7 @@ class MWNamespace {
* Returns the index for a given canonical name, or NULL
* The input *must* be converted to lower case first
*
- * @param $name String: namespace name
+ * @param string $name namespace name
* @return int
*/
public static function getCanonicalIndex( $name ) {
@@ -284,18 +287,18 @@ class MWNamespace {
/**
* Can this namespace ever have a talk namespace?
*
- * @param $index Int: namespace index
+ * @param int $index namespace index
* @return bool
*/
- public static function canTalk( $index ) {
+ public static function canTalk( $index ) {
return $index >= NS_MAIN;
- }
+ }
/**
* Does this namespace contain content, for the purposes of calculating
* statistics, etc?
*
- * @param $index Int: index to check
+ * @param int $index index to check
* @return bool
*/
public static function isContent( $index ) {
@@ -316,7 +319,7 @@ class MWNamespace {
/**
* Does the namespace allow subpages?
*
- * @param $index int Index to check
+ * @param int $index Index to check
* @return bool
*/
public static function hasSubpages( $index ) {
@@ -331,7 +334,7 @@ class MWNamespace {
public static function getContentNamespaces() {
global $wgContentNamespaces;
if ( !is_array( $wgContentNamespaces ) || $wgContentNamespaces === array() ) {
- return NS_MAIN;
+ return array( NS_MAIN );
} elseif ( !in_array( NS_MAIN, $wgContentNamespaces ) ) {
// always force NS_MAIN to be part of array (to match the algorithm used by isContent)
return array_merge( array( NS_MAIN ), $wgContentNamespaces );
@@ -369,7 +372,7 @@ class MWNamespace {
/**
* Is the namespace first-letter capitalized?
*
- * @param $index int Index to check
+ * @param int $index Index to check
* @return bool
*/
public static function isCapitalized( $index ) {
@@ -384,9 +387,9 @@ class MWNamespace {
if ( in_array( $index, self::$alwaysCapitalizedNamespaces ) ) {
return true;
}
- if ( isset( $wgCapitalLinkOverrides[ $index ] ) ) {
+ if ( isset( $wgCapitalLinkOverrides[$index] ) ) {
// $wgCapitalLinkOverrides is explicitly set
- return $wgCapitalLinkOverrides[ $index ];
+ return $wgCapitalLinkOverrides[$index];
}
// Default to the global setting
return $wgCapitalLinks;
@@ -397,7 +400,7 @@ class MWNamespace {
* genders. Not all languages make a distinction here.
*
* @since 1.18
- * @param $index int Index to check
+ * @param int $index Index to check
* @return bool
*/
public static function hasGenderDistinction( $index ) {
@@ -408,7 +411,7 @@ class MWNamespace {
* It is not possible to use pages from this namespace as template?
*
* @since 1.20
- * @param $index int Index to check
+ * @param int $index Index to check
* @return bool
*/
public static function isNonincludable( $index ) {
@@ -416,4 +419,18 @@ class MWNamespace {
return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces );
}
+ /**
+ * Get the default content model for a namespace
+ * This does not mean that all pages in that namespace have the model
+ *
+ * @since 1.21
+ * @param int $index Index to check
+ * @return null|string default model name for the given namespace, if set
+ */
+ public static function getNamespaceContentModel( $index ) {
+ global $wgNamespaceContentModels;
+ return isset( $wgNamespaceContentModels[$index] )
+ ? $wgNamespaceContentModels[$index]
+ : null;
+ }
}
diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php
index 46a43f63..3860b8e2 100644
--- a/includes/OutputHandler.php
+++ b/includes/OutputHandler.php
@@ -22,20 +22,28 @@
/**
* Standard output handler for use with ob_start
- *
+ *
* @param $s string
- *
+ *
* @return string
*/
function wfOutputHandler( $s ) {
global $wgDisableOutputCompression, $wgValidateAllHtml;
$s = wfMangleFlashPolicy( $s );
if ( $wgValidateAllHtml ) {
- $headers = apache_response_headers();
- $isHTML = true;
- foreach ( $headers as $name => $value ) {
- if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false && strpos( $value, 'application/xhtml+xml' ) === false ) {
- $isHTML = false;
+ $headers = headers_list();
+ $isHTML = false;
+ foreach ( $headers as $header ) {
+ $parts = explode( ':', $header, 2 );
+ if ( count( $parts ) !== 2 ) {
+ continue;
+ }
+ $name = strtolower( trim( $parts[0] ) );
+ $value = trim( $parts[1] );
+ if ( $name == 'content-type' && ( strpos( $value, 'text/html' ) === 0
+ || strpos( $value, 'application/xhtml+xml' ) === 0 )
+ ) {
+ $isHTML = true;
break;
}
}
@@ -64,10 +72,10 @@ function wfOutputHandler( $s ) {
*/
function wfRequestExtension() {
/// @todo FIXME: this sort of dupes some code in WebRequest::getRequestUrl()
- if( isset( $_SERVER['REQUEST_URI'] ) ) {
+ if ( isset( $_SERVER['REQUEST_URI'] ) ) {
// Strip the query string...
list( $path ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
- } elseif( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
// Probably IIS. QUERY_STRING appears separately.
$path = $_SERVER['SCRIPT_NAME'];
} else {
@@ -76,7 +84,7 @@ function wfRequestExtension() {
}
$period = strrpos( $path, '.' );
- if( $period !== false ) {
+ if ( $period !== false ) {
return strtolower( substr( $path, $period ) );
}
return '';
@@ -85,23 +93,23 @@ function wfRequestExtension() {
/**
* Handler that compresses data with gzip if allowed by the Accept header.
* Unlike ob_gzhandler, it works for HEAD requests too.
- *
+ *
* @param $s string
*
* @return string
*/
function wfGzipHandler( $s ) {
- if( !function_exists( 'gzencode' ) ) {
- wfDebug( __FUNCTION__ . "() skipping compression (gzencode unavaible)\n" );
+ if ( !function_exists( 'gzencode' ) ) {
+ wfDebug( __FUNCTION__ . "() skipping compression (gzencode unavailable)\n" );
return $s;
}
- if( headers_sent() ) {
+ if ( headers_sent() ) {
wfDebug( __FUNCTION__ . "() skipping compression (headers already sent)\n" );
return $s;
}
$ext = wfRequestExtension();
- if( $ext == '.gz' || $ext == '.tgz' ) {
+ if ( $ext == '.gz' || $ext == '.tgz' ) {
// Don't do gzip compression if the URL path ends in .gz or .tgz
// This confuses Safari and triggers a download of the page,
// even though it's pretty clearly labeled as viewable HTML.
@@ -109,7 +117,7 @@ function wfGzipHandler( $s ) {
return $s;
}
- if( wfClientAcceptsGzip() ) {
+ if ( wfClientAcceptsGzip() ) {
wfDebug( __FUNCTION__ . "() is compressing output\n" );
header( 'Content-Encoding: gzip' );
$s = gzencode( $s, 6 );
@@ -156,7 +164,7 @@ function wfMangleFlashPolicy( $s ) {
* @param $length int
*/
function wfDoContentLength( $length ) {
- if ( !headers_sent() && $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0' ) {
+ if ( !headers_sent() && isset( $_SERVER['SERVER_PROTOCOL'] ) && $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0' ) {
header( "Content-Length: $length" );
}
}
@@ -177,20 +185,8 @@ function wfHtmlValidationHandler( $s ) {
header( 'Cache-Control: no-cache' );
- $out = <<<EOT
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" lang="en" dir="ltr">
-<head>
-<title>HTML validation error</title>
-<style>
-.highlight { background-color: #ffc }
-li { white-space: pre }
-</style>
-</head>
-<body>
-<h1>HTML validation error</h1>
-<ul>
-EOT;
+ $out = Html::element( 'h1', null, 'HTML validation error' );
+ $out .= Html::openElement( 'ul' );
$error = strtok( $errors, "\n" );
$badLines = array();
@@ -198,26 +194,40 @@ EOT;
if ( preg_match( '/^line (\d+)/', $error, $m ) ) {
$lineNum = intval( $m[1] );
$badLines[$lineNum] = true;
- $out .= "<li><a href=\"#line-{$lineNum}\">" . htmlspecialchars( $error ) . "</a></li>\n";
+ $out .= Html::rawElement( 'li', null,
+ Html::element( 'a', array( 'href' => "#line-{$lineNum}" ), $error ) ) . "\n";
}
$error = strtok( "\n" );
}
- $out .= '</ul>';
- $out .= '<pre>' . htmlspecialchars( $errors ) . '</pre>';
- $out .= "<ol>\n";
+ $out .= Html::closeElement( 'ul' );
+ $out .= Html::element( 'pre', null, $errors );
+ $out .= Html::openElement( 'ol' ) . "\n";
$line = strtok( $s, "\n" );
$i = 1;
while ( $line !== false ) {
+ $attrs = array();
if ( isset( $badLines[$i] ) ) {
- $out .= "<li class=\"highlight\" id=\"line-$i\">";
- } else {
- $out .= '<li>';
+ $attrs['class'] = 'highlight';
+ $attrs['id'] = "line-$i";
}
- $out .= htmlspecialchars( $line ) . "</li>\n";
+ $out .= Html::element( 'li', $attrs, $line ) . "\n";
$line = strtok( "\n" );
$i++;
}
- $out .= '</ol></body></html>';
+ $out .= Html::closeElement( 'ol' );
+
+ $style = <<<CSS
+.highlight { background-color: #ffc }
+li { white-space: pre }
+CSS;
+
+ $out = Html::htmlHeader( array( 'lang' => 'en', 'dir' => 'ltr' ) ) .
+ Html::rawElement( 'head', null,
+ Html::element( 'title', null, 'HTML validation error' ) .
+ Html::inlineStyle( $style ) ) .
+ Html::rawElement( 'body', null, $out ) .
+ Html::closeElement( 'html' );
+
return $out;
}
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index b4a81bb1..7f0454f6 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -39,10 +39,8 @@ class OutputPage extends ContextSource {
/// Should be private. Used with addMeta() which adds "<meta>"
var $mMetatags = array();
- /// "<meta keywords='stuff'>" most of the time the first 10 links to an article
- var $mKeywords = array();
-
var $mLinktags = array();
+ var $mCanonicalUrl = false;
/// Additional stylesheets. Looks like this is for extensions. Might be replaced by resource loader.
var $mExtStyles = array();
@@ -122,7 +120,7 @@ class OutputPage extends ContextSource {
var $mScripts = '';
/**
- * Inline CSS styles. Use addInlineStyle() sparsingly
+ * Inline CSS styles. Use addInlineStyle() sparingly
*/
var $mInlineStyles = '';
@@ -248,6 +246,21 @@ class OutputPage extends ContextSource {
private $mRedirectedFrom = null;
/**
+ * Additional key => value data
+ */
+ private $mProperties = array();
+
+ /**
+ * @var string|null: ResourceLoader target for load.php links. If null, will be omitted
+ */
+ private $mTarget = null;
+
+ /**
+ * @var bool: Whether output should contain table of contents
+ */
+ private $mEnableTOC = true;
+
+ /**
* Constructor for OutputPage. This should not be called directly.
* Instead a new RequestContext should be created and it will implicitly create
* a OutputPage tied to that context.
@@ -255,7 +268,7 @@ class OutputPage extends ContextSource {
function __construct( IContextSource $context = null ) {
if ( $context === null ) {
# Extensions should use `new RequestContext` instead of `new OutputPage` now.
- wfDeprecated( __METHOD__ );
+ wfDeprecated( __METHOD__, '1.18' );
} else {
$this->setContext( $context );
}
@@ -264,8 +277,8 @@ class OutputPage extends ContextSource {
/**
* Redirect to $url rather than displaying the normal page
*
- * @param $url String: URL
- * @param $responsecode String: HTTP status code
+ * @param string $url URL
+ * @param string $responsecode HTTP status code
*/
public function redirect( $url, $responsecode = '302' ) {
# Strip newlines as a paranoia check for header injection in PHP<5.1.2
@@ -295,30 +308,19 @@ class OutputPage extends ContextSource {
* Add a new "<meta>" tag
* To add an http-equiv meta tag, precede the name with "http:"
*
- * @param $name String tag name
- * @param $val String tag value
+ * @param string $name tag name
+ * @param string $val tag value
*/
function addMeta( $name, $val ) {
array_push( $this->mMetatags, array( $name, $val ) );
}
/**
- * Add a keyword or a list of keywords in the page header
+ * Add a new \<link\> tag to the page header.
*
- * @param $text String or array of strings
- */
- function addKeyword( $text ) {
- if( is_array( $text ) ) {
- $this->mKeywords = array_merge( $this->mKeywords, $text );
- } else {
- array_push( $this->mKeywords, $text );
- }
- }
-
- /**
- * Add a new \<link\> tag to the page header
+ * Note: use setCanonicalUrl() for rel=canonical.
*
- * @param $linkarr Array: associative array of attributes.
+ * @param array $linkarr associative array of attributes.
*/
function addLink( $linkarr ) {
array_push( $this->mLinktags, $linkarr );
@@ -327,7 +329,7 @@ class OutputPage extends ContextSource {
/**
* Add a new \<link\> with "rel" attribute set to "meta"
*
- * @param $linkarr Array: associative array mapping attribute names to their
+ * @param array $linkarr associative array mapping attribute names to their
* values, both keys and values will be escaped, and the
* "rel" attribute will be automatically added
*/
@@ -337,6 +339,14 @@ class OutputPage extends ContextSource {
}
/**
+ * Set the URL to be used for the <link rel=canonical>. This should be used
+ * in preference to addLink(), to avoid duplicate link tags.
+ */
+ function setCanonicalUrl( $url ) {
+ $this->mCanonicalUrl = $url;
+ }
+
+ /**
* Get the value of the "rel" attribute for metadata links
*
* @return String
@@ -355,7 +365,7 @@ class OutputPage extends ContextSource {
/**
* Add raw HTML to the list of scripts (including \<script\> tag, etc.)
*
- * @param $script String: raw HTML
+ * @param string $script raw HTML
*/
function addScript( $script ) {
$this->mScripts .= $script . "\n";
@@ -364,7 +374,7 @@ class OutputPage extends ContextSource {
/**
* Register and add a stylesheet from an extension directory.
*
- * @param $url String path to sheet. Provide either a full url (beginning
+ * @param string $url path to sheet. Provide either a full url (beginning
* with 'http', etc) or a relative path from the document root
* (beginning with '/'). Otherwise it behaves identically to
* addStyle() and draws from the /skins folder.
@@ -385,27 +395,28 @@ class OutputPage extends ContextSource {
/**
* Add a JavaScript file out of skins/common, or a given relative path.
*
- * @param $file String: filename in skins/common or complete on-server path
+ * @param string $file filename in skins/common or complete on-server path
* (/foo/bar.js)
- * @param $version String: style version of the file. Defaults to $wgStyleVersion
+ * @param string $version style version of the file. Defaults to $wgStyleVersion
*/
public function addScriptFile( $file, $version = null ) {
global $wgStylePath, $wgStyleVersion;
// See if $file parameter is an absolute URL or begins with a slash
- if( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
+ if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
$path = $file;
} else {
$path = "{$wgStylePath}/common/{$file}";
}
- if ( is_null( $version ) )
+ if ( is_null( $version ) ) {
$version = $wgStyleVersion;
+ }
$this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
}
/**
* Add a self-contained script tag with the given contents
*
- * @param $script String: JavaScript text, no "<script>" tags
+ * @param string $script JavaScript text, no "<script>" tags
*/
public function addInlineScript( $script ) {
$this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n";
@@ -424,18 +435,19 @@ class OutputPage extends ContextSource {
* Filter an array of modules to remove insufficiently trustworthy members, and modules
* which are no longer registered (eg a page is cached before an extension is disabled)
* @param $modules Array
- * @param $position String if not null, only return modules with this position
+ * @param string $position if not null, only return modules with this position
* @param $type string
* @return Array
*/
- protected function filterModules( $modules, $position = null, $type = ResourceLoaderModule::TYPE_COMBINED ){
+ protected function filterModules( $modules, $position = null, $type = ResourceLoaderModule::TYPE_COMBINED ) {
$resourceLoader = $this->getResourceLoader();
$filteredModules = array();
- foreach( $modules as $val ){
+ foreach ( $modules as $val ) {
$module = $resourceLoader->getModule( $val );
- if( $module instanceof ResourceLoaderModule
+ if ( $module instanceof ResourceLoaderModule
&& $module->getOrigin() <= $this->getAllowedModules( $type )
- && ( is_null( $position ) || $module->getPosition() == $position ) )
+ && ( is_null( $position ) || $module->getPosition() == $position )
+ && ( !$this->mTarget || in_array( $this->mTarget, $module->getTargets() ) ) )
{
$filteredModules[] = $val;
}
@@ -446,8 +458,8 @@ class OutputPage extends ContextSource {
/**
* Get the list of modules to include on this page
*
- * @param $filter Bool whether to filter out insufficiently trustworthy modules
- * @param $position String if not null, only return modules with this position
+ * @param bool $filter whether to filter out insufficiently trustworthy modules
+ * @param string $position if not null, only return modules with this position
* @param $param string
* @return Array of module names
*/
@@ -501,13 +513,15 @@ class OutputPage extends ContextSource {
* @return Array of module names
*/
public function getModuleStyles( $filter = false, $position = null ) {
- return $this->getModules( $filter, $position, 'mModuleStyles' );
+ return $this->getModules( $filter, $position, 'mModuleStyles' );
}
/**
- * Add only CSS of one or more modules recognized by the resource loader. Module
- * styles added through this function will be loaded by the resource loader when
- * the page loads.
+ * Add only CSS of one or more modules recognized by the resource loader.
+ *
+ * Module styles added through this function will be added using standard link CSS
+ * tags, rather than as a combined Javascript and CSS package. Thus, they will
+ * load when JavaScript is disabled (unless CSS also happens to be disabled).
*
* @param $modules Mixed: module name (string) or array of module names
*/
@@ -539,6 +553,22 @@ class OutputPage extends ContextSource {
}
/**
+ * @return null|string: ResourceLoader target
+ */
+ public function getTarget() {
+ return $this->mTarget;
+ }
+
+ /**
+ * Sets ResourceLoader target for load.php links. If null, will be omitted
+ *
+ * @param $target string|null
+ */
+ public function setTarget( $target ) {
+ $this->mTarget = $target;
+ }
+
+ /**
* Get an array of head items
*
* @return Array
@@ -563,8 +593,8 @@ class OutputPage extends ContextSource {
/**
* Add or replace an header item to the output
*
- * @param $name String: item name
- * @param $value String: raw HTML
+ * @param string $name item name
+ * @param string $value raw HTML
*/
public function addHeadItem( $name, $value ) {
$this->mHeadItems[$name] = $value;
@@ -573,7 +603,7 @@ class OutputPage extends ContextSource {
/**
* Check if the header item $name is already set
*
- * @param $name String: item name
+ * @param string $name item name
* @return Boolean
*/
public function hasHeadItem( $name ) {
@@ -583,7 +613,7 @@ class OutputPage extends ContextSource {
/**
* Set the value of the ETag HTTP header, only used if $wgUseETag is true
*
- * @param $tag String: value of "ETag" header
+ * @param string $tag value of "ETag" header
*/
function setETag( $tag ) {
$this->mETag = $tag;
@@ -610,28 +640,54 @@ class OutputPage extends ContextSource {
}
/**
+ * Set an additional output property
+ * @since 1.21
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function setProperty( $name, $value ) {
+ $this->mProperties[$name] = $value;
+ }
+
+ /**
+ * Get an additional output property
+ * @since 1.21
+ *
+ * @param $name
+ * @return mixed: Property value or null if not found
+ */
+ public function getProperty( $name ) {
+ if ( isset( $this->mProperties[$name] ) ) {
+ return $this->mProperties[$name];
+ } else {
+ return null;
+ }
+ }
+
+ /**
* checkLastModified tells the client to use the client-cached page if
- * possible. If sucessful, the OutputPage is disabled so that
+ * possible. If successful, the OutputPage is disabled so that
* any future call to OutputPage->output() have no effect.
*
* Side effect: sets mLastModified for Last-Modified header
*
* @param $timestamp string
*
- * @return Boolean: true iff cache-ok headers was sent.
+ * @return Boolean: true if cache-ok headers was sent.
*/
public function checkLastModified( $timestamp ) {
- global $wgCachePages, $wgCacheEpoch;
+ global $wgCachePages, $wgCacheEpoch, $wgUseSquid, $wgSquidMaxage;
if ( !$timestamp || $timestamp == '19700101000000' ) {
wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
return false;
}
- if( !$wgCachePages ) {
+ if ( !$wgCachePages ) {
wfDebug( __METHOD__ . ": CACHE DISABLED\n", false );
return false;
}
- if( $this->getUser()->getOption( 'nocache' ) ) {
+ if ( $this->getUser()->getOption( 'nocache' ) ) {
wfDebug( __METHOD__ . ": USER DISABLED CACHE\n", false );
return false;
}
@@ -642,6 +698,10 @@ class OutputPage extends ContextSource {
'user' => $this->getUser()->getTouched(),
'epoch' => $wgCacheEpoch
);
+ if ( $wgUseSquid ) {
+ // bug 44570: the core page itself may not change, but resources might
+ $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $wgSquidMaxage );
+ }
wfRunHooks( 'OutputPageCheckLastModified', array( &$modifiedTimes ) );
$maxModified = max( $modifiedTimes );
@@ -680,7 +740,7 @@ class OutputPage extends ContextSource {
wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false );
wfDebug( __METHOD__ . ": effective Last-Modified: " .
wfTimestamp( TS_ISO_8601, $maxModified ) . "\n", false );
- if( $clientHeaderTime < $maxModified ) {
+ if ( $clientHeaderTime < $maxModified ) {
wfDebug( __METHOD__ . ": STALE, $info\n", false );
return false;
}
@@ -704,7 +764,7 @@ class OutputPage extends ContextSource {
/**
* Override the last modified timestamp
*
- * @param $timestamp String: new timestamp, in a format readable by
+ * @param string $timestamp new timestamp, in a format readable by
* wfTimestamp()
*/
public function setLastModified( $timestamp ) {
@@ -714,7 +774,7 @@ class OutputPage extends ContextSource {
/**
* Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
*
- * @param $policy String: the literal string to output as the contents of
+ * @param string $policy the literal string to output as the contents of
* the meta tag. Will be parsed according to the spec and output in
* standardized form.
* @return null
@@ -722,10 +782,10 @@ class OutputPage extends ContextSource {
public function setRobotPolicy( $policy ) {
$policy = Article::formatRobotPolicy( $policy );
- if( isset( $policy['index'] ) ) {
+ if ( isset( $policy['index'] ) ) {
$this->setIndexPolicy( $policy['index'] );
}
- if( isset( $policy['follow'] ) ) {
+ if ( isset( $policy['follow'] ) ) {
$this->setFollowPolicy( $policy['follow'] );
}
}
@@ -734,12 +794,12 @@ class OutputPage extends ContextSource {
* Set the index policy for the page, but leave the follow policy un-
* touched.
*
- * @param $policy string Either 'index' or 'noindex'.
+ * @param string $policy Either 'index' or 'noindex'.
* @return null
*/
public function setIndexPolicy( $policy ) {
$policy = trim( $policy );
- if( in_array( $policy, array( 'index', 'noindex' ) ) ) {
+ if ( in_array( $policy, array( 'index', 'noindex' ) ) ) {
$this->mIndexPolicy = $policy;
}
}
@@ -748,12 +808,12 @@ class OutputPage extends ContextSource {
* Set the follow policy for the page, but leave the index policy un-
* touched.
*
- * @param $policy String: either 'follow' or 'nofollow'.
+ * @param string $policy either 'follow' or 'nofollow'.
* @return null
*/
public function setFollowPolicy( $policy ) {
$policy = trim( $policy );
- if( in_array( $policy, array( 'follow', 'nofollow' ) ) ) {
+ if ( in_array( $policy, array( 'follow', 'nofollow' ) ) ) {
$this->mFollowPolicy = $policy;
}
}
@@ -762,7 +822,7 @@ class OutputPage extends ContextSource {
* Set the new value of the "action text", this will be added to the
* "HTML title", separated from it with " - ".
*
- * @param $text String: new value of the "action text"
+ * @param string $text new value of the "action text"
*/
public function setPageTitleActionText( $text ) {
$this->mPageTitleActionText = $text;
@@ -777,6 +837,7 @@ class OutputPage extends ContextSource {
if ( isset( $this->mPageTitleActionText ) ) {
return $this->mPageTitleActionText;
}
+ return '';
}
/**
@@ -851,11 +912,10 @@ class OutputPage extends ContextSource {
$this->getContext()->setTitle( $t );
}
-
/**
- * Replace the subtile with $str
+ * Replace the subtitle with $str
*
- * @param $str String|Message: new value of the subtitle
+ * @param string|Message $str new value of the subtitle. String should be safe HTML.
*/
public function setSubtitle( $str ) {
$this->clearSubtitle();
@@ -866,7 +926,7 @@ class OutputPage extends ContextSource {
* Add $str to the subtitle
*
* @deprecated in 1.19; use addSubtitle() instead
- * @param $str String|Message to add to the subtitle
+ * @param string|Message $str to add to the subtitle
*/
public function appendSubtitle( $str ) {
$this->addSubtitle( $str );
@@ -875,7 +935,7 @@ class OutputPage extends ContextSource {
/**
* Add $str to the subtitle
*
- * @param $str String|Message to add to the subtitle
+ * @param string|Message $str to add to the subtitle. String should be safe HTML.
*/
public function addSubtitle( $str ) {
if ( $str instanceof Message ) {
@@ -987,7 +1047,7 @@ class OutputPage extends ContextSource {
* for the new version
* @see addFeedLink()
*
- * @param $val String: query to append to feed links or false to output
+ * @param string $val query to append to feed links or false to output
* default links
*/
public function setFeedAppendQuery( $val ) {
@@ -1007,8 +1067,8 @@ class OutputPage extends ContextSource {
/**
* Add a feed link to the page header
*
- * @param $format String: feed type, should be a key of $wgFeedClasses
- * @param $href String: URL
+ * @param string $format feed type, should be a key of $wgFeedClasses
+ * @param string $href URL
*/
public function addFeedLink( $format, $href ) {
global $wgAdvertisedFeedTypes;
@@ -1092,7 +1152,7 @@ class OutputPage extends ContextSource {
/**
* Add new language links
*
- * @param $newLinkArray array Associative array mapping language code to the page
+ * @param array $newLinkArray Associative array mapping language code to the page
* name
*/
public function addLanguageLinks( $newLinkArray ) {
@@ -1102,7 +1162,7 @@ class OutputPage extends ContextSource {
/**
* Reset the language links and add new language links
*
- * @param $newLinkArray array Associative array mapping language code to the page
+ * @param array $newLinkArray Associative array mapping language code to the page
* name
*/
public function setLanguageLinks( $newLinkArray ) {
@@ -1121,7 +1181,7 @@ class OutputPage extends ContextSource {
/**
* Add an array of categories, with names in the keys
*
- * @param $categories Array mapping category name => sort key
+ * @param array $categories mapping category name => sort key
*/
public function addCategoryLinks( $categories ) {
global $wgContLang;
@@ -1182,7 +1242,7 @@ class OutputPage extends ContextSource {
/**
* Reset the category links (but not the category list) and add $categories
*
- * @param $categories Array mapping category name => sort key
+ * @param array $categories mapping category name => sort key
*/
public function setCategoryLinks( $categories ) {
$this->mCategoryLinks = array();
@@ -1225,7 +1285,6 @@ class OutputPage extends ContextSource {
* Return whether user JavaScript is allowed for this page
* @deprecated since 1.18 Load modules with ResourceLoader, and origin and
* trustworthiness is identified and enforced automagically.
- * Will be removed in 1.20.
* @return Boolean
*/
public function isUserJsAllowed() {
@@ -1236,11 +1295,11 @@ class OutputPage extends ContextSource {
/**
* Show what level of JavaScript / CSS untrustworthiness is allowed on this page
* @see ResourceLoaderModule::$origin
- * @param $type String ResourceLoaderModule TYPE_ constant
+ * @param string $type ResourceLoaderModule TYPE_ constant
* @return Int ResourceLoaderModule ORIGIN_ class constant
*/
- public function getAllowedModules( $type ){
- if( $type == ResourceLoaderModule::TYPE_COMBINED ){
+ public function getAllowedModules( $type ) {
+ if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
return min( array_values( $this->mAllowedModules ) );
} else {
return isset( $this->mAllowedModules[$type] )
@@ -1254,23 +1313,23 @@ class OutputPage extends ContextSource {
* @param $type String ResourceLoaderModule TYPE_ constant
* @param $level Int ResourceLoaderModule class constant
*/
- public function setAllowedModules( $type, $level ){
+ public function setAllowedModules( $type, $level ) {
$this->mAllowedModules[$type] = $level;
}
/**
- * As for setAllowedModules(), but don't inadvertantly make the page more accessible
+ * As for setAllowedModules(), but don't inadvertently make the page more accessible
* @param $type String
* @param $level Int ResourceLoaderModule class constant
*/
- public function reduceAllowedModules( $type, $level ){
- $this->mAllowedModules[$type] = min( $this->getAllowedModules($type), $level );
+ public function reduceAllowedModules( $type, $level ) {
+ $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
}
/**
* Prepend $text to the body HTML
*
- * @param $text String: HTML
+ * @param string $text HTML
*/
public function prependHTML( $text ) {
$this->mBodytext = $text . $this->mBodytext;
@@ -1279,7 +1338,7 @@ class OutputPage extends ContextSource {
/**
* Append $text to the body HTML
*
- * @param $text String: HTML
+ * @param string $text HTML
*/
public function addHTML( $text ) {
$this->mBodytext .= $text;
@@ -1357,7 +1416,7 @@ class OutputPage extends ContextSource {
* @param $timestamp Mixed: string, or null
* @return Mixed: previous value
*/
- public function setRevisionTimestamp( $timestamp) {
+ public function setRevisionTimestamp( $timestamp ) {
return wfSetVar( $this->mRevisionTimestamp, $timestamp );
}
@@ -1423,14 +1482,17 @@ class OutputPage extends ContextSource {
* @param $interface Boolean: is this text in the user interface language?
*/
public function addWikiText( $text, $linestart = true, $interface = true ) {
- $title = $this->getTitle(); // Work arround E_STRICT
+ $title = $this->getTitle(); // Work around E_STRICT
+ if ( !$title ) {
+ throw new MWException( 'Title is null' );
+ }
$this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface );
}
/**
* Add wikitext with a custom Title object
*
- * @param $text String: wikitext
+ * @param string $text wikitext
* @param $title Title object
* @param $linestart Boolean: is this the start of a line?
*/
@@ -1441,7 +1503,7 @@ class OutputPage extends ContextSource {
/**
* Add wikitext with a custom Title object and tidy enabled.
*
- * @param $text String: wikitext
+ * @param string $text wikitext
* @param $title Title object
* @param $linestart Boolean: is this the start of a line?
*/
@@ -1452,7 +1514,7 @@ class OutputPage extends ContextSource {
/**
* Add wikitext with tidy enabled
*
- * @param $text String: wikitext
+ * @param string $text wikitext
* @param $linestart Boolean: is this the start of a line?
*/
public function addWikiTextTidy( $text, $linestart = true ) {
@@ -1463,21 +1525,21 @@ class OutputPage extends ContextSource {
/**
* Add wikitext with a custom Title object
*
- * @param $text String: wikitext
+ * @param string $text wikitext
* @param $title Title object
* @param $linestart Boolean: is this the start of a line?
* @param $tidy Boolean: whether to use tidy
* @param $interface Boolean: whether it is an interface message
* (for example disables conversion)
*/
- public function addWikiTextTitle( $text, &$title, $linestart, $tidy = false, $interface = false ) {
+ public function addWikiTextTitle( $text, Title $title, $linestart, $tidy = false, $interface = false ) {
global $wgParser;
wfProfileIn( __METHOD__ );
$popts = $this->parserOptions();
$oldTidy = $popts->setTidy( $tidy );
- $popts->setInterfaceMessage( (bool) $interface );
+ $popts->setInterfaceMessage( (bool)$interface );
$parserOutput = $wgParser->parse(
$text, $title, $popts,
@@ -1535,6 +1597,10 @@ class OutputPage extends ContextSource {
}
}
+ // Link flags are ignored for now, but may in the future be
+ // used to mark individual language links.
+ $linkFlags = array();
+ wfRunHooks( 'LanguageLinks', array( $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ) );
wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) );
}
@@ -1545,12 +1611,12 @@ class OutputPage extends ContextSource {
*/
function addParserOutput( &$parserOutput ) {
$this->addParserOutputNoText( $parserOutput );
+ $parserOutput->setTOCEnabled( $this->mEnableTOC );
$text = $parserOutput->getText();
wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) );
$this->addHTML( $text );
}
-
/**
* Add the output of a QuickTemplate to the output buffer
*
@@ -1571,15 +1637,16 @@ class OutputPage extends ContextSource {
* @param $interface Boolean: use interface language ($wgLang instead of
* $wgContLang) while parsing language sensitive magic
* words like GRAMMAR and PLURAL. This also disables
- * LanguageConverter.
+ * LanguageConverter.
* @param $language Language object: target language object, will override
* $interface
+ * @throws MWException
* @return String: HTML
*/
public function parse( $text, $linestart = true, $interface = false, $language = null ) {
global $wgParser;
- if( is_null( $this->getTitle() ) ) {
+ if ( is_null( $this->getTitle() ) ) {
throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
}
@@ -1660,6 +1727,7 @@ class OutputPage extends ContextSource {
array(
"{$wgCookiePrefix}Token",
"{$wgCookiePrefix}LoggedOut",
+ "forceHTTPS",
session_name()
),
$wgCacheVaryCookies
@@ -1695,7 +1763,7 @@ class OutputPage extends ContextSource {
/**
* Add an HTTP header that will influence on the cache
*
- * @param $header String: header name
+ * @param string $header header name
* @param $option Array|null
* @todo FIXME: Document the $option parameter; it appears to be for
* X-Vary-Options but what format is acceptable?
@@ -1703,8 +1771,8 @@ class OutputPage extends ContextSource {
public function addVaryHeader( $header, $option = null ) {
if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
$this->mVaryHeader[$header] = (array)$option;
- } elseif( is_array( $option ) ) {
- if( is_array( $this->mVaryHeader[$header] ) ) {
+ } elseif ( is_array( $option ) ) {
+ if ( is_array( $this->mVaryHeader[$header] ) ) {
$this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option );
} else {
$this->mVaryHeader[$header] = $option;
@@ -1738,7 +1806,7 @@ class OutputPage extends ContextSource {
$this->addVaryHeader( 'Cookie', $cookiesOption );
$headers = array();
- foreach( $this->mVaryHeader as $header => $option ) {
+ foreach ( $this->mVaryHeader as $header => $option ) {
$newheader = $header;
if ( is_array( $option ) && count( $option ) > 0 ) {
$newheader .= ';' . implode( ';', $option );
@@ -1760,23 +1828,21 @@ class OutputPage extends ContextSource {
*/
function addAcceptLanguage() {
$lang = $this->getTitle()->getPageLanguage();
- if( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
+ if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
$variants = $lang->getVariants();
$aloption = array();
foreach ( $variants as $variant ) {
- if( $variant === $lang->getCode() ) {
+ if ( $variant === $lang->getCode() ) {
continue;
} else {
$aloption[] = 'string-contains=' . $variant;
- // IE and some other browsers use another form of language code
- // in their Accept-Language header, like "zh-CN" or "zh-TW".
+ // IE and some other browsers use BCP 47 standards in
+ // their Accept-Language header, like "zh-CN" or "zh-Hant".
// We should handle these too.
- $ievariant = explode( '-', $variant );
- if ( count( $ievariant ) == 2 ) {
- $ievariant[1] = strtoupper( $ievariant[1] );
- $ievariant = implode( '-', $ievariant );
- $aloption[] = 'string-contains=' . $ievariant;
+ $variantBCP47 = wfBCP47( $variant );
+ if ( $variantBCP47 !== $variant ) {
+ $aloption[] = 'string-contains=' . $variantBCP47;
}
}
}
@@ -1847,12 +1913,11 @@ class OutputPage extends ContextSource {
$response->header( $this->getXVO() );
}
- if( $this->mEnableClientCache ) {
- if(
+ if ( $this->mEnableClientCache ) {
+ if (
$wgUseSquid && session_id() == '' && !$this->isPrintable() &&
$this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies()
- )
- {
+ ) {
if ( $wgUseESI ) {
# We'll purge the proxy cache explicitly, but require end user agents
# to revalidate against the proxy on each visit.
@@ -1860,7 +1925,7 @@ class OutputPage extends ContextSource {
wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **\n", false );
# start with a shorter timeout for initial testing
# header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
- $response->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"');
+ $response->header( 'Surrogate-Control: max-age=' . $wgSquidMaxage . '+' . $this->mSquidMaxage . ', content="ESI/1.0"' );
$response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
} else {
# We'll purge the proxy cache for anons explicitly, but require end user agents
@@ -1870,7 +1935,7 @@ class OutputPage extends ContextSource {
wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **\n", false );
# start with a shorter timeout for initial testing
# header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
- $response->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' );
+ $response->header( 'Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0' );
}
} else {
# We do want clients to cache if they can, but they *must* check for updates
@@ -1879,7 +1944,7 @@ class OutputPage extends ContextSource {
$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
$response->header( "Cache-Control: private, must-revalidate, max-age=0" );
}
- if($this->mLastModified) {
+ if ( $this->mLastModified ) {
$response->header( "Last-Modified: {$this->mLastModified}" );
}
} else {
@@ -1894,7 +1959,7 @@ class OutputPage extends ContextSource {
}
/**
- * Get the message associed with the HTTP response code $code
+ * Get the message associated with the HTTP response code $code
*
* @param $code Integer: status code
* @return String or null: message or null if $code is not in the list of
@@ -1903,7 +1968,7 @@ class OutputPage extends ContextSource {
* @deprecated since 1.18 Use HttpStatus::getMessage() instead.
*/
public static function getStatusMessage( $code ) {
- wfDeprecated( __METHOD__ );
+ wfDeprecated( __METHOD__, '1.18' );
return HttpStatus::getMessage( $code );
}
@@ -1912,9 +1977,10 @@ class OutputPage extends ContextSource {
* the object, let's actually output it:
*/
public function output() {
- global $wgLanguageCode, $wgDebugRedirects, $wgMimeType, $wgVaryOnXFP;
+ global $wgLanguageCode, $wgDebugRedirects, $wgMimeType, $wgVaryOnXFP,
+ $wgUseAjax, $wgResponsiveImages;
- if( $this->mDoNothing ) {
+ if ( $this->mDoNothing ) {
return;
}
@@ -1929,9 +1995,9 @@ class OutputPage extends ContextSource {
$redirect = $this->mRedirect;
$code = $this->mRedirectCode;
- if( wfRunHooks( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) {
- if( $code == '301' || $code == '303' ) {
- if( !$wgDebugRedirects ) {
+ if ( wfRunHooks( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) {
+ if ( $code == '301' || $code == '303' ) {
+ if ( !$wgDebugRedirects ) {
$message = HttpStatus::getMessage( $code );
$response->header( "HTTP/1.1 $code $message" );
}
@@ -1943,7 +2009,7 @@ class OutputPage extends ContextSource {
$this->sendCacheControl();
$response->header( "Content-Type: text/html; charset=utf-8" );
- if( $wgDebugRedirects ) {
+ if ( $wgDebugRedirects ) {
$url = htmlspecialchars( $redirect );
print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
print "<p>Location: <a href=\"$url\">$url</a></p>\n";
@@ -1975,11 +2041,34 @@ class OutputPage extends ContextSource {
}
if ( $this->mArticleBodyOnly ) {
- $this->out( $this->mBodytext );
+ echo $this->mBodytext;
} else {
- $this->addDefaultModules();
$sk = $this->getSkin();
+ // add skin specific modules
+ $modules = $sk->getDefaultModules();
+
+ // enforce various default modules for all skins
+ $coreModules = array(
+ // keep this list as small as possible
+ 'mediawiki.page.startup',
+ 'mediawiki.user',
+ );
+
+ // Support for high-density display images if enabled
+ if ( $wgResponsiveImages ) {
+ $coreModules[] = 'mediawiki.hidpi';
+ }
+
+ $this->addModules( $coreModules );
+ foreach ( $modules as $group ) {
+ $this->addModules( $group );
+ }
+ MWDebug::addModules( $this );
+ if ( $wgUseAjax ) {
+ // FIXME: deprecate? - not clear why this is useful
+ wfRunHooks( 'AjaxAddScript', array( &$this ) );
+ }
// Hook that allows last minute changes to the output page, e.g.
// adding of CSS or Javascript by extensions.
@@ -1994,16 +2083,20 @@ class OutputPage extends ContextSource {
wfRunHooks( 'AfterFinalPageOutput', array( $this ) );
$this->sendCacheControl();
+
ob_end_flush();
+
wfProfileOut( __METHOD__ );
}
/**
- * Actually output something with print().
+ * Actually output something with print.
*
- * @param $ins String: the string to output
+ * @param string $ins the string to output
+ * @deprecated since 1.22 Use echo yourself.
*/
public function out( $ins ) {
+ wfDeprecated( __METHOD__, '1.22' );
print $ins;
}
@@ -2020,8 +2113,8 @@ class OutputPage extends ContextSource {
* indexing, clear the current text and redirect, set the page's title
* and optionally an custom HTML title (content of the "<title>" tag).
*
- * @param $pageTitle String|Message will be passed directly to setPageTitle()
- * @param $htmlTitle String|Message will be passed directly to setHTMLTitle();
+ * @param string|Message $pageTitle will be passed directly to setPageTitle()
+ * @param string|Message $htmlTitle will be passed directly to setHTMLTitle();
* optional, if not passed the "<title>" attribute will be
* based on $pageTitle
*/
@@ -2047,17 +2140,17 @@ class OutputPage extends ContextSource {
*
* @param $title Mixed: message key (string) for page title, or a Message object
* @param $msg Mixed: message key (string) for page text, or a Message object
- * @param $params Array: message parameters; ignored if $msg is a Message object
+ * @param array $params message parameters; ignored if $msg is a Message object
*/
public function showErrorPage( $title, $msg, $params = array() ) {
- if( !$title instanceof Message ) {
+ if ( !$title instanceof Message ) {
$title = $this->msg( $title );
}
$this->prepareErrorPage( $title );
- if ( $msg instanceof Message ){
- $this->addHTML( $msg->parse() );
+ if ( $msg instanceof Message ) {
+ $this->addHTML( $msg->parseAsBlock() );
} else {
$this->addWikiMsgArray( $msg, $params );
}
@@ -2068,12 +2161,10 @@ class OutputPage extends ContextSource {
/**
* Output a standard permission error page
*
- * @param $errors Array: error message keys
- * @param $action String: action that was denied or null if unknown
+ * @param array $errors error message keys
+ * @param string $action action that was denied or null if unknown
*/
public function showPermissionsErrorPage( $errors, $action = null ) {
- global $wgGroupPermissions;
-
// For some action (read, edit, create and upload), display a "login to do this action"
// error if all of the following conditions are met:
// 1. the user is not logged in
@@ -2082,8 +2173,8 @@ class OutputPage extends ContextSource {
if ( in_array( $action, array( 'read', 'edit', 'createpage', 'createtalk', 'upload' ) )
&& $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
&& ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
- && ( ( isset( $wgGroupPermissions['user'][$action] ) && $wgGroupPermissions['user'][$action] )
- || ( isset( $wgGroupPermissions['autoconfirmed'][$action] ) && $wgGroupPermissions['autoconfirmed'][$action] ) )
+ && ( User::groupHasPermission( 'user', $action )
+ || User::groupHasPermission( 'autoconfirmed', $action ) )
) {
$displayReturnto = null;
@@ -2115,7 +2206,7 @@ class OutputPage extends ContextSource {
unset( $returntoquery['title'] );
unset( $returntoquery['returnto'] );
unset( $returntoquery['returntoquery'] );
- $query['returntoquery'] = wfArrayToCGI( $returntoquery );
+ $query['returntoquery'] = wfArrayToCgi( $returntoquery );
}
}
$loginLink = Linker::linkKnown(
@@ -2155,7 +2246,8 @@ class OutputPage extends ContextSource {
/**
* Display an error page noting that a given permission bit is required.
* @deprecated since 1.18, just throw the exception directly
- * @param $permission String: key required
+ * @param string $permission key required
+ * @throws PermissionsError
*/
public function permissionRequired( $permission ) {
throw new PermissionsError( $permission );
@@ -2173,8 +2265,8 @@ class OutputPage extends ContextSource {
/**
* Format a list of error messages
*
- * @param $errors Array of arrays returned by Title::getUserPermissionsErrors
- * @param $action String: action that was denied or null if unknown
+ * @param array $errors of arrays returned by Title::getUserPermissionsErrors
+ * @param string $action action that was denied or null if unknown
* @return String: the wikitext error-messages, formatted into a list.
*/
public function formatPermissionsErrorMessage( $errors, $action = null ) {
@@ -2192,7 +2284,7 @@ class OutputPage extends ContextSource {
if ( count( $errors ) > 1 ) {
$text .= '<ul class="permissions-errors">' . "\n";
- foreach( $errors as $error ) {
+ foreach ( $errors as $error ) {
$text .= '<li>';
$text .= call_user_func_array( array( $this, 'msg' ), $error )->plain();
$text .= "</li>\n";
@@ -2226,6 +2318,7 @@ class OutputPage extends ContextSource {
* @param $protected Boolean: is this a permissions error?
* @param $reasons Array: list of reasons for this error, as returned by Title::getUserPermissionsErrors().
* @param $action String: action that was denied or null if unknown
+ * @throws ReadOnlyError
*/
public function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
$this->setRobotPolicy( 'noindex,nofollow' );
@@ -2239,7 +2332,7 @@ class OutputPage extends ContextSource {
if ( !empty( $reasons ) ) {
// Permissions error
- if( $source ) {
+ if ( $source ) {
$this->setPageTitle( $this->msg( 'viewsource-title', $this->getTitle()->getPrefixedText() ) );
$this->addBacklinkSubtitle( $this->getTitle() );
} else {
@@ -2252,12 +2345,12 @@ class OutputPage extends ContextSource {
}
// Show source, if supplied
- if( is_string( $source ) ) {
+ if ( is_string( $source ) ) {
$this->addWikiMsg( 'viewsourcetext' );
$pageLang = $this->getTitle()->getPageLanguage();
$params = array(
- 'id' => 'wpTextbox1',
+ 'id' => 'wpTextbox1',
'name' => 'wpTextbox1',
'cols' => $this->getUser()->getOption( 'cols' ),
'rows' => $this->getUser()->getOption( 'rows' ),
@@ -2278,13 +2371,13 @@ $templates
# If the title doesn't exist, it's fairly pointless to print a return
# link to it. After all, you just tried editing it and couldn't, so
# what's there to do there?
- if( $this->getTitle()->exists() ) {
+ if ( $this->getTitle()->exists() ) {
$this->returnToMain( null, $this->getTitle() );
}
}
/**
- * Turn off regular page output and return an error reponse
+ * Turn off regular page output and return an error response
* for when rate limiting has triggered.
*/
public function rateLimited() {
@@ -2302,7 +2395,7 @@ $templates
*/
public function showLagWarning( $lag ) {
global $wgSlaveLagWarning, $wgSlaveLagCritical;
- if( $lag >= $wgSlaveLagWarning ) {
+ if ( $lag >= $wgSlaveLagWarning ) {
$message = $lag < $wgSlaveLagCritical
? 'lag-warn-normal'
: 'lag-warn-high';
@@ -2341,13 +2434,13 @@ $templates
* Add a "return to" link pointing to a specified title
*
* @param $title Title to link
- * @param $query Array query string parameters
- * @param $text String text of the link (input is not escaped)
+ * @param array $query query string parameters
+ * @param string $text text of the link (input is not escaped)
+ * @param $options Options array to pass to Linker
*/
- public function addReturnTo( $title, $query = array(), $text = null ) {
- $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullURL() ) );
+ public function addReturnTo( $title, $query = array(), $text = null, $options = array() ) {
$link = $this->msg( 'returnto' )->rawParams(
- Linker::link( $title, $text, array(), $query ) )->escaped();
+ Linker::link( $title, $text, array(), $query, $options ) )->escaped();
$this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
}
@@ -2357,7 +2450,7 @@ $templates
*
* @param $unused
* @param $returnto Title or String to return to
- * @param $returntoquery String: query string for the return to link
+ * @param string $returntoquery query string for the return to link
*/
public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
if ( $returnto == null ) {
@@ -2390,15 +2483,11 @@ $templates
* @return String: The doctype, opening "<html>", and head element.
*/
public function headElement( Skin $sk, $includeStyle = true ) {
- global $wgContLang;
+ global $wgContLang, $wgMimeType;
$userdir = $this->getLanguage()->getDir();
$sitedir = $wgContLang->getDir();
- if ( $sk->commonPrintStylesheet() ) {
- $this->addModuleStyles( 'mediawiki.legacy.wikiprintable' );
- }
-
$ret = Html::htmlHeader( array( 'lang' => $this->getLanguage()->getHtmlCode(), 'dir' => $userdir, 'class' => 'client-nojs' ) );
if ( $this->getHTMLTitle() == '' ) {
@@ -2411,10 +2500,22 @@ $templates
$ret .= "$openHead\n";
}
+ if ( !Html::isXmlMimeType( $wgMimeType ) ) {
+ // Add <meta charset="UTF-8">
+ // This should be before <title> since it defines the charset used by
+ // text including the text inside <title>.
+ // The spec recommends defining XHTML5's charset using the XML declaration
+ // instead of meta.
+ // Our XML declaration is output by Html::htmlHeader.
+ // http://www.whatwg.org/html/semantics.html#attr-meta-http-equiv-content-type
+ // http://www.whatwg.org/html/semantics.html#charset
+ $ret .= Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
+ }
+
$ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n";
$ret .= implode( "\n", array(
- $this->getHeadLinks( null, true ),
+ $this->getHeadLinks(),
$this->buildCssLinks(),
$this->getHeadScripts(),
$this->getHeadItems()
@@ -2425,20 +2526,29 @@ $templates
$ret .= "$closeHead\n";
}
- $bodyAttrs = array();
+ $bodyClasses = array();
+ $bodyClasses[] = 'mediawiki';
# Classes for LTR/RTL directionality support
- $bodyAttrs['class'] = "mediawiki $userdir sitedir-$sitedir";
+ $bodyClasses[] = $userdir;
+ $bodyClasses[] = "sitedir-$sitedir";
if ( $this->getLanguage()->capitalizeAllNouns() ) {
# A <body> class is probably not the best way to do this . . .
- $bodyAttrs['class'] .= ' capitalize-all-nouns';
+ $bodyClasses[] = 'capitalize-all-nouns';
}
- $bodyAttrs['class'] .= ' ' . $sk->getPageClasses( $this->getTitle() );
- $bodyAttrs['class'] .= ' skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
- $bodyAttrs['class'] .= ' action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
- $sk->addToBodyAttributes( $this, $bodyAttrs ); // Allow skins to add body attributes they need
+ $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
+ $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
+ $bodyClasses[] = 'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
+
+ $bodyAttrs = array();
+ // While the implode() is not strictly needed, it's used for backwards compatibility
+ // (this used to be built as a string and hooks likely still expect that).
+ $bodyAttrs['class'] = implode( ' ', $bodyClasses );
+
+ // Allow skins and extensions to add body attributes they need
+ $sk->addToBodyAttributes( $this, $bodyAttrs );
wfRunHooks( 'OutputPageBodyAttributes', array( $this, $sk, &$bodyAttrs ) );
$ret .= Html::openElement( 'body', $bodyAttrs ) . "\n";
@@ -2447,54 +2557,6 @@ $templates
}
/**
- * Add the default ResourceLoader modules to this object
- */
- private function addDefaultModules() {
- global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
- $wgAjaxWatch;
-
- // Add base resources
- $this->addModules( array(
- 'mediawiki.user',
- 'mediawiki.page.startup',
- 'mediawiki.page.ready',
- ) );
- if ( $wgIncludeLegacyJavaScript ){
- $this->addModules( 'mediawiki.legacy.wikibits' );
- }
-
- if ( $wgPreloadJavaScriptMwUtil ) {
- $this->addModules( 'mediawiki.util' );
- }
-
- MWDebug::addModules( $this );
-
- // Add various resources if required
- if ( $wgUseAjax ) {
- $this->addModules( 'mediawiki.legacy.ajax' );
-
- wfRunHooks( 'AjaxAddScript', array( &$this ) );
-
- if( $wgAjaxWatch && $this->getUser()->isLoggedIn() ) {
- $this->addModules( 'mediawiki.page.watch.ajax' );
- }
-
- if ( !$this->getUser()->getOption( 'disablesuggest', false ) ) {
- $this->addModules( 'mediawiki.searchSuggest' );
- }
- }
-
- if ( $this->getUser()->getBoolOption( 'editsectiononrightclick' ) ) {
- $this->addModules( 'mediawiki.action.view.rightClickEdit' );
- }
-
- # Crazy edit-on-double-click stuff
- if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) {
- $this->addModules( 'mediawiki.action.view.dblClickEdit' );
- }
- }
-
- /**
* Get a ResourceLoader object associated with this OutputPage
*
* @return ResourceLoader
@@ -2509,16 +2571,16 @@ $templates
/**
* TODO: Document
* @param $modules Array/string with the module name(s)
- * @param $only String ResourceLoaderModule TYPE_ class constant
+ * @param string $only ResourceLoaderModule TYPE_ class constant
* @param $useESI boolean
- * @param $extraQuery Array with extra query parameters to add to each request. array( param => value )
+ * @param array $extraQuery with extra query parameters to add to each request. array( param => value )
* @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a "<script src='...'>" tag
* @return string html "<script>" and "<style>" tags
*/
protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) {
global $wgResourceLoaderUseESI;
- $modules = (array) $modules;
+ $modules = (array)$modules;
if ( !count( $modules ) ) {
return '';
@@ -2539,6 +2601,9 @@ $templates
return $links;
}
}
+ if ( !is_null( $this->mTarget ) ) {
+ $extraQuery['target'] = $this->mTarget;
+ }
// Create keyed-by-group list of module objects from modules list
$groups = array();
@@ -2551,8 +2616,8 @@ $templates
&& $only == ResourceLoaderModule::TYPE_SCRIPTS )
|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
&& $only == ResourceLoaderModule::TYPE_STYLES )
- )
- {
+ || ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) )
+ ) {
continue;
}
@@ -2587,7 +2652,7 @@ $templates
);
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
// Extract modules that know they're empty
- $emptyModules = array ();
+ $emptyModules = array();
foreach ( $grpModules as $key => $module ) {
if ( $module->isKnownEmpty( $context ) ) {
$emptyModules[$key] = 'ready';
@@ -2598,13 +2663,9 @@ $templates
if ( count( $emptyModules ) > 0 && $only !== ResourceLoaderModule::TYPE_STYLES ) {
// If we're only getting the styles, we don't need to do anything for empty modules.
$links .= Html::inlineScript(
-
ResourceLoader::makeLoaderConditionalScript(
-
ResourceLoader::makeLoaderStateScript( $emptyModules )
-
)
-
) . "\n";
}
@@ -2634,7 +2695,7 @@ $templates
}
// Special handling for the user group; because users might change their stuff
// on-wiki like user pages, or user preferences; we need to find the highest
- // timestamp of these user-changable modules so we can ensure cache misses on change
+ // timestamp of these user-changeable modules so we can ensure cache misses on change
// This should NOT be done for the site group (bug 27564) because anons get that too
// and we shouldn't be putting timestamps in Squid-cached HTML
$version = null;
@@ -2671,7 +2732,7 @@ $templates
// Automatically select style/script elements
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
$link = Html::linkedStyle( $url );
- } else if ( $loadCall ) {
+ } elseif ( $loadCall ) {
$link = Html::inlineScript(
ResourceLoader::makeLoaderConditionalScript(
Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
@@ -2682,7 +2743,7 @@ $templates
}
}
- if( $group == 'noscript' ){
+ if ( $group == 'noscript' ) {
$links .= Html::rawElement( 'noscript', array(), $link ) . "\n";
} else {
$links .= $link . "\n";
@@ -2786,14 +2847,14 @@ $templates
);
$defaultModules['site'] = 'loading';
} else {
- // The wiki is configured to not allow a site module.
- $defaultModules['site'] = 'missing';
+ // Site module is empty, save request by marking ready in advance (bug 46857)
+ $defaultModules['site'] = 'ready';
}
// Add user JS if enabled
if ( $wgAllowUserJs ) {
if ( $this->getUser()->isLoggedIn() ) {
- if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
+ if ( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
# XXX: additional security check/prompt?
// We're on a preview of a JS subpage
// Exclude this page from the user module in case it's in there (bug 26283)
@@ -2813,15 +2874,14 @@ $templates
}
$defaultModules['user'] = 'loading';
} else {
- // Non-logged-in users have no user module. Treat it as empty and 'ready' to avoid
- // blocking default gadgets that might depend on it. Although arguably default-enabled
- // gadgets should not depend on the user module, it's harmless and less error-prone to
- // handle this case.
+ // Non-logged-in users have an empty user module.
+ // Save request by marking ready in advance (bug 46857)
$defaultModules['user'] = 'ready';
}
} else {
- // User JS disabled
- $defaultModules['user'] = 'missing';
+ // User modules are disabled on this wiki.
+ // Save request by marking ready in advance (bug 46857)
+ $defaultModules['user'] = 'ready';
}
// Group JS is only enabled if site JS is enabled.
@@ -2832,13 +2892,13 @@ $templates
);
$defaultModules['user.groups'] = 'loading';
} else {
- // Non-logged-in users have no user.groups module. Treat it as empty and 'ready' to
- // avoid blocking gadgets that might depend upon the module.
+ // Non-logged-in users have no user.groups module.
+ // Save request by marking ready in advance (bug 46857)
$defaultModules['user.groups'] = 'ready';
}
} else {
// Site (and group JS) disabled
- $defaultModules['user.groups'] = 'missing';
+ $defaultModules['user.groups'] = 'ready';
}
$loaderInit = '';
@@ -2866,11 +2926,18 @@ $templates
*/
function getBottomScripts() {
global $wgResourceLoaderExperimentalAsyncLoading;
+
+ // Optimise jQuery ready event cross-browser.
+ // This also enforces $.isReady to be true at </body> which fixes the
+ // mw.loader bug in Firefox with using document.write between </body>
+ // and the DOMContentReady event (bug 47457).
+ $html = Html::inlineScript( 'window.jQuery && jQuery.ready();' );
+
if ( !$wgResourceLoaderExperimentalAsyncLoading ) {
- return $this->getScriptsForBottomQueue( false );
- } else {
- return '';
+ $html .= $this->getScriptsForBottomQueue( false );
}
+
+ return $html;
}
/**
@@ -2890,40 +2957,39 @@ $templates
$this->mJsConfigVars[$keys] = $value;
}
-
/**
* Get an array containing the variables to be set in mw.config in JavaScript.
*
* DO NOT CALL THIS FROM OUTSIDE OF THIS CLASS OR Skin::makeGlobalVariablesScript().
* This is only public until that function is removed. You have been warned.
*
- * Do not add things here which can be evaluated in ResourceLoaderStartupScript
+ * Do not add things here which can be evaluated in ResourceLoaderStartUpModule
* - in other words, page-independent/site-wide variables (without state).
* You will only be adding bloat to the html page and causing page caches to
* have to be purged on configuration changes.
* @return array
*/
public function getJSVars() {
- global $wgUseAjax, $wgContLang;
+ global $wgContLang;
- $latestRevID = 0;
- $pageID = 0;
- $canonicalName = false; # bug 21115
+ $curRevisionId = 0;
+ $articleId = 0;
+ $canonicalSpecialPageName = false; # bug 21115
$title = $this->getTitle();
$ns = $title->getNamespace();
- $nsname = MWNamespace::exists( $ns ) ? MWNamespace::getCanonicalName( $ns ) : $title->getNsText();
+ $canonicalNamespace = MWNamespace::exists( $ns ) ? MWNamespace::getCanonicalName( $ns ) : $title->getNsText();
// Get the relevant title so that AJAX features can use the correct page name
// when making API requests from certain special pages (bug 34972).
$relevantTitle = $this->getSkin()->getRelevantTitle();
if ( $ns == NS_SPECIAL ) {
- list( $canonicalName, /*...*/ ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
+ list( $canonicalSpecialPageName, /*...*/ ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
} elseif ( $this->canUseWikiPage() ) {
$wikiPage = $this->getWikiPage();
- $latestRevID = $wikiPage->getLatest();
- $pageID = $wikiPage->getId();
+ $curRevisionId = $wikiPage->getLatest();
+ $articleId = $wikiPage->getId();
}
$lang = $title->getPageLanguage();
@@ -2942,31 +3008,48 @@ $templates
implode( "\t", $digitTransTable ),
);
+ $user = $this->getUser();
+
$vars = array(
- 'wgCanonicalNamespace' => $nsname,
- 'wgCanonicalSpecialPageName' => $canonicalName,
+ 'wgCanonicalNamespace' => $canonicalNamespace,
+ 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
'wgNamespaceNumber' => $title->getNamespace(),
- 'wgPageName' => $title->getPrefixedDBKey(),
+ 'wgPageName' => $title->getPrefixedDBkey(),
'wgTitle' => $title->getText(),
- 'wgCurRevisionId' => $latestRevID,
- 'wgArticleId' => $pageID,
+ 'wgCurRevisionId' => $curRevisionId,
+ 'wgRevisionId' => (int)$this->getRevisionId(),
+ 'wgArticleId' => $articleId,
'wgIsArticle' => $this->isArticle(),
+ 'wgIsRedirect' => $title->isRedirect(),
'wgAction' => Action::getActionName( $this->getContext() ),
- 'wgUserName' => $this->getUser()->isAnon() ? null : $this->getUser()->getName(),
- 'wgUserGroups' => $this->getUser()->getEffectiveGroups(),
+ 'wgUserName' => $user->isAnon() ? null : $user->getName(),
+ 'wgUserGroups' => $user->getEffectiveGroups(),
'wgCategories' => $this->getCategories(),
'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
'wgPageContentLanguage' => $lang->getCode(),
+ 'wgPageContentModel' => $title->getContentModel(),
'wgSeparatorTransformTable' => $compactSeparatorTransTable,
'wgDigitTransformTable' => $compactDigitTransTable,
'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
'wgMonthNames' => $lang->getMonthNamesArray(),
'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
- 'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(),
+ 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
);
+ if ( $user->isLoggedIn() ) {
+ $vars['wgUserId'] = $user->getId();
+ $vars['wgUserEditCount'] = $user->getEditCount();
+ $userReg = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
+ $vars['wgUserRegistration'] = $userReg !== null ? ( $userReg * 1000 ) : null;
+ // Get the revision ID of the oldest new message on the user's talk
+ // page. This can be used for constructing new message alerts on
+ // the client side.
+ $vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
+ }
if ( $wgContLang->hasVariants() ) {
$vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
- }
+ }
+ // Same test as SkinTemplate
+ $vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user ) && ( $title->exists() || $title->quickUserCan( 'create', $user ) );
foreach ( $title->getRestrictionTypes() as $type ) {
$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
}
@@ -2974,7 +3057,7 @@ $templates
$vars['wgIsMainPage'] = true;
}
if ( $this->mRedirectedFrom ) {
- $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBKey();
+ $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
}
// Allow extensions to add their custom variables to the mw.config map.
@@ -3012,35 +3095,18 @@ $templates
}
/**
- * @param $addContentType bool: Whether "<meta>" specifying content type should be returned
- *
* @return array in format "link name or number => 'link html'".
*/
- public function getHeadLinksArray( $addContentType = false ) {
+ public function getHeadLinksArray() {
global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI,
- $wgSitename, $wgVersion, $wgHtml5, $wgMimeType,
+ $wgSitename, $wgVersion,
$wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes,
$wgDisableLangConversion, $wgCanonicalLanguageLinks,
$wgRightsPage, $wgRightsUrl;
$tags = array();
- if ( $addContentType ) {
- if ( $wgHtml5 ) {
- # More succinct than <meta http-equiv=Content-Type>, has the
- # same effect
- $tags['meta-charset'] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
- } else {
- $tags['meta-content-type'] = Html::element( 'meta', array(
- 'http-equiv' => 'Content-Type',
- 'content' => "$wgMimeType; charset=UTF-8"
- ) );
- $tags['meta-content-style-type'] = Html::element( 'meta', array( // bug 15835
- 'http-equiv' => 'Content-Style-Type',
- 'content' => 'text/css'
- ) );
- }
- }
+ $canonicalUrl = $this->mCanonicalUrl;
$tags['meta-generator'] = Html::element( 'meta', array(
'name' => 'generator',
@@ -3048,7 +3114,7 @@ $templates
) );
$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
- if( $p !== 'index,follow' ) {
+ if ( $p !== 'index,follow' ) {
// http://www.robotstxt.org/wc/meta-user.html
// Only show if it's different from the default robots policy
$tags['meta-robots'] = Html::element( 'meta', array(
@@ -3057,21 +3123,6 @@ $templates
) );
}
- if ( count( $this->mKeywords ) > 0 ) {
- $strip = array(
- "/<.*?" . ">/" => '',
- "/_/" => ' '
- );
- $tags['meta-keywords'] = Html::element( 'meta', array(
- 'name' => 'keywords',
- 'content' => preg_replace(
- array_keys( $strip ),
- array_values( $strip ),
- implode( ',', $this->mKeywords )
- )
- ) );
- }
-
foreach ( $this->mMetatags as $tag ) {
if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
$a = 'http-equiv';
@@ -3151,7 +3202,6 @@ $templates
) );
}
-
# Language variants
if ( !$wgDisableLangConversion && $wgCanonicalLanguageLinks ) {
$lang = $this->getTitle()->getPageLanguage();
@@ -3164,15 +3214,12 @@ $templates
foreach ( $variants as $_v ) {
$tags["variant-$_v"] = Html::element( 'link', array(
'rel' => 'alternate',
- 'hreflang' => $_v,
+ 'hreflang' => wfBCP47( $_v ),
'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) )
);
}
} else {
- $tags['canonical'] = Html::element( 'link', array(
- 'rel' => 'canonical',
- 'href' => $this->getTitle()->getCanonicalUrl()
- ) );
+ $canonicalUrl = $this->getTitle()->getLocalURL();
}
}
}
@@ -3200,7 +3247,7 @@ $templates
# Feeds
if ( $wgFeed ) {
- foreach( $this->getSyndicationLinks() as $format => $link ) {
+ foreach ( $this->getSyndicationLinks() as $format => $link ) {
# Use the page name for the title. In principle, this could
# lead to issues with having the same name for different feeds
# corresponding to the same page, but we can't avoid that at
@@ -3235,31 +3282,46 @@ $templates
foreach ( $wgAdvertisedFeedTypes as $format ) {
$tags[] = $this->feedLink(
$format,
- $rctitle->getLocalURL( "feed={$format}" ),
+ $rctitle->getLocalURL( array( 'feed' => $format ) ),
$this->msg( "site-{$format}-feed", $wgSitename )->text() # For grep: 'site-rss-feed', 'site-atom-feed'.
);
}
}
}
+
+ # Canonical URL
+ global $wgEnableCanonicalServerLink;
+ if ( $wgEnableCanonicalServerLink ) {
+ if ( $canonicalUrl !== false ) {
+ $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
+ } else {
+ $reqUrl = $this->getRequest()->getRequestURL();
+ $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
+ }
+ }
+ if ( $canonicalUrl !== false ) {
+ $tags[] = Html::element( 'link', array(
+ 'rel' => 'canonical',
+ 'href' => $canonicalUrl
+ ) );
+ }
+
return $tags;
}
/**
- * @param $unused
- * @param $addContentType bool: Whether "<meta>" specifying content type should be returned
- *
* @return string HTML tag links to be put in the header.
*/
- public function getHeadLinks( $unused = null, $addContentType = false ) {
- return implode( "\n", $this->getHeadLinksArray( $addContentType ) );
+ public function getHeadLinks() {
+ return implode( "\n", $this->getHeadLinksArray() );
}
/**
* Generate a "<link rel/>" for a feed.
*
- * @param $type String: feed type
- * @param $url String: URL to the feed
- * @param $text String: value of the "title" attribute
+ * @param string $type feed type
+ * @param string $url URL to the feed
+ * @param string $text value of the "title" attribute
* @return String: HTML fragment
*/
private function feedLink( $type, $url, $text ) {
@@ -3275,22 +3337,22 @@ $templates
* Add a local or specified stylesheet, with the given media options.
* Meant primarily for internal use...
*
- * @param $style String: URL to the file
- * @param $media String: to specify a media type, 'screen', 'printable', 'handheld' or any.
- * @param $condition String: for IE conditional comments, specifying an IE version
- * @param $dir String: set to 'rtl' or 'ltr' for direction-specific sheets
+ * @param string $style URL to the file
+ * @param string $media to specify a media type, 'screen', 'printable', 'handheld' or any.
+ * @param string $condition for IE conditional comments, specifying an IE version
+ * @param string $dir set to 'rtl' or 'ltr' for direction-specific sheets
*/
public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
$options = array();
// Even though we expect the media type to be lowercase, but here we
// force it to lowercase to be safe.
- if( $media ) {
+ if ( $media ) {
$options['media'] = $media;
}
- if( $condition ) {
+ if ( $condition ) {
$options['condition'] = $condition;
}
- if( $dir ) {
+ if ( $dir ) {
$options['dir'] = $dir;
}
$this->styles[$style] = $options;
@@ -3299,10 +3361,10 @@ $templates
/**
* Adds inline CSS styles
* @param $style_css Mixed: inline CSS
- * @param $flip String: Set to 'flip' to flip the CSS if needed
+ * @param string $flip Set to 'flip' to flip the CSS if needed
*/
public function addInlineStyle( $style_css, $flip = 'noflip' ) {
- if( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
+ if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
# If wanted, and the interface is right-to-left, flip the CSS
$style_css = CSSJanus::transform( $style_css, true, false );
}
@@ -3316,8 +3378,7 @@ $templates
* @return string
*/
public function buildCssLinks() {
- global $wgUseSiteCss, $wgAllowUserCss, $wgAllowUserCssPrefs,
- $wgLang, $wgContLang;
+ global $wgUseSiteCss, $wgAllowUserCss, $wgAllowUserCssPrefs, $wgContLang;
$this->getSkin()->setupSkinUserCss( $this );
@@ -3333,7 +3394,7 @@ $templates
if ( $wgUseSiteCss ) {
$moduleStyles[] = 'site';
$moduleStyles[] = 'noscript';
- if( $this->getUser()->isLoggedIn() ){
+ if ( $this->getUser()->isLoggedIn() ) {
$moduleStyles[] = 'user.groups';
}
}
@@ -3351,7 +3412,7 @@ $templates
// If needed, Janus it first. This is user-supplied CSS, so it's
// assumed to be right for the content language directionality.
$previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
- if ( $wgLang->getDir() !== $wgContLang->getDir() ) {
+ if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) {
$previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
}
$otherTags .= Html::inlineStyle( $previewedCSS );
@@ -3414,9 +3475,9 @@ $templates
}
$this->mExtStyles = array();
- foreach( $this->styles as $file => $options ) {
+ foreach ( $this->styles as $file => $options ) {
$link = $this->styleLink( $file, $options );
- if( $link ) {
+ if ( $link ) {
$links[$file] = $link;
}
}
@@ -3426,28 +3487,28 @@ $templates
/**
* Generate \<link\> tags for stylesheets
*
- * @param $style String: URL to the file
- * @param $options Array: option, can contain 'condition', 'dir', 'media'
+ * @param string $style URL to the file
+ * @param array $options option, can contain 'condition', 'dir', 'media'
* keys
* @return String: HTML fragment
*/
protected function styleLink( $style, $options ) {
- if( isset( $options['dir'] ) ) {
- if( $this->getLanguage()->getDir() != $options['dir'] ) {
+ if ( isset( $options['dir'] ) ) {
+ if ( $this->getLanguage()->getDir() != $options['dir'] ) {
return '';
}
}
- if( isset( $options['media'] ) ) {
+ if ( isset( $options['media'] ) ) {
$media = self::transformCssMedia( $options['media'] );
- if( is_null( $media ) ) {
+ if ( is_null( $media ) ) {
return '';
}
} else {
$media = 'all';
}
- if( substr( $style, 0, 1 ) == '/' ||
+ if ( substr( $style, 0, 1 ) == '/' ||
substr( $style, 0, 5 ) == 'http:' ||
substr( $style, 0, 6 ) == 'https:' ) {
$url = $style;
@@ -3458,7 +3519,7 @@ $templates
$link = Html::linkedStyle( $url, $media );
- if( isset( $options['condition'] ) ) {
+ if ( isset( $options['condition'] ) ) {
$condition = htmlspecialchars( $options['condition'] );
$link = "<!--[if $condition]>$link<![endif]-->";
}
@@ -3468,39 +3529,43 @@ $templates
/**
* Transform "media" attribute based on request parameters
*
- * @param $media String: current value of the "media" attribute
- * @return String: modified value of the "media" attribute
+ * @param string $media current value of the "media" attribute
+ * @return String: modified value of the "media" attribute, or null to skip
+ * this stylesheet
*/
public static function transformCssMedia( $media ) {
- global $wgRequest, $wgHandheldForIPhone;
+ global $wgRequest;
+
+ // http://www.w3.org/TR/css3-mediaqueries/#syntax
+ $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
// Switch in on-screen display for media testing
$switches = array(
'printable' => 'print',
'handheld' => 'handheld',
);
- foreach( $switches as $switch => $targetMedia ) {
- if( $wgRequest->getBool( $switch ) ) {
- if( $media == $targetMedia ) {
+ foreach ( $switches as $switch => $targetMedia ) {
+ if ( $wgRequest->getBool( $switch ) ) {
+ if ( $media == $targetMedia ) {
$media = '';
- } elseif( $media == 'screen' ) {
- return null;
+ } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
+ // This regex will not attempt to understand a comma-separated media_query_list
+ //
+ // Example supported values for $media: 'screen', 'only screen', 'screen and (min-width: 982px)' ),
+ // Example NOT supported value for $media: '3d-glasses, screen, print and resolution > 90dpi'
+ //
+ // If it's a print request, we never want any kind of screen stylesheets
+ // If it's a handheld request (currently the only other choice with a switch),
+ // we don't want simple 'screen' but we might want screen queries that
+ // have a max-width or something, so we'll pass all others on and let the
+ // client do the query.
+ if ( $targetMedia == 'print' || $media == 'screen' ) {
+ return null;
+ }
}
}
}
- // Expand longer media queries as iPhone doesn't grok 'handheld'
- if( $wgHandheldForIPhone ) {
- $mediaAliases = array(
- 'screen' => 'screen and (min-device-width: 481px)',
- 'handheld' => 'handheld, only screen and (max-device-width: 480px)',
- );
-
- if( isset( $mediaAliases[$media] ) ) {
- $media = $mediaAliases[$media];
- }
- }
-
return $media;
}
@@ -3557,7 +3622,6 @@ $templates
$msgSpecs = array_values( $msgSpecs );
$s = $wrap;
foreach ( $msgSpecs as $n => $spec ) {
- $options = array();
if ( is_array( $spec ) ) {
$args = $spec;
$name = array_shift( $args );
@@ -3568,7 +3632,7 @@ $templates
'1.20'
);
}
- } else {
+ } else {
$args = array();
$name = $spec;
}
@@ -3581,7 +3645,7 @@ $templates
* Include jQuery core. Use this to avoid loading it multiple times
* before we get a usable script loader.
*
- * @param $modules Array: list of jQuery modules which should be loaded
+ * @param array $modules list of jQuery modules which should be loaded
* @return Array: the list of modules which were not loaded.
* @since 1.16
* @deprecated since 1.17
@@ -3590,4 +3654,20 @@ $templates
return array();
}
+ /**
+ * Enables/disables TOC, doesn't override __NOTOC__
+ * @param bool $flag
+ * @since 1.22
+ */
+ public function enableTOC( $flag = true ) {
+ $this->mEnableTOC = $flag;
+ }
+
+ /**
+ * @return bool
+ * @since 1.22
+ */
+ public function isTOCEnabled() {
+ return $this->mEnableTOC;
+ }
}
diff --git a/includes/PHPVersionError.php b/includes/PHPVersionError.php
index dad71f82..02d3546f 100644
--- a/includes/PHPVersionError.php
+++ b/includes/PHPVersionError.php
@@ -22,13 +22,13 @@
/**
* Display something vaguely comprehensible in the event of a totally unrecoverable error.
- * Does not assume access to *anything*; no globals, no autloader, no database, no localisation.
+ * Does not assume access to *anything*; no globals, no autoloader, no database, no localisation.
* Safe for PHP4 (and putting this here means that WebStart.php and GlobalSettings.php
* no longer need to be).
*
* Calling this function kills execution immediately.
*
- * @param $type String Which entry point we are protecting. One of:
+ * @param string $type Which entry point we are protecting. One of:
* - index.php
* - load.php
* - api.php
@@ -37,29 +37,36 @@
* @note Since we can't rely on anything, the minimum PHP versions and MW current
* version are hardcoded here
*/
-function wfPHPVersionError( $type ){
- $mwVersion = '1.20';
- $phpVersion = PHP_VERSION;
- $message = "MediaWiki $mwVersion requires at least PHP version 5.3.2, you are using PHP $phpVersion.";
- if( $type == 'index.php' ) {
+function wfPHPVersionError( $type ) {
+ $mwVersion = '1.22';
+ $minimumVersionPHP = '5.3.2';
+
+ $phpVersion = phpversion();
+ $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
+ $message = "MediaWiki $mwVersion requires at least PHP version $minimumVersionPHP, you are using PHP $phpVersion.";
+ if ( $type == 'cli' ) {
+ $finalOutput = "You are using PHP version $phpVersion but MediaWiki $mwVersion needs PHP $minimumVersionPHP or higher. ABORTING.\n" .
+ "Check if you have a newer php executable with a different name, such as php5.\n";
+ } elseif ( $type == 'index.php' ) {
+ $pathinfo = pathinfo( $_SERVER['SCRIPT_NAME'] );
$encLogo = htmlspecialchars(
- str_replace( '//', '/', pathinfo( $_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME ) . '/'
- ) . 'skins/common/images/mediawiki.png'
+ str_replace( '//', '/', $pathinfo['dirname'] . '/' ) .
+ 'skins/common/images/mediawiki.png'
);
- header( $_SERVER['SERVER_PROTOCOL'] . ' 500 MediaWiki configuration Error', true, 500 );
+ header( "$protocol 500 MediaWiki configuration Error" );
header( 'Content-type: text/html; charset=UTF-8' );
// Don't cache error pages! They cause no end of trouble...
header( 'Cache-control: none' );
- header( 'Pragma: nocache' );
+ header( 'Pragma: no-cache' );
$finalOutput = <<<HTML
-<!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'>
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
<head>
+ <meta charset="UTF-8" />
<title>MediaWiki {$mwVersion}</title>
- <meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
- <style type='text/css' media='screen'>
+ <style media='screen'>
body {
color: #000;
background-color: #fff;
@@ -103,10 +110,8 @@ HTML;
} else {
// So nothing thinks this is JS or CSS
$finalOutput = ( $type == 'load.php' ) ? "/* $message */" : $message;
- if( $type != 'cli' ) {
- header( $_SERVER['SERVER_PROTOCOL'] . ' 500 MediaWiki configuration Error', true, 500 );
- }
+ header( "$protocol 500 MediaWiki configuration Error" );
}
- echo( "$finalOutput\n" );
+ echo "$finalOutput\n";
die( 1 );
}
diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php
index 01a2439f..61a535d6 100644
--- a/includes/PageQueryPage.php
+++ b/includes/PageQueryPage.php
@@ -27,12 +27,11 @@
* @ingroup SpecialPage
*/
abstract class PageQueryPage extends QueryPage {
-
/**
* Format the result as a simple link to the page
*
- * @param $skin Skin
- * @param $row Object: result row
+ * @param Skin $skin
+ * @param object $row Result row
* @return string
*/
public function formatResult( $skin, $row ) {
diff --git a/includes/Pager.php b/includes/Pager.php
index 96ba446e..4a14c7e0 100644
--- a/includes/Pager.php
+++ b/includes/Pager.php
@@ -143,25 +143,26 @@ abstract class IndexPager extends ContextSource implements Pager {
$this->mOffset = $this->mRequest->getText( 'offset' );
# Use consistent behavior for the limit options
- $this->mDefaultLimit = intval( $this->getUser()->getOption( 'rclimit' ) );
+ $this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' );
if ( !$this->mLimit ) {
// Don't override if a subclass calls $this->setLimit() in its constructor.
list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
}
$this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
- $this->mDb = wfGetDB( DB_SLAVE );
+ # Let the subclass set the DB here; otherwise use a slave DB for the current wiki
+ $this->mDb = $this->mDb ?: wfGetDB( DB_SLAVE );
$index = $this->getIndexField(); // column to sort on
$extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning
$order = $this->mRequest->getVal( 'order' );
- if( is_array( $index ) && isset( $index[$order] ) ) {
+ if ( is_array( $index ) && isset( $index[$order] ) ) {
$this->mOrderType = $order;
$this->mIndexField = $index[$order];
$this->mExtraSortFields = isset( $extraSort[$order] )
? (array)$extraSort[$order]
: array();
- } elseif( is_array( $index ) ) {
+ } elseif ( is_array( $index ) ) {
# First element is the default
reset( $index );
list( $this->mOrderType, $this->mIndexField ) = each( $index );
@@ -175,7 +176,7 @@ abstract class IndexPager extends ContextSource implements Pager {
$this->mExtraSortFields = (array)$extraSort;
}
- if( !isset( $this->mDefaultDirection ) ) {
+ if ( !isset( $this->mDefaultDirection ) ) {
$dir = $this->getDefaultDirections();
$this->mDefaultDirection = is_array( $dir )
? $dir[$this->mOrderType]
@@ -206,13 +207,25 @@ abstract class IndexPager extends ContextSource implements Pager {
# Plus an extra row so that we can tell the "next" link should be shown
$queryLimit = $this->mLimit + 1;
+ if ( $this->mOffset == '' ) {
+ $isFirst = true;
+ } else {
+ // If there's an offset, we may or may not be at the first entry.
+ // The only way to tell is to run the query in the opposite
+ // direction see if we get a row.
+ $oldIncludeOffset = $this->mIncludeOffset;
+ $this->mIncludeOffset = !$this->mIncludeOffset;
+ $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, !$descending )->numRows();
+ $this->mIncludeOffset = $oldIncludeOffset;
+ }
+
$this->mResult = $this->reallyDoQuery(
$this->mOffset,
$queryLimit,
$descending
);
- $this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult );
+ $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult );
$this->mQueryDone = true;
$this->preprocessResults( $this->mResult );
@@ -244,7 +257,7 @@ abstract class IndexPager extends ContextSource implements Pager {
* @param $limit Int|String
*/
function setLimit( $limit ) {
- $limit = (int) $limit;
+ $limit = (int)$limit;
// WebRequest::getLimitOffset() puts a cap of 5000, so do same here.
if ( $limit > 5000 ) {
$limit = 5000;
@@ -269,11 +282,12 @@ abstract class IndexPager extends ContextSource implements Pager {
* Extract some useful data from the result object for use by
* the navigation bar, put it into $this
*
- * @param $offset String: index offset, inclusive
+ * @param $isFirst bool: False if there are rows before those fetched (i.e.
+ * if a "previous" link would make sense)
* @param $limit Integer: exact query limit
* @param $res ResultWrapper
*/
- function extractResultInfo( $offset, $limit, ResultWrapper $res ) {
+ function extractResultInfo( $isFirst, $limit, ResultWrapper $res ) {
$numRows = $res->numRows();
if ( $numRows ) {
# Remove any table prefix from index field
@@ -287,8 +301,7 @@ abstract class IndexPager extends ContextSource implements Pager {
if ( $numRows > $this->mLimit && $numRows > 1 ) {
$res->seek( $numRows - 1 );
$this->mPastTheEndRow = $res->fetchObject();
- $indexField = $this->mIndexField;
- $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexField;
+ $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn;
$res->seek( $numRows - 2 );
$row = $res->fetchRow();
$lastIndex = $row[$indexColumn];
@@ -312,11 +325,11 @@ abstract class IndexPager extends ContextSource implements Pager {
if ( $this->mIsBackwards ) {
$this->mIsFirst = ( $numRows < $limit );
- $this->mIsLast = ( $offset == '' );
+ $this->mIsLast = $isFirst;
$this->mLastShown = $firstIndex;
$this->mFirstShown = $lastIndex;
} else {
- $this->mIsFirst = ( $offset == '' );
+ $this->mIsFirst = $isFirst;
$this->mIsLast = ( $numRows < $limit );
$this->mLastShown = $lastIndex;
$this->mFirstShown = $firstIndex;
@@ -336,7 +349,7 @@ abstract class IndexPager extends ContextSource implements Pager {
* Do a query with specified parameters, rather than using the object
* context
*
- * @param $offset String: index offset, inclusive
+ * @param string $offset index offset, inclusive
* @param $limit Integer: exact query limit
* @param $descending Boolean: query direction, false for ascending, true for descending
* @return ResultWrapper
@@ -349,7 +362,7 @@ abstract class IndexPager extends ContextSource implements Pager {
/**
* Build variables to use by the database wrapper.
*
- * @param $offset String: index offset, inclusive
+ * @param string $offset index offset, inclusive
* @param $limit Integer: exact query limit
* @param $descending Boolean: query direction, false for ascending, true for descending
* @return array
@@ -432,9 +445,9 @@ abstract class IndexPager extends ContextSource implements Pager {
/**
* Make a self-link
*
- * @param $text String: text displayed on the link
- * @param $query Array: associative array of paramter to be in the query string
- * @param $type String: value of the "rel" attribute
+ * @param string $text text displayed on the link
+ * @param array $query associative array of parameter to be in the query string
+ * @param string $type value of the "rel" attribute
*
* @return String: HTML fragment
*/
@@ -444,12 +457,12 @@ abstract class IndexPager extends ContextSource implements Pager {
}
$attrs = array();
- if( in_array( $type, array( 'first', 'prev', 'next', 'last' ) ) ) {
+ if ( in_array( $type, array( 'first', 'prev', 'next', 'last' ) ) ) {
# HTML5 rel attributes
$attrs['rel'] = $type;
}
- if( $type ) {
+ if ( $type ) {
$attrs['class'] = "mw-{$type}link";
}
@@ -581,7 +594,7 @@ abstract class IndexPager extends ContextSource implements Pager {
$this->doQuery();
}
// Hide navigation by default if there is nothing to page
- return !($this->mIsFirst && $this->mIsLast);
+ return !( $this->mIsFirst && $this->mIsLast );
}
/**
@@ -686,11 +699,13 @@ abstract class IndexPager extends ContextSource implements Pager {
*
* @return Array
*/
- protected function getExtraSortFields() { return array(); }
+ protected function getExtraSortFields() {
+ return array();
+ }
/**
- * Return the default sorting direction: false for ascending, true for de-
- * scending. You can also have an associative array of ordertype => dir,
+ * Return the default sorting direction: false for ascending, true for
+ * descending. You can also have an associative array of ordertype => dir,
* if multiple order types are supported. In this case getIndexField()
* must return an array, and the keys of that must exactly match the keys
* of this.
@@ -707,10 +722,11 @@ abstract class IndexPager extends ContextSource implements Pager {
*
* @return Boolean
*/
- protected function getDefaultDirections() { return false; }
+ protected function getDefaultDirections() {
+ return false;
+ }
}
-
/**
* IndexPager with an alphabetic list and a formatted navigation bar
* @ingroup Pager
@@ -728,7 +744,7 @@ abstract class AlphabeticPager extends IndexPager {
return '';
}
- if( isset( $this->mNavigationBar ) ) {
+ if ( isset( $this->mNavigationBar ) ) {
return $this->mNavigationBar;
}
@@ -751,7 +767,7 @@ abstract class AlphabeticPager extends IndexPager {
$this->msg( 'viewprevnext' )->rawParams( $pagingLinks['prev'],
$pagingLinks['next'], $limits )->escaped();
- if( !is_array( $this->getIndexField() ) ) {
+ if ( !is_array( $this->getIndexField() ) ) {
# Early return to avoid undue nesting
return $this->mNavigationBar;
}
@@ -759,14 +775,14 @@ abstract class AlphabeticPager extends IndexPager {
$extra = '';
$first = true;
$msgs = $this->getOrderTypeMessages();
- foreach( array_keys( $msgs ) as $order ) {
- if( $first ) {
+ foreach ( array_keys( $msgs ) as $order ) {
+ if ( $first ) {
$first = false;
} else {
$extra .= $this->msg( 'pipe-separator' )->escaped();
}
- if( $order == $this->mOrderType ) {
+ if ( $order == $this->mOrderType ) {
$extra .= $this->msg( $msgs[$order] )->escaped();
} else {
$extra .= $this->makeLink(
@@ -776,7 +792,7 @@ abstract class AlphabeticPager extends IndexPager {
}
}
- if( $extra !== '' ) {
+ if ( $extra !== '' ) {
$extra = ' ' . $this->msg( 'parentheses' )->rawParams( $extra )->escaped();
$this->mNavigationBar .= $extra;
}
@@ -786,8 +802,8 @@ abstract class AlphabeticPager extends IndexPager {
/**
* If this supports multiple order type messages, give the message key for
- * enabling each one in getNavigationBar. The return type is an associa-
- * tive array whose keys must exactly match the keys of the array returned
+ * enabling each one in getNavigationBar. The return type is an associative
+ * array whose keys must exactly match the keys of the array returned
* by getIndexField(), and whose values are message keys.
*
* @return Array
@@ -857,9 +873,10 @@ abstract class ReverseChronologicalPager extends IndexPager {
$year = $this->mYear;
} else {
// If no year given, assume the current one
- $year = gmdate( 'Y' );
+ $timestamp = MWTimestamp::getInstance();
+ $year = $timestamp->format( 'Y' );
// If this month hasn't happened yet this year, go back to last year's month
- if( $this->mMonth > gmdate( 'n' ) ) {
+ if ( $this->mMonth > $timestamp->format( 'n' ) ) {
$year--;
}
}
@@ -867,7 +884,7 @@ abstract class ReverseChronologicalPager extends IndexPager {
if ( $this->mMonth ) {
$month = $this->mMonth + 1;
// For December, we want January 1 of the next year
- if ($month > 12) {
+ if ( $month > 12 ) {
$month = 1;
$year++;
}
@@ -906,7 +923,9 @@ abstract class TablePager extends IndexPager {
}
$this->mSort = $this->getRequest()->getText( 'sort' );
- if ( !array_key_exists( $this->mSort, $this->getFieldNames() ) ) {
+ if ( !array_key_exists( $this->mSort, $this->getFieldNames() )
+ || !$this->isFieldSortable( $this->mSort )
+ ) {
$this->mSort = $this->getDefaultSort();
}
if ( $this->getRequest()->getBool( 'asc' ) ) {
@@ -924,16 +943,15 @@ abstract class TablePager extends IndexPager {
*/
function getStartBody() {
global $wgStylePath;
- $tableClass = htmlspecialchars( $this->getTableClass() );
- $sortClass = htmlspecialchars( $this->getSortHeaderClass() );
+ $sortClass = $this->getSortHeaderClass();
- $s = "<table style='border:1px;' class=\"mw-datatable $tableClass\"><thead><tr>\n";
+ $s = '';
$fields = $this->getFieldNames();
# Make table header
foreach ( $fields as $field => $name ) {
if ( strval( $name ) == '' ) {
- $s .= "<th>&#160;</th>\n";
+ $s .= Html::rawElement( 'th', array(), '&#160;' ) . "\n";
} elseif ( $this->isFieldSortable( $field ) ) {
$query = array( 'sort' => $field, 'limit' => $this->mLimit );
if ( $field == $this->mSort ) {
@@ -952,20 +970,26 @@ abstract class TablePager extends IndexPager {
$query['desc'] = '1';
$alt = $this->msg( 'ascending_abbrev' )->escaped();
}
- $image = htmlspecialchars( "$wgStylePath/common/images/$image" );
+ $image = "$wgStylePath/common/images/$image";
$link = $this->makeLink(
- "<img width=\"12\" height=\"12\" alt=\"$alt\" src=\"$image\" />" .
- htmlspecialchars( $name ), $query );
- $s .= "<th class=\"$sortClass\">$link</th>\n";
+ Html::element( 'img', array( 'width' => 12, 'height' => 12,
+ 'alt' => $alt, 'src' => $image ) ) . htmlspecialchars( $name ), $query );
+ $s .= Html::rawElement( 'th', array( 'class' => $sortClass ), $link ) . "\n";
} else {
- $s .= '<th>' . $this->makeLink( htmlspecialchars( $name ), $query ) . "</th>\n";
+ $s .= Html::rawElement( 'th', array(),
+ $this->makeLink( htmlspecialchars( $name ), $query ) ) . "\n";
}
} else {
- $s .= '<th>' . htmlspecialchars( $name ) . "</th>\n";
+ $s .= Html::element( 'th', array(), $name ) . "\n";
}
}
- $s .= "</tr></thead><tbody>\n";
- return $s;
+
+ $tableClass = $this->getTableClass();
+ $ret = Html::openElement( 'table', array( 'style' => 'border:1px;', 'class' => "mw-datatable $tableClass" ) );
+ $ret .= Html::rawElement( 'thead', array(), Html::rawElement( 'tr', array(), "\n" . $s . "\n" ) );
+ $ret .= Html::openElement( 'tbody' ) . "\n";
+
+ return $ret;
}
/**
@@ -982,8 +1006,9 @@ abstract class TablePager extends IndexPager {
*/
function getEmptyBody() {
$colspan = count( $this->getFieldNames() );
- $msgEmpty = $this->msg( 'table_pager_empty' )->escaped();
- return "<tr><td colspan=\"$colspan\">$msgEmpty</td></tr>\n";
+ $msgEmpty = $this->msg( 'table_pager_empty' )->text();
+ return Html::rawElement( 'tr', array(),
+ Html::element( 'td', array( 'colspan' => $colspan ), $msgEmpty ) );
}
/**
@@ -993,7 +1018,7 @@ abstract class TablePager extends IndexPager {
*/
function formatRow( $row ) {
$this->mCurrentRow = $row; // In case formatValue etc need to know
- $s = Xml::openElement( 'tr', $this->getRowAttrs( $row ) );
+ $s = Html::openElement( 'tr', $this->getRowAttrs( $row ) ) . "\n";
$fieldNames = $this->getFieldNames();
foreach ( $fieldNames as $field => $name ) {
@@ -1004,10 +1029,10 @@ abstract class TablePager extends IndexPager {
$formatted = '&#160;';
}
- $s .= Xml::tags( 'td', $this->getCellAttrs( $field, $value ), $formatted );
+ $s .= Html::rawElement( 'td', $this->getCellAttrs( $field, $value ), $formatted ) . "\n";
}
- $s .= "</tr>\n";
+ $s .= Html::closeElement( 'tr' ) . "\n";
return $s;
}
@@ -1049,8 +1074,8 @@ abstract class TablePager extends IndexPager {
*
* @protected
*
- * @param $field String The column
- * @param $value String The cell contents
+ * @param string $field The column
+ * @param string $value The cell contents
* @return Array of attr => value
*/
function getCellAttrs( $field, $value ) {
@@ -1119,7 +1144,7 @@ abstract class TablePager extends IndexPager {
'next' => 'arrow_disabled_right_25.png',
'last' => 'arrow_disabled_last_25.png',
);
- if( $this->getLanguage()->isRTL() ) {
+ if ( $this->getLanguage()->isRTL() ) {
$keys = array_keys( $labels );
$images = array_combine( $keys, array_reverse( $images ) );
$disabledImages = array_combine( $keys, array_reverse( $disabledImages ) );
@@ -1129,49 +1154,67 @@ abstract class TablePager extends IndexPager {
$disabledTexts = array();
foreach ( $labels as $type => $label ) {
$msgLabel = $this->msg( $label )->escaped();
- $linkTexts[$type] = "<img src=\"$path/{$images[$type]}\" alt=\"$msgLabel\"/><br />$msgLabel";
- $disabledTexts[$type] = "<img src=\"$path/{$disabledImages[$type]}\" alt=\"$msgLabel\"/><br />$msgLabel";
+ $linkTexts[$type] = Html::element( 'img', array( 'src' => "$path/{$images[$type]}",
+ 'alt' => $msgLabel ) ) . "<br />$msgLabel";
+ $disabledTexts[$type] = Html::element( 'img', array( 'src' => "$path/{$disabledImages[$type]}",
+ 'alt' => $msgLabel ) ) . "<br />$msgLabel";
}
$links = $this->getPagingLinks( $linkTexts, $disabledTexts );
- $navClass = htmlspecialchars( $this->getNavClass() );
- $s = "<table class=\"$navClass\"><tr>\n";
+ $s = Html::openElement( 'table', array( 'class' => $this->getNavClass() ) );
+ $s .= Html::openElement( 'tr' ) . "\n";
$width = 100 / count( $links ) . '%';
foreach ( $labels as $type => $label ) {
- $s .= "<td style='width:$width;'>{$links[$type]}</td>\n";
+ $s .= Html::rawElement( 'td', array( 'style' => "width:$width;" ), $links[$type] ) . "\n";
}
- $s .= "</tr></table>\n";
+ $s .= Html::closeElement( 'tr' ) . Html::closeElement( 'table' ) . "\n";
return $s;
}
/**
* Get a "<select>" element which has options for each of the allowed limits
*
+ * @param $attribs String: Extra attributes to set
* @return String: HTML fragment
*/
- public function getLimitSelect() {
+ public function getLimitSelect( $attribs = array() ) {
+ $select = new XmlSelect( 'limit', false, $this->mLimit );
+ $select->addOptions( $this->getLimitSelectList() );
+ foreach ( $attribs as $name => $value ) {
+ $select->setAttribute( $name, $value );
+ }
+ return $select->getHTML();
+ }
+
+ /**
+ * Get a list of items to show in a "<select>" element of limits.
+ * This can be passed directly to XmlSelect::addOptions().
+ *
+ * @since 1.22
+ * @return array
+ */
+ public function getLimitSelectList() {
# Add the current limit from the query string
# to avoid that the limit is lost after clicking Go next time
if ( !in_array( $this->mLimit, $this->mLimitsShown ) ) {
$this->mLimitsShown[] = $this->mLimit;
sort( $this->mLimitsShown );
}
- $s = Html::openElement( 'select', array( 'name' => 'limit' ) ) . "\n";
+ $ret = array();
foreach ( $this->mLimitsShown as $key => $value ) {
# The pair is either $index => $limit, in which case the $value
# will be numeric, or $limit => $text, in which case the $value
# will be a string.
- if( is_int( $value ) ){
+ if ( is_int( $value ) ) {
$limit = $value;
$text = $this->getLanguage()->formatNum( $limit );
} else {
$limit = $key;
$text = $value;
}
- $s .= Xml::option( $text, $limit, $limit == $this->mLimit ) . "\n";
+ $ret[$text] = $limit;
}
- $s .= Html::closeElement( 'select' );
- return $s;
+ return $ret;
}
/**
@@ -1179,7 +1222,7 @@ abstract class TablePager extends IndexPager {
* Resubmits all defined elements of the query string, except for a
* blacklist, passed in the $blacklist parameter.
*
- * @param $blacklist Array parameters from the request query which should not be resubmitted
+ * @param array $blacklist parameters from the request query which should not be resubmitted
* @return String: HTML fragment
*/
function getHiddenFields( $blacklist = array() ) {
@@ -1190,9 +1233,7 @@ abstract class TablePager extends IndexPager {
}
$s = '';
foreach ( $query as $name => $value ) {
- $encName = htmlspecialchars( $name );
- $encValue = htmlspecialchars( $value );
- $s .= "<input type=\"hidden\" name=\"$encName\" value=\"$encValue\"/>\n";
+ $s .= Html::hidden( $name, $value ) . "\n";
}
return $s;
}
@@ -1205,12 +1246,14 @@ abstract class TablePager extends IndexPager {
function getLimitForm() {
global $wgScript;
- return Xml::openElement(
+ return Html::rawElement(
'form',
array(
'method' => 'get',
'action' => $wgScript
- ) ) . "\n" . $this->getLimitDropdown() . "</form>\n";
+ ),
+ "\n" . $this->getLimitDropdown()
+ ) . "\n";
}
/**
@@ -1245,8 +1288,8 @@ abstract class TablePager extends IndexPager {
*
* @protected
*
- * @param $name String: the database field name
- * @param $value String: the value retrieved from the database
+ * @param string $name the database field name
+ * @param string $value the value retrieved from the database
*/
abstract function formatValue( $name, $value );
diff --git a/includes/PathRouter.php b/includes/PathRouter.php
index 2dbc7ec0..435e09ef 100644
--- a/includes/PathRouter.php
+++ b/includes/PathRouter.php
@@ -141,10 +141,10 @@ class PathRouter {
}
$pattern = (object)array(
- 'path' => $path,
- 'params' => $params,
+ 'path' => $path,
+ 'params' => $params,
'options' => $options,
- 'key' => $key,
+ 'key' => $key,
);
$pattern->weight = self::makeWeight( $pattern );
$this->patterns[] = $pattern;
@@ -153,9 +153,9 @@ class PathRouter {
/**
* Add a new path pattern to the path router
*
- * @param $path string|array The path pattern to add
- * @param $params array The params for this path pattern
- * @param $options array The options for this path pattern
+ * @param string|array $path The path pattern to add
+ * @param array $params The params for this path pattern
+ * @param array $options The options for this path pattern
*/
public function add( $path, $params = array(), $options = array() ) {
if ( is_array( $path ) ) {
@@ -185,7 +185,7 @@ class PathRouter {
*/
protected function sortByWeight() {
$weights = array();
- foreach( $this->patterns as $key => $pattern ) {
+ foreach ( $this->patterns as $key => $pattern ) {
$weights[$key] = $pattern->weight;
}
array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
@@ -203,7 +203,7 @@ class PathRouter {
$path = explode( '/', $pattern->path );
# For each level of the path
- foreach( $path as $piece ) {
+ foreach ( $path as $piece ) {
if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
# For a piece that is only a $1 variable add 1 points of weight
$weight += 1;
@@ -232,7 +232,7 @@ class PathRouter {
/**
* Parse a path and return the query matches for the path
*
- * @param $path string The path to parse
+ * @param string $path The path to parse
* @return Array The array of matches for the path
*/
public function parse( $path ) {
@@ -293,7 +293,7 @@ class PathRouter {
foreach ( $m as $matchKey => $matchValue ) {
if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
$n = intval( substr( $matchKey, 3 ) );
- $data['$'.$n] = rawurldecode( $matchValue );
+ $data['$' . $n] = rawurldecode( $matchValue );
}
}
// If present give our $data array a $key as well
diff --git a/includes/PoolCounter.php b/includes/PoolCounter.php
index 452dbc54..2dac9388 100644
--- a/includes/PoolCounter.php
+++ b/includes/PoolCounter.php
@@ -22,69 +22,55 @@
*/
/**
- * When you have many workers (threads/servers) giving service, and a
+ * When you have many workers (threads/servers) giving service, and a
* cached item expensive to produce expires, you may get several workers
* doing the job at the same time.
*
- * Given enough requests and the item expiring fast (non-cacheable,
+ * Given enough requests and the item expiring fast (non-cacheable,
* lots of edits...) that single work can end up unfairly using most (all)
* of the cpu of the pool. This is also known as 'Michael Jackson effect'
* since this effect triggered on the english wikipedia on the day Michael
* Jackson died, the biographical article got hit with several edits per
* minutes and hundreds of read hits.
*
- * The PoolCounter provides semaphore semantics for restricting the number
+ * The PoolCounter provides semaphore semantics for restricting the number
* of workers that may be concurrently performing such single task.
*
- * By default PoolCounter_Stub is used, which provides no locking. You
+ * By default PoolCounter_Stub is used, which provides no locking. You
* can get a useful one in the PoolCounter extension.
*/
abstract class PoolCounter {
-
/* Return codes */
- const LOCKED = 1; /* Lock acquired */
+ const LOCKED = 1; /* Lock acquired */
const RELEASED = 2; /* Lock released */
- const DONE = 3; /* Another worker did the work for you */
+ const DONE = 3; /* Another worker did the work for you */
- const ERROR = -1; /* Indeterminate error */
+ const ERROR = -1; /* Indeterminate error */
const NOT_LOCKED = -2; /* Called release() with no lock held */
const QUEUE_FULL = -3; /* There are already maxqueue workers on this lock */
- const TIMEOUT = -4; /* Timeout exceeded */
- const LOCK_HELD = -5; /* Cannot acquire another lock while you have one lock held */
+ const TIMEOUT = -4; /* Timeout exceeded */
+ const LOCK_HELD = -5; /* Cannot acquire another lock while you have one lock held */
- /**
- * I want to do this task and I need to do it myself.
- *
- * @return Locked/Error
- */
- abstract function acquireForMe();
+ /** @var string All workers with the same key share the lock */
+ protected $key;
+ /** @var integer Maximum number of workers doing the task simultaneously */
+ protected $workers;
+ /** @var integer If this number of workers are already working/waiting, fail instead of wait */
+ protected $maxqueue;
+ /** @var float Maximum time in seconds to wait for the lock */
+ protected $timeout;
/**
- * I want to do this task, but if anyone else does it
- * instead, it's also fine for me. I will read its cached data.
- *
- * @return Locked/Done/Error
+ * @param array $conf
+ * @param string $type
+ * @param string $key
*/
- abstract function acquireForAnyone();
-
- /**
- * I have successfully finished my task.
- * Lets another one grab the lock, and returns the workers
- * waiting on acquireForAnyone()
- *
- * @return Released/NotLocked/Error
- */
- abstract function release();
-
- /**
- * $key: All workers with the same key share the lock.
- * $workers: It wouldn't be a good idea to have more than this number of
- * workers doing the task simultaneously.
- * $maxqueue: If this number of workers are already working/waiting,
- * fail instead of wait.
- * $timeout: Maximum time in seconds to wait for the lock.
- */
- protected $key, $workers, $maxqueue, $timeout;
+ protected function __construct( $conf, $type, $key ) {
+ $this->key = $key;
+ $this->workers = $conf['workers'];
+ $this->maxqueue = $conf['maxqueue'];
+ $this->timeout = $conf['timeout'];
+ }
/**
* Create a Pool counter. This should only be called from the PoolWorks.
@@ -105,58 +91,74 @@ abstract class PoolCounter {
return new $class( $conf, $type, $key );
}
- protected function __construct( $conf, $type, $key ) {
- $this->key = $key;
- $this->workers = $conf['workers'];
- $this->maxqueue = $conf['maxqueue'];
- $this->timeout = $conf['timeout'];
- }
-}
-
-class PoolCounter_Stub extends PoolCounter {
-
/**
- * @return Status
+ * I want to do this task and I need to do it myself.
+ *
+ * @return Status Value is one of Locked/Error
*/
- function acquireForMe() {
- return Status::newGood( PoolCounter::LOCKED );
- }
+ abstract public function acquireForMe();
/**
- * @return Status
+ * I want to do this task, but if anyone else does it
+ * instead, it's also fine for me. I will read its cached data.
+ *
+ * @return Status Value is one of Locked/Done/Error
*/
- function acquireForAnyone() {
- return Status::newGood( PoolCounter::LOCKED );
- }
+ abstract public function acquireForAnyone();
/**
- * @return Status
+ * I have successfully finished my task.
+ * Lets another one grab the lock, and returns the workers
+ * waiting on acquireForAnyone()
+ *
+ * @return Status value is one of Released/NotLocked/Error
*/
- function release() {
- return Status::newGood( PoolCounter::RELEASED );
- }
+ abstract public function release();
+}
+class PoolCounter_Stub extends PoolCounter {
public function __construct() {
/* No parameters needed */
}
+
+ public function acquireForMe() {
+ return Status::newGood( PoolCounter::LOCKED );
+ }
+
+ public function acquireForAnyone() {
+ return Status::newGood( PoolCounter::LOCKED );
+ }
+
+ public function release() {
+ return Status::newGood( PoolCounter::RELEASED );
+ }
}
/**
- * Handy class for dealing with PoolCounters using class members instead of callbacks.
+ * Class for dealing with PoolCounters using class members
*/
abstract class PoolCounterWork {
protected $cacheable = false; //Does this override getCachedWork() ?
/**
- * Actually perform the work, caching it if needed.
+ * @param string $type The type of PoolCounter to use
+ * @param string $key Key that identifies the queue this work is placed on
*/
- abstract function doWork();
+ public function __construct( $type, $key ) {
+ $this->poolCounter = PoolCounter::factory( $type, $key );
+ }
+
+ /**
+ * Actually perform the work, caching it if needed
+ * @return mixed work result or false
+ */
+ abstract public function doWork();
/**
* Retrieve the work from cache
* @return mixed work result or false
*/
- function getCachedWork() {
+ public function getCachedWork() {
return false;
}
@@ -165,7 +167,7 @@ abstract class PoolCounterWork {
* message.
* @return mixed work result or false
*/
- function fallback() {
+ public function fallback() {
return false;
}
@@ -181,17 +183,28 @@ abstract class PoolCounterWork {
* Log an error
*
* @param $status Status
+ * @return void
*/
function logError( $status ) {
wfDebugLog( 'poolcounter', $status->getWikiText() );
}
/**
- * Get the result of the work (whatever it is), or false.
+ * Get the result of the work (whatever it is), or the result of the error() function.
+ * This returns the result of the first applicable method that returns a non-false value,
+ * where the methods are checked in the following order:
+ * - a) doWork() : Applies if the work is exclusive or no another process
+ * is doing it, and on the condition that either this process
+ * successfully entered the pool or the pool counter is down.
+ * - b) doCachedWork() : Applies if the work is cacheable and this blocked on another
+ * process which finished the work.
+ * - c) fallback() : Applies for all remaining cases.
+ * If these all fall through (by returning false), then the result of error() is returned.
+ *
* @param $skipcache bool
- * @return bool|mixed
+ * @return mixed
*/
- function execute( $skipcache = false ) {
+ public function execute( $skipcache = false ) {
if ( $this->cacheable && !$skipcache ) {
$status = $this->poolCounter->acquireForAnyone();
} else {
@@ -232,15 +245,85 @@ abstract class PoolCounterWork {
/* These two cases should never be hit... */
case PoolCounter::ERROR:
default:
- $errors = array( PoolCounter::QUEUE_FULL => 'pool-queuefull', PoolCounter::TIMEOUT => 'pool-timeout' );
+ $errors = array(
+ PoolCounter::QUEUE_FULL => 'pool-queuefull',
+ PoolCounter::TIMEOUT => 'pool-timeout' );
- $status = Status::newFatal( isset( $errors[$status->value] ) ? $errors[$status->value] : 'pool-errorunknown' );
+ $status = Status::newFatal( isset( $errors[$status->value] )
+ ? $errors[$status->value]
+ : 'pool-errorunknown' );
$this->logError( $status );
return $this->error( $status );
}
}
+}
- function __construct( $type, $key ) {
- $this->poolCounter = PoolCounter::factory( $type, $key );
+/**
+ * Convenience class for dealing with PoolCounters using callbacks
+ * @since 1.22
+ */
+class PoolCounterWorkViaCallback extends PoolCounterWork {
+ /** @var callable */
+ protected $doWork;
+ /** @var callable|null */
+ protected $doCachedWork;
+ /** @var callable|null */
+ protected $fallback;
+ /** @var callable|null */
+ protected $error;
+
+ /**
+ * Build a PoolCounterWork class from a type, key, and callback map.
+ *
+ * The callback map must at least have a callback for the 'doWork' method.
+ * Additionally, callbacks can be provided for the 'doCachedWork', 'fallback',
+ * and 'error' methods. Methods without callbacks will be no-ops that return false.
+ * If a 'doCachedWork' callback is provided, then execute() may wait for any prior
+ * process in the pool to finish and reuse its cached result.
+ *
+ * @param string $type
+ * @param string $key
+ * @param array $callbacks Map of callbacks
+ * @throws MWException
+ */
+ public function __construct( $type, $key, array $callbacks ) {
+ parent::__construct( $type, $key );
+ foreach ( array( 'doWork', 'doCachedWork', 'fallback', 'error' ) as $name ) {
+ if ( isset( $callbacks[$name] ) ) {
+ if ( !is_callable( $callbacks[$name] ) ) {
+ throw new MWException( "Invalid callback provided for '$name' function." );
+ }
+ $this->$name = $callbacks[$name];
+ }
+ }
+ if ( !isset( $this->doWork ) ) {
+ throw new MWException( "No callback provided for 'doWork' function." );
+ }
+ $this->cacheable = isset( $this->doCachedWork );
+ }
+
+ public function doWork() {
+ return call_user_func_array( $this->doWork, array() );
+ }
+
+ public function getCachedWork() {
+ if ( $this->doCachedWork ) {
+ return call_user_func_array( $this->doCachedWork, array() );
+ }
+ return false;
+ }
+
+ function fallback() {
+ if ( $this->fallback ) {
+ return call_user_func_array( $this->fallback, array() );
+ }
+ return false;
+ }
+
+ function error( $status ) {
+ if ( $this->error ) {
+ return call_user_func_array( $this->error, array( $status ) );
+ }
+ return false;
}
}
diff --git a/includes/Preferences.php b/includes/Preferences.php
index db231573..c9caf4f7 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -1,6 +1,6 @@
<?php
/**
- * Form to edit user perferences.
+ * Form to edit user preferences.
*
* 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
@@ -56,6 +56,12 @@ class Preferences {
'searchlimit' => array( 'Preferences', 'filterIntval' ),
);
+ // Stuff that shouldn't be saved as a preference.
+ private static $saveBlacklist = array(
+ 'realname',
+ 'emailaddress',
+ );
+
/**
* @throws MWException
* @param $user User
@@ -90,10 +96,19 @@ class Preferences {
}
}
+ ## Make sure that form fields have their parent set. See bug 41337.
+ $dummyForm = new HTMLForm( array(), $context );
+
+ $disable = !$user->isAllowed( 'editmyoptions' );
+
## Prod in defaults from the user
foreach ( $defaultPreferences as $name => &$info ) {
$prefFromUser = self::getOptionFromUser( $name, $info, $user );
+ if ( $disable && !in_array( $name, self::$saveBlacklist ) ) {
+ $info['disabled'] = 'disabled';
+ }
$field = HTMLForm::loadInputFromParameters( $name, $info ); // For validation
+ $field->mParent = $dummyForm;
$defaultOptions = User::getDefaultOptions();
$globalDefault = isset( $defaultOptions[$name] )
? $defaultOptions[$name]
@@ -129,7 +144,7 @@ class Preferences {
static function getOptionFromUser( $name, $info, $user ) {
$val = $user->getOption( $name );
- // Handling for array-type preferences
+ // Handling for multiselect preferences
if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
$options = HTMLFormField::flattenOptions( $info['options'] );
@@ -143,6 +158,23 @@ class Preferences {
}
}
+ // Handling for checkmatrix preferences
+ if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
+ ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
+ $columns = HTMLFormField::flattenOptions( $info['columns'] );
+ $rows = HTMLFormField::flattenOptions( $info['rows'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+ $val = array();
+
+ foreach ( $columns as $column ) {
+ foreach ( $rows as $row ) {
+ if ( $user->getOption( "$prefix$column-$row" ) ) {
+ $val[] = "$column-$row";
+ }
+ }
+ }
+ }
+
return $val;
}
@@ -156,20 +188,24 @@ class Preferences {
global $wgAuth, $wgContLang, $wgParser, $wgCookieExpiration, $wgLanguageCode,
$wgDisableTitleConversion, $wgDisableLangConversion, $wgMaxSigChars,
$wgEnableEmail, $wgEmailConfirmToEdit, $wgEnableUserEmail, $wgEmailAuthentication,
- $wgEnotifWatchlist, $wgEnotifUserTalk, $wgEnotifRevealEditorAddress;
+ $wgEnotifWatchlist, $wgEnotifUserTalk, $wgEnotifRevealEditorAddress,
+ $wgSecureLogin;
+
+ // retrieving user name for GENDER and misc.
+ $userName = $user->getName();
## User info #####################################
// Information panel
$defaultPreferences['username'] = array(
'type' => 'info',
- 'label-message' => 'username',
- 'default' => $user->getName(),
+ 'label-message' => array( 'username', $userName ),
+ 'default' => $userName,
'section' => 'personal/info',
);
$defaultPreferences['userid'] = array(
'type' => 'info',
- 'label-message' => 'uid',
+ 'label-message' => array( 'uid', $userName ),
'default' => $user->getId(),
'section' => 'personal/info',
);
@@ -182,10 +218,10 @@ class Preferences {
// Skip the default * group, seems useless here
continue;
}
- $groupName = User::getGroupName( $ueg );
+ $groupName = User::getGroupName( $ueg );
$userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName );
- $memberName = User::getGroupMember( $ueg, $user->getName() );
+ $memberName = User::getGroupMember( $ueg, $userName );
$userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName );
}
asort( $userGroups );
@@ -196,7 +232,7 @@ class Preferences {
$defaultPreferences['usergroups'] = array(
'type' => 'info',
'label' => $context->msg( 'prefs-memberingroups' )->numParams(
- count( $userGroups ) )->parse(),
+ count( $userGroups ) )->params( $userName )->parse(),
'default' => $context->msg( 'prefs-memberingroups-type',
$lang->commaList( $userGroups ),
$lang->commaList( $userMembers )
@@ -205,10 +241,14 @@ class Preferences {
'section' => 'personal/info',
);
+ $editCount = Linker::link( SpecialPage::getTitleFor( "Contributions", $userName ),
+ $lang->formatNum( $user->getEditCount() ) );
+
$defaultPreferences['editcount'] = array(
'type' => 'info',
+ 'raw' => true,
'label-message' => 'prefs-edits',
- 'default' => $lang->formatNum( $user->getEditCount() ),
+ 'default' => $editCount,
'section' => 'personal/info',
);
@@ -228,31 +268,23 @@ class Preferences {
);
}
+ $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
+ $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
+
// Actually changeable stuff
$defaultPreferences['realname'] = array(
- 'type' => $wgAuth->allowPropChange( 'realname' ) ? 'text' : 'info',
+ // (not really "private", but still shouldn't be edited without permission)
+ 'type' => $canEditPrivateInfo && $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(
- $context->msg( 'gender-male' )->text() => 'male',
- $context->msg( 'gender-female' )->text() => 'female',
- $context->msg( 'gender-unknown' )->text() => 'unknown',
- ),
- 'label-message' => 'yourgender',
- 'help-message' => 'prefs-help-gender',
- );
-
- if ( $wgAuth->allowPasswordChange() ) {
+ if ( $canEditPrivateInfo && $wgAuth->allowPasswordChange() ) {
$link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ),
$context->msg( 'prefs-resetpass' )->escaped(), array(),
- array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' ) ) );
+ array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) );
$defaultPreferences['password'] = array(
'type' => 'info',
@@ -270,6 +302,15 @@ class Preferences {
'section' => 'personal/info',
);
}
+ // Only show preferhttps if secure login is turned on
+ if ( $wgSecureLogin && wfCanIPUseHTTPS( $context->getRequest()->getIP() ) ) {
+ $defaultPreferences['prefershttps'] = array(
+ 'type' => 'toggle',
+ 'label-message' => 'tog-prefershttps',
+ 'help-message' => 'prefs-help-prefershttps',
+ 'section' => 'personal/info'
+ );
+ }
// Language
$languages = Language::fetchLanguageNames( null, 'mw' );
@@ -290,39 +331,74 @@ class Preferences {
'label-message' => 'yourlanguage',
);
- /* see if there are multiple language variants to choose from*/
- $variantArray = array();
+ $defaultPreferences['gender'] = array(
+ 'type' => 'radio',
+ 'section' => 'personal/i18n',
+ 'options' => array(
+ $context->msg( 'parentheses',
+ $context->msg( 'gender-unknown' )->text()
+ )->text() => 'unknown',
+ $context->msg( 'gender-female' )->text() => 'female',
+ $context->msg( 'gender-male' )->text() => 'male',
+ ),
+ 'label-message' => 'yourgender',
+ 'help-message' => 'prefs-help-gender',
+ );
+
+ // see if there are multiple language variants to choose from
if ( !$wgDisableLangConversion ) {
- $variants = $wgContLang->getVariants();
+ foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
+ if ( $langCode == $wgContLang->getCode() ) {
+ $variants = $wgContLang->getVariants();
- foreach ( $variants as $v ) {
- $v = str_replace( '_', '-', strtolower( $v ) );
- $variantArray[$v] = $wgContLang->getVariantname( $v, false );
- }
+ if ( count( $variants ) <= 1 ) {
+ continue;
+ }
- $options = array();
- foreach ( $variantArray as $code => $name ) {
- $display = wfBCP47( $code ) . ' - ' . $name;
- $options[$display] = $code;
- }
+ $variantArray = array();
+ foreach ( $variants as $v ) {
+ $v = str_replace( '_', '-', strtolower( $v ) );
+ $variantArray[$v] = $lang->getVariantname( $v, false );
+ }
- if ( count( $variantArray ) > 1 ) {
- $defaultPreferences['variant'] = array(
- 'label-message' => 'yourvariant',
- 'type' => 'select',
- 'options' => $options,
- 'section' => 'personal/i18n',
- 'help-message' => 'prefs-help-variant',
- );
+ $options = array();
+ foreach ( $variantArray as $code => $name ) {
+ $display = wfBCP47( $code ) . ' - ' . $name;
+ $options[$display] = $code;
+ }
+
+ $defaultPreferences['variant'] = array(
+ 'label-message' => 'yourvariant',
+ 'type' => 'select',
+ 'options' => $options,
+ 'section' => 'personal/i18n',
+ 'help-message' => 'prefs-help-variant',
+ );
+
+ if ( !$wgDisableTitleConversion ) {
+ $defaultPreferences['noconvertlink'] = array(
+ 'type' => 'toggle',
+ 'section' => 'personal/i18n',
+ 'label-message' => 'tog-noconvertlink',
+ );
+ }
+ } else {
+ $defaultPreferences["variant-$langCode"] = array(
+ 'type' => 'api',
+ );
+ }
}
}
- if ( count( $variantArray ) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion ) {
- $defaultPreferences['noconvertlink'] =
- array(
+ // Stuff from Language::getExtraUserToggles()
+ // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
+ $toggles = $wgContLang->getExtraUserToggles();
+
+ foreach ( $toggles as $toggle ) {
+ $defaultPreferences[$toggle] = array(
'type' => 'toggle',
'section' => 'personal/i18n',
- 'label-message' => 'tog-noconvertlink',
+ 'label-message' => "tog-$toggle",
);
}
@@ -354,44 +430,45 @@ class Preferences {
## Email stuff
if ( $wgEnableEmail ) {
- $helpMessages[] = $wgEmailConfirmToEdit
- ? 'prefs-help-email-required'
- : 'prefs-help-email' ;
+ if ( $canViewPrivateInfo ) {
+ $helpMessages[] = $wgEmailConfirmToEdit
+ ? 'prefs-help-email-required'
+ : 'prefs-help-email';
+
+ if ( $wgEnableUserEmail ) {
+ // additional messages when users can send email to each other
+ $helpMessages[] = 'prefs-help-email-others';
+ }
- if( $wgEnableUserEmail ) {
- // additional messages when users can send email to each other
- $helpMessages[] = 'prefs-help-email-others';
- }
+ $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
+ if ( $canEditPrivateInfo && $wgAuth->allowPropChange( 'emailaddress' ) ) {
+ $link = Linker::link(
+ SpecialPage::getTitleFor( 'ChangeEmail' ),
+ $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
+ array(),
+ array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) );
+
+ $emailAddress .= $emailAddress == '' ? $link : (
+ $context->msg( 'word-separator' )->plain()
+ . $context->msg( 'parentheses' )->rawParams( $link )->plain()
+ );
+ }
- $link = Linker::link(
- SpecialPage::getTitleFor( 'ChangeEmail' ),
- $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
- array(),
- array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' ) ) );
-
- $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
- if ( $wgAuth->allowPropChange( 'emailaddress' ) ) {
- $emailAddress .= $emailAddress == '' ? $link : (
- $context->msg( 'word-separator' )->plain()
- . $context->msg( 'parentheses' )->rawParams( $link )->plain()
+ $defaultPreferences['emailaddress'] = array(
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $emailAddress,
+ 'label-message' => 'youremail',
+ 'section' => 'personal/email',
+ 'help-messages' => $helpMessages,
+ # 'cssclass' chosen below
);
}
-
- $defaultPreferences['emailaddress'] = array(
- 'type' => 'info',
- 'raw' => true,
- 'default' => $emailAddress,
- 'label-message' => 'youremail',
- 'section' => 'personal/email',
- 'help-messages' => $helpMessages,
- # 'cssclass' chosen below
- );
-
$disableEmailPrefs = false;
- $emailauthenticationclass = 'mw-email-not-authenticated';
if ( $wgEmailAuthentication ) {
+ $emailauthenticationclass = 'mw-email-not-authenticated';
if ( $user->getEmail() ) {
if ( $user->getEmailAuthenticationTimestamp() ) {
// date and time are separate parameters to facilitate localisation.
@@ -413,7 +490,7 @@ class Preferences {
SpecialPage::getTitleFor( 'Confirmemail' ),
$context->msg( 'emailconfirmlink' )->escaped()
) . '<br />';
- $emailauthenticationclass="mw-email-not-authenticated";
+ $emailauthenticationclass = "mw-email-not-authenticated";
}
} else {
$disableEmailPrefs = true;
@@ -421,17 +498,19 @@ class Preferences {
$emailauthenticationclass = 'mw-email-none';
}
- $defaultPreferences['emailauthentication'] = array(
- 'type' => 'info',
- 'raw' => true,
- 'section' => 'personal/email',
- 'label-message' => 'prefs-emailconfirm-label',
- 'default' => $emailauthenticated,
- # Apply the same CSS class used on the input to the message:
- 'cssclass' => $emailauthenticationclass,
- );
+ if ( $canViewPrivateInfo ) {
+ $defaultPreferences['emailauthentication'] = array(
+ 'type' => 'info',
+ 'raw' => true,
+ 'section' => 'personal/email',
+ 'label-message' => 'prefs-emailconfirm-label',
+ 'default' => $emailauthenticated,
+ # Apply the same CSS class used on the input to the message:
+ 'cssclass' => $emailauthenticationclass,
+ );
+ $defaultPreferences['emailaddress']['cssclass'] = $emailauthenticationclass;
+ }
}
- $defaultPreferences['emailaddress']['cssclass'] = $emailauthenticationclass;
if ( $wgEnableUserEmail && $user->isAllowed( 'sendemail' ) ) {
$defaultPreferences['disablemail'] = array(
@@ -507,14 +586,15 @@ class Preferences {
# be nice to somehow merge this back in there to avoid redundancy.
if ( $wgAllowUserCss || $wgAllowUserJs ) {
$linkTools = array();
+ $userName = $user->getName();
if ( $wgAllowUserCss ) {
- $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/common.css' );
+ $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
$linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() );
}
if ( $wgAllowUserJs ) {
- $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/common.js' );
+ $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
$linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() );
}
@@ -526,18 +606,6 @@ class Preferences {
'section' => 'rendering/skin',
);
}
-
- $selectedSkin = $user->getOption( 'skin' );
- if ( in_array( $selectedSkin, array( 'cologneblue', 'standard' ) ) ) {
- $settings = array_flip( $context->getLanguage()->getQuickbarSettings() );
-
- $defaultPreferences['quickbar'] = array(
- 'type' => 'radio',
- 'options' => $settings,
- 'section' => 'rendering/skin',
- 'label-message' => 'qbsettings',
- );
- }
}
/**
@@ -640,6 +708,18 @@ class Preferences {
* @param $defaultPreferences Array
*/
static function renderingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
+ ## Diffs ####################################
+ $defaultPreferences['diffonly'] = array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/diffs',
+ 'label-message' => 'tog-diffonly',
+ );
+ $defaultPreferences['norollbackdiff'] = array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/diffs',
+ 'label-message' => 'tog-norollbackdiff',
+ );
+
## Page Rendering ##############################
global $wgAllowUserCssPrefs;
if ( $wgAllowUserCssPrefs ) {
@@ -666,7 +746,7 @@ class Preferences {
'section' => 'rendering/advancedrendering',
'options' => $stubThresholdOptions,
'size' => 20,
- 'label' => $context->msg( 'stub-threshold' )->text(), // Raw HTML message. Yay?
+ 'label-raw' => $context->msg( 'stub-threshold' )->text(), // Raw HTML message. Yay?
);
if ( $wgAllowUserCssPrefs ) {
@@ -686,11 +766,6 @@ class Preferences {
'section' => 'rendering/advancedrendering',
'label-message' => 'tog-showhiddencats'
);
- $defaultPreferences['showjumplinks'] = array(
- 'type' => 'toggle',
- 'section' => 'rendering/advancedrendering',
- 'label-message' => 'tog-showjumplinks',
- );
if ( $wgAllowUserCssPrefs ) {
$defaultPreferences['justify'] = array(
@@ -713,28 +788,31 @@ class Preferences {
* @param $defaultPreferences Array
*/
static function editingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- global $wgUseExternalEditor, $wgAllowUserCssPrefs;
+ global $wgAllowUserCssPrefs;
## Editing #####################################
- $defaultPreferences['cols'] = array(
- 'type' => 'int',
- 'label-message' => 'columns',
- 'section' => 'editing/textboxsize',
- 'min' => 4,
- 'max' => 1000,
+ if ( $wgAllowUserCssPrefs ) {
+ $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['rows'] = array(
- 'type' => 'int',
- 'label-message' => 'rows',
- 'section' => 'editing/textboxsize',
- 'min' => 4,
- 'max' => 1000,
+ $defaultPreferences['editondblclick'] = array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editondblclick',
);
if ( $wgAllowUserCssPrefs ) {
$defaultPreferences['editfont'] = array(
'type' => 'select',
- 'section' => 'editing/advancedediting',
+ 'section' => 'editing/editor',
'label-message' => 'editfont-style',
'options' => array(
$context->msg( 'editfont-default' )->text() => 'default',
@@ -744,73 +822,59 @@ class Preferences {
)
);
}
- $defaultPreferences['previewontop'] = array(
- 'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-previewontop',
+ $defaultPreferences['cols'] = array(
+ 'type' => 'int',
+ 'label-message' => 'columns',
+ 'section' => 'editing/editor',
+ 'min' => 4,
+ 'max' => 1000,
);
- $defaultPreferences['previewonfirst'] = array(
- 'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-previewonfirst',
+ $defaultPreferences['rows'] = array(
+ 'type' => 'int',
+ 'label-message' => 'rows',
+ 'section' => 'editing/editor',
+ 'min' => 4,
+ 'max' => 1000,
);
-
- if ( $wgAllowUserCssPrefs ) {
- $defaultPreferences['editsection'] = array(
+ if ( $user->isAllowed( 'minoredit' ) ) {
+ $defaultPreferences['minordefault'] = array(
'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-editsection',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-minordefault',
);
}
- $defaultPreferences['editsectiononrightclick'] = array(
+ $defaultPreferences['forceeditsummary'] = array(
'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-editsectiononrightclick',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-forceeditsummary',
);
- $defaultPreferences['editondblclick'] = array(
+ $defaultPreferences['useeditwarning'] = array(
'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-editondblclick',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-useeditwarning',
);
$defaultPreferences['showtoolbar'] = array(
'type' => 'toggle',
- 'section' => 'editing/advancedediting',
+ 'section' => 'editing/editor',
'label-message' => 'tog-showtoolbar',
);
- if ( $user->isAllowed( 'minoredit' ) ) {
- $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(
+ $defaultPreferences['previewonfirst'] = array(
'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-forceeditsummary',
+ 'section' => 'editing/preview',
+ 'label-message' => 'tog-previewonfirst',
+ );
+ $defaultPreferences['previewontop'] = array(
+ 'type' => 'toggle',
+ 'section' => 'editing/preview',
+ 'label-message' => 'tog-previewontop',
);
-
-
$defaultPreferences['uselivepreview'] = array(
'type' => 'toggle',
- 'section' => 'editing/advancedediting',
+ 'section' => 'editing/preview',
'label-message' => 'tog-uselivepreview',
);
+
}
/**
@@ -879,7 +943,7 @@ class Preferences {
global $wgUseRCPatrol, $wgEnableAPI, $wgRCMaxAge;
$watchlistdaysMax = ceil( $wgRCMaxAge / ( 3600 * 24 ) );
-
+
## Watchlist #####################################
$defaultPreferences['watchlistdays'] = array(
'type' => 'float',
@@ -887,7 +951,7 @@ class Preferences {
'max' => $watchlistdaysMax,
'section' => 'watchlist/displaywatchlist',
'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
- $watchlistdaysMax )->text(),
+ $watchlistdaysMax )->text(),
'label-message' => 'prefs-watchlist-days',
);
$defaultPreferences['wllimit'] = array(
@@ -937,19 +1001,6 @@ class Preferences {
);
}
- if ( $wgEnableAPI ) {
- # Some random gibberish as a proposed default
- // @todo Fixme: this should use CryptRand but we may not want to read urandom on every view
- $hash = sha1( mt_rand() . microtime( true ) );
-
- $defaultPreferences['watchlisttoken'] = array(
- 'type' => 'text',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'prefs-watchlist-token',
- 'help' => $context->msg( 'prefs-help-watchlist-token', $hash )->escaped()
- );
- }
-
$watchTypes = array(
'edit' => 'watchdefault',
'move' => 'watchmoves',
@@ -963,6 +1014,8 @@ class Preferences {
foreach ( $watchTypes as $action => $pref ) {
if ( $user->isAllowed( $action ) ) {
+ // Messages:
+ // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations
$defaultPreferences[$pref] = array(
'type' => 'toggle',
'section' => 'watchlist/advancedwatchlist',
@@ -970,6 +1023,19 @@ class Preferences {
);
}
}
+
+ if ( $wgEnableAPI ) {
+ $defaultPreferences['watchlisttoken'] = array(
+ 'type' => 'api',
+ );
+ $defaultPreferences['watchlisttoken-info'] = array(
+ 'type' => 'info',
+ 'section' => 'watchlist/tokenwatchlist',
+ 'label-message' => 'prefs-watchlist-token',
+ 'default' => $user->getTokenFromOption( 'watchlisttoken' ),
+ 'help-message' => 'prefs-help-watchlist-token2',
+ );
+ }
}
/**
@@ -988,7 +1054,6 @@ class Preferences {
'min' => 0,
);
-
if ( $wgVectorUseSimpleSearch ) {
$defaultPreferences['vector-simplesearch'] = array(
'type' => 'toggle',
@@ -1009,62 +1074,27 @@ class Preferences {
'section' => 'searchoptions/advancedsearchoptions',
);
- $nsOptions = array();
-
- foreach ( $wgContLang->getNamespaces() as $ns => $name ) {
+ $nsOptions = $wgContLang->getFormattedNamespaces();
+ $nsOptions[0] = $context->msg( 'blanknamespace' )->text();
+ foreach ( $nsOptions as $ns => $name ) {
if ( $ns < 0 ) {
- continue;
- }
-
- $displayNs = str_replace( '_', ' ', $name );
-
- if ( !$displayNs ) {
- $displayNs = $context->msg( 'blanknamespace' )->text();
+ unset( $nsOptions[$ns] );
}
-
- $displayNs = htmlspecialchars( $displayNs );
- $nsOptions[$displayNs] = $ns;
}
$defaultPreferences['searchnamespaces'] = array(
'type' => 'multiselect',
'label-message' => 'defaultns',
- 'options' => $nsOptions,
+ 'options' => array_flip( $nsOptions ),
'section' => 'searchoptions/advancedsearchoptions',
'prefix' => 'searchNs',
);
}
/**
- * @param $user User
- * @param $context IContextSource
- * @param $defaultPreferences Array
+ * Dummy, kept for backwards-compatibility.
*/
static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- global $wgContLang;
-
- ## 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()
- $toggles = $wgContLang->getExtraUserToggles();
-
- foreach ( $toggles as $toggle ) {
- $defaultPreferences[$toggle] = array(
- 'type' => 'toggle',
- 'section' => 'personal/i18n',
- 'label-message' => "tog-$toggle",
- );
- }
}
/**
@@ -1102,7 +1132,7 @@ class Preferences {
}
# Create preview link
- $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) );
+ $mplink = htmlspecialchars( $mptitle->getLocalURL( array( 'useskin' => $skinkey ) ) );
$linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
# Create links to user CSS/JS pages
@@ -1236,7 +1266,7 @@ class Preferences {
* @param $user User
* @param $context IContextSource
* @param $formClass string
- * @param $remove Array: array of items to remove
+ * @param array $remove array of items to remove
* @return HtmlForm
*/
static function getFormObject( $user, IContextSource $context, $formClass = 'PreferencesForm', array $remove = array() ) {
@@ -1270,15 +1300,17 @@ class Preferences {
}
/**
+ * @param $context IContextSource
* @return array
*/
static function getTimezoneOptions( IContextSource $context ) {
$opt = array();
- global $wgLocalTZoffset, $wgLocaltimezone;
- // Check that $wgLocalTZoffset is the same as $wgLocaltimezone
- if ( $wgLocalTZoffset == date( 'Z' ) / 60 ) {
- $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $wgLocaltimezone )->text();
+ global $wgLocalTZoffset;
+ $timestamp = MWTimestamp::getLocalInstance();
+ // Check that $wgLocalTZoffset is the same as the local time zone offset
+ if ( $wgLocalTZoffset == $timestamp->format( 'Z' ) / 60 ) {
+ $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $timestamp->getTimezone()->getName() )->text();
} else {
$tzstring = sprintf( '%+03d:%02d', floor( $wgLocalTZoffset / 60 ), abs( $wgLocalTZoffset ) % 60 );
$server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
@@ -1339,7 +1371,7 @@ class Preferences {
* @param $alldata
* @return int
*/
- static function filterIntval( $value, $alldata ){
+ static function filterIntval( $value, $alldata ) {
return intval( $value );
}
@@ -1360,7 +1392,9 @@ class Preferences {
$data[0] = intval( $data[0] );
$data[1] = intval( $data[1] );
$minDiff = abs( $data[0] ) * 60 + $data[1];
- if ( $data[0] < 0 ) $minDiff = - $minDiff;
+ if ( $data[0] < 0 ) {
+ $minDiff = - $minDiff;
+ }
} else {
$minDiff = intval( $data[0] ) * 60;
}
@@ -1374,17 +1408,23 @@ class Preferences {
}
/**
+ * Handle the form submission if everything validated properly
+ *
* @param $formData
* @param $form PreferencesForm
* @param $entryPoint string
* @return bool|Status|string
*/
static function tryFormSubmit( $formData, $form, $entryPoint = 'internal' ) {
- global $wgHiddenPrefs;
+ global $wgHiddenPrefs, $wgAuth;
$user = $form->getModifiedUser();
$result = true;
+ if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
+ return Status::newFatal( 'mypreferencesprotected' );
+ }
+
// Filter input
foreach ( array_keys( $formData ) as $name ) {
if ( isset( self::$saveFilters[$name] ) ) {
@@ -1393,41 +1433,39 @@ class Preferences {
}
}
- // Stuff that shouldn't be saved as a preference.
- $saveBlacklist = array(
- 'realname',
- 'emailaddress',
- );
-
// Fortunately, the realname field is MUCH simpler
- if ( !in_array( 'realname', $wgHiddenPrefs ) ) {
+ // (not really "private", but still shouldn't be edited without permission)
+ if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->isAllowed( 'editmyprivateinfo' ) ) {
$realName = $formData['realname'];
$user->setRealName( $realName );
}
- foreach ( $saveBlacklist as $b ) {
- unset( $formData[$b] );
- }
+ if ( $user->isAllowed( 'editmyoptions' ) ) {
+ foreach ( self::$saveBlacklist as $b ) {
+ unset( $formData[$b] );
+ }
- # If users have saved a value for a preference which has subsequently been disabled
- # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
- # is subsequently re-enabled
- # TODO: maintenance script to actually delete these
- foreach( $wgHiddenPrefs as $pref ){
- # If the user has not set a non-default value here, the default will be returned
- # and subsequently discarded
- $formData[$pref] = $user->getOption( $pref, null, true );
- }
+ # If users have saved a value for a preference which has subsequently been disabled
+ # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
+ # is subsequently re-enabled
+ # TODO: maintenance script to actually delete these
+ foreach ( $wgHiddenPrefs as $pref ) {
+ # If the user has not set a non-default value here, the default will be returned
+ # and subsequently discarded
+ $formData[$pref] = $user->getOption( $pref, null, true );
+ }
- // Keeps old preferences from interfering due to back-compat
- // code, etc.
- $user->resetOptions();
+ // Keep old preferences from interfering due to back-compat code, etc.
+ $user->resetOptions( 'unused', $form->getContext() );
- foreach ( $formData as $key => $value ) {
- $user->setOption( $key, $value );
+ foreach ( $formData as $key => $value ) {
+ $user->setOption( $key, $value );
+ }
+
+ $user->saveSettings();
}
- $user->saveSettings();
+ $wgAuth->updateExternalDB( $user );
return $result;
}
@@ -1460,11 +1498,12 @@ class Preferences {
/**
* Try to set a user's email address.
* This does *not* try to validate the address.
- * Caller is responsible for checking $wgAuth.
+ * Caller is responsible for checking $wgAuth and 'editmyprivateinfo'
+ * right.
*
* @deprecated in 1.20; use User::setEmailWithConfirmation() instead.
* @param $user User
- * @param $newaddr string New email address
+ * @param string $newaddr New email address
* @return Array (true on success or Status on failure, info string)
*/
public static function trySetUserEmail( User $user, $newaddr ) {
@@ -1479,7 +1518,7 @@ class Preferences {
}
/**
- * @deprecated in 1.19; will be removed in 1.20.
+ * @deprecated in 1.19
* @param $user User
* @return array
*/
@@ -1549,34 +1588,37 @@ class PreferencesForm extends HTMLForm {
* @return String
*/
function getButtons() {
+ if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
+ return '';
+ }
+
$html = parent::getButtons();
- $t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
+ if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
+ $t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
- $html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped() );
+ $html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped() );
- $html = Xml::tags( 'div', array( 'class' => 'mw-prefs-buttons' ), $html );
+ $html = Xml::tags( 'div', array( 'class' => 'mw-prefs-buttons' ), $html );
+ }
return $html;
}
/**
+ * Separate multi-option preferences into multiple preferences, since we
+ * have to store them separately
* @param $data array
* @return array
*/
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'] );
+ if ( $field instanceof HTMLNestedFilterable ) {
+ $info = $field->mParams;
$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
-
- foreach ( $options as $opt ) {
- $data["$prefix$opt"] = in_array( $opt, $data[$fieldname] );
+ foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
+ $data["$prefix$key"] = $value;
}
-
unset( $data[$fieldname] );
}
}
diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php
index 5d4b35c1..3c464c58 100644
--- a/includes/PrefixSearch.php
+++ b/includes/PrefixSearch.php
@@ -32,21 +32,21 @@ class PrefixSearch {
*
* @param $search String
* @param $limit Integer
- * @param $namespaces Array: used if query is not explicitely prefixed
+ * @param array $namespaces used if query is not explicitly prefixed
* @return Array of strings
*/
public static function titleSearch( $search, $limit, $namespaces = array() ) {
$search = trim( $search );
- if( $search == '' ) {
+ if ( $search == '' ) {
return array(); // Return empty result
}
$namespaces = self::validateNamespaces( $namespaces );
// Find a Title which is not an interwiki and is in NS_MAIN
$title = Title::newFromText( $search );
- if( $title && $title->getInterwiki() == '' ) {
- $ns = array($title->getNamespace());
- if( $ns[0] == NS_MAIN ) {
+ if ( $title && $title->getInterwiki() == '' ) {
+ $ns = array( $title->getNamespace() );
+ if ( $ns[0] == NS_MAIN ) {
$ns = $namespaces; // no explicit prefix, use default namespaces
}
return self::searchBackend(
@@ -55,7 +55,7 @@ class PrefixSearch {
// Is this a namespace prefix?
$title = Title::newFromText( $search . 'Dummy' );
- if( $title && $title->getText() == 'Dummy'
+ if ( $title && $title->getText() == 'Dummy'
&& $title->getNamespace() != NS_MAIN
&& $title->getInterwiki() == '' ) {
return self::searchBackend(
@@ -73,16 +73,16 @@ class PrefixSearch {
* @return Array of strings
*/
protected static function searchBackend( $namespaces, $search, $limit ) {
- if( count( $namespaces ) == 1 ) {
+ if ( count( $namespaces ) == 1 ) {
$ns = $namespaces[0];
- if( $ns == NS_MEDIA ) {
+ if ( $ns == NS_MEDIA ) {
$namespaces = array( NS_FILE );
- } elseif( $ns == NS_SPECIAL ) {
+ } elseif ( $ns == NS_SPECIAL ) {
return self::specialSearch( $search, $limit );
}
}
$srchres = array();
- if( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
+ if ( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
return self::defaultSearchBackend( $namespaces, $search, $limit );
}
return $srchres;
@@ -91,7 +91,7 @@ class PrefixSearch {
/**
* Prefix search special-case for Special: namespace.
*
- * @param $search String: term
+ * @param string $search term
* @param $limit Integer: max number of items to return
* @return Array
*/
@@ -106,24 +106,24 @@ class PrefixSearch {
// Unlike SpecialPage itself, we want the canonical forms of both
// canonical and alias title forms...
$keys = array();
- foreach( SpecialPageFactory::getList() as $page => $class ) {
+ foreach ( SpecialPageFactory::getList() as $page => $class ) {
$keys[$wgContLang->caseFold( $page )] = $page;
}
- foreach( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
- if( !array_key_exists( $page, SpecialPageFactory::getList() ) ) {# bug 20885
+ foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
+ if ( !array_key_exists( $page, SpecialPageFactory::getList() ) ) {# bug 20885
continue;
}
- foreach( $aliases as $alias ) {
+ foreach ( $aliases as $alias ) {
$keys[$wgContLang->caseFold( $alias )] = $alias;
}
}
ksort( $keys );
$srchres = array();
- foreach( $keys as $pageKey => $page ) {
- if( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
+ foreach ( $keys as $pageKey => $page ) {
+ if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
wfSuppressWarnings();
// bug 27671: Don't use SpecialPage::getTitleFor() here because it
// localizes its input leading to searches for e.g. Special:All
@@ -133,7 +133,7 @@ class PrefixSearch {
wfRestoreWarnings();
}
- if( count( $srchres ) >= $limit ) {
+ if ( count( $srchres ) >= $limit ) {
break;
}
}
@@ -147,14 +147,14 @@ class PrefixSearch {
* be automatically capitalized by Title::secureAndSpit()
* later on depending on $wgCapitalLinks)
*
- * @param $namespaces Array: namespaces to search in
- * @param $search String: term
+ * @param array $namespaces namespaces to search in
+ * @param string $search 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 ) ) {
+ if ( in_array( NS_MAIN, $namespaces ) ) {
$ns = NS_MAIN; // if searching on many always default to main
}
@@ -175,7 +175,7 @@ class PrefixSearch {
$data = $module->getResultData();
// Reformat useful data for future printing by JSON engine
- $srchres = array ();
+ $srchres = array();
foreach ( (array)$data['query']['allpages'] as $pageinfo ) {
// Note: this data will no be printable by the xml engine
// because it does not support lists of unnamed items
@@ -196,14 +196,14 @@ class PrefixSearch {
// We will look at each given namespace against wgContLang namespaces
$validNamespaces = $wgContLang->getNamespaces();
- if( is_array( $namespaces ) && count( $namespaces ) > 0 ) {
+ if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) {
$valid = array();
foreach ( $namespaces as $ns ) {
- if( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) {
+ if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) {
$valid[] = $ns;
}
}
- if( count( $valid ) > 0 ) {
+ if ( count( $valid ) > 0 ) {
return $valid;
}
}
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index ce0e36b0..f10317a9 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -63,7 +63,7 @@ class ProtectionForm {
$this->mArticle = $article;
$this->mTitle = $article->getTitle();
$this->mApplicableTypes = $this->mTitle->getRestrictionTypes();
-
+
// Check if the form should be disabled.
// If it is, the form will be available in read-only to show levels.
$this->mPermErrors = $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser );
@@ -91,7 +91,7 @@ class ProtectionForm {
$this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' );
$this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade );
- foreach( $this->mApplicableTypes as $action ) {
+ foreach ( $this->mApplicableTypes as $action ) {
// @todo FIXME: This form currently requires individual selections,
// but the db allows multiples separated by commas.
@@ -132,15 +132,20 @@ class ProtectionForm {
}
$val = $wgRequest->getVal( "mwProtect-level-$action" );
- if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
+ if ( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
// Prevent users from setting levels that they cannot later unset
- if( $val == 'sysop' ) {
- // Special case, rewrite sysop to either protect and editprotected
- if( !$wgUser->isAllowedAny( 'protect', 'editprotected' ) )
+ if ( $val == 'sysop' ) {
+ // Special case, rewrite sysop to editprotected
+ if ( !$wgUser->isAllowed( 'editprotected' ) ) {
continue;
- } else {
- if( !$wgUser->isAllowed($val) )
+ }
+ } elseif ( $val == 'autoconfirmed' ) {
+ // Special case, rewrite autoconfirmed to editsemiprotected
+ if ( !$wgUser->isAllowed( 'editsemiprotected' ) ) {
continue;
+ }
+ } elseif ( !$wgUser->isAllowed( $val ) ) {
+ continue;
}
$this->mRestrictions[$action] = $val;
}
@@ -188,10 +193,10 @@ class ProtectionForm {
throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
}
- if( $wgRequest->wasPosted() ) {
- if( $this->save() ) {
+ if ( $wgRequest->wasPosted() ) {
+ if ( $this->save() ) {
$q = $this->mArticle->isRedirect() ? 'redirect=no' : '';
- $wgOut->redirect( $this->mTitle->getFullUrl( $q ) );
+ $wgOut->redirect( $this->mTitle->getFullURL( $q ) );
}
} else {
$this->show();
@@ -201,12 +206,13 @@ class ProtectionForm {
/**
* Show the input form with optional error message
*
- * @param $err String: error message or null if there's no error
+ * @param string $err error message or null if there's no error
*/
function show( $err = null ) {
global $wgOut;
$wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->addBacklinkSubtitle( $this->mTitle );
if ( is_array( $err ) ) {
$wgOut->wrapWikiMsg( "<p class='error'>\n$1\n</p>\n", $err );
@@ -214,15 +220,27 @@ class ProtectionForm {
$wgOut->addHTML( "<p class='error'>{$err}</p>\n" );
}
+ if ( $this->mTitle->getRestrictionTypes() === array() ) {
+ // No restriction types available for the current title
+ // this might happen if an extension alters the available types
+ $wgOut->setPageTitle( wfMessage( 'protect-norestrictiontypes-title', $this->mTitle->getPrefixedText() ) );
+ $wgOut->addWikiText( wfMessage( 'protect-norestrictiontypes-text' )->text() );
+
+ // Show the log in case protection was possible once
+ $this->showLogExtract( $wgOut );
+ // return as there isn't anything else we can do
+ return;
+ }
+
list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
- if ( $cascadeSources && count($cascadeSources) > 0 ) {
+ if ( $cascadeSources && count( $cascadeSources ) > 0 ) {
$titles = '';
foreach ( $cascadeSources as $title ) {
$titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
}
- $wgOut->wrapWikiMsg( "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>", array( 'protect-cascadeon', count($cascadeSources) ) );
+ $wgOut->wrapWikiMsg( "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>", array( 'protect-cascadeon', count( $cascadeSources ) ) );
}
# Show an appropriate message if the user isn't allowed or able to change
@@ -236,7 +254,6 @@ class ProtectionForm {
wfEscapeWikiText( $this->mTitle->getPrefixedText() ) );
}
- $wgOut->addBacklinkSubtitle( $this->mTitle );
$wgOut->addHTML( $this->buildForm() );
$this->showLogExtract( $wgOut );
}
@@ -270,10 +287,11 @@ class ProtectionForm {
$reasonstr = $this->mReason;
}
$expiry = array();
- foreach( $this->mApplicableTypes as $action ) {
+ foreach ( $this->mApplicableTypes as $action ) {
$expiry[$action] = $this->getExpiry( $action );
- if( empty($this->mRestrictions[$action]) )
+ if ( empty( $this->mRestrictions[$action] ) ) {
continue; // unprotected
+ }
if ( !$expiry[$action] ) {
$this->show( array( 'protect_expiry_invalid' ) );
return false;
@@ -284,15 +302,7 @@ class ProtectionForm {
}
}
- # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied
- # to a semi-protected page.
- global $wgGroupPermissions;
-
- $edit_restriction = isset( $this->mRestrictions['edit'] ) ? $this->mRestrictions['edit'] : '';
$this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' );
- if ($this->mCascade && ($edit_restriction != 'protect') &&
- !(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) )
- $this->mCascade = false;
$status = $this->mArticle->doUpdateRestrictions( $this->mRestrictions, $expiry, $this->mCascade, $reasonstr, $wgUser );
@@ -308,23 +318,18 @@ class ProtectionForm {
* you can also return an array of message name and its parameters
*/
$errorMsg = '';
- if( !wfRunHooks( 'ProtectionForm::save', array( $this->mArticle, &$errorMsg ) ) ) {
+ if ( !wfRunHooks( 'ProtectionForm::save', array( $this->mArticle, &$errorMsg, $reasonstr ) ) ) {
if ( $errorMsg == '' ) {
$errorMsg = array( 'hookaborted' );
}
}
- if( $errorMsg != '' ) {
+ if ( $errorMsg != '' ) {
$this->show( $errorMsg );
return false;
}
- if ( $wgUser->isLoggedIn() && $wgRequest->getCheck( 'mwProtectWatch' ) != $wgUser->isWatched( $this->mTitle ) ) {
- if ( $wgRequest->getCheck( 'mwProtectWatch' ) ) {
- WatchAction::doWatch( $this->mTitle, $wgUser );
- } else {
- WatchAction::doUnwatch( $this->mTitle, $wgUser );
- }
- }
+ WatchAction::doWatchOrUnwatch( $wgRequest->getCheck( 'mwProtectWatch' ), $this->mTitle, $wgUser );
+
return true;
}
@@ -346,10 +351,10 @@ class ProtectionForm {
);
$out = '';
- if( !$this->disabled ) {
+ if ( !$this->disabled ) {
$wgOut->addModules( 'mediawiki.legacy.protect' );
$out .= Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
+ 'action' => $this->mTitle->getLocalURL( 'action=protect' ),
'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) );
}
@@ -358,10 +363,12 @@ class ProtectionForm {
Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) .
Xml::openElement( 'tbody' );
- foreach( $this->mRestrictions as $action => $selected ) {
- /* Not all languages have V_x <-> N_x relation */
+ // Not all languages have V_x <-> N_x relation
+ foreach ( $this->mRestrictions as $action => $selected ) {
+ // Messages:
+ // restriction-edit, restriction-move, restriction-create, restriction-upload
$msg = wfMessage( 'restriction-' . $action );
- $out .= "<tr><td>".
+ $out .= "<tr><td>" .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, $msg->exists() ? $msg->text() : $action ) .
Xml::openElement( 'table', array( 'id' => "mw-protect-table-$action" ) ) .
@@ -374,7 +381,7 @@ class ProtectionForm {
'mwProtect-reason', 4 );
$scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text();
- $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled);
+ $showProtectOptions = $scExpiryOptions !== '-' && !$this->disabled;
$mProtectexpiry = Xml::label(
wfMessage( 'protectexpiry' )->text(),
@@ -402,18 +409,18 @@ class ProtectionForm {
wfMessage( 'protect-othertime-op' )->text(),
"othertime"
) . "\n";
- foreach( explode(',', $scExpiryOptions) as $option ) {
- if ( strpos($option, ":") === false ) {
+ foreach ( explode( ',', $scExpiryOptions ) as $option ) {
+ if ( strpos( $option, ":" ) === false ) {
$show = $value = $option;
} else {
- list($show, $value) = explode(":", $option);
+ list( $show, $value ) = explode( ":", $option );
}
- $show = htmlspecialchars($show);
- $value = htmlspecialchars($value);
+ $show = htmlspecialchars( $show );
+ $value = htmlspecialchars( $value );
$expiryFormOptions .= Xml::option( $show, $value, $this->mExpirySelection[$action] === $value ) . "\n";
}
# Add expiry dropdown
- if( $showProtectOptions && !$this->disabled ) {
+ if ( $showProtectOptions && !$this->disabled ) {
$out .= "
<table><tr>
<td class='mw-label'>
@@ -448,12 +455,12 @@ class ProtectionForm {
"</td></tr>";
}
# Give extensions a chance to add items to the form
- wfRunHooks( 'ProtectionForm::buildForm', array($this->mArticle,&$out) );
+ wfRunHooks( 'ProtectionForm::buildForm', array( $this->mArticle, &$out ) );
$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
// JavaScript will add another row with a value-chaining checkbox
- if( $this->mTitle->exists() ) {
+ if ( $this->mTitle->exists() ) {
$out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) .
Xml::openElement( 'tbody' );
$out .= '<tr>
@@ -471,8 +478,8 @@ class ProtectionForm {
}
# Add manual and custom reason field/selects as well as submit
- if( !$this->disabled ) {
- $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) .
+ if ( !$this->disabled ) {
+ $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) .
Xml::openElement( 'tbody' );
$out .= "
<tr>
@@ -496,14 +503,14 @@ class ProtectionForm {
"</td>
</tr>";
# Disallow watching is user is not logged in
- if( $wgUser->isLoggedIn() ) {
+ if ( $wgUser->isLoggedIn() ) {
$out .= "
<tr>
<td></td>
<td class='mw-input'>" .
Xml::checkLabel( wfMessage( 'watchthis' )->text(),
'mwProtectWatch', 'mwProtectWatch',
- $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) ) .
+ $wgUser->isWatched( $this->mTitle ) || $wgUser->getOption( 'watchdefault' ) ) .
"</td>
</tr>";
}
@@ -544,23 +551,30 @@ class ProtectionForm {
/**
* Build protection level selector
*
- * @param $action String: action to protect
- * @param $selected String: current protection level
+ * @param string $action action to protect
+ * @param string $selected current protection level
* @return String: HTML fragment
*/
function buildSelector( $action, $selected ) {
global $wgRestrictionLevels, $wgUser;
$levels = array();
- foreach( $wgRestrictionLevels as $key ) {
+ foreach ( $wgRestrictionLevels as $key ) {
//don't let them choose levels above their own (aka so they can still unprotect and edit the page). but only when the form isn't disabled
- if( $key == 'sysop' ) {
- //special case, rewrite sysop to protect and editprotected
- if( !$wgUser->isAllowedAny( 'protect', 'editprotected' ) && !$this->disabled )
+ if ( $key == 'sysop' ) {
+ //special case, rewrite sysop to editprotected
+ if ( !$wgUser->isAllowed( 'editprotected' ) && !$this->disabled ) {
+ continue;
+ }
+ } elseif ( $key == 'autoconfirmed' ) {
+ //special case, rewrite autoconfirmed to editsemiprotected
+ if ( !$wgUser->isAllowed( 'editsemiprotected' ) && !$this->disabled ) {
continue;
+ }
} else {
- if( !$wgUser->isAllowed($key) && !$this->disabled )
+ if ( !$wgUser->isAllowed( $key ) && !$this->disabled ) {
continue;
+ }
}
$levels[] = $key;
}
@@ -574,7 +588,7 @@ class ProtectionForm {
) + $this->disabledAttrib;
$out = Xml::openElement( 'select', $attribs );
- foreach( $levels as $key ) {
+ foreach ( $levels as $key ) {
$out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected );
}
$out .= Xml::closeElement( 'select' );
@@ -584,15 +598,16 @@ class ProtectionForm {
/**
* Prepare the label for a protection selector option
*
- * @param $permission String: permission required
+ * @param string $permission permission required
* @return String
*/
private function getOptionLabel( $permission ) {
- if( $permission == '' ) {
+ if ( $permission == '' ) {
return wfMessage( 'protect-default' )->text();
} else {
+ // Messages: protect-level-autoconfirmed, protect-level-sysop
$msg = wfMessage( "protect-level-{$permission}" );
- if( $msg->exists() ) {
+ if ( $msg->exists() ) {
return $msg->text();
}
return wfMessage( 'protect-fallback', $permission )->text();
@@ -600,16 +615,9 @@ class ProtectionForm {
}
function buildCleanupScript() {
- global $wgRestrictionLevels, $wgGroupPermissions, $wgOut;
-
- $cascadeableLevels = array();
- foreach( $wgRestrictionLevels as $key ) {
- if ( ( isset( $wgGroupPermissions[$key]['protect'] ) && $wgGroupPermissions[$key]['protect'] )
- || $key == 'protect'
- ) {
- $cascadeableLevels[] = $key;
- }
- }
+ global $wgCascadingRestrictionLevels, $wgOut;
+
+ $cascadeableLevels = $wgCascadingRestrictionLevels;
$options = array(
'tableId' => 'mwProtectSet',
'labelText' => wfMessage( 'protect-unchain-permissions' )->plain(),
@@ -634,6 +642,6 @@ class ProtectionForm {
$out->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
LogEventsList::showLogExtract( $out, 'protect', $this->mTitle );
# Let extensions add other relevant log extracts
- wfRunHooks( 'ProtectionForm::showLogExtract', array($this->mArticle,$out) );
+ wfRunHooks( 'ProtectionForm::showLogExtract', array( $this->mArticle, $out ) );
}
}
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
index 349789fe..bf1c4059 100644
--- a/includes/ProxyTools.php
+++ b/includes/ProxyTools.php
@@ -60,8 +60,8 @@ function wfGetIP() {
}
/**
- * Checks if an IP is a trusted proxy providor.
- * Useful to tell if X-Fowarded-For data is possibly bogus.
+ * Checks if an IP is a trusted proxy provider.
+ * Useful to tell if X-Forwarded-For data is possibly bogus.
* Squid cache servers for the site are whitelisted.
*
* @param $ip String
@@ -84,43 +84,3 @@ function wfIsConfiguredProxy( $ip ) {
in_array( $ip, $wgSquidServersNoPurge );
return $trusted;
}
-
-/**
- * Forks processes to scan the originating IP for an open proxy server
- * MemCached can be used to skip IPs that have already been scanned
- */
-function wfProxyCheck() {
- global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath;
- global $wgMemc, $wgProxyMemcExpiry, $wgRequest;
- global $wgProxyKey;
-
- if ( !$wgBlockOpenProxies ) {
- return;
- }
-
- $ip = $wgRequest->getIP();
-
- # Get MemCached key
- $mcKey = wfMemcKey( 'proxy', 'ip', $ip );
- $mcValue = $wgMemc->get( $mcKey );
- $skip = (bool)$mcValue;
-
- # Fork the processes
- if ( !$skip ) {
- $title = SpecialPage::getTitleFor( 'Blockme' );
- $iphash = md5( $ip . $wgProxyKey );
- $url = wfExpandUrl( $title->getFullURL( 'ip='.$iphash ), PROTO_HTTP );
-
- foreach ( $wgProxyPorts as $port ) {
- $params = implode( ' ', array(
- escapeshellarg( $wgProxyScriptPath ),
- escapeshellarg( $ip ),
- escapeshellarg( $port ),
- escapeshellarg( $url )
- ));
- exec( "php $params >" . wfGetNull() . " 2>&1 &" );
- }
- # Set MemCached key
- $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry );
- }
-}
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index ac559dc5..51b5706f 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -35,7 +35,6 @@ $wgQueryPages = array(
array( 'AncientPagesPage', 'Ancientpages' ),
array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
array( 'DeadendPagesPage', 'Deadendpages' ),
- array( 'DisambiguationsPage', 'Disambiguations' ),
array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ),
array( 'LinkSearchPage', 'LinkSearch' ),
@@ -69,9 +68,9 @@ $wgQueryPages = array(
wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
global $wgDisableCounters;
-if ( !$wgDisableCounters )
+if ( !$wgDisableCounters ) {
$wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
-
+}
/**
* This is a class for doing query pages; since they're almost all the same,
@@ -90,7 +89,7 @@ abstract class QueryPage extends SpecialPage {
/**
* The offset and limit in use, as passed to the query() function
*
- * @var integer
+ * @var int
*/
var $offset = 0;
var $limit = 0;
@@ -112,7 +111,7 @@ abstract class QueryPage extends SpecialPage {
/**
* A mutator for $this->listoutput;
*
- * @param $bool Boolean
+ * @param bool $bool
*/
function setListoutput( $bool ) {
$this->listoutput = $bool;
@@ -150,7 +149,8 @@ abstract class QueryPage extends SpecialPage {
/**
* For back-compat, subclasses may return a raw SQL query here, as a string.
- * This is stronly deprecated; getQueryInfo() should be overridden instead.
+ * This is strongly deprecated; getQueryInfo() should be overridden instead.
+ * @throws MWException
* @return string
*/
function getSQL() {
@@ -186,7 +186,7 @@ abstract class QueryPage extends SpecialPage {
/**
* Override to sort by increasing values
*
- * @return Boolean
+ * @return bool
*/
function sortDescending() {
return true;
@@ -197,7 +197,7 @@ abstract class QueryPage extends SpecialPage {
* don't let it run in miser mode. $wgDisableQueryPages causes all query
* pages to be declared expensive. Some query pages are always expensive.
*
- * @return Boolean
+ * @return bool
*/
function isExpensive() {
global $wgDisableQueryPages;
@@ -208,7 +208,7 @@ abstract class QueryPage extends SpecialPage {
* Is the output of this query cacheable? Non-cacheable expensive pages
* will be disabled in miser mode and will not have their results written
* to the querycache table.
- * @return Boolean
+ * @return bool
* @since 1.18
*/
public function isCacheable() {
@@ -219,7 +219,7 @@ abstract class QueryPage extends SpecialPage {
* Whether or not the output of the page in question is retrieved from
* the database cache.
*
- * @return Boolean
+ * @return bool
*/
function isCached() {
global $wgMiserMode;
@@ -228,9 +228,9 @@ abstract class QueryPage extends SpecialPage {
}
/**
- * Sometime we dont want to build rss / atom feeds.
+ * Sometime we don't want to build rss / atom feeds.
*
- * @return Boolean
+ * @return bool
*/
function isSyndicated() {
return true;
@@ -241,19 +241,16 @@ abstract class QueryPage extends SpecialPage {
* skin; you can use it for making links. The result is a single row of
* result data. You should be able to grab SQL results off of it.
* If the function returns false, the line output will be skipped.
- * @param $skin Skin
- * @param $result object Result row
- * @return mixed String or false to skip
- *
- * @param $skin Skin object
- * @param $result Object: database row
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string|bool String or false to skip
*/
abstract function formatResult( $skin, $result );
/**
* The content returned by this function will be output before any result
*
- * @return String
+ * @return string
*/
function getPageHeader() {
return '';
@@ -264,7 +261,7 @@ abstract class QueryPage extends SpecialPage {
* as an associative array. They will be encoded and added to the paging
* links (prev/next/lengths).
*
- * @return Array
+ * @return array
*/
function linkParameters() {
return array();
@@ -285,8 +282,9 @@ abstract class QueryPage extends SpecialPage {
/**
* Clear the cache and save new results
*
- * @param $limit Integer: limit for SQL statement
- * @param $ignoreErrors Boolean: whether to ignore database errors
+ * @param int|bool $limit Limit for SQL statement
+ * @param bool $ignoreErrors Whether to ignore database errors
+ * @throws DBError|Exception
* @return bool|int
*/
function recache( $limit, $ignoreErrors = true ) {
@@ -301,62 +299,58 @@ abstract class QueryPage extends SpecialPage {
return false;
}
- if ( $ignoreErrors ) {
- $ignoreW = $dbw->ignoreErrors( true );
- $ignoreR = $dbr->ignoreErrors( true );
- }
-
- # Clear out any old cached data
- $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
- # Do query
- $res = $this->reallyDoQuery( $limit, false );
- $num = false;
- if ( $res ) {
- $num = $res->numRows();
- # Fetch results
- $vals = array();
- while ( $res && $row = $dbr->fetchObject( $res ) ) {
- if ( isset( $row->value ) ) {
- if ( $this->usesTimestamps() ) {
- $value = wfTimestamp( TS_UNIX,
- $row->value );
+ try {
+ # Clear out any old cached data
+ $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
+ # Do query
+ $res = $this->reallyDoQuery( $limit, false );
+ $num = false;
+ if ( $res ) {
+ $num = $res->numRows();
+ # Fetch results
+ $vals = array();
+ while ( $res && $row = $dbr->fetchObject( $res ) ) {
+ if ( isset( $row->value ) ) {
+ if ( $this->usesTimestamps() ) {
+ $value = wfTimestamp( TS_UNIX,
+ $row->value );
+ } else {
+ $value = intval( $row->value ); // @bug 14414
+ }
} else {
- $value = intval( $row->value ); // @bug 14414
+ $value = 0;
}
- } else {
- $value = 0;
- }
- $vals[] = array( 'qc_type' => $this->getName(),
- 'qc_namespace' => $row->namespace,
- 'qc_title' => $row->title,
- 'qc_value' => $value );
- }
+ $vals[] = array( 'qc_type' => $this->getName(),
+ 'qc_namespace' => $row->namespace,
+ 'qc_title' => $row->title,
+ 'qc_value' => $value );
+ }
- # Save results into the querycache table on the master
- if ( count( $vals ) ) {
- if ( !$dbw->insert( 'querycache', $vals, __METHOD__ ) ) {
- // Set result to false to indicate error
- $num = false;
+ # Save results into the querycache table on the master
+ if ( count( $vals ) ) {
+ $dbw->insert( 'querycache', $vals, __METHOD__ );
}
+ # Update the querycache_info record for the page
+ $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
+ $dbw->insert( 'querycache_info',
+ array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
+ $fname );
}
- if ( $ignoreErrors ) {
- $dbw->ignoreErrors( $ignoreW );
- $dbr->ignoreErrors( $ignoreR );
+ } catch ( DBError $e ) {
+ if ( !$ignoreErrors ) {
+ throw $e; // report query error
}
-
- # Update the querycache_info record for the page
- $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
- $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname );
-
+ $num = false; // set result to false to indicate error
}
+
return $num;
}
/**
* Run the query and return the result
- * @param $limit mixed Numerical limit or false for no limit
- * @param $offset mixed Numerical offset or false for no offset
+ * @param int|bool $limit Numerical limit or false for no limit
+ * @param int|bool $offset Numerical offset or false for no offset
* @return ResultWrapper
* @since 1.18
*/
@@ -365,23 +359,28 @@ abstract class QueryPage extends SpecialPage {
$dbr = wfGetDB( DB_SLAVE );
$query = $this->getQueryInfo();
$order = $this->getOrderFields();
+
if ( $this->sortDescending() ) {
foreach ( $order as &$field ) {
$field .= ' DESC';
}
}
+
if ( is_array( $query ) ) {
$tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
$fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
$conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
$options = isset( $query['options'] ) ? (array)$query['options'] : array();
$join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
+
if ( count( $order ) ) {
$options['ORDER BY'] = $order;
}
+
if ( $limit !== false ) {
$options['LIMIT'] = intval( $limit );
}
+
if ( $offset !== false ) {
$options['OFFSET'] = intval( $offset );
}
@@ -396,11 +395,14 @@ abstract class QueryPage extends SpecialPage {
$sql = $dbr->limitResult( $sql, $limit, $offset );
$res = $dbr->query( $sql, $fname );
}
+
return $dbr->resultObject( $res );
}
/**
* Somewhat deprecated, you probably want to be using execute()
+ * @param int|bool $offset
+ * @oaram int|bool $limit
* @return ResultWrapper
*/
function doQuery( $offset = false, $limit = false ) {
@@ -413,14 +415,14 @@ abstract class QueryPage extends SpecialPage {
/**
* Fetch the query results from the query cache
- * @param $limit mixed Numerical limit or false for no limit
- * @param $offset mixed Numerical offset or false for no offset
+ * @param int|bool $limit Numerical limit or false for no limit
+ * @param int|bool $offset Numerical offset or false for no offset
* @return ResultWrapper
* @since 1.18
*/
function fetchFromCache( $limit, $offset = false ) {
$dbr = wfGetDB( DB_SLAVE );
- $options = array ();
+ $options = array();
if ( $limit !== false ) {
$options['LIMIT'] = intval( $limit );
}
@@ -455,6 +457,7 @@ abstract class QueryPage extends SpecialPage {
/**
* This is the actual workhorse. It does everything needed to make a
* real, honest-to-gosh query page.
+ * @para $par
* @return int
*/
function execute( $par ) {
@@ -566,12 +569,12 @@ abstract class QueryPage extends SpecialPage {
* Format and output report results using the given information plus
* OutputPage
*
- * @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
+ * @param OutputPage $out OutputPage to print to
+ * @param Skin $skin User skin to use
+ * @param DatabaseBase $dbr Database (read) connection to use
+ * @param int $res Result pointer
+ * @param int $num Number of available result rows
+ * @param int $offset Paging offset
*/
protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
global $wgContLang;
@@ -584,7 +587,7 @@ abstract class QueryPage extends SpecialPage {
# $res might contain the whole 1,000 rows, so we read up to
# $num [should update this to use a Pager]
- for ( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
+ for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
$line = $this->formatResult( $skin, $row );
if ( $line ) {
$attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
@@ -639,25 +642,26 @@ abstract class QueryPage extends SpecialPage {
/**
* Do any necessary preprocessing of the result object.
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {}
/**
* Similar to above, but packaging in a syndicated feed instead of a web page
+ * @param string $class
+ * @param int $limit
* @return bool
*/
function doFeed( $class = '', $limit = 50 ) {
- global $wgFeed, $wgFeedClasses;
+ global $wgFeed, $wgFeedClasses, $wgFeedLimit;
if ( !$wgFeed ) {
$this->getOutput()->addWikiMsg( 'feed-unavailable' );
- return;
+ return false;
}
- global $wgFeedLimit;
- if ( $limit > $wgFeedLimit ) {
- $limit = $wgFeedLimit;
- }
+ $limit = min( $limit, $wgFeedLimit );
if ( isset( $wgFeedClasses[$class] ) ) {
$feed = new $wgFeedClasses[$class](
@@ -684,6 +688,7 @@ abstract class QueryPage extends SpecialPage {
/**
* Override for custom handling. If the titles/links are ok, just do
* feedItemDesc()
+ * @param object $row
* @return FeedItem|null
*/
function feedResult( $row ) {
@@ -739,7 +744,6 @@ abstract class QueryPage extends SpecialPage {
* WantedPages, WantedTemplates, etc
*/
abstract class WantedQueryPage extends QueryPage {
-
function isExpensive() {
return true;
}
@@ -750,6 +754,8 @@ abstract class WantedQueryPage extends QueryPage {
/**
* Cache page existence for performance
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
if ( !$res->numRows() ) {
@@ -781,8 +787,8 @@ abstract class WantedQueryPage extends QueryPage {
/**
* Format an individual result
*
- * @param $skin Skin to use for UI elements
- * @param $result Result row
+ * @param Skin $skin Skin to use for UI elements
+ * @param object $result Result row
* @return string
*/
public function formatResult( $skin, $result ) {
@@ -816,8 +822,8 @@ abstract class WantedQueryPage extends QueryPage {
/**
* Make a "what links here" link for a given title
*
- * @param $title Title to make the link for
- * @param $result Object: result row
+ * @param Title $title Title to make the link for
+ * @param object $result Result row
* @return string
*/
private function makeWlhLink( $title, $result ) {
diff --git a/includes/Revision.php b/includes/Revision.php
index 20cc8f58..233eac01 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -25,6 +25,10 @@
*/
class Revision implements IDBAccessObject {
protected $mId;
+
+ /**
+ * @var int|null
+ */
protected $mPage;
protected $mUserText;
protected $mOrigUserText;
@@ -38,8 +42,29 @@ class Revision implements IDBAccessObject {
protected $mComment;
protected $mText;
protected $mTextRow;
+
+ /**
+ * @var null|Title
+ */
protected $mTitle;
protected $mCurrent;
+ protected $mContentModel;
+ protected $mContentFormat;
+
+ /**
+ * @var Content|null|bool
+ */
+ protected $mContent;
+
+ /**
+ * @var null|ContentHandler
+ */
+ protected $mContentHandler;
+
+ /**
+ * @var int
+ */
+ protected $mQueryFlags = 0;
// Revision deletion constants
const DELETED_TEXT = 1;
@@ -83,21 +108,21 @@ class Revision implements IDBAccessObject {
* @param $flags Integer Bitfield (optional)
* @return Revision or null
*/
- public static function newFromTitle( $title, $id = 0, $flags = null ) {
+ public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
$conds = array(
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
+ 'page_title' => $title->getDBkey()
);
if ( $id ) {
// Use the specified ID
$conds['rev_id'] = $id;
+ return self::newFromConds( $conds, (int)$flags );
} else {
// Use a join to get the latest revision
$conds[] = 'rev_id=page_latest';
- // Callers assume this will be up-to-date
- $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
+ return self::loadFromConds( $db, $conds, $flags );
}
- return self::newFromConds( $conds, (int)$flags );
}
/**
@@ -106,7 +131,7 @@ class Revision implements IDBAccessObject {
* Returns null if no such revision can be found.
*
* $flags include:
- * Revision::READ_LATEST : Select the data from the master
+ * Revision::READ_LATEST : Select the data from the master (since 1.20)
* Revision::READ_LOCKING : Select & lock the data from the master
*
* @param $revId Integer
@@ -114,15 +139,13 @@ class Revision implements IDBAccessObject {
* @param $flags Integer Bitfield (optional)
* @return Revision or null
*/
- public static function newFromPageId( $pageId, $revId = 0, $flags = null ) {
+ public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
$conds = array( 'page_id' => $pageId );
if ( $revId ) {
$conds['rev_id'] = $revId;
} else {
// Use a join to get the latest revision
$conds[] = 'rev_id = page_latest';
- // Callers assume this will be up-to-date
- $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c
}
return self::newFromConds( $conds, (int)$flags );
}
@@ -135,9 +158,12 @@ class Revision implements IDBAccessObject {
* @param $row
* @param $overrides array
*
+ * @throws MWException
* @return Revision
*/
public static function newFromArchiveRow( $row, $overrides = array() ) {
+ global $wgContentHandlerUseDB;
+
$attribs = $overrides + array(
'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
@@ -150,7 +176,22 @@ class Revision implements IDBAccessObject {
'deleted' => $row->ar_deleted,
'len' => $row->ar_len,
'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
+ 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
+ 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
);
+
+ if ( !$wgContentHandlerUseDB ) {
+ unset( $attribs['content_model'] );
+ unset( $attribs['content_format'] );
+ }
+
+ if ( !isset( $attribs['title'] )
+ && isset( $row->ar_namespace )
+ && isset( $row->ar_title ) ) {
+
+ $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+ }
+
if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
// Pre-1.5 ar_text row
$attribs['text'] = self::getRevisionText( $row, 'ar_' );
@@ -194,8 +235,8 @@ class Revision implements IDBAccessObject {
* @return Revision or null
*/
public static function loadFromPageId( $db, $pageid, $id = 0 ) {
- $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) );
- if( $id ) {
+ $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) );
+ if ( $id ) {
$conds['rev_id'] = intval( $id );
} else {
$conds[] = 'rev_id=page_latest';
@@ -214,15 +255,17 @@ class Revision implements IDBAccessObject {
* @return Revision or null
*/
public static function loadFromTitle( $db, $title, $id = 0 ) {
- if( $id ) {
+ if ( $id ) {
$matchId = intval( $id );
} else {
$matchId = 'page_latest';
}
return self::loadFromConds( $db,
- array( "rev_id=$matchId",
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() )
+ array(
+ "rev_id=$matchId",
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ )
);
}
@@ -238,9 +281,11 @@ class Revision implements IDBAccessObject {
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
return self::loadFromConds( $db,
- array( 'rev_timestamp' => $db->timestamp( $timestamp ),
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() )
+ array(
+ 'rev_timestamp' => $db->timestamp( $timestamp ),
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ )
);
}
@@ -260,6 +305,9 @@ class Revision implements IDBAccessObject {
$rev = self::loadFromConds( $dbw, $conditions, $flags );
}
}
+ if ( $rev ) {
+ $rev->mQueryFlags = $flags;
+ }
return $rev;
}
@@ -274,9 +322,9 @@ class Revision implements IDBAccessObject {
*/
private static function loadFromConds( $db, $conditions, $flags = 0 ) {
$res = self::fetchFromConds( $db, $conditions, $flags );
- if( $res ) {
+ if ( $res ) {
$row = $res->fetchObject();
- if( $row ) {
+ if ( $row ) {
$ret = new Revision( $row );
return $ret;
}
@@ -296,9 +344,11 @@ class Revision implements IDBAccessObject {
public static function fetchRevision( $title ) {
return self::fetchFromConds(
wfGetDB( DB_SLAVE ),
- array( 'rev_id=page_latest',
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() )
+ array(
+ 'rev_id=page_latest',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ )
);
}
@@ -343,7 +393,7 @@ class Revision implements IDBAccessObject {
}
/**
- * Return the value of a select() page conds array for the paeg table.
+ * Return the value of a select() page conds array for the page table.
* This will assure that the revision(s) are not orphaned from live pages.
* @since 1.19
* @return Array
@@ -358,7 +408,9 @@ class Revision implements IDBAccessObject {
* @return array
*/
public static function selectFields() {
- return array(
+ global $wgContentHandlerUseDB;
+
+ $fields = array(
'rev_id',
'rev_page',
'rev_text_id',
@@ -370,8 +422,15 @@ class Revision implements IDBAccessObject {
'rev_deleted',
'rev_len',
'rev_parent_id',
- 'rev_sha1'
+ 'rev_sha1',
);
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'rev_content_format';
+ $fields[] = 'rev_content_model';
+ }
+
+ return $fields;
}
/**
@@ -436,10 +495,11 @@ class Revision implements IDBAccessObject {
* Constructor
*
* @param $row Mixed: either a database row or an array
+ * @throws MWException
* @access private
*/
function __construct( $row ) {
- if( is_object( $row ) ) {
+ if ( is_object( $row ) ) {
$this->mId = intval( $row->rev_id );
$this->mPage = intval( $row->rev_page );
$this->mTextId = intval( $row->rev_text_id );
@@ -449,13 +509,13 @@ class Revision implements IDBAccessObject {
$this->mTimestamp = $row->rev_timestamp;
$this->mDeleted = intval( $row->rev_deleted );
- if( !isset( $row->rev_parent_id ) ) {
- $this->mParentId = is_null( $row->rev_parent_id ) ? null : 0;
+ if ( !isset( $row->rev_parent_id ) ) {
+ $this->mParentId = null;
} else {
- $this->mParentId = intval( $row->rev_parent_id );
+ $this->mParentId = intval( $row->rev_parent_id );
}
- if( !isset( $row->rev_len ) || is_null( $row->rev_len ) ) {
+ if ( !isset( $row->rev_len ) ) {
$this->mSize = null;
} else {
$this->mSize = intval( $row->rev_len );
@@ -467,7 +527,7 @@ class Revision implements IDBAccessObject {
$this->mSha1 = $row->rev_sha1;
}
- if( isset( $row->page_latest ) ) {
+ if ( isset( $row->page_latest ) ) {
$this->mCurrent = ( $row->rev_id == $row->page_latest );
$this->mTitle = Title::newFromRow( $row );
} else {
@@ -475,9 +535,21 @@ class Revision implements IDBAccessObject {
$this->mTitle = null;
}
+ if ( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
+ $this->mContentModel = null; # determine on demand if needed
+ } else {
+ $this->mContentModel = strval( $row->rev_content_model );
+ }
+
+ if ( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
+ $this->mContentFormat = null; # determine on demand if needed
+ } else {
+ $this->mContentFormat = strval( $row->rev_content_format );
+ }
+
// Lazy extraction...
- $this->mText = null;
- if( isset( $row->old_text ) ) {
+ $this->mText = null;
+ if ( isset( $row->old_text ) ) {
$this->mTextRow = $row;
} else {
// 'text' table row entry will be lazy-loaded
@@ -492,10 +564,24 @@ class Revision implements IDBAccessObject {
$this->mUserText = $row->user_name; // logged-in user
}
$this->mOrigUserText = $row->rev_user_text;
- } elseif( is_array( $row ) ) {
+ } elseif ( is_array( $row ) ) {
// Build a new revision to be saved...
global $wgUser; // ugh
+ # if we have a content object, use it to set the model and type
+ if ( !empty( $row['content'] ) ) {
+ // @todo when is that set? test with external store setup! check out insertOn() [dk]
+ if ( !empty( $row['text_id'] ) ) {
+ throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
+ "can't serialize content object" );
+ }
+
+ $row['content_model'] = $row['content']->getModel();
+ # note: mContentFormat is initializes later accordingly
+ # note: content is serialized later in this method!
+ # also set text to null?
+ }
+
$this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
$this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
$this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
@@ -508,21 +594,67 @@ class Revision implements IDBAccessObject {
$this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
$this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+ $this->mContentModel = isset( $row['content_model'] ) ? strval( $row['content_model'] ) : null;
+ $this->mContentFormat = isset( $row['content_format'] ) ? strval( $row['content_format'] ) : null;
+
// Enforce spacing trimming on supplied text
$this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
$this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
$this->mTextRow = null;
- $this->mTitle = null; # Load on demand if needed
- $this->mCurrent = false;
- # If we still have no length, see it we have the text to figure it out
+ $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
+
+ // if we have a Content object, override mText and mContentModel
+ if ( !empty( $row['content'] ) ) {
+ if ( !( $row['content'] instanceof Content ) ) {
+ throw new MWException( '`content` field must contain a Content object.' );
+ }
+
+ $handler = $this->getContentHandler();
+ $this->mContent = $row['content'];
+
+ $this->mContentModel = $this->mContent->getModel();
+ $this->mContentHandler = null;
+
+ $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
+ } elseif ( !is_null( $this->mText ) ) {
+ $handler = $this->getContentHandler();
+ $this->mContent = $handler->unserializeContent( $this->mText );
+ }
+
+ // If we have a Title object, make sure it is consistent with mPage.
+ if ( $this->mTitle && $this->mTitle->exists() ) {
+ if ( $this->mPage === null ) {
+ // if the page ID wasn't known, set it now
+ $this->mPage = $this->mTitle->getArticleID();
+ } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
+ // Got different page IDs. This may be legit (e.g. during undeletion),
+ // but it seems worth mentioning it in the log.
+ wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
+ $this->mTitle->getArticleID() . " provided by the Title object." );
+ }
+ }
+
+ $this->mCurrent = false;
+
+ // If we still have no length, see it we have the text to figure it out
if ( !$this->mSize ) {
- $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
+ if ( !is_null( $this->mContent ) ) {
+ $this->mSize = $this->mContent->getSize();
+ } else {
+ #NOTE: this should never happen if we have either text or content object!
+ $this->mSize = null;
+ }
}
- # Same for sha1
+
+ // Same for sha1
if ( $this->mSha1 === null ) {
$this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
}
+
+ // force lazy init
+ $this->getContentModel();
+ $this->getContentFormat();
} else {
throw new MWException( 'Revision constructor passed invalid row format.' );
}
@@ -592,21 +724,26 @@ class Revision implements IDBAccessObject {
* @return Title|null
*/
public function getTitle() {
- if( isset( $this->mTitle ) ) {
+ if ( isset( $this->mTitle ) ) {
return $this->mTitle;
}
- if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
+ if ( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow(
array( 'page', 'revision' ),
self::selectPageFields(),
array( 'page_id=rev_page',
- 'rev_id' => $this->mId ),
+ 'rev_id' => $this->mId ),
__METHOD__ );
if ( $row ) {
$this->mTitle = Title::newFromRow( $row );
}
}
+
+ if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
+ $this->mTitle = Title::newFromID( $this->mPage );
+ }
+
return $this->mTitle;
}
@@ -642,9 +779,9 @@ class Revision implements IDBAccessObject {
* @return Integer
*/
public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
return 0;
- } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
return 0;
} else {
return $this->mUser;
@@ -674,9 +811,9 @@ class Revision implements IDBAccessObject {
* @return string
*/
public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
- if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
return '';
- } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
return '';
} else {
return $this->getRawUserText();
@@ -714,9 +851,9 @@ class Revision implements IDBAccessObject {
* @return String
*/
function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
return '';
- } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
return '';
} else {
return $this->mComment;
@@ -740,28 +877,41 @@ class Revision implements IDBAccessObject {
}
/**
- * @return Integer rcid of the unpatrolled row, zero if there isn't one
+ * @return integer rcid of the unpatrolled row, zero if there isn't one
*/
public function isUnpatrolled() {
- if( $this->mUnpatrolled !== null ) {
+ if ( $this->mUnpatrolled !== null ) {
return $this->mUnpatrolled;
}
+ $rc = $this->getRecentChange();
+ if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
+ $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
+ } else {
+ $this->mUnpatrolled = 0;
+ }
+ return $this->mUnpatrolled;
+ }
+
+ /**
+ * Get the RC object belonging to the current revision, if there's one
+ *
+ * @since 1.22
+ * @return RecentChange|null
+ */
+ public function getRecentChange() {
$dbr = wfGetDB( DB_SLAVE );
- $this->mUnpatrolled = $dbr->selectField( 'recentchanges',
- 'rc_id',
- array( // Add redundant user,timestamp condition so we can use the existing index
- 'rc_user_text' => $this->getRawUserText(),
- 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
- 'rc_this_oldid' => $this->getId(),
- 'rc_patrolled' => 0
+ return RecentChange::newFromConds(
+ array(
+ 'rc_user_text' => $this->getRawUserText(),
+ 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
+ 'rc_this_oldid' => $this->getId()
),
__METHOD__
);
- return (int)$this->mUnpatrolled;
}
/**
- * @param $field int one of DELETED_* bitfield constants
+ * @param int $field one of DELETED_* bitfield constants
*
* @return Boolean
*/
@@ -789,15 +939,39 @@ class Revision implements IDBAccessObject {
* Revision::RAW get the text regardless of permissions
* @param $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
+ *
+ * @deprecated in 1.21, use getContent() instead
+ * @todo Replace usage in core
* @return String
*/
public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
- if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
- return '';
- } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
- return '';
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ $content = $this->getContent( $audience, $user );
+ return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
+ }
+
+ /**
+ * Fetch revision content if it's available to the specified audience.
+ * If the specified audience does not have the ability to view this
+ * revision, null will be returned.
+ *
+ * @param $audience Integer: one of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @since 1.21
+ * @return Content|null
+ */
+ public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+ return null;
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+ return null;
} else {
- return $this->getRawText();
+ return $this->getContentInternal();
}
}
@@ -816,16 +990,121 @@ class Revision implements IDBAccessObject {
* Fetch revision text without regard for view restrictions
*
* @return String
+ *
+ * @deprecated since 1.21. Instead, use Revision::getContent( Revision::RAW )
+ * or Revision::getSerializedData() as appropriate.
*/
public function getRawText() {
- if( is_null( $this->mText ) ) {
- // Revision text is immutable. Load on demand:
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+ return $this->getText( self::RAW );
+ }
+
+ /**
+ * Fetch original serialized data without regard for view restrictions
+ *
+ * @since 1.21
+ * @return String
+ */
+ public function getSerializedData() {
+ if ( is_null( $this->mText ) ) {
$this->mText = $this->loadText();
}
+
return $this->mText;
}
/**
+ * Gets the content object for the revision (or null on failure).
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * @since 1.21
+ * @return Content|null the Revision's content, or null on failure.
+ */
+ protected function getContentInternal() {
+ if ( is_null( $this->mContent ) ) {
+ // Revision is immutable. Load on demand:
+ if ( is_null( $this->mText ) ) {
+ $this->mText = $this->loadText();
+ }
+
+ if ( $this->mText !== null && $this->mText !== false ) {
+ // Unserialize content
+ $handler = $this->getContentHandler();
+ $format = $this->getContentFormat();
+
+ $this->mContent = $handler->unserializeContent( $this->mText, $format );
+ } else {
+ $this->mContent = false; // negative caching!
+ }
+ }
+
+ // NOTE: copy() will return $this for immutable content objects
+ return $this->mContent ? $this->mContent->copy() : null;
+ }
+
+ /**
+ * Returns the content model for this revision.
+ *
+ * If no content model was stored in the database, $this->getTitle()->getContentModel() is
+ * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
+ * is used as a last resort.
+ *
+ * @return String the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
+ **/
+ public function getContentModel() {
+ if ( !$this->mContentModel ) {
+ $title = $this->getTitle();
+ $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
+
+ assert( !empty( $this->mContentModel ) );
+ }
+
+ return $this->mContentModel;
+ }
+
+ /**
+ * Returns the content format for this revision.
+ *
+ * If no content format was stored in the database, the default format for this
+ * revision's content model is returned.
+ *
+ * @return String the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
+ **/
+ public function getContentFormat() {
+ if ( !$this->mContentFormat ) {
+ $handler = $this->getContentHandler();
+ $this->mContentFormat = $handler->getDefaultFormat();
+
+ assert( !empty( $this->mContentFormat ) );
+ }
+
+ return $this->mContentFormat;
+ }
+
+ /**
+ * Returns the content handler appropriate for this revision's content model.
+ *
+ * @throws MWException
+ * @return ContentHandler
+ */
+ public function getContentHandler() {
+ if ( !$this->mContentHandler ) {
+ $model = $this->getContentModel();
+ $this->mContentHandler = ContentHandler::getForModelID( $model );
+
+ $format = $this->getContentFormat();
+
+ if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
+ throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
+ }
+ }
+
+ return $this->mContentHandler;
+ }
+
+ /**
* @return String
*/
public function getTimestamp() {
@@ -842,12 +1121,12 @@ class Revision implements IDBAccessObject {
/**
* Get previous revision for this title
*
- * @return Revision or null
+ * @return Revision|null
*/
public function getPrevious() {
- if( $this->getTitle() ) {
+ if ( $this->getTitle() ) {
$prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
- if( $prev ) {
+ if ( $prev ) {
return self::newFromTitle( $this->getTitle(), $prev );
}
}
@@ -860,7 +1139,7 @@ class Revision implements IDBAccessObject {
* @return Revision or null
*/
public function getNext() {
- if( $this->getTitle() ) {
+ if ( $this->getTitle() ) {
$next = $this->getTitle()->getNextRevisionID( $this->getId() );
if ( $next ) {
return self::newFromTitle( $this->getTitle(), $next );
@@ -877,11 +1156,11 @@ class Revision implements IDBAccessObject {
* @return Integer
*/
private function getPreviousRevisionId( $db ) {
- if( is_null( $this->mPage ) ) {
+ if ( is_null( $this->mPage ) ) {
return 0;
}
# Use page_latest if ID is not given
- if( !$this->mId ) {
+ if ( !$this->mId ) {
$prevId = $db->selectField( 'page', 'page_latest',
array( 'page_id' => $this->mPage ),
__METHOD__ );
@@ -900,23 +1179,27 @@ class Revision implements IDBAccessObject {
* field must be included
*
* @param $row Object: the text data
- * @param $prefix String: table prefix (default 'old_')
+ * @param string $prefix table prefix (default 'old_')
+ * @param string|false $wiki the name of the wiki to load the revision text from
+ * (same as the the wiki $row was loaded from) or false to indicate the local
+ * wiki (this is the default). Otherwise, it must be a symbolic wiki database
+ * identifier as understood by the LoadBalancer class.
* @return String: text the text requested or false on failure
*/
- public static function getRevisionText( $row, $prefix = 'old_' ) {
+ public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
wfProfileIn( __METHOD__ );
# Get data
$textField = $prefix . 'text';
$flagsField = $prefix . 'flags';
- if( isset( $row->$flagsField ) ) {
+ if ( isset( $row->$flagsField ) ) {
$flags = explode( ',', $row->$flagsField );
} else {
$flags = array();
}
- if( isset( $row->$textField ) ) {
+ if ( isset( $row->$textField ) ) {
$text = $row->$textField;
} else {
wfProfileOut( __METHOD__ );
@@ -927,44 +1210,16 @@ class Revision implements IDBAccessObject {
if ( in_array( 'external', $flags ) ) {
$url = $text;
$parts = explode( '://', $url, 2 );
- if( count( $parts ) == 1 || $parts[1] == '' ) {
+ if ( count( $parts ) == 1 || $parts[1] == '' ) {
wfProfileOut( __METHOD__ );
return false;
}
- $text = ExternalStore::fetchFromURL( $url );
+ $text = ExternalStore::fetchFromURL( $url, array( 'wiki' => $wiki ) );
}
// If the text was fetched without an error, convert it
if ( $text !== false ) {
- if( in_array( 'gzip', $flags ) ) {
- # Deal with optional compression of archived pages.
- # This can be done periodically via maintenance/compressOld.php, and
- # as pages are saved if $wgCompressRevisions is set.
- $text = gzinflate( $text );
- }
-
- if( in_array( 'object', $flags ) ) {
- # Generic compressed storage
- $obj = unserialize( $text );
- if ( !is_object( $obj ) ) {
- // Invalid object
- wfProfileOut( __METHOD__ );
- return false;
- }
- $text = $obj->getText();
- }
-
- global $wgLegacyEncoding;
- if( $text !== false && $wgLegacyEncoding
- && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
- {
- # Old revisions kept around in a legacy encoding?
- # Upconvert on demand.
- # ("utf8" checked for compatibility with some broken
- # conversion scripts 2008-12-30)
- global $wgContLang;
- $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
- }
+ $text = self::decompressRevisionText( $text, $flags );
}
wfProfileOut( __METHOD__ );
return $text;
@@ -988,8 +1243,8 @@ class Revision implements IDBAccessObject {
# on load if $wgLegacyCharset is set in the future.
$flags[] = 'utf-8';
- if( $wgCompressRevisions ) {
- if( function_exists( 'gzdeflate' ) ) {
+ if ( $wgCompressRevisions ) {
+ if ( function_exists( 'gzdeflate' ) ) {
$text = gzdeflate( $text );
$flags[] = 'gzip';
} else {
@@ -1000,73 +1255,140 @@ class Revision implements IDBAccessObject {
}
/**
+ * Re-converts revision text according to it's flags.
+ *
+ * @param $text Mixed: reference to a text
+ * @param $flags array: compression flags
+ * @return String|bool decompressed text, or false on failure
+ */
+ public static function decompressRevisionText( $text, $flags ) {
+ if ( in_array( 'gzip', $flags ) ) {
+ # Deal with optional compression of archived pages.
+ # This can be done periodically via maintenance/compressOld.php, and
+ # as pages are saved if $wgCompressRevisions is set.
+ $text = gzinflate( $text );
+ }
+
+ if ( in_array( 'object', $flags ) ) {
+ # Generic compressed storage
+ $obj = unserialize( $text );
+ if ( !is_object( $obj ) ) {
+ // Invalid object
+ return false;
+ }
+ $text = $obj->getText();
+ }
+
+ global $wgLegacyEncoding;
+ if ( $text !== false && $wgLegacyEncoding
+ && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
+ {
+ # Old revisions kept around in a legacy encoding?
+ # Upconvert on demand.
+ # ("utf8" checked for compatibility with some broken
+ # conversion scripts 2008-12-30)
+ global $wgContLang;
+ $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
+ }
+
+ return $text;
+ }
+
+ /**
* Insert a new revision into the database, returning the new revision ID
* number on success and dies horribly on failure.
*
* @param $dbw DatabaseBase: (master connection)
+ * @throws MWException
* @return Integer
*/
public function insertOn( $dbw ) {
- global $wgDefaultExternalStore;
+ global $wgDefaultExternalStore, $wgContentHandlerUseDB;
wfProfileIn( __METHOD__ );
+ $this->checkContentModel();
+
$data = $this->mText;
$flags = self::compressRevisionText( $data );
# Write to external storage if required
- if( $wgDefaultExternalStore ) {
+ if ( $wgDefaultExternalStore ) {
// Store and get the URL
$data = ExternalStore::insertToDefault( $data );
- if( !$data ) {
+ if ( !$data ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Unable to store text to external storage" );
}
- if( $flags ) {
+ if ( $flags ) {
$flags .= ',';
}
$flags .= 'external';
}
# Record the text (or external storage URL) to the text table
- if( !isset( $this->mTextId ) ) {
+ if ( !isset( $this->mTextId ) ) {
$old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
$dbw->insert( 'text',
array(
- 'old_id' => $old_id,
- 'old_text' => $data,
+ 'old_id' => $old_id,
+ 'old_text' => $data,
'old_flags' => $flags,
), __METHOD__
);
$this->mTextId = $dbw->insertId();
}
- if ( $this->mComment === null ) $this->mComment = "";
+ if ( $this->mComment === null ) {
+ $this->mComment = "";
+ }
# Record the edit in revisions
$rev_id = isset( $this->mId )
? $this->mId
: $dbw->nextSequenceValue( 'revision_rev_id_seq' );
- $dbw->insert( 'revision',
- array(
- 'rev_id' => $rev_id,
- 'rev_page' => $this->mPage,
- 'rev_text_id' => $this->mTextId,
- 'rev_comment' => $this->mComment,
- 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
- 'rev_user' => $this->mUser,
- 'rev_user_text' => $this->mUserText,
- 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
- 'rev_deleted' => $this->mDeleted,
- 'rev_len' => $this->mSize,
- 'rev_parent_id' => is_null( $this->mParentId )
- ? $this->getPreviousRevisionId( $dbw )
- : $this->mParentId,
- 'rev_sha1' => is_null( $this->mSha1 )
- ? self::base36Sha1( $this->mText )
- : $this->mSha1
- ), __METHOD__
+ $row = array(
+ 'rev_id' => $rev_id,
+ 'rev_page' => $this->mPage,
+ 'rev_text_id' => $this->mTextId,
+ 'rev_comment' => $this->mComment,
+ 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
+ 'rev_user' => $this->mUser,
+ 'rev_user_text' => $this->mUserText,
+ 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
+ 'rev_deleted' => $this->mDeleted,
+ 'rev_len' => $this->mSize,
+ 'rev_parent_id' => is_null( $this->mParentId )
+ ? $this->getPreviousRevisionId( $dbw )
+ : $this->mParentId,
+ 'rev_sha1' => is_null( $this->mSha1 )
+ ? Revision::base36Sha1( $this->mText )
+ : $this->mSha1,
);
+ if ( $wgContentHandlerUseDB ) {
+ //NOTE: Store null for the default model and format, to save space.
+ //XXX: Makes the DB sensitive to changed defaults. Make this behavior optional? Only in miser mode?
+
+ $model = $this->getContentModel();
+ $format = $this->getContentFormat();
+
+ $title = $this->getTitle();
+
+ if ( $title === null ) {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Insufficient information to determine the title of the revision's page!" );
+ }
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+ $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+ $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+ }
+
+ $dbw->insert( 'revision', $row, __METHOD__ );
+
$this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
@@ -1075,6 +1397,52 @@ class Revision implements IDBAccessObject {
return $this->mId;
}
+ protected function checkContentModel() {
+ global $wgContentHandlerUseDB;
+
+ $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
+
+ $model = $this->getContentModel();
+ $format = $this->getContentFormat();
+ $handler = $this->getContentHandler();
+
+ if ( !$handler->isSupportedFormat( $format ) ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Can't use format $format with content model $model on $t" );
+ }
+
+ if ( !$wgContentHandlerUseDB && $title ) {
+ // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+ $defaultFormat = $defaultHandler->getDefaultFormat();
+
+ if ( $this->getContentModel() != $defaultModel ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
+ . "model is $model , default for $t is $defaultModel" );
+ }
+
+ if ( $this->getContentFormat() != $defaultFormat ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
+ . "format is $format, default for $t is $defaultFormat" );
+ }
+ }
+
+ $content = $this->getContent( Revision::RAW );
+
+ if ( !$content || !$content->isValid() ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Content of $t is not valid! Content model is $model" );
+ }
+ }
+
/**
* Get the base 36 SHA-1 value for a string of text
* @param $text String
@@ -1088,7 +1456,7 @@ class Revision implements IDBAccessObject {
* Lazy-load the revision's text.
* Currently hardcoded to the 'text' table storage engine.
*
- * @return String
+ * @return String|bool the revision's text, or false on failure
*/
protected function loadText() {
wfProfileIn( __METHOD__ );
@@ -1097,9 +1465,9 @@ class Revision implements IDBAccessObject {
global $wgRevisionCacheExpiry, $wgMemc;
$textId = $this->getTextId();
$key = wfMemcKey( 'revisiontext', 'textid', $textId );
- if( $wgRevisionCacheExpiry ) {
+ if ( $wgRevisionCacheExpiry ) {
$text = $wgMemc->get( $key );
- if( is_string( $text ) ) {
+ if ( is_string( $text ) ) {
wfDebug( __METHOD__ . ": got id $textId from cache\n" );
wfProfileOut( __METHOD__ );
return $text;
@@ -1114,28 +1482,38 @@ class Revision implements IDBAccessObject {
$row = null;
}
- if( !$row ) {
+ if ( !$row ) {
// Text data is immutable; check slaves first.
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'text',
array( 'old_text', 'old_flags' ),
- array( 'old_id' => $this->getTextId() ),
+ array( 'old_id' => $textId ),
__METHOD__ );
}
- if( !$row && wfGetLB()->getServerCount() > 1 ) {
- // Possible slave lag!
+ // Fallback to the master in case of slave lag. Also use FOR UPDATE if it was
+ // used to fetch this revision to avoid missing the row due to REPEATABLE-READ.
+ $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING );
+ if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) {
$dbw = wfGetDB( DB_MASTER );
$row = $dbw->selectRow( 'text',
array( 'old_text', 'old_flags' ),
- array( 'old_id' => $this->getTextId() ),
- __METHOD__ );
+ array( 'old_id' => $textId ),
+ __METHOD__,
+ $forUpdate ? array( 'FOR UPDATE' ) : array() );
+ }
+
+ if ( !$row ) {
+ wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
}
$text = self::getRevisionText( $row );
+ if ( $row && $text === false ) {
+ wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
+ }
# No negative caching -- negative hits on text rows may be due to corrupted slave servers
- if( $wgRevisionCacheExpiry && $text !== false ) {
+ if ( $wgRevisionCacheExpiry && $text !== false ) {
$wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
}
@@ -1154,25 +1532,34 @@ class Revision implements IDBAccessObject {
*
* @param $dbw DatabaseBase
* @param $pageId Integer: ID number of the page to read from
- * @param $summary String: revision's summary
+ * @param string $summary revision's summary
* @param $minor Boolean: whether the revision should be considered as minor
* @return Revision|null on error
*/
public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
+ global $wgContentHandlerUseDB;
+
wfProfileIn( __METHOD__ );
+ $fields = array( 'page_latest', 'page_namespace', 'page_title',
+ 'rev_text_id', 'rev_len', 'rev_sha1' );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'rev_content_model';
+ $fields[] = 'rev_content_format';
+ }
+
$current = $dbw->selectRow(
array( 'page', 'revision' ),
- array( 'page_latest', 'page_namespace', 'page_title',
- 'rev_text_id', 'rev_len', 'rev_sha1' ),
+ $fields,
array(
'page_id' => $pageId,
'page_latest=rev_id',
),
__METHOD__ );
- if( $current ) {
- $revision = new Revision( array(
+ if ( $current ) {
+ $row = array(
'page' => $pageId,
'comment' => $summary,
'minor_edit' => $minor,
@@ -1180,7 +1567,14 @@ class Revision implements IDBAccessObject {
'parent_id' => $current->page_latest,
'len' => $current->rev_len,
'sha1' => $current->rev_sha1
- ) );
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $row['content_model'] = $current->rev_content_model;
+ $row['content_format'] = $current->rev_content_format;
+ }
+
+ $revision = new Revision( $row );
$revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
} else {
$revision = null;
@@ -1217,7 +1611,7 @@ class Revision implements IDBAccessObject {
* @return Boolean
*/
public static function userCanBitfield( $bitfield, $field, User $user = null ) {
- if( $bitfield & $field ) { // aspect is deleted
+ if ( $bitfield & $field ) { // aspect is deleted
if ( $bitfield & self::DELETED_RESTRICTED ) {
$permission = 'suppressrevision';
} elseif ( $field & self::DELETED_TEXT ) {
@@ -1245,7 +1639,7 @@ class Revision implements IDBAccessObject {
*/
static function getTimestampFromId( $title, $id ) {
$dbr = wfGetDB( DB_SLAVE );
- // Casting fix for DB2
+ // Casting fix for databases that can't take '' for rev_id
if ( $id == '' ) {
$id = 0;
}
@@ -1270,7 +1664,7 @@ class Revision implements IDBAccessObject {
static function countByPageId( $db, $id ) {
$row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
array( 'rev_page' => $id ), __METHOD__ );
- if( $row ) {
+ if ( $row ) {
return $row->revCount;
}
return 0;
@@ -1285,7 +1679,7 @@ class Revision implements IDBAccessObject {
*/
static function countByTitle( $db, $title ) {
$id = $title->getArticleID();
- if( $id ) {
+ if ( $id ) {
return self::countByPageId( $db, $id );
}
return 0;
@@ -1307,7 +1701,9 @@ class Revision implements IDBAccessObject {
* @return bool True if the given user was the only one to edit since the given timestamp
*/
public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
- if ( !$userId ) return false;
+ if ( !$userId ) {
+ return false;
+ }
if ( is_int( $db ) ) {
$db = wfGetDB( $db );
@@ -1328,4 +1724,4 @@ class Revision implements IDBAccessObject {
}
return true;
}
-} \ No newline at end of file
+}
diff --git a/includes/RevisionList.php b/includes/RevisionList.php
index 3c5cfa8e..1b865bb0 100644
--- a/includes/RevisionList.php
+++ b/includes/RevisionList.php
@@ -105,7 +105,7 @@ abstract class RevisionListBase extends ContextSource {
* @return int
*/
public function length() {
- if( !$this->res ) {
+ if ( !$this->res ) {
return 0;
} else {
return $this->res->numRows();
@@ -190,7 +190,7 @@ abstract class RevisionItemBase {
}
/**
- * Get the date, formatted in user's languae
+ * Get the date, formatted in user's language
* @return String
*/
public function formatDate() {
@@ -199,7 +199,7 @@ abstract class RevisionItemBase {
}
/**
- * Get the time, formatted in user's languae
+ * Get the time, formatted in user's language
* @return String
*/
public function formatTime() {
@@ -352,8 +352,7 @@ class RevisionItem extends RevisionItemBase {
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $this->context->msg( 'diff' )->escaped();
} else {
- return
- Linker::link(
+ return Linker::link(
$this->list->title,
$this->context->msg( 'diff' )->escaped(),
array(),
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index b443ce14..9e9ac38b 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -1,6 +1,6 @@
<?php
/**
- * XHTML sanitizer for %MediaWiki.
+ * HTML sanitizer for %MediaWiki.
*
* Copyright © 2002-2005 Brion Vibber <brion@pobox.com> et al
* http://www.mediawiki.org/
@@ -25,7 +25,7 @@
*/
/**
- * XHTML sanitizer for MediaWiki
+ * HTML sanitizer for MediaWiki
* @ingroup Parser
*/
class Sanitizer {
@@ -54,9 +54,8 @@ class Sanitizer {
* List of all named character entities defined in HTML 4.01
* http://www.w3.org/TR/html4/sgml/entities.html
* As well as &apos; which is only defined starting in XHTML1.
- * @private
*/
- static $htmlEntities = array(
+ private static $htmlEntities = array(
'Aacute' => 193,
'aacute' => 225,
'Acirc' => 194,
@@ -315,7 +314,7 @@ class Sanitizer {
/**
* Character entity aliases accepted by MediaWiki
*/
- static $htmlEntityAliases = array(
+ private static $htmlEntityAliases = array(
'רלמ' => 'rlm',
'رلم' => 'rlm',
);
@@ -323,7 +322,7 @@ class Sanitizer {
/**
* Lazy-initialised attributes regex, see getAttribsRegex()
*/
- static $attribsRegex;
+ private static $attribsRegex;
/**
* Regular expression to match HTML/XML attribute pairs within a tag.
@@ -357,51 +356,61 @@ class Sanitizer {
* removes HTML comments
* @private
* @param $text String
- * @param $processCallback Callback to do any variable or parameter replacements in HTML attribute values
- * @param $args Array for the processing callback
- * @param $extratags Array for any extra tags to include
- * @param $removetags Array for any tags (default or extra) to exclude
+ * @param $processCallback Callback to do any variable or parameter
+ * replacements in HTML attribute values
+ * @param array $args for the processing callback
+ * @param array $extratags for any extra tags to include
+ * @param array $removetags for any tags (default or extra) to exclude
* @return string
*/
- static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array(), $removetags = array() ) {
- global $wgUseTidy;
+ static function removeHTMLtags( $text, $processCallback = null,
+ $args = array(), $extratags = array(), $removetags = array()
+ ) {
+ global $wgUseTidy, $wgAllowMicrodataAttributes, $wgAllowImageTag;
static $htmlpairsStatic, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
$htmllist, $listtags, $htmlsingleallowed, $htmlelementsStatic, $staticInitialised;
wfProfileIn( __METHOD__ );
- if ( !$staticInitialised ) {
+ // Base our staticInitialised variable off of the global config state so that if the globals
+ // are changed (like in the screwed up test system) we will re-initialise the settings.
+ $globalContext = implode( '-', compact( 'wgAllowMicrodataAttributes', 'wgAllowImageTag' ) );
+ if ( !$staticInitialised || $staticInitialised != $globalContext ) {
$htmlpairsStatic = array( # Tags that must be closed
'b', 'bdi', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
'strike', 'strong', 'tt', 'var', 'div', 'center',
'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
- 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'abbr', 'dfn',
- 'kbd', 'samp'
+ 'ruby', 'rt', 'rb', 'rp', 'p', 'span', 'abbr', 'dfn',
+ 'kbd', 'samp', 'data', 'time', 'mark'
);
$htmlsingle = array(
- 'br', 'hr', 'li', 'dt', 'dd'
+ 'br', 'wbr', 'hr', 'li', 'dt', 'dd'
);
$htmlsingleonly = array( # Elements that cannot have close tags
- 'br', 'hr'
+ 'br', 'wbr', 'hr'
);
+ if ( $wgAllowMicrodataAttributes ) {
+ $htmlsingle[] = $htmlsingleonly[] = 'meta';
+ $htmlsingle[] = $htmlsingleonly[] = 'link';
+ }
$htmlnest = array( # Tags that can be nested--??
'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
- 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span'
+ 'li', 'dl', 'dt', 'dd', 'font', 'big', 'small', 'sub', 'sup', 'span',
+ 'var', 'kbd', 'samp', 'em', 'strong', 'q', 'ruby', 'bdo'
);
$tabletags = array( # Can only appear inside table, we will close them
'td', 'th', 'tr',
);
$htmllist = array( # Tags used by list
- 'ul','ol',
+ 'ul', 'ol',
);
$listtags = array( # Tags that can appear in a list
'li',
);
- global $wgAllowImageTag;
if ( $wgAllowImageTag ) {
$htmlsingle[] = 'img';
$htmlsingleonly[] = 'img';
@@ -416,13 +425,13 @@ class Sanitizer {
foreach ( $vars as $var ) {
$$var = array_flip( $$var );
}
- $staticInitialised = true;
+ $staticInitialised = $globalContext;
}
# Populate $htmlpairs and $htmlelements with the $extratags and $removetags arrays
$extratags = array_flip( $extratags );
$removetags = array_flip( $removetags );
$htmlpairs = array_merge( $extratags, $htmlpairsStatic );
- $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ) , $removetags );
+ $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ), $removetags );
# Remove HTML comments
$text = Sanitizer::removeHTMLcomments( $text );
@@ -437,7 +446,7 @@ class Sanitizer {
# $params: String between element name and >
# $brace: Ending '>' or '/>'
# $rest: Everything until the next element of $bits
- if( preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) {
+ if ( preg_match( '!^(/?)([^\\s/>]+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) {
list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
} else {
$slash = $t = $params = $brace = $rest = null;
@@ -498,18 +507,22 @@ class Sanitizer {
!in_array( 'table', $tagstack ) ) {
$badtag = true;
} elseif ( in_array( $t, $tagstack ) &&
- !isset( $htmlnest [$t ] ) ) {
+ !isset( $htmlnest[$t] ) ) {
$badtag = true;
# Is it a self closed htmlpair ? (bug 5487)
} elseif ( $brace == '/>' &&
isset( $htmlpairs[$t] ) ) {
$badtag = true;
} elseif ( isset( $htmlsingleonly[$t] ) ) {
- # Hack to force empty tag for uncloseable elements
+ # Hack to force empty tag for unclosable elements
$brace = '/>';
} elseif ( isset( $htmlsingle[$t] ) ) {
# Hack to not close $htmlsingle tags
$brace = null;
+ # Still need to push this optionally-closed tag to
+ # the tag stack so that we can match end tags
+ # instead of marking them as bad.
+ array_push( $tagstack, $t );
} elseif ( isset( $tabletags[$t] )
&& in_array( $t, $tagstack ) ) {
// New table tag but forgot to close the previous one
@@ -524,10 +537,14 @@ class Sanitizer {
# Replace any variables or template parameters with
# plaintext results.
- if( is_callable( $processCallback ) ) {
+ if ( is_callable( $processCallback ) ) {
call_user_func_array( $processCallback, array( &$params, $args ) );
}
+ if ( !Sanitizer::validateTag( $params, $t ) ) {
+ $badtag = true;
+ }
+
# Strip non-approved attributes from the tag
$newparams = Sanitizer::fixTagAttributes( $params, $t );
}
@@ -538,12 +555,14 @@ class Sanitizer {
continue;
}
}
- $text .= '&lt;' . str_replace( '>', '&gt;', $x);
+ $text .= '&lt;' . str_replace( '>', '&gt;', $x );
}
# Close off any remaining tags
- while ( is_array( $tagstack ) && ($t = array_pop( $tagstack )) ) {
+ while ( is_array( $tagstack ) && ( $t = array_pop( $tagstack ) ) ) {
$text .= "</$t>\n";
- if ( $t == 'table' ) { $tagstack = array_pop( $tablestack ); }
+ if ( $t == 'table' ) {
+ $tagstack = array_pop( $tablestack );
+ }
}
} else {
# this might be possible using tidy itself
@@ -551,16 +570,24 @@ class Sanitizer {
preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/',
$x, $regs );
@list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
+ $badtag = false;
if ( isset( $htmlelements[$t = strtolower( $t )] ) ) {
- if( is_callable( $processCallback ) ) {
+ if ( is_callable( $processCallback ) ) {
call_user_func_array( $processCallback, array( &$params, $args ) );
}
+
+ if ( !Sanitizer::validateTag( $params, $t ) ) {
+ $badtag = true;
+ }
+
$newparams = Sanitizer::fixTagAttributes( $params, $t );
- $rest = str_replace( '>', '&gt;', $rest );
- $text .= "<$slash$t$newparams$brace$rest";
- } else {
- $text .= '&lt;' . str_replace( '>', '&gt;', $x);
+ if ( !$badtag ) {
+ $rest = str_replace( '>', '&gt;', $rest );
+ $text .= "<$slash$t$newparams$brace$rest";
+ continue;
+ }
}
+ $text .= '&lt;' . str_replace( '>', '&gt;', $x );
}
}
wfProfileOut( __METHOD__ );
@@ -579,9 +606,9 @@ class Sanitizer {
*/
static function removeHTMLcomments( $text ) {
wfProfileIn( __METHOD__ );
- while (($start = strpos($text, '<!--')) !== false) {
- $end = strpos($text, '-->', $start + 4);
- if ($end === false) {
+ while ( ( $start = strpos( $text, '<!--' ) ) !== false ) {
+ $end = strpos( $text, '-->', $start + 4 );
+ if ( $end === false ) {
# Unterminated comment; bail out
break;
}
@@ -590,22 +617,24 @@ class Sanitizer {
# Trim space and newline if the comment is both
# preceded and followed by a newline
- $spaceStart = max($start - 1, 0);
+ $spaceStart = max( $start - 1, 0 );
$spaceLen = $end - $spaceStart;
- while (substr($text, $spaceStart, 1) === ' ' && $spaceStart > 0) {
+ while ( substr( $text, $spaceStart, 1 ) === ' ' && $spaceStart > 0 ) {
$spaceStart--;
$spaceLen++;
}
- while (substr($text, $spaceStart + $spaceLen, 1) === ' ')
+ while ( substr( $text, $spaceStart + $spaceLen, 1 ) === ' ' ) {
$spaceLen++;
- if (substr($text, $spaceStart, 1) === "\n" and substr($text, $spaceStart + $spaceLen, 1) === "\n") {
+ }
+ if ( substr( $text, $spaceStart, 1 ) === "\n"
+ && substr( $text, $spaceStart + $spaceLen, 1 ) === "\n" ) {
# Remove the comment, leading and trailing
# spaces, and leave only one newline.
- $text = substr_replace($text, "\n", $spaceStart, $spaceLen + 1);
+ $text = substr_replace( $text, "\n", $spaceStart, $spaceLen + 1 );
}
else {
# Remove just the comment.
- $text = substr_replace($text, '', $start, $end - $start);
+ $text = substr_replace( $text, '', $start, $end - $start );
}
}
wfProfileOut( __METHOD__ );
@@ -613,12 +642,45 @@ class Sanitizer {
}
/**
+ * Takes attribute names and values for a tag and the tag name and
+ * validates that the tag is allowed to be present.
+ * This DOES NOT validate the attributes, nor does it validate the
+ * tags themselves. This method only handles the special circumstances
+ * where we may want to allow a tag within content but ONLY when it has
+ * specific attributes set.
+ *
+ * @param $params
+ * @param $element
+ * @return bool
+ */
+ static function validateTag( $params, $element ) {
+ $params = Sanitizer::decodeTagAttributes( $params );
+
+ if ( $element == 'meta' || $element == 'link' ) {
+ if ( !isset( $params['itemprop'] ) ) {
+ // <meta> and <link> must have an itemprop="" otherwise they are not valid or safe in content
+ return false;
+ }
+ if ( $element == 'meta' && !isset( $params['content'] ) ) {
+ // <meta> must have a content="" for the itemprop
+ return false;
+ }
+ if ( $element == 'link' && !isset( $params['href'] ) ) {
+ // <link> must have an associated href=""
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
* Take an array of attribute names and values and normalize or discard
* illegal values for the given element type.
*
* - Discards attributes not on a whitelist for the given element
* - Unsafe style attributes are discarded
- * - Invalid id attributes are reencoded
+ * - Invalid id attributes are re-encoded
*
* @param $attribs Array
* @param $element String
@@ -638,23 +700,23 @@ class Sanitizer {
*
* - Discards attributes not the given whitelist
* - Unsafe style attributes are discarded
- * - Invalid id attributes are reencoded
+ * - Invalid id attributes are re-encoded
*
* @param $attribs Array
- * @param $whitelist Array: list of allowed attribute names
+ * @param array $whitelist list of allowed attribute names
* @return Array
*
* @todo Check for legal values where the DTD limits things.
* @todo Check for unique id attribute :P
*/
static function validateAttributes( $attribs, $whitelist ) {
- global $wgAllowRdfaAttributes, $wgAllowMicrodataAttributes, $wgHtml5;
+ global $wgAllowRdfaAttributes, $wgAllowMicrodataAttributes;
$whitelist = array_flip( $whitelist );
$hrefExp = '/^(' . wfUrlProtocols() . ')[^\s]+$/';
$out = array();
- foreach( $attribs as $attribute => $value ) {
+ foreach ( $attribs as $attribute => $value ) {
#allow XML namespace declaration if RDFa is enabled
if ( $wgAllowRdfaAttributes && preg_match( self::XMLNS_ATTRIBUTE_PATTERN, $attribute ) ) {
if ( !preg_match( self::EVIL_URI_PATTERN, $value ) ) {
@@ -664,14 +726,14 @@ class Sanitizer {
continue;
}
- # Allow any attribute beginning with "data-", if in HTML5 mode
- if ( !($wgHtml5 && preg_match( '/^data-/i', $attribute )) && !isset( $whitelist[$attribute] ) ) {
+ # Allow any attribute beginning with "data-"
+ if ( !preg_match( '/^data-/i', $attribute ) && !isset( $whitelist[$attribute] ) ) {
continue;
}
# Strip javascript "expression" from stylesheets.
# http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp
- if( $attribute == 'style' ) {
+ if ( $attribute == 'style' ) {
$value = Sanitizer::checkCss( $value );
}
@@ -679,13 +741,28 @@ class Sanitizer {
$value = Sanitizer::escapeId( $value, 'noninitial' );
}
- //RDFa and microdata properties allow URLs, URIs and/or CURIs. check them for sanity
- if ( $attribute === 'rel' || $attribute === 'rev' ||
- $attribute === 'about' || $attribute === 'property' || $attribute === 'resource' || #RDFa
- $attribute === 'datatype' || $attribute === 'typeof' || #RDFa
- $attribute === 'itemid' || $attribute === 'itemprop' || $attribute === 'itemref' || #HTML5 microdata
- $attribute === 'itemscope' || $attribute === 'itemtype' ) { #HTML5 microdata
+ # WAI-ARIA
+ # http://www.w3.org/TR/wai-aria/
+ # http://www.whatwg.org/html/elements.html#wai-aria
+ # For now we only support role="presentation" until we work out what roles should be
+ # usable by content and we ensure that our code explicitly rejects patterns that
+ # violate HTML5's ARIA restrictions.
+ if ( $attribute === 'role' && $value !== 'presentation' ) {
+ continue;
+ }
+ // RDFa and microdata properties allow URLs, URIs and/or CURIs.
+ // Check them for sanity.
+ if ( $attribute === 'rel' || $attribute === 'rev'
+ # RDFa
+ || $attribute === 'about' || $attribute === 'property'
+ || $attribute === 'resource' || $attribute === 'datatype'
+ || $attribute === 'typeof'
+ # HTML5 microdata
+ || $attribute === 'itemid' || $attribute === 'itemprop'
+ || $attribute === 'itemref' || $attribute === 'itemscope'
+ || $attribute === 'itemtype'
+ ) {
//Paranoia. Allow "simple" values but suppress javascript
if ( preg_match( self::EVIL_URI_PATTERN, $value ) ) {
continue;
@@ -697,7 +774,7 @@ class Sanitizer {
if ( $attribute === 'href' || $attribute === 'src' ) {
if ( !preg_match( $hrefExp, $value ) ) {
continue; //drop any href or src attributes not using an allowed protocol.
- //NOTE: this also drops all relative URLs
+ // NOTE: this also drops all relative URLs
}
}
@@ -713,7 +790,7 @@ class Sanitizer {
unset( $out['itemid'] );
unset( $out['itemref'] );
}
- # TODO: Strip itemprop if we aren't descendants of an itemscope.
+ # TODO: Strip itemprop if we aren't descendants of an itemscope or pointed to by an itemref.
}
return $out;
}
@@ -730,9 +807,10 @@ class Sanitizer {
*/
static function mergeAttributes( $a, $b ) {
$out = array_merge( $a, $b );
- if( isset( $a['class'] ) && isset( $b['class'] )
- && is_string( $a['class'] ) && is_string( $b['class'] )
- && $a['class'] !== $b['class'] ) {
+ if ( isset( $a['class'] ) && isset( $b['class'] )
+ && is_string( $a['class'] ) && is_string( $b['class'] )
+ && $a['class'] !== $b['class']
+ ) {
$classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}",
-1, PREG_SPLIT_NO_EMPTY );
$out['class'] = implode( ' ', array_unique( $classes ) );
@@ -743,9 +821,10 @@ class Sanitizer {
/**
* Pick apart some CSS and check it for forbidden or unsafe structures.
* Returns a sanitized string. This sanitized string will have
- * character references and escape sequences decoded, and comments
- * stripped. If the input is just too evil, only a comment complaining
- * about evilness will be returned.
+ * character references and escape sequences decoded and comments
+ * stripped (unless it is itself one valid comment, in which case the value
+ * will be passed through). If the input is just too evil, only a comment
+ * complaining about evilness will be returned.
*
* Currently URL references, 'expression', 'tps' are forbidden.
*
@@ -786,25 +865,77 @@ class Sanitizer {
$value = preg_replace_callback( $decodeRegex,
array( __CLASS__, 'cssDecodeCallback' ), $value );
- // Remove any comments; IE gets token splitting wrong
- // This must be done AFTER decoding character references and
- // escape sequences, because those steps can introduce comments
- // This step cannot introduce character references or escape
- // sequences, because it replaces comments with spaces rather
- // than removing them completely.
- $value = StringUtils::delimiterReplace( '/*', '*/', ' ', $value );
-
- // Remove anything after a comment-start token, to guard against
- // incorrect client implementations.
- $commentPos = strpos( $value, '/*' );
- if ( $commentPos !== false ) {
- $value = substr( $value, 0, $commentPos );
+ // Normalize Halfwidth and Fullwidth Unicode block that IE6 might treat as ascii
+ $value = preg_replace_callback(
+ '/[!-[]-z]/u', // U+FF01 to U+FF5A, excluding U+FF3C (bug 58088)
+ function ( $matches ) {
+ $cp = utf8ToCodepoint( $matches[0] );
+ if ( $cp === false ) {
+ return '';
+ }
+ return chr( $cp - 65248 ); // ASCII range \x21-\x7A
+ },
+ $value
+ );
+
+ // Convert more characters IE6 might treat as ascii
+ // U+0280, U+0274, U+207F, U+029F, U+026A, U+207D, U+208D
+ $value = str_replace(
+ array( 'ʀ', 'ɴ', 'ⁿ', 'ʟ', 'ɪ', '⁽', '₍' ),
+ array( 'r', 'n', 'n', 'l', 'i', '(', '(' ),
+ $value
+ );
+
+ // Let the value through if it's nothing but a single comment, to
+ // allow other functions which may reject it to pass some error
+ // message through.
+ if ( !preg_match( '! ^ \s* /\* [^*\\/]* \*/ \s* $ !x', $value ) ) {
+ // Remove any comments; IE gets token splitting wrong
+ // This must be done AFTER decoding character references and
+ // escape sequences, because those steps can introduce comments
+ // This step cannot introduce character references or escape
+ // sequences, because it replaces comments with spaces rather
+ // than removing them completely.
+ $value = StringUtils::delimiterReplace( '/*', '*/', ' ', $value );
+
+ // Remove anything after a comment-start token, to guard against
+ // incorrect client implementations.
+ $commentPos = strpos( $value, '/*' );
+ if ( $commentPos !== false ) {
+ $value = substr( $value, 0, $commentPos );
+ }
}
+ // S followed by repeat, iteration, or prolonged sound marks,
+ // which IE will treat as "ss"
+ $value = preg_replace(
+ '/s(?:
+ \xE3\x80\xB1 | # U+3031
+ \xE3\x82\x9D | # U+309D
+ \xE3\x83\xBC | # U+30FC
+ \xE3\x83\xBD | # U+30FD
+ \xEF\xB9\xBC | # U+FE7C
+ \xEF\xB9\xBD | # U+FE7D
+ \xEF\xBD\xB0 # U+FF70
+ )/ix',
+ 'ss',
+ $value
+ );
+
// Reject problematic keywords and control characters
- if ( preg_match( '/[\000-\010\016-\037\177]/', $value ) ) {
+ if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
return '/* invalid control char */';
- } elseif ( preg_match( '! expression | filter\s*: | accelerator\s*: | url\s*\( !ix', $value ) ) {
+ } elseif ( preg_match(
+ '! expression
+ | filter\s*:
+ | accelerator\s*:
+ | -o-link\s*:
+ | -o-link-source\s*:
+ | -o-replace\s*:
+ | url\s*\(
+ | image\s*\(
+ | image-set\s*\(
+ !ix', $value ) ) {
return '/* insecure input */';
}
return $value;
@@ -855,21 +986,14 @@ class Sanitizer {
* @return String
*/
static function fixTagAttributes( $text, $element ) {
- if( trim( $text ) == '' ) {
+ if ( trim( $text ) == '' ) {
return '';
}
$decoded = Sanitizer::decodeTagAttributes( $text );
$stripped = Sanitizer::validateTagAttributes( $decoded, $element );
- $attribs = array();
- foreach( $stripped as $attribute => $value ) {
- $encAttribute = htmlspecialchars( $attribute );
- $encValue = Sanitizer::safeEncodeAttribute( $value );
-
- $attribs[] = "$encAttribute=\"$encValue\"";
- }
- return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
+ return Sanitizer::safeEncodeTagAttributes( $stripped );
}
/**
@@ -942,10 +1066,10 @@ class Sanitizer {
* in the id and
* name attributes
* @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
- * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-id-attribute
+ * @see http://www.whatwg.org/html/elements.html#the-id-attribute
* HTML5 definition of id attribute
*
- * @param $id String: id to escape
+ * @param string $id id to escape
* @param $options Mixed: string or array of strings (default is array()):
* 'noninitial': This is a non-initial fragment of an id, not a full id,
* so don't pay attention if the first character isn't valid at the
@@ -957,10 +1081,10 @@ class Sanitizer {
* @return String
*/
static function escapeId( $id, $options = array() ) {
- global $wgHtml5, $wgExperimentalHtmlIds;
+ global $wgExperimentalHtmlIds;
$options = (array)$options;
- if ( $wgHtml5 && $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
+ if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
$id = Sanitizer::decodeCharReferences( $id );
$id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
$id = trim( $id, '_' );
@@ -982,7 +1106,7 @@ class Sanitizer {
$id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
if ( !preg_match( '/^[a-zA-Z]/', $id )
- && !in_array( 'noninitial', $options ) ) {
+ && !in_array( 'noninitial', $options ) ) {
// Initial character must be a letter!
$id = "x$id";
}
@@ -1002,17 +1126,17 @@ class Sanitizer {
*/
static function escapeClass( $class ) {
// Convert ugly stuff to underscores and kill underscores in ugly places
- return rtrim(preg_replace(
- array('/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/','/_+/'),
+ return rtrim( preg_replace(
+ array( '/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/', '/_+/' ),
'_',
- $class ), '_');
+ $class ), '_' );
}
/**
- * Given HTML input, escape with htmlspecialchars but un-escape entites.
+ * Given HTML input, escape with htmlspecialchars but un-escape entities.
* This allows (generally harmless) entities like &#160; to survive.
*
- * @param $html String to escape
+ * @param string $html to escape
* @return String: escaped input
*/
static function escapeHtmlAllowEntities( $html ) {
@@ -1041,13 +1165,13 @@ class Sanitizer {
* @return Array
*/
public static function decodeTagAttributes( $text ) {
- if( trim( $text ) == '' ) {
+ if ( trim( $text ) == '' ) {
return array();
}
$attribs = array();
$pairs = array();
- if( !preg_match_all(
+ if ( !preg_match_all(
self::getAttribsRegex(),
$text,
$pairs,
@@ -1055,7 +1179,7 @@ class Sanitizer {
return $attribs;
}
- foreach( $pairs as $set ) {
+ foreach ( $pairs as $set ) {
$attribute = strtolower( $set[1] );
$value = Sanitizer::getTagAttributeCallback( $set );
@@ -1070,26 +1194,45 @@ class Sanitizer {
}
/**
+ * Build a partial tag string from an associative array of attribute
+ * names and values as returned by decodeTagAttributes.
+ *
+ * @param $assoc_array Array
+ * @return String
+ */
+ public static function safeEncodeTagAttributes( $assoc_array ) {
+ $attribs = array();
+ foreach ( $assoc_array as $attribute => $value ) {
+ $encAttribute = htmlspecialchars( $attribute );
+ $encValue = Sanitizer::safeEncodeAttribute( $value );
+
+ $attribs[] = "$encAttribute=\"$encValue\"";
+ }
+ return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
+ }
+
+ /**
* Pick the appropriate attribute value from a match set from the
* attribs regex matches.
*
* @param $set Array
+ * @throws MWException
* @return String
*/
private static function getTagAttributeCallback( $set ) {
- if( isset( $set[6] ) ) {
+ if ( isset( $set[6] ) ) {
# Illegal #XXXXXX color with no quotes.
return $set[6];
- } elseif( isset( $set[5] ) ) {
+ } elseif ( isset( $set[5] ) ) {
# No quotes.
return $set[5];
- } elseif( isset( $set[4] ) ) {
+ } elseif ( isset( $set[4] ) ) {
# Single-quoted
return $set[4];
- } elseif( isset( $set[3] ) ) {
+ } elseif ( isset( $set[3] ) ) {
# Double-quoted
return $set[3];
- } elseif( !isset( $set[2] ) ) {
+ } elseif ( !isset( $set[2] ) ) {
# In XHTML, attributes must have a value.
# For 'reduced' form, return explicitly the attribute name here.
return $set[1];
@@ -1165,14 +1308,14 @@ class Sanitizer {
*/
static function normalizeCharReferencesCallback( $matches ) {
$ret = null;
- if( $matches[1] != '' ) {
+ if ( $matches[1] != '' ) {
$ret = Sanitizer::normalizeEntity( $matches[1] );
- } elseif( $matches[2] != '' ) {
+ } elseif ( $matches[2] != '' ) {
$ret = Sanitizer::decCharReference( $matches[2] );
- } elseif( $matches[3] != '' ) {
+ } elseif ( $matches[3] != '' ) {
$ret = Sanitizer::hexCharReference( $matches[3] );
}
- if( is_null( $ret ) ) {
+ if ( is_null( $ret ) ) {
return htmlspecialchars( $matches[0] );
} else {
return $ret;
@@ -1208,7 +1351,7 @@ class Sanitizer {
*/
static function decCharReference( $codepoint ) {
$point = intval( $codepoint );
- if( Sanitizer::validateCodepoint( $point ) ) {
+ if ( Sanitizer::validateCodepoint( $point ) ) {
return sprintf( '&#%d;', $point );
} else {
return null;
@@ -1221,7 +1364,7 @@ class Sanitizer {
*/
static function hexCharReference( $codepoint ) {
$point = hexdec( $codepoint );
- if( Sanitizer::validateCodepoint( $point ) ) {
+ if ( Sanitizer::validateCodepoint( $point ) ) {
return sprintf( '&#x%x;', $point );
} else {
return null;
@@ -1234,12 +1377,12 @@ class Sanitizer {
* @return Boolean
*/
private static function validateCodepoint( $codepoint ) {
- return ($codepoint == 0x09)
- || ($codepoint == 0x0a)
- || ($codepoint == 0x0d)
- || ($codepoint >= 0x20 && $codepoint <= 0xd7ff)
- || ($codepoint >= 0xe000 && $codepoint <= 0xfffd)
- || ($codepoint >= 0x10000 && $codepoint <= 0x10ffff);
+ return $codepoint == 0x09
+ || $codepoint == 0x0a
+ || $codepoint == 0x0d
+ || ( $codepoint >= 0x20 && $codepoint <= 0xd7ff )
+ || ( $codepoint >= 0xe000 && $codepoint <= 0xfffd )
+ || ( $codepoint >= 0x10000 && $codepoint <= 0x10ffff );
}
/**
@@ -1263,7 +1406,7 @@ class Sanitizer {
* This is useful for page titles, not for text to be displayed,
* MediaWiki allows HTML entities to escape normalization as a feature.
*
- * @param $text String (already normalized, containing entities)
+ * @param string $text (already normalized, containing entities)
* @return String (still normalized, without entities)
*/
public static function decodeCharReferencesAndNormalize( $text ) {
@@ -1285,12 +1428,12 @@ class Sanitizer {
* @return String
*/
static function decodeCharReferencesCallback( $matches ) {
- if( $matches[1] != '' ) {
+ if ( $matches[1] != '' ) {
return Sanitizer::decodeEntity( $matches[1] );
- } elseif( $matches[2] != '' ) {
- return Sanitizer::decodeChar( intval( $matches[2] ) );
- } elseif( $matches[3] != '' ) {
- return Sanitizer::decodeChar( hexdec( $matches[3] ) );
+ } elseif ( $matches[2] != '' ) {
+ return Sanitizer::decodeChar( intval( $matches[2] ) );
+ } elseif ( $matches[3] != '' ) {
+ return Sanitizer::decodeChar( hexdec( $matches[3] ) );
}
# Last case should be an ampersand by itself
return $matches[0];
@@ -1304,7 +1447,7 @@ class Sanitizer {
* @private
*/
static function decodeChar( $codepoint ) {
- if( Sanitizer::validateCodepoint( $codepoint ) ) {
+ if ( Sanitizer::validateCodepoint( $codepoint ) ) {
return codepointToUtf8( $codepoint );
} else {
return UTF8_REPLACEMENT;
@@ -1323,7 +1466,7 @@ class Sanitizer {
if ( isset( self::$htmlEntityAliases[$name] ) ) {
$name = self::$htmlEntityAliases[$name];
}
- if( isset( self::$htmlEntities[$name] ) ) {
+ if ( isset( self::$htmlEntities[$name] ) ) {
return codepointToUtf8( self::$htmlEntities[$name] );
} else {
return "&$name;";
@@ -1337,10 +1480,7 @@ class Sanitizer {
* @return Array
*/
static function attributeWhitelist( $element ) {
- static $list;
- if( !isset( $list ) ) {
- $list = Sanitizer::setupAttributeWhitelist();
- }
+ $list = Sanitizer::setupAttributeWhitelist();
return isset( $list[$element] )
? $list[$element]
: array();
@@ -1352,41 +1492,62 @@ class Sanitizer {
* @return Array
*/
static function setupAttributeWhitelist() {
- global $wgAllowRdfaAttributes, $wgHtml5, $wgAllowMicrodataAttributes;
+ global $wgAllowRdfaAttributes, $wgAllowMicrodataAttributes;
+
+ static $whitelist, $staticInitialised;
+ $globalContext = implode( '-', compact( 'wgAllowRdfaAttributes', 'wgAllowMicrodataAttributes' ) );
- $common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' );
+ if ( isset( $whitelist ) && $staticInitialised == $globalContext ) {
+ return $whitelist;
+ }
+
+ $common = array(
+ # HTML
+ 'id',
+ 'class',
+ 'style',
+ 'lang',
+ 'dir',
+ 'title',
+
+ # WAI-ARIA
+ 'role',
+ );
if ( $wgAllowRdfaAttributes ) {
- #RDFa attributes as specified in section 9 of http://www.w3.org/TR/2008/REC-rdfa-syntax-20081014
+ # RDFa attributes as specified in section 9 of
+ # http://www.w3.org/TR/2008/REC-rdfa-syntax-20081014
$common = array_merge( $common, array(
- 'about', 'property', 'resource', 'datatype', 'typeof',
+ 'about', 'property', 'resource', 'datatype', 'typeof',
) );
}
- if ( $wgHtml5 && $wgAllowMicrodataAttributes ) {
- # add HTML5 microdata tages as pecified by http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-microdata-model
+ if ( $wgAllowMicrodataAttributes ) {
+ # add HTML5 microdata tags as specified by
+ # http://www.whatwg.org/html/microdata.html#the-microdata-model
$common = array_merge( $common, array(
- 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype'
+ 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype'
) );
}
$block = array_merge( $common, array( 'align' ) );
$tablealign = array( 'align', 'char', 'charoff', 'valign' );
- $tablecell = array( 'abbr',
- 'axis',
- 'headers',
- 'scope',
- 'rowspan',
- 'colspan',
- 'nowrap', # deprecated
- 'width', # deprecated
- 'height', # deprecated
- 'bgcolor' # deprecated
- );
+ $tablecell = array(
+ 'abbr',
+ 'axis',
+ 'headers',
+ 'scope',
+ 'rowspan',
+ 'colspan',
+ 'nowrap', # deprecated
+ 'width', # deprecated
+ 'height', # deprecated
+ 'bgcolor', # deprecated
+ );
# Numbers refer to sections in HTML 4.01 standard describing the element.
# See: http://www.w3.org/TR/html4/
- $whitelist = array (
+ $whitelist = array(
# 7.5.4
'div' => $block,
'center' => $common, # deprecated
@@ -1432,6 +1593,9 @@ class Sanitizer {
# 9.3.2
'br' => array( 'id', 'class', 'title', 'style', 'clear' ),
+ # http://www.whatwg.org/html/text-level-semantics.html#the-wbr-element
+ 'wbr' => array( 'id', 'class', 'title', 'style' ),
+
# 9.3.4
'pre' => array_merge( $common, array( 'width' ) ),
@@ -1475,7 +1639,9 @@ class Sanitizer {
'td' => array_merge( $common, $tablecell, $tablealign ),
'th' => array_merge( $common, $tablecell, $tablealign ),
- # 12.2 # NOTE: <a> is not allowed directly, but the attrib whitelist is used from the Parser object
+ # 12.2
+ # NOTE: <a> is not allowed directly, but the attrib
+ # whitelist is used from the Parser object
'a' => array_merge( $common, array( 'href', 'rel', 'rev' ) ), # rel/rev esp. for RDFa
# 13.2
@@ -1501,8 +1667,8 @@ class Sanitizer {
# 15.3
'hr' => array_merge( $common, array( 'noshade', 'size', 'width' ) ),
- # XHTML Ruby annotation text module, simple ruby only.
- # http://www.w3c.org/TR/ruby/
+ # HTML Ruby annotation text module, simple ruby only.
+ # http://www.whatwg.org/html/text-level-semantics.html#the-ruby-element
'ruby' => $common,
# rbc
# rtc
@@ -1518,7 +1684,23 @@ class Sanitizer {
# HTML 5 section 4.6
'bdi' => $common,
- );
+ # HTML5 elements, defined by:
+ # http://www.whatwg.org/html/
+ 'data' => array_merge( $common, array( 'value' ) ),
+ 'time' => array_merge( $common, array( 'datetime' ) ),
+ 'mark' => $common,
+
+ // meta and link are only permitted by removeHTMLtags when Microdata
+ // is enabled so we don't bother adding a conditional to hide these
+ // Also meta and link are only valid in WikiText as Microdata elements
+ // (ie: validateTag rejects tags missing the attributes needed for Microdata)
+ // So we don't bother including $common attributes that have no purpose.
+ 'meta' => array( 'itemprop', 'content' ),
+ 'link' => array( 'itemprop', 'href' ),
+ );
+
+ $staticInitialised = $globalContext;
+
return $whitelist;
}
@@ -1529,7 +1711,7 @@ class Sanitizer {
* Warning: this return value must be further escaped for literal
* inclusion in HTML output as of 1.10!
*
- * @param $text String: HTML fragment
+ * @param string $text HTML fragment
* @return String
*/
static function stripAllTags( $text ) {
@@ -1554,7 +1736,7 @@ class Sanitizer {
*/
static function hackDocType() {
$out = "<!DOCTYPE html [\n";
- foreach( self::$htmlEntities as $entity => $codepoint ) {
+ foreach ( self::$htmlEntities as $entity => $codepoint ) {
$out .= "<!ENTITY $entity \"&#$codepoint;\">";
}
$out .= "]>\n";
@@ -1576,7 +1758,7 @@ class Sanitizer {
# Validate hostname portion
$matches = array();
- if( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) {
+ if ( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) {
list( /* $whole */, $protocol, $host, $rest ) = $matches;
// Characters that will be ignored in IDNs.
@@ -1620,7 +1802,7 @@ class Sanitizer {
* Does a string look like an e-mail address?
*
* This validates an email address using an HTML5 specification found at:
- * http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
+ * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
* Which as of 2011-01-24 says:
*
* A valid e-mail address is a string that matches the ABNF production
@@ -1641,20 +1823,20 @@ class Sanitizer {
*
* @since 1.18
*
- * @param $addr String E-mail address
+ * @param string $addr E-mail address
* @return Bool
*/
public static function validateEmail( $addr ) {
$result = null;
- if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
+ if ( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
return $result;
}
// Please note strings below are enclosed in brackets [], this make the
// hyphen "-" a range indicator. Hence it is double backslashed below.
// See bug 26948
- $rfc5322_atext = "a-z0-9!#$%&'*+\\-\/=?^_`{|}~" ;
- $rfc1034_ldh_str = "a-z0-9\\-" ;
+ $rfc5322_atext = "a-z0-9!#$%&'*+\\-\/=?^_`{|}~";
+ $rfc1034_ldh_str = "a-z0-9\\-";
$HTML5_email_regexp = "/
^ # start of string
@@ -1663,8 +1845,8 @@ class Sanitizer {
[$rfc1034_ldh_str]+ # First domain part
(\\.[$rfc1034_ldh_str]+)* # Following part prefixed with a dot
$ # End of string
- /ix" ; // case Insensitive, eXtended
+ /ix"; // case Insensitive, eXtended
- return (bool) preg_match( $HTML5_email_regexp, $addr );
+ return (bool)preg_match( $HTML5_email_regexp, $addr );
}
}
diff --git a/includes/ScopedCallback.php b/includes/ScopedCallback.php
new file mode 100644
index 00000000..ef22e0a3
--- /dev/null
+++ b/includes/ScopedCallback.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * This file deals with RAII style scoped callbacks.
+ *
+ * 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
+ */
+
+/**
+ * Class for asserting that a callback happens when an dummy object leaves scope
+ *
+ * @since 1.21
+ */
+class ScopedCallback {
+ /** @var callable */
+ protected $callback;
+
+ /**
+ * @param callable $callback
+ * @throws MWException
+ */
+ public function __construct( $callback ) {
+ if ( !is_callable( $callback ) ) {
+ throw new MWException( "Provided callback is not valid." );
+ }
+ $this->callback = $callback;
+ }
+
+ /**
+ * Trigger a scoped callback and destroy it.
+ * This is the same is just setting it to null.
+ *
+ * @param ScopedCallback $sc
+ */
+ public static function consume( ScopedCallback &$sc = null ) {
+ $sc = null;
+ }
+
+ /**
+ * Destroy a scoped callback without triggering it
+ *
+ * @param ScopedCallback $sc
+ */
+ public static function cancel( ScopedCallback &$sc = null ) {
+ if ( $sc ) {
+ $sc->callback = null;
+ }
+ $sc = null;
+ }
+
+ /**
+ * Trigger the callback when this leaves scope
+ */
+ function __destruct() {
+ if ( $this->callback !== null ) {
+ call_user_func( $this->callback );
+ }
+ }
+}
diff --git a/includes/SeleniumWebSettings.php b/includes/SeleniumWebSettings.php
deleted file mode 100644
index 7b98568d..00000000
--- a/includes/SeleniumWebSettings.php
+++ /dev/null
@@ -1,222 +0,0 @@
-<?php
-/**
- * Dynamically change configuration variables based on the test suite name and a cookie value.
- *
- * For details on how to configure a wiki for a Selenium test, see:
- * http://www.mediawiki.org/wiki/SeleniumFramework#Test_Wiki_configuration
- *
- * 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
- */
-
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( 1 );
-}
-
-require_once( "$IP/includes/GlobalFunctions.php" );
-
-$fname = 'SeleniumWebSettings.php';
-wfProfileIn( $fname );
-
-$cookiePrefix = $wgSitename . '-';
-$cookieName = $cookiePrefix . 'Selenium';
-
-// this is a fallback SQL file
-$testSqlFile = false;
-$testImageZip = false;
-
-// if we find a request parameter containing the test name, set a cookie with the test name
-if ( isset( $_GET['setupTestSuite'] ) ) {
- $setupTestSuiteName = $_GET['setupTestSuite'];
-
- if (
- preg_match( '/[^a-zA-Z0-9_-]/', $setupTestSuiteName ) ||
- !isset( $wgSeleniumTestConfigs[$setupTestSuiteName] )
- )
- {
- return;
- }
- if ( strlen( $setupTestSuiteName ) > 0 ) {
- $expire = time() + 600;
- setcookie(
- $cookieName,
- $setupTestSuiteName,
- $expire,
- $wgCookiePath,
- $wgCookieDomain,
- $wgCookieSecure,
- true
- );
- }
-
- $testIncludes = array(); // array containing all the includes needed for this test
- $testGlobalConfigs = array(); // an array containg all the global configs needed for this test
- $testResourceFiles = array(); // an array containing all the resource files needed for this test
- $callback = $wgSeleniumTestConfigs[$setupTestSuiteName];
- call_user_func_array( $callback, array( &$testIncludes, &$testGlobalConfigs, &$testResourceFiles));
-
- if ( isset( $testResourceFiles['images'] ) ) {
- $testImageZip = $testResourceFiles['images'];
- }
-
- if ( isset( $testResourceFiles['db'] ) ) {
- $testSqlFile = $testResourceFiles['db'];
- $testResourceName = getTestResourceNameFromTestSuiteName( $setupTestSuiteName );
-
- switchToTestResources( $testResourceName, false ); // false means do not switch database yet
- setupTestResources( $testResourceName, $testSqlFile, $testImageZip );
- }
-}
-
-// clear the cookie based on a request param
-if ( isset( $_GET['clearTestSuite'] ) ) {
- $testSuiteName = getTestSuiteNameFromCookie( $cookieName );
-
- $expire = time() - 600;
- setcookie(
- $cookieName,
- '',
- $expire,
- $wgCookiePath,
- $wgCookieDomain,
- $wgCookieSecure,
- true
- );
-
- $testResourceName = getTestResourceNameFromTestSuiteName( $testSuiteName );
- teardownTestResources( $testResourceName );
-}
-
-// if a cookie is found, run the appropriate callback to get the config params.
-if ( isset( $_COOKIE[$cookieName] ) ) {
- $testSuiteName = getTestSuiteNameFromCookie( $cookieName );
- if ( !isset( $wgSeleniumTestConfigs[$testSuiteName] ) ) {
- return;
- }
-
- $testIncludes = array(); // array containing all the includes needed for this test
- $testGlobalConfigs = array(); // an array containg all the global configs needed for this test
- $testResourceFiles = array(); // an array containing all the resource files needed for this test
- $callback = $wgSeleniumTestConfigs[$testSuiteName];
- call_user_func_array( $callback, array( &$testIncludes, &$testGlobalConfigs, &$testResourceFiles));
-
- if ( isset( $testResourceFiles['db'] ) ) {
- $testResourceName = getTestResourceNameFromTestSuiteName( $testSuiteName );
- switchToTestResources( $testResourceName );
- }
- foreach ( $testIncludes as $includeFile ) {
- $file = $IP . '/' . $includeFile;
- require_once( $file );
- }
- foreach ( $testGlobalConfigs as $key => $value ) {
- if ( is_array( $value ) ) {
- $GLOBALS[$key] = array_merge( $GLOBALS[$key], $value );
-
- } else {
- $GLOBALS[$key] = $value;
- }
- }
-}
-
-wfProfileOut( $fname );
-
-function getTestSuiteNameFromCookie( $cookieName ) {
- $testSuiteName = null;
- if ( isset( $_COOKIE[$cookieName] ) ) {
- $testSuiteName = $_COOKIE[$cookieName];
- }
- return $testSuiteName;
-}
-
-function getTestResourceNameFromTestSuiteName( $testSuiteName ) {
- $testResourceName = null;
- if ( isset( $testSuiteName ) ) {
- $testResourceName = $testSuiteName;
- }
- return $testResourceName;
-}
-
-function getTestUploadPathFromResourceName( $testResourceName ) {
- global $IP;
- $testUploadPath = "$IP/images/$testResourceName";
- return $testUploadPath;
-}
-
-function setupTestResources( $testResourceName, $testSqlFile, $testImageZip ) {
- global $wgDBname;
-
- // Basic security. Do not allow to drop productive database.
- if ( $testResourceName == $wgDBname ) {
- die( 'Cannot override productive database.' );
- }
- if ( $testResourceName == '' ) {
- die( 'Cannot identify a test the resources should be installed for.' );
- }
-
- // create tables
- $dbw = wfGetDB( DB_MASTER );
- $dbw->query( 'DROP DATABASE IF EXISTS ' . $testResourceName );
- $dbw->query( 'CREATE DATABASE ' . $testResourceName );
-
- // do not set the new DB name before database is setup
- $wgDBname = $testResourceName;
- $dbw->selectDB( $testResourceName );
- // populate from SQL file
- if ( $testSqlFile ) {
- $dbw->sourceFile( $testSqlFile );
- }
-
- // create test image dir
- $testUploadPath = getTestUploadPathFromResourceName( $testResourceName );
- if ( !file_exists( $testUploadPath ) ) {
- mkdir( $testUploadPath );
- }
-
- if ( $testImageZip ) {
- $zip = new ZipArchive();
- $zip->open( $testImageZip );
- $zip->extractTo( $testUploadPath );
- $zip->close();
- }
-}
-
-function teardownTestResources( $testResourceName ) {
- // remove test database
- $dbw = wfGetDB( DB_MASTER );
- $dbw->query( 'DROP DATABASE IF EXISTS ' . $testResourceName );
-
- $testUploadPath = getTestUploadPathFromResourceName( $testResourceName );
- // remove test image dir
- if ( file_exists( $testUploadPath ) ) {
- wfRecursiveRemoveDir( $testUploadPath );
- }
-}
-
-function switchToTestResources( $testResourceName, $switchDB = true ) {
- global $wgDBuser, $wgDBpassword, $wgDBname;
- global $wgDBtestuser, $wgDBtestpassword;
- global $wgUploadPath;
-
- if ( $switchDB ) {
- $wgDBname = $testResourceName;
- }
- $wgDBuser = $wgDBtestuser;
- $wgDBpassword = $wgDBtestpassword;
-
- $testUploadPath = getTestUploadPathFromResourceName( $testResourceName );
- $wgUploadPath = $testUploadPath;
-}
diff --git a/includes/Setup.php b/includes/Setup.php
index 924c3c07..2e083d83 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -44,42 +44,59 @@ if ( !isset( $wgVersion ) ) {
}
// Set various default paths sensibly...
-if ( $wgScript === false ) $wgScript = "$wgScriptPath/index$wgScriptExtension";
-if ( $wgRedirectScript === false ) $wgRedirectScript = "$wgScriptPath/redirect$wgScriptExtension";
-if ( $wgLoadScript === false ) $wgLoadScript = "$wgScriptPath/load$wgScriptExtension";
+if ( $wgScript === false ) {
+ $wgScript = "$wgScriptPath/index$wgScriptExtension";
+}
+if ( $wgLoadScript === false ) {
+ $wgLoadScript = "$wgScriptPath/load$wgScriptExtension";
+}
if ( $wgArticlePath === false ) {
if ( $wgUsePathInfo ) {
- $wgArticlePath = "$wgScript/$1";
+ $wgArticlePath = "$wgScript/$1";
} else {
- $wgArticlePath = "$wgScript?title=$1";
+ $wgArticlePath = "$wgScript?title=$1";
}
}
-if ( !empty($wgActionPaths) && !isset($wgActionPaths['view']) ) {
+if ( !empty( $wgActionPaths ) && !isset( $wgActionPaths['view'] ) ) {
# 'view' is assumed the default action path everywhere in the code
# but is rarely filled in $wgActionPaths
$wgActionPaths['view'] = $wgArticlePath;
}
-if ( !empty($wgActionPaths) && !isset($wgActionPaths['view']) ) {
- # 'view' is assumed the default action path everywhere in the code
- # but is rarely filled in $wgActionPaths
- $wgActionPaths['view'] = $wgArticlePath ;
+if ( $wgStylePath === false ) {
+ $wgStylePath = "$wgScriptPath/skins";
+}
+if ( $wgLocalStylePath === false ) {
+ $wgLocalStylePath = "$wgScriptPath/skins";
+}
+if ( $wgStyleDirectory === false ) {
+ $wgStyleDirectory = "$IP/skins";
+}
+if ( $wgExtensionAssetsPath === false ) {
+ $wgExtensionAssetsPath = "$wgScriptPath/extensions";
}
-if ( $wgStylePath === false ) $wgStylePath = "$wgScriptPath/skins";
-if ( $wgLocalStylePath === false ) $wgLocalStylePath = "$wgScriptPath/skins";
-if ( $wgStyleDirectory === false ) $wgStyleDirectory = "$IP/skins";
-if ( $wgExtensionAssetsPath === false ) $wgExtensionAssetsPath = "$wgScriptPath/extensions";
-
-if ( $wgLogo === false ) $wgLogo = "$wgStylePath/common/images/wiki.png";
+if ( $wgLogo === false ) {
+ $wgLogo = "$wgStylePath/common/images/wiki.png";
+}
-if ( $wgUploadPath === false ) $wgUploadPath = "$wgScriptPath/images";
-if ( $wgUploadDirectory === false ) $wgUploadDirectory = "$IP/images";
-if ( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR";
-if ( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache";
-if ( $wgDeletedDirectory === false ) $wgDeletedDirectory = "{$wgUploadDirectory}/deleted";
+if ( $wgUploadPath === false ) {
+ $wgUploadPath = "$wgScriptPath/images";
+}
+if ( $wgUploadDirectory === false ) {
+ $wgUploadDirectory = "$IP/images";
+}
+if ( $wgReadOnlyFile === false ) {
+ $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR";
+}
+if ( $wgFileCacheDirectory === false ) {
+ $wgFileCacheDirectory = "{$wgUploadDirectory}/cache";
+}
+if ( $wgDeletedDirectory === false ) {
+ $wgDeletedDirectory = "{$wgUploadDirectory}/deleted";
+}
if ( isset( $wgFileStore['deleted']['directory'] ) ) {
$wgDeletedDirectory = $wgFileStore['deleted']['directory'];
@@ -130,13 +147,13 @@ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
* Initialise $wgLockManagers to include basic FS version
*/
$wgLockManagers[] = array(
- 'name' => 'fsLockManager',
- 'class' => 'FSLockManager',
+ 'name' => 'fsLockManager',
+ 'class' => 'FSLockManager',
'lockDirectory' => "{$wgUploadDirectory}/lockdir",
);
$wgLockManagers[] = array(
- 'name' => 'nullLockManager',
- 'class' => 'NullLockManager',
+ 'name' => 'nullLockManager',
+ 'class' => 'NullLockManager',
);
/**
@@ -202,15 +219,15 @@ if ( $wgUseSharedUploads ) {
}
if ( $wgUseInstantCommons ) {
$wgForeignFileRepos[] = array(
- 'class' => 'ForeignAPIRepo',
- 'name' => 'wikimediacommons',
- 'apibase' => WebRequest::detectProtocol() === 'https' ?
+ 'class' => 'ForeignAPIRepo',
+ 'name' => 'wikimediacommons',
+ 'apibase' => WebRequest::detectProtocol() === 'https' ?
'https://commons.wikimedia.org/w/api.php' :
'http://commons.wikimedia.org/w/api.php',
- 'hashLevels' => 2,
- 'fetchDescription' => true,
+ 'hashLevels' => 2,
+ 'fetchDescription' => true,
'descriptionCacheExpiry' => 43200,
- 'apiThumbCacheExpiry' => 86400,
+ 'apiThumbCacheExpiry' => 86400,
);
}
/*
@@ -276,6 +293,13 @@ if ( $wgMetaNamespace === false ) {
$wgMetaNamespace = str_replace( ' ', '_', $wgSitename );
}
+
+// Default value is either the suhosin limit or -1 for unlimited
+if ( $wgResourceLoaderMaxQueryLength === false ) {
+ $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
+ $wgResourceLoaderMaxQueryLength = $maxValueLength > 0 ? $maxValueLength : -1;
+}
+
/**
* Definitions of the NS_ constants are in Defines.php
* @private
@@ -301,7 +325,7 @@ $wgCanonicalNamespaceNames = array(
);
/// @todo UGLY UGLY
-if( is_array( $wgExtraNamespaces ) ) {
+if ( is_array( $wgExtraNamespaces ) ) {
$wgCanonicalNamespaceNames = $wgCanonicalNamespaceNames + $wgExtraNamespaces;
}
@@ -317,23 +341,23 @@ if ( $wgUseFileCache || $wgUseSquid ) {
$wgDebugToolbar = false;
}
-# $wgAllowRealName and $wgAllowUserSkin were removed in 1.16
-# in favor of $wgHiddenPrefs, handle b/c here
-if ( !$wgAllowRealName ) {
- $wgHiddenPrefs[] = 'realname';
-}
-
# Doesn't make sense to have if disabled.
if ( !$wgEnotifMinorEdits ) {
$wgHiddenPrefs[] = 'enotifminoredits';
}
# $wgDisabledActions is deprecated as of 1.18
-foreach( $wgDisabledActions as $action ){
+foreach ( $wgDisabledActions as $action ) {
$wgActions[$action] = false;
}
-if ( !$wgHtml5Version && $wgHtml5 && $wgAllowRdfaAttributes ) {
+# We always output HTML5 since 1.22, overriding these is no longer supported
+# we set them here for extensions that depend on its value.
+$wgHtml5 = true;
+$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
+$wgJsMimeType = 'text/javascript';
+
+if ( !$wgHtml5Version && $wgAllowRdfaAttributes ) {
# see http://www.w3.org/TR/rdfa-in-html/#document-conformance
if ( $wgMimeType == 'application/xhtml+xml' ) {
$wgHtml5Version = 'XHTML+RDFa 1.0';
@@ -343,7 +367,7 @@ if ( !$wgHtml5Version && $wgHtml5 && $wgAllowRdfaAttributes ) {
}
# Blacklisted file extensions shouldn't appear on the "allowed" list
-$wgFileExtensions = array_diff ( $wgFileExtensions, $wgFileBlacklist );
+$wgFileExtensions = array_values( array_diff ( $wgFileExtensions, $wgFileBlacklist ) );
if ( $wgArticleCountMethod === null ) {
$wgArticleCountMethod = $wgUseCommaCount ? 'comma' : 'link';
@@ -353,23 +377,29 @@ if ( $wgInvalidateCacheOnLocalSettingsChange ) {
$wgCacheEpoch = max( $wgCacheEpoch, gmdate( 'YmdHis', @filemtime( "$IP/LocalSettings.php" ) ) );
}
-if ( $wgAjaxUploadDestCheck ) {
- $wgAjaxExportList[] = 'SpecialUpload::ajaxGetExistsWarning';
-}
-
if ( $wgNewUserLog ) {
# Add a new log type
- $wgLogTypes[] = 'newusers';
- $wgLogNames['newusers'] = 'newuserlogpage';
- $wgLogHeaders['newusers'] = 'newuserlogpagetext';
+ $wgLogTypes[] = 'newusers';
+ $wgLogNames['newusers'] = 'newuserlogpage';
+ $wgLogHeaders['newusers'] = 'newuserlogpagetext';
$wgLogActionsHandlers['newusers/newusers'] = 'NewUsersLogFormatter';
$wgLogActionsHandlers['newusers/create'] = 'NewUsersLogFormatter';
$wgLogActionsHandlers['newusers/create2'] = 'NewUsersLogFormatter';
+ $wgLogActionsHandlers['newusers/byemail'] = 'NewUsersLogFormatter';
$wgLogActionsHandlers['newusers/autocreate'] = 'NewUsersLogFormatter';
}
if ( $wgCookieSecure === 'detect' ) {
- $wgCookieSecure = ( WebRequest::detectProtocol() === 'https:' );
+ $wgCookieSecure = ( WebRequest::detectProtocol() === 'https' );
+}
+
+if ( $wgRC2UDPAddress ) {
+ $wgRCFeeds['default'] = array(
+ 'formatter' => 'IRCColourfulRCFeedFormatter',
+ 'uri' => "udp://$wgRC2UDPAddress:$wgRC2UDPPort/$wgRC2UDPPrefix",
+ 'add_interwiki_prefix' => &$wgRC2UDPInterwikiPrefix,
+ 'omit_bots' => &$wgRC2UDPOmitBots,
+ );
}
// Disable MWDebug for command line mode, this prevents MWDebug from eating up
@@ -379,21 +409,24 @@ if ( $wgDebugToolbar && !$wgCommandLineMode ) {
MWDebug::init();
}
-if ( !defined( 'MW_COMPILED' ) ) {
- if ( !MWInit::classExists( 'AutoLoader' ) ) {
- require_once( "$IP/includes/AutoLoader.php" );
- }
+if ( !class_exists( 'AutoLoader' ) ) {
+ require_once "$IP/includes/AutoLoader.php";
+}
+
+wfProfileIn( $fname . '-exception' );
+MWExceptionHandler::installHandler();
+wfProfileOut( $fname . '-exception' );
- wfProfileIn( $fname . '-exception' );
- MWExceptionHandler::installHandler();
- wfProfileOut( $fname . '-exception' );
+wfProfileIn( $fname . '-includes' );
+require_once "$IP/includes/normal/UtfNormalUtil.php";
+require_once "$IP/includes/GlobalFunctions.php";
+require_once "$IP/includes/ProxyTools.php";
+require_once "$IP/includes/normal/UtfNormalDefines.php";
+wfProfileOut( $fname . '-includes' );
- wfProfileIn( $fname . '-includes' );
- require_once( "$IP/includes/normal/UtfNormalUtil.php" );
- require_once( "$IP/includes/GlobalFunctions.php" );
- require_once( "$IP/includes/ProxyTools.php" );
- require_once( "$IP/includes/normal/UtfNormalDefines.php" );
- wfProfileOut( $fname . '-includes' );
+if ( $wgSecureLogin && substr( $wgServer, 0, 2 ) !== '//' ) {
+ $wgSecureLogin = false;
+ wfWarn( 'Secure login was enabled on a server that only supports HTTP or HTTPS. Disabling secure login.' );
}
# Now that GlobalFunctions is loaded, set defaults that depend
@@ -406,9 +439,16 @@ if ( $wgCanonicalServer === false ) {
$wgCanonicalServer = wfExpandUrl( $wgServer, PROTO_HTTP );
}
-// Initialize $wgHTCPMulticastRouting from backwards-compatible settings
-if ( !$wgHTCPMulticastRouting && $wgHTCPMulticastAddress ) {
- $wgHTCPMulticastRouting = array(
+// $wgHTCPMulticastRouting got renamed to $wgHTCPRouting in MediaWiki 1.22
+// ensure back compatibility.
+if ( !$wgHTCPRouting && $wgHTCPMulticastRouting ) {
+ $wgHTCPRouting = $wgHTCPMulticastRouting;
+}
+
+// Initialize $wgHTCPRouting from backwards-compatible settings that
+// comes from pre 1.20 version.
+if ( !$wgHTCPRouting && $wgHTCPMulticastAddress ) {
+ $wgHTCPRouting = array(
'' => array(
'host' => $wgHTCPMulticastAddress,
'port' => $wgHTCPPort,
@@ -426,14 +466,14 @@ wfMemoryLimit();
* that happens whenever you use a date function without the timezone being
* explicitly set. Inspired by phpMyAdmin's treatment of the problem.
*/
-if ( is_null( $wgLocaltimezone) ) {
+if ( is_null( $wgLocaltimezone ) ) {
wfSuppressWarnings();
$wgLocaltimezone = date_default_timezone_get();
wfRestoreWarnings();
}
date_default_timezone_set( $wgLocaltimezone );
-if( is_null( $wgLocalTZoffset ) ) {
+if ( is_null( $wgLocalTZoffset ) ) {
$wgLocalTZoffset = date( 'Z' ) / 60;
}
@@ -539,10 +579,11 @@ foreach ( $wgExtensionFunctions as $func ) {
if ( is_object( $func ) && $func instanceof Closure ) {
$profName = $fname . '-extensions-closure';
} elseif ( is_array( $func ) ) {
- if ( is_object( $func[0] ) )
+ if ( is_object( $func[0] ) ) {
$profName = $fname . '-extensions-' . get_class( $func[0] ) . '::' . $func[1];
- else
+ } else {
$profName = $fname . '-extensions-' . implode( '::', $func );
+ }
} else {
$profName = $fname . '-extensions-' . strval( $func );
}
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index 6a861d8e..fa871fe0 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -83,7 +83,7 @@
* $conf->settings = array(
* 'wgMergeSetting' = array(
* # Value that will be shared among all wikis:
- * 'default' => array( NS_USER => true ),
+ * 'default' => array( NS_USER => true ),
*
* # Leading '+' means merging the array of value with the defaults
* '+beta' => array( NS_HELP => true ),
@@ -162,12 +162,18 @@ class SiteConfiguration {
public $siteParamsCallback = null;
/**
+ * Configuration cache for getConfig()
+ * @var array
+ */
+ protected $cfgCache = array();
+
+ /**
* Retrieves a configuration setting for a given wiki.
- * @param $settingName String ID of the setting name to retrieve
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param string $settingName ID of the setting name to retrieve
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $params List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param array $wikiTags The tags assigned to the wiki.
* @return Mixed the value of the setting requested.
*/
public function get( $settingName, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
@@ -178,18 +184,18 @@ class SiteConfiguration {
/**
* Really retrieves a configuration setting for a given wiki.
*
- * @param $settingName String ID of the setting name to retrieve.
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $params Array: array of parameters.
+ * @param string $settingName ID of the setting name to retrieve.
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param array $params array of parameters.
* @return Mixed the value of the setting requested.
*/
- protected function getSetting( $settingName, $wiki, /*array*/ $params ){
+ protected function getSetting( $settingName, $wiki, /*array*/ $params ) {
$retval = null;
- if( array_key_exists( $settingName, $this->settings ) ) {
+ if ( array_key_exists( $settingName, $this->settings ) ) {
$thisSetting =& $this->settings[$settingName];
do {
// Do individual wiki settings
- if( array_key_exists( $wiki, $thisSetting ) ) {
+ if ( array_key_exists( $wiki, $thisSetting ) ) {
$retval = $thisSetting[$wiki];
break;
} elseif ( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) {
@@ -197,16 +203,16 @@ class SiteConfiguration {
}
// Do tag settings
- foreach( $params['tags'] as $tag ) {
- if( array_key_exists( $tag, $thisSetting ) ) {
+ foreach ( $params['tags'] as $tag ) {
+ if ( array_key_exists( $tag, $thisSetting ) ) {
if ( isset( $retval ) && is_array( $retval ) && is_array( $thisSetting[$tag] ) ) {
$retval = self::arrayMerge( $retval, $thisSetting[$tag] );
} else {
$retval = $thisSetting[$tag];
}
break 2;
- } elseif( array_key_exists( "+$tag", $thisSetting ) && is_array($thisSetting["+$tag"]) ) {
- if( !isset( $retval ) ) {
+ } elseif ( array_key_exists( "+$tag", $thisSetting ) && is_array( $thisSetting["+$tag"] ) ) {
+ if ( !isset( $retval ) ) {
$retval = array();
}
$retval = self::arrayMerge( $retval, $thisSetting["+$tag"] );
@@ -214,15 +220,15 @@ class SiteConfiguration {
}
// Do suffix settings
$suffix = $params['suffix'];
- if( !is_null( $suffix ) ) {
- if( array_key_exists( $suffix, $thisSetting ) ) {
- if ( isset($retval) && is_array($retval) && is_array($thisSetting[$suffix]) ) {
+ if ( !is_null( $suffix ) ) {
+ if ( array_key_exists( $suffix, $thisSetting ) ) {
+ if ( isset( $retval ) && is_array( $retval ) && is_array( $thisSetting[$suffix] ) ) {
$retval = self::arrayMerge( $retval, $thisSetting[$suffix] );
} else {
$retval = $thisSetting[$suffix];
}
break;
- } elseif ( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) {
+ } elseif ( array_key_exists( "+$suffix", $thisSetting ) && is_array( $thisSetting["+$suffix"] ) ) {
if ( !isset( $retval ) ) {
$retval = array();
}
@@ -231,8 +237,8 @@ class SiteConfiguration {
}
// Fall back to default.
- if( array_key_exists( 'default', $thisSetting ) ) {
- if( is_array( $retval ) && is_array( $thisSetting['default'] ) ) {
+ if ( array_key_exists( 'default', $thisSetting ) ) {
+ if ( is_array( $retval ) && is_array( $thisSetting['default'] ) ) {
$retval = self::arrayMerge( $retval, $thisSetting['default'] );
} else {
$retval = $thisSetting['default'];
@@ -242,7 +248,7 @@ class SiteConfiguration {
} while ( false );
}
- if( !is_null( $retval ) && count( $params['params'] ) ) {
+ if ( !is_null( $retval ) && count( $params['params'] ) ) {
foreach ( $params['params'] as $key => $value ) {
$retval = $this->doReplace( '$' . $key, $value, $retval );
}
@@ -260,10 +266,10 @@ class SiteConfiguration {
* @return string
*/
function doReplace( $from, $to, $in ) {
- if( is_string( $in ) ) {
+ if ( is_string( $in ) ) {
return str_replace( $from, $to, $in );
- } elseif( is_array( $in ) ) {
- foreach( $in as $key => $val ) {
+ } elseif ( is_array( $in ) ) {
+ foreach ( $in as $key => $val ) {
$in[$key] = $this->doReplace( $from, $to, $val );
}
return $in;
@@ -274,16 +280,16 @@ class SiteConfiguration {
/**
* Gets all settings for a wiki
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $params List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param array $wikiTags The tags assigned to the wiki.
* @return Array Array of settings requested.
*/
public function getAll( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
$params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
$localSettings = array();
- foreach( $this->settings as $varname => $stuff ) {
+ foreach ( $this->settings as $varname => $stuff ) {
$append = false;
$var = $varname;
if ( substr( $varname, 0, 1 ) == '+' ) {
@@ -304,14 +310,14 @@ class SiteConfiguration {
/**
* Retrieves a configuration setting for a given wiki, forced to a boolean.
- * @param $setting String ID of the setting name to retrieve
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param string $setting ID of the setting name to retrieve
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $wikiTags The tags assigned to the wiki.
* @return bool The value of the setting requested.
*/
public function getBool( $setting, $wiki, $suffix = null, $wikiTags = array() ) {
- return (bool)($this->get( $setting, $wiki, $suffix, array(), $wikiTags ) );
+ return (bool)$this->get( $setting, $wiki, $suffix, array(), $wikiTags );
}
/**
@@ -325,12 +331,12 @@ class SiteConfiguration {
/**
* Retrieves the value of a given setting, and places it in a variable passed by reference.
- * @param $setting String ID of the setting name to retrieve
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $var array Reference The variable to insert the value into.
- * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param string $setting ID of the setting name to retrieve
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $var Reference The variable to insert the value into.
+ * @param array $params List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param array $wikiTags The tags assigned to the wiki.
*/
public function extractVar( $setting, $wiki, $suffix, &$var, $params = array(), $wikiTags = array() ) {
$value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags );
@@ -341,11 +347,11 @@ class SiteConfiguration {
/**
* Retrieves the value of a given setting, and places it in its corresponding global variable.
- * @param $setting String ID of the setting name to retrieve
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param string $setting ID of the setting name to retrieve
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $params List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param array $wikiTags The tags assigned to the wiki.
*/
public function extractGlobal( $setting, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
$params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
@@ -360,9 +366,9 @@ class SiteConfiguration {
public function extractGlobalSetting( $setting, $wiki, $params ) {
$value = $this->getSetting( $setting, $wiki, $params );
if ( !is_null( $value ) ) {
- if (substr($setting,0,1) == '+' && is_array($value)) {
- $setting = substr($setting,1);
- if ( is_array($GLOBALS[$setting]) ) {
+ if ( substr( $setting, 0, 1 ) == '+' && is_array( $value ) ) {
+ $setting = substr( $setting, 1 );
+ if ( is_array( $GLOBALS[$setting] ) ) {
$GLOBALS[$setting] = self::arrayMerge( $GLOBALS[$setting], $value );
} else {
$GLOBALS[$setting] = $value;
@@ -375,10 +381,10 @@ class SiteConfiguration {
/**
* Retrieves the values of all settings, and places them in their corresponding global variables.
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $params List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param array $wikiTags The tags assigned to the wiki.
*/
public function extractAllGlobals( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
$params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
@@ -395,7 +401,7 @@ class SiteConfiguration {
* @param $wiki String
* @return array
*/
- protected function getWikiParams( $wiki ){
+ protected function getWikiParams( $wiki ) {
static $default = array(
'suffix' => null,
'lang' => null,
@@ -403,18 +409,18 @@ class SiteConfiguration {
'params' => array(),
);
- if( !is_callable( $this->siteParamsCallback ) ) {
+ if ( !is_callable( $this->siteParamsCallback ) ) {
return $default;
}
$ret = call_user_func_array( $this->siteParamsCallback, array( $this, $wiki ) );
# Validate the returned value
- if( !is_array( $ret ) ) {
+ if ( !is_array( $ret ) ) {
return $default;
}
- foreach( $default as $name => $def ){
- if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) {
+ foreach ( $default as $name => $def ) {
+ if ( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) {
$ret[$name] = $default[$name];
}
}
@@ -427,17 +433,17 @@ class SiteConfiguration {
* by self::$siteParamsCallback for backward compatibility
* Values returned by self::getWikiParams() have the priority.
*
- * @param $wiki String Wiki ID of the wiki in question.
- * @param $suffix String The suffix of the wiki in question.
- * @param $params Array List of parameters. $.'key' is replaced by $value in
+ * @param string $wiki Wiki ID of the wiki in question.
+ * @param string $suffix The suffix of the wiki in question.
+ * @param array $params List of parameters. $.'key' is replaced by $value in
* all returned data.
- * @param $wikiTags Array The tags assigned to the wiki.
+ * @param array $wikiTags The tags assigned to the wiki.
* @return array
*/
- protected function mergeParams( $wiki, $suffix, /*array*/ $params, /*array*/ $wikiTags ){
+ protected function mergeParams( $wiki, $suffix, /*array*/ $params, /*array*/ $wikiTags ) {
$ret = $this->getWikiParams( $wiki );
- if( is_null( $ret['suffix'] ) ) {
+ if ( is_null( $ret['suffix'] ) ) {
$ret['suffix'] = $suffix;
}
@@ -446,10 +452,10 @@ class SiteConfiguration {
$ret['params'] += $params;
// Automatically fill that ones if needed
- if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) ){
+ if ( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) ) {
$ret['params']['lang'] = $ret['lang'];
}
- if( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) ) {
+ if ( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) ) {
$ret['params']['site'] = $ret['suffix'];
}
@@ -465,19 +471,19 @@ class SiteConfiguration {
public function siteFromDB( $db ) {
// Allow override
$def = $this->getWikiParams( $db );
- if( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) {
+ if ( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) {
return array( $def['suffix'], $def['lang'] );
}
$site = null;
$lang = null;
- foreach ( $this->suffixes as $suffix ) {
+ foreach ( $this->suffixes as $altSite => $suffix ) {
if ( $suffix === '' ) {
$site = '';
$lang = $db;
break;
} elseif ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
- $site = $suffix == 'wiki' ? 'wikipedia' : $suffix;
+ $site = is_numeric( $altSite ) ? $suffix : $altSite;
$lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) );
break;
}
@@ -487,6 +493,67 @@ class SiteConfiguration {
}
/**
+ * Get the resolved (post-setup) configuration of a potentially foreign wiki.
+ * For foreign wikis, this is expensive, and only works if maintenance
+ * scripts are setup to handle the --wiki parameter such as in wiki farms.
+ *
+ * @param string $wiki
+ * @param array|string $settings A setting name or array of setting names
+ * @return Array|mixed Array if $settings is an array, otherwise the value
+ * @throws MWException
+ * @since 1.21
+ */
+ public function getConfig( $wiki, $settings ) {
+ global $IP;
+
+ $multi = is_array( $settings );
+ $settings = (array)$settings;
+ if ( $wiki === wfWikiID() ) { // $wiki is this wiki
+ $res = array();
+ foreach ( $settings as $name ) {
+ if ( !preg_match( '/^wg[A-Z]/', $name ) ) {
+ throw new MWException( "Variable '$name' does start with 'wg'." );
+ } elseif ( !isset( $GLOBALS[$name] ) ) {
+ throw new MWException( "Variable '$name' is not set." );
+ }
+ $res[$name] = $GLOBALS[$name];
+ }
+ } else { // $wiki is a foreign wiki
+ if ( isset( $this->cfgCache[$wiki] ) ) {
+ $res = array_intersect_key( $this->cfgCache[$wiki], array_flip( $settings ) );
+ if ( count( $res ) == count( $settings ) ) {
+ return $res; // cache hit
+ }
+ } elseif ( !in_array( $wiki, $this->wikis ) ) {
+ throw new MWException( "No such wiki '$wiki'." );
+ } else {
+ $this->cfgCache[$wiki] = array();
+ }
+ $retVal = 1;
+ $cmd = wfShellWikiCmd(
+ "$IP/maintenance/getConfiguration.php",
+ array(
+ '--wiki', $wiki,
+ '--settings', implode( ' ', $settings ),
+ '--format', 'PHP'
+ )
+ );
+ // ulimit5.sh breaks this call
+ $data = trim( wfShellExec( $cmd, $retVal, array(), array( 'memory' => 0 ) ) );
+ if ( $retVal != 0 || !strlen( $data ) ) {
+ throw new MWException( "Failed to run getConfiguration.php." );
+ }
+ $res = unserialize( $data );
+ if ( !is_array( $res ) ) {
+ throw new MWException( "Failed to unserialize configuration array." );
+ }
+ $this->cfgCache[$wiki] = $this->cfgCache[$wiki] + $res;
+ }
+
+ return $multi ? $res : current( $res );
+ }
+
+ /**
* Returns true if the given vhost is handled locally.
* @param $vhost String
* @return bool
@@ -507,11 +574,11 @@ class SiteConfiguration {
*/
static function arrayMerge( $array1/* ... */ ) {
$out = $array1;
- for( $i = 1; $i < func_num_args(); $i++ ) {
- foreach( func_get_arg( $i ) as $key => $value ) {
- if ( isset($out[$key]) && is_array($out[$key]) && is_array($value) ) {
+ for ( $i = 1; $i < func_num_args(); $i++ ) {
+ foreach ( func_get_arg( $i ) as $key => $value ) {
+ if ( isset( $out[$key] ) && is_array( $out[$key] ) && is_array( $value ) ) {
$out[$key] = self::arrayMerge( $out[$key], $value );
- } elseif ( !isset($out[$key]) || !$out[$key] && !is_numeric($key) ) {
+ } elseif ( !isset( $out[$key] ) || !$out[$key] && !is_numeric( $key ) ) {
// Values that evaluate to true given precedence, for the primary purpose of merging permissions arrays.
$out[$key] = $value;
} elseif ( is_numeric( $key ) ) {
diff --git a/includes/SiteStats.php b/includes/SiteStats.php
index 1c2c454d..355993c6 100644
--- a/includes/SiteStats.php
+++ b/includes/SiteStats.php
@@ -48,8 +48,7 @@ class SiteStats {
# Update schema
$u = new SiteStatsUpdate( 0, 0, 0 );
$u->doUpdate();
- $dbr = wfGetDB( DB_SLAVE );
- self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ );
+ self::$row = self::doLoad( wfGetDB( DB_SLAVE ) );
}
self::$loaded = true;
@@ -62,13 +61,13 @@ class SiteStats {
wfDebug( __METHOD__ . ": reading site_stats from slave\n" );
$row = self::doLoad( wfGetDB( DB_SLAVE ) );
- if( !self::isSane( $row ) ) {
+ if ( !self::isSane( $row ) ) {
// Might have just been initialized during this request? Underflow?
wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" );
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
- if( !self::isSane( $row ) ) {
+ if ( !self::isSane( $row ) ) {
// Normally the site_stats table is initialized at install time.
// Some manual construction scenarios may leave the table empty or
// broken, however, for instance when importing from a dump into a
@@ -80,7 +79,7 @@ class SiteStats {
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
- if( !self::isSane( $row ) ) {
+ if ( !self::isSane( $row ) ) {
wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
}
return $row;
@@ -91,7 +90,16 @@ class SiteStats {
* @return Bool|ResultWrapper
*/
static function doLoad( $db ) {
- return $db->selectRow( 'site_stats', '*', false, __METHOD__ );
+ return $db->selectRow( 'site_stats', array(
+ 'ss_row_id',
+ 'ss_total_views',
+ 'ss_total_edits',
+ 'ss_good_articles',
+ 'ss_total_pages',
+ 'ss_users',
+ 'ss_active_users',
+ 'ss_images',
+ ), false, __METHOD__ );
}
/**
@@ -152,7 +160,7 @@ class SiteStats {
/**
* Find the number of users in a given user group.
- * @param $group String: name of group
+ * @param string $group name of group
* @return Integer
*/
static function numberingroup( $group ) {
@@ -181,7 +189,7 @@ class SiteStats {
static function jobs() {
if ( !isset( self::$jobs ) ) {
$dbr = wfGetDB( DB_SLAVE );
- self::$jobs = $dbr->estimateRowCount( 'job' );
+ self::$jobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
/* Zero rows still do single row read for row that doesn't exist, but people are annoyed by that */
if ( self::$jobs == 1 ) {
self::$jobs = 0;
@@ -197,7 +205,7 @@ class SiteStats {
*/
static function pagesInNs( $ns ) {
wfProfileIn( __METHOD__ );
- if( !isset( self::$pageCount[$ns] ) ) {
+ if ( !isset( self::$pageCount[$ns] ) ) {
$dbr = wfGetDB( DB_SLAVE );
self::$pageCount[$ns] = (int)$dbr->selectField(
'page',
@@ -218,20 +226,24 @@ class SiteStats {
* @return bool
*/
private static function isSane( $row ) {
- if(
- $row === false
+ if ( $row === false
|| $row->ss_total_pages < $row->ss_good_articles
|| $row->ss_total_edits < $row->ss_total_pages
+ || $row->ss_users < $row->ss_active_users
) {
return false;
}
// Now check for underflow/overflow
- foreach( array( 'total_views', 'total_edits', 'good_articles',
- 'total_pages', 'users', 'images' ) as $member ) {
- if(
- $row->{"ss_$member"} > 2000000000
- || $row->{"ss_$member"} < 0
- ) {
+ foreach ( array(
+ 'ss_total_views',
+ 'ss_total_edits',
+ 'ss_good_articles',
+ 'ss_total_pages',
+ 'ss_users',
+ 'ss_active_users',
+ 'ss_images',
+ ) as $member ) {
+ if ( $row->$member > 2000000000 || $row->$member < 0 ) {
return false;
}
}
@@ -250,7 +262,7 @@ class SiteStatsUpdate implements DeferrableUpdate {
protected $users = 0;
protected $images = 0;
- // @TODO: deprecate this constructor
+ // @todo deprecate this constructor
function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) {
$this->views = $views;
$this->edits = $edits;
@@ -285,50 +297,56 @@ class SiteStatsUpdate implements DeferrableUpdate {
if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) {
$this->doUpdatePendingDeltas();
} else {
- $dbw = wfGetDB( DB_MASTER );
-
- $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID
- if ( $rate ) {
- // Lock the table so we don't have double DB/memcached updates
- if ( !$dbw->lockIsFree( $lockKey, __METHOD__ )
- || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout
- ) {
- $this->doUpdatePendingDeltas();
- return;
- }
- $pd = $this->getPendingDeltas();
- // Piggy-back the async deltas onto those of this stats update....
- $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] );
- $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] );
- $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] );
- $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] );
- $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] );
- $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] );
- }
-
// Need a separate transaction because this a global lock
- $dbw->begin( __METHOD__ );
-
- // Build up an SQL query of deltas and apply them...
- $updates = '';
- $this->appendUpdate( $updates, 'ss_total_views', $this->views );
- $this->appendUpdate( $updates, 'ss_total_edits', $this->edits );
- $this->appendUpdate( $updates, 'ss_good_articles', $this->articles );
- $this->appendUpdate( $updates, 'ss_total_pages', $this->pages );
- $this->appendUpdate( $updates, 'ss_users', $this->users );
- $this->appendUpdate( $updates, 'ss_images', $this->images );
- if ( $updates != '' ) {
- $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ );
- }
+ wfGetDB( DB_MASTER )->onTransactionIdle( array( $this, 'tryDBUpdateInternal' ) );
+ }
+ }
+
+ /**
+ * Do not call this outside of SiteStatsUpdate
+ *
+ * @return void
+ */
+ public function tryDBUpdateInternal() {
+ global $wgSiteStatsAsyncFactor;
- if ( $rate ) {
- // Decrement the async deltas now that we applied them
- $this->removePendingDeltas( $pd );
- // Commit the updates and unlock the table
- $dbw->unlock( $lockKey, __METHOD__ );
+ $dbw = wfGetDB( DB_MASTER );
+ $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID
+ if ( $wgSiteStatsAsyncFactor ) {
+ // Lock the table so we don't have double DB/memcached updates
+ if ( !$dbw->lockIsFree( $lockKey, __METHOD__ )
+ || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout
+ ) {
+ $this->doUpdatePendingDeltas();
+ return;
}
+ $pd = $this->getPendingDeltas();
+ // Piggy-back the async deltas onto those of this stats update....
+ $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] );
+ $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] );
+ $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] );
+ $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] );
+ $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] );
+ $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] );
+ }
+
+ // Build up an SQL query of deltas and apply them...
+ $updates = '';
+ $this->appendUpdate( $updates, 'ss_total_views', $this->views );
+ $this->appendUpdate( $updates, 'ss_total_edits', $this->edits );
+ $this->appendUpdate( $updates, 'ss_good_articles', $this->articles );
+ $this->appendUpdate( $updates, 'ss_total_pages', $this->pages );
+ $this->appendUpdate( $updates, 'ss_users', $this->users );
+ $this->appendUpdate( $updates, 'ss_images', $this->images );
+ if ( $updates != '' ) {
+ $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ );
+ }
- $dbw->commit( __METHOD__ );
+ if ( $wgSiteStatsAsyncFactor ) {
+ // Decrement the async deltas now that we applied them
+ $this->removePendingDeltas( $pd );
+ // Commit the updates and unlock the table
+ $dbw->unlock( $lockKey, __METHOD__ );
}
}
@@ -348,7 +366,7 @@ class SiteStatsUpdate implements DeferrableUpdate {
'rc_user != 0',
'rc_bot' => 0,
'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL',
- 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ) ),
+ 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays * 24 * 3600 ) ),
),
__METHOD__
);
@@ -390,7 +408,7 @@ class SiteStatsUpdate implements DeferrableUpdate {
/**
* @param $type string
- * @param $sign string ('+' or '-')
+ * @param string $sign ('+' or '-')
* @return string
*/
private function getTypeCacheKey( $type, $sign ) {
@@ -443,7 +461,7 @@ class SiteStatsUpdate implements DeferrableUpdate {
/**
* Reduce pending delta counters after updates have been applied
- * @param Array $pd Result of getPendingDeltas(), used for DB update
+ * @param array $pd Result of getPendingDeltas(), used for DB update
* @return void
*/
protected function removePendingDeltas( array $pd ) {
@@ -566,7 +584,7 @@ class SiteStatsInit {
* @param $database DatabaseBase|bool
* - Boolean: whether to use the master DB
* - DatabaseBase: database connection to use
- * @param $options Array of options, may contain the following values
+ * @param array $options of options, may contain the following values
* - update Boolean: whether to update the current stats (true) or write fresh (false) (default: false)
* - views Boolean: when true, do not update the number of page views (default: true)
* - activeUsers Boolean: whether to update the number of active users (default: false)
@@ -584,19 +602,19 @@ class SiteStatsInit {
$counter->files();
// Only do views if we don't want to not count them
- if( $options['views'] ) {
+ if ( $options['views'] ) {
$counter->views();
}
// Update/refresh
- if( $options['update'] ) {
+ if ( $options['update'] ) {
$counter->update();
} else {
$counter->refresh();
}
// Count active users if need be
- if( $options['activeUsers'] ) {
+ if ( $options['activeUsers'] ) {
SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) );
}
}
diff --git a/includes/Skin.php b/includes/Skin.php
index 968f215e..5801806c 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -25,7 +25,7 @@
*/
/**
- * The main skin class that provide methods and properties for all other skins.
+ * The main skin class which provides methods and properties for all other skins.
* This base class is also the "Standard" skin.
*
* See docs/skin.txt for more information.
@@ -56,37 +56,40 @@ abstract class Skin extends ContextSource {
$skinDir = dir( $wgStyleDirectory );
- # while code from www.php.net
- while ( false !== ( $file = $skinDir->read() ) ) {
- // Skip non-PHP files, hidden files, and '.dep' includes
- $matches = array();
+ if ( $skinDir !== false && $skinDir !== null ) {
+ # while code from www.php.net
+ while ( false !== ( $file = $skinDir->read() ) ) {
+ // Skip non-PHP files, hidden files, and '.dep' includes
+ $matches = array();
- if ( preg_match( '/^([^.]*)\.php$/', $file, $matches ) ) {
- $aSkin = $matches[1];
- $wgValidSkinNames[strtolower( $aSkin )] = $aSkin;
+ if ( preg_match( '/^([^.]*)\.php$/', $file, $matches ) ) {
+ $aSkin = $matches[1];
+ $wgValidSkinNames[strtolower( $aSkin )] = $aSkin;
+ }
}
+ $skinDir->close();
}
- $skinDir->close();
$skinsInitialised = true;
wfProfileOut( __METHOD__ . '-init' );
}
return $wgValidSkinNames;
}
- /**
+ /**
* Fetch the skinname messages for available skins.
* @return array of strings
*/
static function getSkinNameMessages() {
$messages = array();
- foreach( self::getSkinNames() as $skinKey => $skinName ) {
+ foreach ( self::getSkinNames() as $skinKey => $skinName ) {
+ // Messages: skinname-cologneblue, skinname-monobook, skinname-modern, skinname-vector
$messages[] = "skinname-$skinKey";
}
return $messages;
}
/**
- * Fetch the list of usable skins in regards to $wgSkipSkins.
+ * Fetch the list of user-selectable skins in regards to $wgSkipSkins.
* Useful for Special:Preferences and other places where you
* only want to show skins users _can_ use.
* @return array of strings
@@ -94,20 +97,20 @@ abstract class Skin extends ContextSource {
public static function getUsableSkins() {
global $wgSkipSkins;
- $usableSkins = self::getSkinNames();
+ $allowedSkins = self::getSkinNames();
foreach ( $wgSkipSkins as $skip ) {
- unset( $usableSkins[$skip] );
+ unset( $allowedSkins[$skip] );
}
- return $usableSkins;
+ return $allowedSkins;
}
/**
* Normalize a skin preference value to a form that can be loaded.
* If a skin can't be found, it will fall back to the configured
* default (or the old 'Classic' skin if that's broken).
- * @param $key String: 'monobook', 'standard', etc.
+ * @param string $key 'monobook', 'standard', etc.
* @return string
*/
static function normalizeKey( $key ) {
@@ -129,7 +132,6 @@ abstract class Skin extends ContextSource {
// in the user preferences.
$fallback = array(
0 => $wgDefaultSkin,
- 1 => 'nostalgia',
2 => 'cologneblue'
);
@@ -148,7 +150,7 @@ abstract class Skin extends ContextSource {
/**
* Factory method for loading a skin of a given type
- * @param $key String: 'monobook', 'standard', etc.
+ * @param string $key 'monobook', 'standard', etc.
* @return Skin
*/
static function &newFromKey( $key ) {
@@ -161,23 +163,19 @@ abstract class Skin extends ContextSource {
$className = "Skin{$skinName}";
# Grab the skin class and initialise it.
- if ( !MWInit::classExists( $className ) ) {
+ if ( !class_exists( $className ) ) {
- if ( !defined( 'MW_COMPILED' ) ) {
- require_once( "{$wgStyleDirectory}/{$skinName}.php" );
- }
+ require_once "{$wgStyleDirectory}/{$skinName}.php";
- # Check if we got if not failback to default skin
- if ( !MWInit::classExists( $className ) ) {
+ # Check if we got if not fallback to default skin
+ if ( !class_exists( $className ) ) {
# DO NOT die if the class isn't found. This breaks maintenance
# scripts and can cause a user account to be unrecoverable
# except by SQL manipulation if a previously valid skin name
# is no longer valid.
wfDebug( "Skin class does not exist: $className\n" );
$className = 'SkinVector';
- if ( !defined( 'MW_COMPILED' ) ) {
- require_once( "{$wgStyleDirectory}/Vector.php" );
- }
+ require_once "{$wgStyleDirectory}/Vector.php";
}
}
$skin = new $className( $key );
@@ -201,6 +199,68 @@ abstract class Skin extends ContextSource {
}
/**
+ * Defines the ResourceLoader modules that should be added to the skin
+ * It is recommended that skins wishing to override call parent::getDefaultModules()
+ * and substitute out any modules they wish to change by using a key to look them up
+ * @return Array of modules with helper keys for easy overriding
+ */
+ public function getDefaultModules() {
+ global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
+ $wgAjaxWatch, $wgEnableAPI, $wgEnableWriteAPI;
+
+ $out = $this->getOutput();
+ $user = $out->getUser();
+ $modules = array(
+ // modules that enhance the page content in some way
+ 'content' => array(
+ 'mediawiki.page.ready',
+ ),
+ // modules that exist for legacy reasons
+ 'legacy' => array(),
+ // modules relating to search functionality
+ 'search' => array(),
+ // modules relating to functionality relating to watching an article
+ 'watch' => array(),
+ // modules which relate to the current users preferences
+ 'user' => array(),
+ );
+ if ( $wgIncludeLegacyJavaScript ) {
+ $modules['legacy'][] = 'mediawiki.legacy.wikibits';
+ }
+
+ if ( $wgPreloadJavaScriptMwUtil ) {
+ $modules['legacy'][] = 'mediawiki.util';
+ }
+
+ // Add various resources if required
+ if ( $wgUseAjax ) {
+ $modules['legacy'][] = 'mediawiki.legacy.ajax';
+
+ if ( $wgEnableAPI ) {
+ if ( $wgEnableWriteAPI && $wgAjaxWatch && $user->isLoggedIn()
+ && $user->isAllowed( 'writeapi' )
+ ) {
+ $modules['watch'][] = 'mediawiki.page.watch.ajax';
+ }
+
+ if ( !$user->getOption( 'disablesuggest', false ) ) {
+ $modules['search'][] = 'mediawiki.searchSuggest';
+ }
+ }
+ }
+
+ if ( $user->getBoolOption( 'editsectiononrightclick' ) ) {
+ $modules['user'][] = 'mediawiki.action.view.rightClickEdit';
+ }
+
+ // Crazy edit-on-double-click stuff
+ if ( $out->isArticle() && $user->getOption( 'editondblclick' ) ) {
+ $modules['user'][] = 'mediawiki.action.view.dblClickEdit';
+ }
+ return $modules;
+ }
+
+ /**
* Preload the existence of three commonly-requested pages in a single query
*/
function preloadExistence() {
@@ -262,7 +322,7 @@ abstract class Skin extends ContextSource {
* @return Title
*/
public function getRelevantTitle() {
- if ( isset($this->mRelevantTitle) ) {
+ if ( isset( $this->mRelevantTitle ) ) {
return $this->mRelevantTitle;
}
return $this->getTitle();
@@ -286,12 +346,12 @@ abstract class Skin extends ContextSource {
* @return User
*/
public function getRelevantUser() {
- if ( isset($this->mRelevantUser) ) {
+ if ( isset( $this->mRelevantUser ) ) {
return $this->mRelevantUser;
}
$title = $this->getRelevantTitle();
- if( $title->getNamespace() == NS_USER || $title->getNamespace() == NS_USER_TALK ) {
- $rootUser = strtok( $title->getText(), '/' );
+ if ( $title->hasSubjectNamespace( NS_USER ) ) {
+ $rootUser = $title->getRootText();
if ( User::isIP( $rootUser ) ) {
$this->mRelevantUser = User::newFromName( $rootUser, false );
} else {
@@ -435,7 +495,7 @@ abstract class Skin extends ContextSource {
$colon = $this->msg( 'colon-separator' )->escaped();
if ( !empty( $allCats['normal'] ) ) {
- $t = $embed . implode( "{$pop}{$embed}" , $allCats['normal'] ) . $pop;
+ $t = $embed . implode( "{$pop}{$embed}", $allCats['normal'] ) . $pop;
$msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped();
$linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text();
@@ -456,7 +516,7 @@ abstract class Skin extends ContextSource {
$s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" .
$this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
- $colon . '<ul>' . $embed . implode( "{$pop}{$embed}" , $allCats['hidden'] ) . $pop . '</ul>' .
+ $colon . '<ul>' . $embed . implode( "{$pop}{$embed}", $allCats['hidden'] ) . $pop . '</ul>' .
'</div>';
}
@@ -481,8 +541,8 @@ abstract class Skin extends ContextSource {
}
/**
- * Render the array as a serie of links.
- * @param $tree Array: categories tree returned by Title::getParentCategoryTree
+ * Render the array as a series of links.
+ * @param array $tree categories tree returned by Title::getParentCategoryTree
* @return String separated by &gt;, terminate with "\n"
*/
function drawCategoryBrowser( $tree ) {
@@ -499,7 +559,7 @@ abstract class Skin extends ContextSource {
# add our current element to the list
$eltitle = Title::newFromText( $element );
- $return .= Linker::link( $eltitle, htmlspecialchars( $eltitle->getText() ) );
+ $return .= Linker::link( $eltitle, htmlspecialchars( $eltitle->getText() ) );
}
return $return;
@@ -613,7 +673,6 @@ abstract class Skin extends ContextSource {
( $this->getTitle()->getArticleID() == 0 || $action == 'history' ) ) {
$n = $this->getTitle()->isDeleted();
-
if ( $n ) {
if ( $this->getUser()->isAllowed( 'undelete' ) ) {
$msg = 'thisisdeleted';
@@ -668,7 +727,7 @@ abstract class Skin extends ContextSource {
if ( $c > 1 ) {
$subpages .= $wgLang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
- } else {
+ } else {
$subpages .= '&lt; ';
}
@@ -851,16 +910,16 @@ abstract class Skin extends ContextSource {
}
/**
- * Renders a $wgFooterIcons icon acording to the method's arguments
- * @param $icon Array: The icon to build the html for, see $wgFooterIcons for the format of this array
- * @param $withImage Bool|String: Whether to use the icon's image or output a text-only footericon
+ * Renders a $wgFooterIcons icon according to the method's arguments
+ * @param array $icon The icon to build the html for, see $wgFooterIcons for the format of this array
+ * @param bool|String $withImage Whether to use the icon's image or output a text-only footericon
* @return String HTML
*/
function makeFooterIcon( $icon, $withImage = 'withImage' ) {
if ( is_string( $icon ) ) {
$html = $icon;
} else { // Assuming array
- $url = isset($icon["url"]) ? $icon["url"] : null;
+ $url = isset( $icon["url"] ) ? $icon["url"] : null;
unset( $icon["url"] );
if ( isset( $icon["src"] ) && $withImage === 'withImage' ) {
$html = Html::element( 'img', $icon ); // do this the lazy way, just pass icon data as an attribute array
@@ -969,7 +1028,7 @@ abstract class Skin extends ContextSource {
* Return a fully resolved style path url to images or styles stored in the common folder.
* This method returns a url resolved using the configured skin style path
* and includes the style version inside of the url.
- * @param $name String: The name or path of a skin resource file
+ * @param string $name The name or path of a skin resource file
* @return String The fully resolved style path url including styleversion
*/
function getCommonStylePath( $name ) {
@@ -978,10 +1037,10 @@ abstract class Skin extends ContextSource {
}
/**
- * Return a fully resolved style path url to images or styles stored in the curent skins's folder.
+ * Return a fully resolved style path url to images or styles stored in the current skins's folder.
* This method returns a url resolved using the configured skin style path
* and includes the style version inside of the url.
- * @param $name String: The name or path of a skin resource file
+ * @param string $name The name or path of a skin resource file
* @return String The fully resolved style path url including styleversion
*/
function getSkinStylePath( $name ) {
@@ -1008,14 +1067,14 @@ abstract class Skin extends ContextSource {
* If $proto is set to null, make a local URL. Otherwise, make a full
* URL with the protocol specified.
*
- * @param $name string Name of the Special page
- * @param $urlaction string Query to append
+ * @param string $name Name of the Special page
+ * @param string $urlaction Query to append
* @param $proto Protocol to use or null for a local URL
* @return String
*/
static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
$title = SpecialPage::getSafeTitleFor( $name );
- if( is_null( $proto ) ) {
+ if ( is_null( $proto ) ) {
return $title->getLocalURL( $urlaction );
} else {
return $title->getFullURL( $urlaction, false, $proto );
@@ -1102,7 +1161,7 @@ abstract class Skin extends ContextSource {
/**
* Make URL details where the article exists (or at least it's convenient to think so)
- * @param $name String Article name
+ * @param string $name Article name
* @param $urlaction String
* @return Array
*/
@@ -1132,7 +1191,23 @@ abstract class Skin extends ContextSource {
}
/**
- * Build an array that represents the sidebar(s), the navigation bar among them
+ * Build an array that represents the sidebar(s), the navigation bar among them.
+ *
+ * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
+ *
+ * The format of the returned array is array( heading => content, ... ), where:
+ * - heading is the heading of a navigation portlet. It is either:
+ * - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
+ * - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
+ * - plain text, which should be HTML-escaped by the skin
+ * - content is the contents of the portlet. It is either:
+ * - HTML text (<ul><li>...</li>...</ul>)
+ * - array of link data in a format accepted by BaseTemplate::makeListItem()
+ * - (for a magic string as a key, any value)
+ *
+ * Note that extensions can control the sidebar contents using the SkinBuildSidebar hook
+ * and can technically insert anything in here; skin creators are expected to handle
+ * values described above.
*
* @return array
*/
@@ -1237,7 +1312,7 @@ abstract class Skin extends ContextSource {
}
global $wgExternalLinkTarget;
- if ( $wgExternalLinkTarget) {
+ if ( $wgExternalLinkTarget ) {
$extraAttribs['target'] = $wgExternalLinkTarget;
}
} else {
@@ -1267,28 +1342,43 @@ abstract class Skin extends ContextSource {
}
/**
- * Should we load mediawiki.legacy.wikiprintable? 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().)
+ * This function previously controlled whether the 'mediawiki.legacy.wikiprintable' module
+ * should be loaded by OutputPage. That module no longer exists and the return value of this
+ * method is ignored.
*
+ * If your skin doesn't provide its own print styles, the 'mediawiki.legacy.commonPrint' module
+ * can be used instead (SkinTemplate-based skins do it automatically).
+ *
+ * @deprecated since 1.22
* @return bool
*/
public function commonPrintStylesheet() {
- return true;
+ wfDeprecated( __METHOD__, '1.22' );
+ return false;
}
/**
- * Gets new talk page messages for the current user.
- * @return MediaWiki message or if no new talk page messages, nothing
+ * Gets new talk page messages for the current user and returns an
+ * appropriate alert message (or an empty string if there are no messages)
+ * @return String
*/
function getNewtalks() {
+
+ $newMessagesAlert = '';
+ $user = $this->getUser();
+ $newtalks = $user->getNewMessageLinks();
$out = $this->getOutput();
- $newtalks = $this->getUser()->getNewMessageLinks();
- $ntl = '';
+ // Allow extensions to disable or modify the new messages alert
+ if ( !wfRunHooks( 'GetNewMessagesAlert', array( &$newMessagesAlert, $newtalks, $user, $out ) ) ) {
+ return '';
+ }
+ if ( $newMessagesAlert ) {
+ return $newMessagesAlert;
+ }
if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
- $uTalkTitle = $this->getUser()->getTalkPage();
+ $uTalkTitle = $user->getTalkPage();
if ( !$uTalkTitle->equals( $out->getTitle() ) ) {
$lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null;
@@ -1327,26 +1417,25 @@ abstract class Skin extends ContextSource {
);
if ( $nofAuthors >= 1 && $nofAuthors <= 10 ) {
- $ntl = $this->msg(
+ $newMessagesAlert = $this->msg(
'youhavenewmessagesfromusers',
$newMessagesLink,
$newMessagesDiffLink
)->numParams( $nofAuthors );
} else {
// $nofAuthors === 11 signifies "11 or more" ("more than 10")
- $ntl = $this->msg(
+ $newMessagesAlert = $this->msg(
$nofAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
$newMessagesLink,
$newMessagesDiffLink
);
}
- $ntl = $ntl->text();
+ $newMessagesAlert = $newMessagesAlert->text();
# Disable Squid cache
$out->setSquidMaxage( 0 );
}
} elseif ( count( $newtalks ) ) {
- // _>" " for BC <= 1.16
- $sep = str_replace( '_', ' ', $this->msg( 'newtalkseparator' )->escaped() );
+ $sep = $this->msg( 'newtalkseparator' )->escaped();
$msgs = array();
foreach ( $newtalks as $newtalk ) {
@@ -1356,17 +1445,17 @@ abstract class Skin extends ContextSource {
);
}
$parts = implode( $sep, $msgs );
- $ntl = $this->msg( 'youhavenewmessagesmulti' )->rawParams( $parts )->escaped();
+ $newMessagesAlert = $this->msg( 'youhavenewmessagesmulti' )->rawParams( $parts )->escaped();
$out->setSquidMaxage( 0 );
}
- return $ntl;
+ return $newMessagesAlert;
}
/**
* Get a cached notice
*
- * @param $name String: message name, or 'default' for $wgSiteNotice
+ * @param string $name message name, or 'default' for $wgSiteNotice
* @return String: HTML fragment
*/
private function getCachedNotice( $name ) {
@@ -1376,17 +1465,17 @@ abstract class Skin extends ContextSource {
$needParse = false;
- if( $name === 'default' ) {
+ if ( $name === 'default' ) {
// special case
global $wgSiteNotice;
$notice = $wgSiteNotice;
- if( empty( $notice ) ) {
+ if ( empty( $notice ) ) {
wfProfileOut( __METHOD__ );
return false;
}
} else {
$msg = $this->msg( $name )->inContentLanguage();
- if( $msg->isDisabled() ) {
+ if ( $msg->isDisabled() ) {
wfProfileOut( __METHOD__ );
return false;
}
@@ -1396,8 +1485,8 @@ abstract class Skin extends ContextSource {
// Use the extra hash appender to let eg SSL variants separately cache.
$key = wfMemcKey( $name . $wgRenderHashAppend );
$cachedNotice = $parserMemc->get( $key );
- if( is_array( $cachedNotice ) ) {
- if( md5( $notice ) == $cachedNotice['hash'] ) {
+ if ( is_array( $cachedNotice ) ) {
+ if ( md5( $notice ) == $cachedNotice['hash'] ) {
$notice = $cachedNotice['html'];
} else {
$needParse = true;
@@ -1474,9 +1563,9 @@ abstract class Skin extends ContextSource {
*
* @param $nt Title The title being linked to (may not be the same as
* $wgTitle, if the section is included from a template)
- * @param $section string The designation of the section being pointed to,
+ * @param string $section The designation of the section being pointed to,
* to be included in the link, like "&section=$section"
- * @param $tooltip string The tooltip to use for the link: will be escaped
+ * @param string $tooltip The tooltip to use for the link: will be escaped
* and wrapped in the 'editsectionhint' message
* @param $lang string Language code
* @return string HTML to use for edit link
@@ -1500,31 +1589,12 @@ abstract class Skin extends ContextSource {
array( 'noclasses', 'known' )
);
- # Run the old hook. This takes up half of the function . . . hopefully
- # we can rid of it someday.
- $attribs = '';
- if ( $tooltip ) {
- $attribs = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
- ->inLanguage( $lang )->escaped();
- $attribs = " title=\"$attribs\"";
- }
- $result = null;
- wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $attribs, $link, &$result, $lang ) );
- if ( !is_null( $result ) ) {
- # For reverse compatibility, add the brackets *after* the hook is
- # run, and even add them to hook-provided text. (This is the main
- # reason that the EditSectionLink hook is deprecated in favor of
- # DoEditSectionLink: it can't change the brackets or the span.)
- $result = wfMessage( 'editsection-brackets' )->rawParams( $result )
- ->inLanguage( $lang )->escaped();
- return "<span class=\"editsection\">$result</span>";
- }
-
- # Add the brackets and the span, and *then* run the nice new hook, with
- # clean and non-redundant arguments.
- $result = wfMessage( 'editsection-brackets' )->rawParams( $link )
- ->inLanguage( $lang )->escaped();
- $result = "<span class=\"editsection\">$result</span>";
+ # Add the brackets and the span and run the hook.
+ $result = '<span class="mw-editsection">'
+ . '<span class="mw-editsection-bracket">[</span>'
+ . $link
+ . '<span class="mw-editsection-bracket">]</span>'
+ . '</span>';
wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result, $lang ) );
return $result;
@@ -1534,13 +1604,15 @@ abstract class Skin extends ContextSource {
* Use PHP's magic __call handler to intercept legacy calls to the linker
* for backwards compatibility.
*
- * @param $fname String Name of called method
- * @param $args Array Arguments to the method
+ * @param string $fname Name of called method
+ * @param array $args Arguments to the method
+ * @throws MWException
* @return mixed
*/
function __call( $fname, $args ) {
$realFunction = array( 'Linker', $fname );
if ( is_callable( $realFunction ) ) {
+ wfDeprecated( get_class( $this ) . '::' . $fname, '1.21' );
return call_user_func_array( $realFunction, $args );
} else {
$className = get_class( $this );
diff --git a/includes/SkinLegacy.php b/includes/SkinLegacy.php
deleted file mode 100644
index e695ba6c..00000000
--- a/includes/SkinLegacy.php
+++ /dev/null
@@ -1,882 +0,0 @@
-<?php
-/**
- * Base class for legacy skins.
- *
- * 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
- */
-
-class SkinLegacy extends SkinTemplate {
- var $useHeadElement = true;
- protected $mWatchLinkNum = 0; // Appended to end of watch link id's
-
- /**
- * Add skin specific stylesheets
- * @param $out OutputPage
- */
- function setupSkinUserCss( OutputPage $out ) {
- $out->addModuleStyles( 'mediawiki.legacy.shared' );
- $out->addModuleStyles( 'mediawiki.legacy.oldshared' );
- }
-
- public function commonPrintStylesheet() {
- return true;
- }
-
- /**
- * This was for the old skins and for users with 640x480 screen.
- * Please note old skins are still used and might prove useful for
- * users having old computers or visually impaired.
- */
- var $mSuppressQuickbar = false;
-
- /**
- * Suppress the quickbar from the output, only for skin supporting
- * the quickbar
- */
- public function suppressQuickbar() {
- $this->mSuppressQuickbar = true;
- }
-
- /**
- * Return whether the quickbar should be suppressed from the output
- *
- * @return Boolean
- */
- public function isQuickbarSuppressed() {
- return $this->mSuppressQuickbar;
- }
-
- function qbSetting() {
- global $wgUser;
- if ( $this->isQuickbarSuppressed() ) {
- return 0;
- }
- $q = $wgUser->getOption( 'quickbar', 0 );
- if( $q == 5 ) {
- # 5 is the default, which chooses the setting
- # depending on the directionality of your interface language
- global $wgLang;
- return $wgLang->isRTL() ? 2 : 1;
- }
- return $q;
- }
-
-}
-
-class LegacyTemplate extends BaseTemplate {
-
- // How many search boxes have we made? Avoid duplicate id's.
- protected $searchboxes = '';
-
- function execute() {
- $this->html( 'headelement' );
- echo $this->beforeContent();
- $this->html( 'bodytext' );
- echo "\n";
- echo $this->afterContent();
- $this->html( 'dataAfterContent' );
- $this->printTrail();
- echo "\n</body></html>";
- }
-
- /**
- * This will be called immediately after the "<body>" tag. Split into
- * two functions to make it easier to subclass.
- * @return string
- */
- function beforeContent() {
- return $this->doBeforeContent();
- }
-
- function doBeforeContent() {
- global $wgLang;
- wfProfileIn( __METHOD__ );
-
- $s = '';
-
- $langlinks = $this->otherLanguages();
- if ( $langlinks ) {
- $rows = 2;
- $borderhack = '';
- } else {
- $rows = 1;
- $langlinks = false;
- $borderhack = 'class="top"';
- }
-
- $s .= "\n<div id='content'>\n<div id='topbar'>\n" .
- "<table cellspacing='0' width='100%'>\n<tr>\n";
-
- if ( $this->getSkin()->qbSetting() == 0 ) {
- $s .= "<td class='top' style='text-align: left; vertical-align: top;' rowspan='{$rows}'>\n" .
- $this->getSkin()->logoText( $wgLang->alignStart() ) . '</td>';
- }
-
- $l = $wgLang->alignStart();
- $s .= "<td {$borderhack} style='text-align: $l; vertical-align: top;'>\n";
-
- $s .= $this->topLinks();
- $s .= '<p class="subtitle">' . $this->pageTitleLinks() . "</p>\n";
-
- $r = $wgLang->alignEnd();
- $s .= "</td>\n<td {$borderhack} style='text-align: $r; vertical-align: top;' nowrap='nowrap'>";
- $s .= $this->nameAndLogin();
- $s .= "\n<br />" . $this->searchForm() . '</td>';
-
- if ( $langlinks ) {
- $s .= "</tr>\n<tr>\n<td class='top' colspan=\"2\">$langlinks</td>\n";
- }
-
- $s .= "</tr>\n</table>\n</div>\n";
- $s .= "\n<div id='article'>\n";
-
- $notice = $this->getSkin()->getSiteNotice();
-
- if ( $notice ) {
- $s .= "\n<div id='siteNotice'>$notice</div>\n";
- }
- $s .= $this->pageTitle();
- $s .= $this->pageSubtitle();
- $s .= $this->getSkin()->getCategories();
-
- wfProfileOut( __METHOD__ );
- return $s;
- }
-
- /**
- * This gets called shortly before the "</body>" tag.
- * @return String HTML to be put before "</body>"
- */
- function afterContent() {
- return $this->doAfterContent();
- }
-
- /** overloaded by derived classes
- * @return string
- */
- function doAfterContent() {
- return '</div></div>';
- }
-
- function searchForm() {
- global $wgRequest, $wgUseTwoButtonsSearchForm;
-
- $search = $wgRequest->getText( 'search' );
-
- $s = '<form id="searchform' . $this->searchboxes . '" name="search" class="inline" method="post" action="'
- . $this->getSkin()->escapeSearchLink() . "\">\n"
- . '<input type="text" id="searchInput' . $this->searchboxes . '" name="search" size="19" value="'
- . htmlspecialchars( substr( $search, 0, 256 ) ) . "\" />\n"
- . '<input type="submit" name="go" value="' . wfMessage( 'searcharticle' )->text() . '" />';
-
- if ( $wgUseTwoButtonsSearchForm ) {
- $s .= '&#160;<input type="submit" name="fulltext" value="' . wfMessage( 'searchbutton' )->text() . "\" />\n";
- } else {
- $s .= ' <a href="' . $this->getSkin()->escapeSearchLink() . '" rel="search">' . wfMessage( 'powersearch-legend' )->text() . "</a>\n";
- }
-
- $s .= '</form>';
-
- // Ensure unique id's for search boxes made after the first
- $this->searchboxes = $this->searchboxes == '' ? 2 : $this->searchboxes + 1;
-
- return $s;
- }
-
- function pageStats() {
- $ret = array();
- $items = array( 'viewcount', 'credits', 'lastmod', 'numberofwatchingusers', 'copyright' );
-
- foreach( $items as $item ) {
- if ( $this->data[$item] !== false ) {
- $ret[] = $this->data[$item];
- }
- }
-
- return implode( ' ', $ret );
- }
-
- function topLinks() {
- global $wgOut;
-
- $s = array(
- $this->getSkin()->mainPageLink(),
- Linker::specialLink( 'Recentchanges' )
- );
-
- if ( $wgOut->isArticleRelated() ) {
- $s[] = $this->editThisPage();
- $s[] = $this->historyLink();
- }
-
- # Many people don't like this dropdown box
- # $s[] = $this->specialPagesList();
-
- if ( $this->variantLinks() ) {
- $s[] = $this->variantLinks();
- }
-
- if ( $this->extensionTabLinks() ) {
- $s[] = $this->extensionTabLinks();
- }
-
- // @todo FIXME: Is using Language::pipeList impossible here? Do not quite understand the use of the newline
- return implode( $s, wfMessage( 'pipe-separator' )->escaped() . "\n" );
- }
-
- /**
- * Language/charset variant links for classic-style skins
- * @return string
- */
- function variantLinks() {
- $s = '';
-
- /* show links to different language variants */
- global $wgDisableLangConversion, $wgLang;
-
- $title = $this->getSkin()->getTitle();
- $lang = $title->getPageLanguage();
- $variants = $lang->getVariants();
-
- if ( !$wgDisableLangConversion && sizeof( $variants ) > 1
- && !$title->isSpecialPage() ) {
- foreach ( $variants as $code ) {
- $varname = $lang->getVariantname( $code );
-
- if ( $varname == 'disable' ) {
- continue;
- }
- $s = $wgLang->pipeList( array(
- $s,
- '<a href="' . htmlspecialchars( $title->getLocalURL( 'variant=' . $code ) ) . '" lang="' . $code . '" hreflang="' . $code . '">' . htmlspecialchars( $varname ) . '</a>'
- ) );
- }
- }
-
- return $s;
- }
-
- /**
- * Compatibility for extensions adding functionality through tabs.
- * Eventually these old skins should be replaced with SkinTemplate-based
- * versions, sigh...
- * @return string
- * @todo Exterminate! ...that, and replace it with normal SkinTemplate stuff
- */
- function extensionTabLinks() {
- $tabs = array();
- $out = '';
- $s = array();
- wfRunHooks( 'SkinTemplateTabs', array( $this->getSkin(), &$tabs ) );
- foreach ( $tabs as $tab ) {
- $s[] = Xml::element( 'a',
- array( 'href' => $tab['href'] ),
- $tab['text'] );
- }
-
- if ( count( $s ) ) {
- global $wgLang;
-
- $out = wfMessage( 'pipe-separator' )->escaped();
- $out .= $wgLang->pipeList( $s );
- }
-
- return $out;
- }
-
- function bottomLinks() {
- global $wgOut, $wgUser;
- $sep = wfMessage( 'pipe-separator' )->escaped() . "\n";
-
- $s = '';
- if ( $wgOut->isArticleRelated() ) {
- $element[] = '<strong>' . $this->editThisPage() . '</strong>';
-
- if ( $wgUser->isLoggedIn() ) {
- $element[] = $this->watchThisPage();
- }
-
- $element[] = $this->talkLink();
- $element[] = $this->historyLink();
- $element[] = $this->whatLinksHere();
- $element[] = $this->watchPageLinksLink();
-
- $title = $this->getSkin()->getTitle();
-
- if (
- $title->getNamespace() == NS_USER ||
- $title->getNamespace() == NS_USER_TALK
- ) {
- $id = User::idFromName( $title->getText() );
- $ip = User::isIP( $title->getText() );
-
- # Both anons and non-anons have contributions list
- if ( $id || $ip ) {
- $element[] = $this->userContribsLink();
- }
-
- if ( $this->getSkin()->showEmailUser( $id ) ) {
- $element[] = $this->emailUserLink();
- }
- }
-
- $s = implode( $element, $sep );
-
- if ( $title->getArticleID() ) {
- $s .= "\n<br />";
-
- // Delete/protect/move links for privileged users
- if ( $wgUser->isAllowed( 'delete' ) ) {
- $s .= $this->deleteThisPage();
- }
-
- if ( $wgUser->isAllowed( 'protect' ) ) {
- $s .= $sep . $this->protectThisPage();
- }
-
- if ( $wgUser->isAllowed( 'move' ) ) {
- $s .= $sep . $this->moveThisPage();
- }
- }
-
- $s .= "<br />\n" . $this->otherLanguages();
- }
-
- return $s;
- }
-
- function otherLanguages() {
- global $wgOut, $wgLang, $wgHideInterlanguageLinks;
-
- if ( $wgHideInterlanguageLinks ) {
- return '';
- }
-
- $a = $wgOut->getLanguageLinks();
-
- if ( 0 == count( $a ) ) {
- return '';
- }
-
- $s = wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text();
- $first = true;
-
- if ( $wgLang->isRTL() ) {
- $s .= '<span dir="ltr">';
- }
-
- foreach ( $a as $l ) {
- if ( !$first ) {
- $s .= wfMessage( 'pipe-separator' )->escaped();
- }
-
- $first = false;
-
- $nt = Title::newFromText( $l );
- $text = Language::fetchLanguageName( $nt->getInterwiki() );
-
- $s .= Html::element( 'a',
- array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ),
- $text == '' ? $l : $text );
- }
-
- if ( $wgLang->isRTL() ) {
- $s .= '</span>';
- }
-
- return $s;
- }
-
- /**
- * Show a drop-down box of special pages
- * @return string
- */
- function specialPagesList() {
- global $wgScript;
-
- $select = new XmlSelect( 'title' );
- $pages = SpecialPageFactory::getUsablePages();
- array_unshift( $pages, SpecialPageFactory::getPage( 'SpecialPages' ) );
- foreach ( $pages as $obj ) {
- $select->addOption( $obj->getDescription(),
- $obj->getTitle()->getPrefixedDBkey() );
- }
-
- return Html::rawElement( 'form',
- array( 'id' => 'specialpages', 'method' => 'get', 'action' => $wgScript ),
- $select->getHTML() . Xml::submitButton( wfMessage( 'go' )->text() ) );
- }
-
- function pageTitleLinks() {
- global $wgOut, $wgUser, $wgRequest, $wgLang;
-
- $oldid = $wgRequest->getVal( 'oldid' );
- $diff = $wgRequest->getVal( 'diff' );
- $action = $wgRequest->getText( 'action' );
-
- $skin = $this->getSkin();
- $title = $skin->getTitle();
-
- $s[] = $this->printableLink();
- $disclaimer = $skin->disclaimerLink(); # may be empty
-
- if ( $disclaimer ) {
- $s[] = $disclaimer;
- }
-
- $privacy = $skin->privacyLink(); # may be empty too
-
- if ( $privacy ) {
- $s[] = $privacy;
- }
-
- if ( $wgOut->isArticleRelated() ) {
- if ( $title->getNamespace() == NS_FILE ) {
- $image = wfFindFile( $title );
-
- if ( $image ) {
- $href = $image->getURL();
- $s[] = Html::element( 'a', array( 'href' => $href,
- 'title' => $href ), $title->getText() );
-
- }
- }
- }
-
- if ( 'history' == $action || isset( $diff ) || isset( $oldid ) ) {
- $s[] .= Linker::linkKnown(
- $title,
- wfMessage( 'currentrev' )->text()
- );
- }
-
- if ( $wgUser->getNewtalk() ) {
- # do not show "You have new messages" text when we are viewing our
- # own talk page
- if ( !$title->equals( $wgUser->getTalkPage() ) ) {
- $tl = Linker::linkKnown(
- $wgUser->getTalkPage(),
- wfMessage( 'newmessageslink' )->escaped(),
- array(),
- array( 'redirect' => 'no' )
- );
-
- $dl = Linker::linkKnown(
- $wgUser->getTalkPage(),
- wfMessage( 'newmessagesdifflink' )->escaped(),
- array(),
- array( 'diff' => 'cur' )
- );
- $s[] = '<strong>' . wfMessage( 'youhavenewmessages', $tl, $dl )->text() . '</strong>';
- # disable caching
- $wgOut->setSquidMaxage( 0 );
- $wgOut->enableClientCache( false );
- }
- }
-
- $undelete = $skin->getUndeleteLink();
-
- if ( !empty( $undelete ) ) {
- $s[] = $undelete;
- }
-
- return $wgLang->pipeList( $s );
- }
-
- /**
- * Gets the h1 element with the page title.
- * @return string
- */
- function pageTitle() {
- global $wgOut;
- $s = '<h1 class="pagetitle"><span dir="auto">' . $wgOut->getPageTitle() . '</span></h1>';
- return $s;
- }
-
- function pageSubtitle() {
- global $wgOut;
-
- $sub = $wgOut->getSubtitle();
-
- if ( $sub == '' ) {
- global $wgExtraSubtitle;
- $sub = wfMessage( 'tagline' )->parse() . $wgExtraSubtitle;
- }
-
- $subpages = $this->getSkin()->subPageSubtitle();
- $sub .= !empty( $subpages ) ? "</p><p class='subpages'>$subpages" : '';
- $s = "<p class='subtitle'>{$sub}</p>\n";
-
- return $s;
- }
-
- function printableLink() {
- global $wgOut, $wgRequest, $wgLang;
-
- $s = array();
-
- if ( !$wgOut->isPrintable() ) {
- $printurl = htmlspecialchars( $this->getSkin()->getTitle()->getLocalUrl(
- $wgRequest->appendQueryValue( 'printable', 'yes', true ) ) );
- $s[] = "<a href=\"$printurl\" rel=\"alternate\">"
- . wfMessage( 'printableversion' )->text() . '</a>';
- }
-
- if ( $wgOut->isSyndicated() ) {
- foreach ( $wgOut->getSyndicationLinks() as $format => $link ) {
- $feedurl = htmlspecialchars( $link );
- $s[] = "<a href=\"$feedurl\" rel=\"alternate\" type=\"application/{$format}+xml\""
- . " class=\"feedlink\">" . wfMessage( "feed-$format" )->escaped() . "</a>";
- }
- }
- return $wgLang->pipeList( $s );
- }
-
- /**
- * @deprecated in 1.19
- * @return string
- */
- function getQuickbarCompensator( $rows = 1 ) {
- wfDeprecated( __METHOD__, '1.19' );
- return "<td width='152' rowspan='{$rows}'>&#160;</td>";
- }
-
- function editThisPage() {
- global $wgOut;
-
- if ( !$wgOut->isArticleRelated() ) {
- $s = wfMessage( 'protectedpage' )->text();
- } else {
- $title = $this->getSkin()->getTitle();
- if ( $title->quickUserCan( 'edit' ) && $title->exists() ) {
- $t = wfMessage( 'editthispage' )->text();
- } elseif ( $title->quickUserCan( 'create' ) && !$title->exists() ) {
- $t = wfMessage( 'create-this-page' )->text();
- } else {
- $t = wfMessage( 'viewsource' )->text();
- }
-
- $s = Linker::linkKnown(
- $title,
- $t,
- array(),
- $this->getSkin()->editUrlOptions()
- );
- }
-
- return $s;
- }
-
- function deleteThisPage() {
- global $wgUser, $wgRequest;
-
- $diff = $wgRequest->getVal( 'diff' );
- $title = $this->getSkin()->getTitle();
-
- if ( $title->getArticleID() && ( !$diff ) && $wgUser->isAllowed( 'delete' ) ) {
- $t = wfMessage( 'deletethispage' )->text();
-
- $s = Linker::linkKnown(
- $title,
- $t,
- array(),
- array( 'action' => 'delete' )
- );
- } else {
- $s = '';
- }
-
- return $s;
- }
-
- function protectThisPage() {
- global $wgUser, $wgRequest;
-
- $diff = $wgRequest->getVal( 'diff' );
- $title = $this->getSkin()->getTitle();
-
- if ( $title->getArticleID() && ( ! $diff ) && $wgUser->isAllowed( 'protect' ) ) {
- if ( $title->isProtected() ) {
- $text = wfMessage( 'unprotectthispage' )->text();
- $query = array( 'action' => 'unprotect' );
- } else {
- $text = wfMessage( 'protectthispage' )->text();
- $query = array( 'action' => 'protect' );
- }
-
- $s = Linker::linkKnown(
- $title,
- $text,
- array(),
- $query
- );
- } else {
- $s = '';
- }
-
- return $s;
- }
-
- function watchThisPage() {
- global $wgOut, $wgUser;
- ++$this->mWatchLinkNum;
-
- // Cache
- $title = $this->getSkin()->getTitle();
-
- if ( $wgOut->isArticleRelated() ) {
- if ( $wgUser->isWatched( $title ) ) {
- $text = wfMessage( 'unwatchthispage' )->text();
- $query = array(
- 'action' => 'unwatch',
- 'token' => UnwatchAction::getUnwatchToken( $title, $wgUser ),
- );
- $id = 'mw-unwatch-link' . $this->mWatchLinkNum;
- } else {
- $text = wfMessage( 'watchthispage' )->text();
- $query = array(
- 'action' => 'watch',
- 'token' => WatchAction::getWatchToken( $title, $wgUser ),
- );
- $id = 'mw-watch-link' . $this->mWatchLinkNum;
- }
-
- $s = Linker::linkKnown(
- $title,
- $text,
- array( 'id' => $id ),
- $query
- );
- } else {
- $s = wfMessage( 'notanarticle' )->text();
- }
-
- return $s;
- }
-
- function moveThisPage() {
- if ( $this->getSkin()->getTitle()->quickUserCan( 'move' ) ) {
- return Linker::linkKnown(
- SpecialPage::getTitleFor( 'Movepage' ),
- wfMessage( 'movethispage' )->text(),
- array(),
- array( 'target' => $this->getSkin()->getTitle()->getPrefixedDBkey() )
- );
- } else {
- // no message if page is protected - would be redundant
- return '';
- }
- }
-
- function historyLink() {
- return Linker::link(
- $this->getSkin()->getTitle(),
- wfMessage( 'history' )->escaped(),
- array( 'rel' => 'archives' ),
- array( 'action' => 'history' )
- );
- }
-
- function whatLinksHere() {
- return Linker::linkKnown(
- SpecialPage::getTitleFor( 'Whatlinkshere', $this->getSkin()->getTitle()->getPrefixedDBkey() ),
- wfMessage( 'whatlinkshere' )->escaped()
- );
- }
-
- function userContribsLink() {
- return Linker::linkKnown(
- SpecialPage::getTitleFor( 'Contributions', $this->getSkin()->getTitle()->getDBkey() ),
- wfMessage( 'contributions' )->escaped()
- );
- }
-
- function emailUserLink() {
- return Linker::linkKnown(
- SpecialPage::getTitleFor( 'Emailuser', $this->getSkin()->getTitle()->getDBkey() ),
- wfMessage( 'emailuser' )->escaped()
- );
- }
-
- function watchPageLinksLink() {
- global $wgOut;
-
- if ( !$wgOut->isArticleRelated() ) {
- return wfMessage( 'parentheses', wfMessage( 'notanarticle' )->text() )->escaped();
- } else {
- return Linker::linkKnown(
- SpecialPage::getTitleFor( 'Recentchangeslinked', $this->getSkin()->getTitle()->getPrefixedDBkey() ),
- wfMessage( 'recentchangeslinked-toolbox' )->escaped()
- );
- }
- }
-
- function talkLink() {
- $title = $this->getSkin()->getTitle();
- if ( NS_SPECIAL == $title->getNamespace() ) {
- # No discussion links for special pages
- return '';
- }
-
- $linkOptions = array();
-
- if ( $title->isTalkPage() ) {
- $link = $title->getSubjectPage();
- switch( $link->getNamespace() ) {
- case NS_MAIN:
- $text = wfMessage( 'articlepage' );
- break;
- case NS_USER:
- $text = wfMessage( 'userpage' );
- break;
- case NS_PROJECT:
- $text = wfMessage( 'projectpage' );
- break;
- case NS_FILE:
- $text = wfMessage( 'imagepage' );
- # Make link known if image exists, even if the desc. page doesn't.
- if ( wfFindFile( $link ) )
- $linkOptions[] = 'known';
- break;
- case NS_MEDIAWIKI:
- $text = wfMessage( 'mediawikipage' );
- break;
- case NS_TEMPLATE:
- $text = wfMessage( 'templatepage' );
- break;
- case NS_HELP:
- $text = wfMessage( 'viewhelppage' );
- break;
- case NS_CATEGORY:
- $text = wfMessage( 'categorypage' );
- break;
- default:
- $text = wfMessage( 'articlepage' );
- }
- } else {
- $link = $title->getTalkPage();
- $text = wfMessage( 'talkpage' );
- }
-
- $s = Linker::link( $link, $text->text(), array(), array(), $linkOptions );
-
- return $s;
- }
-
- function commentLink() {
- global $wgOut;
-
- $title = $this->getSkin()->getTitle();
- if ( $title->isSpecialPage() ) {
- return '';
- }
-
- # __NEWSECTIONLINK___ changes behaviour here
- # If it is present, the link points to this page, otherwise
- # it points to the talk page
- if ( !$title->isTalkPage() && !$wgOut->showNewSectionLink() ) {
- $title = $title->getTalkPage();
- }
-
- return Linker::linkKnown(
- $title,
- wfMessage( 'postcomment' )->text(),
- array(),
- array(
- 'action' => 'edit',
- 'section' => 'new'
- )
- );
- }
-
- function getUploadLink() {
- global $wgUploadNavigationUrl;
-
- if ( $wgUploadNavigationUrl ) {
- # Using an empty class attribute to avoid automatic setting of "external" class
- return Linker::makeExternalLink( $wgUploadNavigationUrl,
- wfMessage( 'upload' )->escaped(),
- false, null, array( 'class' => '' ) );
- } else {
- return Linker::linkKnown(
- SpecialPage::getTitleFor( 'Upload' ),
- wfMessage( 'upload' )->escaped()
- );
- }
- }
-
- function nameAndLogin() {
- global $wgUser, $wgLang, $wgRequest;
-
- $returnTo = $this->getSkin()->getTitle();
- $ret = '';
-
- if ( $wgUser->isAnon() ) {
- if ( $this->getSkin()->showIPinHeader() ) {
- $name = $wgRequest->getIP();
-
- $talkLink = Linker::link( $wgUser->getTalkPage(),
- $wgLang->getNsText( NS_TALK ) );
- $talkLink = wfMessage( 'parentheses' )->rawParams( $talkLink )->escaped();
-
- $ret .= "$name $talkLink";
- } else {
- $ret .= wfMessage( 'notloggedin' )->text();
- }
-
- $query = array();
-
- if ( !$returnTo->isSpecial( 'Userlogout' ) ) {
- $query['returnto'] = $returnTo->getPrefixedDBkey();
- }
-
- $loginlink = $wgUser->isAllowed( 'createaccount' )
- ? 'nav-login-createaccount'
- : 'login';
- $ret .= "\n<br />" . Linker::link(
- SpecialPage::getTitleFor( 'Userlogin' ),
- wfMessage( $loginlink )->text(), array(), $query
- );
- } else {
- $talkLink = Linker::link( $wgUser->getTalkPage(),
- $wgLang->getNsText( NS_TALK ) );
- $talkLink = wfMessage( 'parentheses' )->rawParams( $talkLink )->escaped();
-
- $ret .= Linker::link( $wgUser->getUserPage(),
- htmlspecialchars( $wgUser->getName() ) );
- $ret .= " $talkLink<br />";
- $ret .= $wgLang->pipeList( array(
- Linker::link(
- SpecialPage::getTitleFor( 'Userlogout' ), wfMessage( 'logout' )->text(),
- array(), array( 'returnto' => $returnTo->getPrefixedDBkey() )
- ),
- Linker::specialLink( 'Preferences' ),
- ) );
- }
-
- $ret = $wgLang->pipeList( array(
- $ret,
- Linker::link(
- Title::newFromText( wfMessage( 'helppage' )->inContentLanguage()->text() ),
- wfMessage( 'help' )->text()
- ),
- ) );
-
- return $ret;
- }
-}
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index bda43957..53f11998 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -43,7 +43,7 @@ class MediaWiki_I18N {
$value = wfMessage( $value )->text();
// interpolate variables
$m = array();
- while( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) {
+ while ( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) {
list( $src, $var ) = $m;
wfSuppressWarnings();
$varValue = $this->_context[$var];
@@ -113,7 +113,7 @@ class SkinTemplate extends Skin {
* roughly equivalent to PHPTAL 0.7.
*
* @param $classname String
- * @param $repository string: subdirectory where we keep template files
+ * @param string $repository subdirectory where we keep template files
* @param $cache_dir string
* @return QuickTemplate
* @private
@@ -123,38 +123,61 @@ class SkinTemplate extends Skin {
}
/**
- * initialize various variables and generate the template
+ * Generates array of language links for the current page
*
- * @param $out OutputPage
+ * @return array
+ * @public
*/
- function outputPage( OutputPage $out=null ) {
- global $wgContLang;
- global $wgScript, $wgStylePath;
- global $wgMimeType, $wgJsMimeType;
- global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces, $wgHtml5Version;
- global $wgDisableCounters, $wgSitename, $wgLogo, $wgHideInterlanguageLinks;
- global $wgMaxCredits, $wgShowCreditsIfMax;
- global $wgPageShowWatchingUsers;
- global $wgArticlePath, $wgScriptPath, $wgServer;
+ public function getLanguages() {
+ global $wgHideInterlanguageLinks;
+ $out = $this->getOutput();
+ $userLang = $this->getLanguage();
- wfProfileIn( __METHOD__ );
- Profiler::instance()->setTemplated( true );
+ # Language links
+ $language_urls = array();
- $oldContext = null;
- if ( $out !== null ) {
- // @todo Add wfDeprecated in 1.20
- $oldContext = $this->getContext();
- $this->setContext( $out->getContext() );
+ if ( !$wgHideInterlanguageLinks ) {
+ foreach ( $out->getLanguageLinks() as $languageLinkText ) {
+ $languageLinkParts = explode( ':', $languageLinkText, 2 );
+ $class = 'interwiki-' . $languageLinkParts[0];
+ unset( $languageLinkParts );
+ $languageLinkTitle = Title::newFromText( $languageLinkText );
+ if ( $languageLinkTitle ) {
+ $ilInterwikiCode = $languageLinkTitle->getInterwiki();
+ $ilLangName = Language::fetchLanguageName( $ilInterwikiCode );
+
+ if ( strval( $ilLangName ) === '' ) {
+ $ilLangName = $languageLinkText;
+ } else {
+ $ilLangName = $this->formatLanguageName( $ilLangName );
+ }
+
+ // CLDR extension or similar is required to localize the language name;
+ // otherwise we'll end up with the autonym again.
+ $ilLangLocalName = Language::fetchLanguageName( $ilInterwikiCode, $userLang->getCode() );
+
+ $language_urls[] = array(
+ 'href' => $languageLinkTitle->getFullURL(),
+ 'text' => $ilLangName,
+ 'title' => wfMessage( 'interlanguage-link-title', $languageLinkTitle->getText(), $ilLangLocalName )->text(),
+ 'class' => $class,
+ 'lang' => wfBCP47( $ilInterwikiCode ),
+ 'hreflang' => wfBCP47( $ilInterwikiCode ),
+ );
+ }
+ }
}
+ return $language_urls;
+ }
+
+ protected function setupTemplateForOutput() {
+ wfProfileIn( __METHOD__ );
- $out = $this->getOutput();
$request = $this->getRequest();
$user = $this->getUser();
$title = $this->getTitle();
wfProfileIn( __METHOD__ . '-init' );
- $this->initPage( $out );
-
$tpl = $this->setupTemplate( $this->template, 'skins' );
wfProfileOut( __METHOD__ . '-init' );
@@ -169,7 +192,7 @@ class SkinTemplate extends Skin {
unset( $query['returnto'] );
unset( $query['returntoquery'] );
}
- $this->thisquery = wfArrayToCGI( $query );
+ $this->thisquery = wfArrayToCgi( $query );
$this->loggedin = $user->isLoggedIn();
$this->username = $user->getName();
@@ -183,6 +206,47 @@ class SkinTemplate extends Skin {
wfProfileOut( __METHOD__ . '-stuff' );
+ wfProfileOut( __METHOD__ );
+
+ return $tpl;
+ }
+
+ /**
+ * initialize various variables and generate the template
+ *
+ * @param $out OutputPage
+ */
+ function outputPage( OutputPage $out = null ) {
+ global $wgContLang;
+ global $wgScript, $wgStylePath;
+ global $wgMimeType, $wgJsMimeType;
+ global $wgXhtmlNamespaces, $wgHtml5Version;
+ global $wgDisableCounters, $wgSitename, $wgLogo;
+ global $wgMaxCredits, $wgShowCreditsIfMax;
+ global $wgPageShowWatchingUsers;
+ global $wgArticlePath, $wgScriptPath, $wgServer;
+
+ wfProfileIn( __METHOD__ );
+ Profiler::instance()->setTemplated( true );
+
+ $oldContext = null;
+ if ( $out !== null ) {
+ // @todo Add wfDeprecated in 1.20
+ $oldContext = $this->getContext();
+ $this->setContext( $out->getContext() );
+ }
+
+ $out = $this->getOutput();
+ $request = $this->getRequest();
+ $user = $this->getUser();
+ $title = $this->getTitle();
+
+ wfProfileIn( __METHOD__ . '-init' );
+ $this->initPage( $out );
+ wfProfileOut( __METHOD__ . '-init' );
+
+ $tpl = $this->setupTemplateForOutput();
+
wfProfileIn( __METHOD__ . '-stuff-head' );
if ( !$this->useHeadElement ) {
$tpl->set( 'pagecss', false );
@@ -193,7 +257,7 @@ class SkinTemplate extends Skin {
$tpl->set( 'jsvarurl', false );
- $tpl->setRef( 'xhtmldefaultnamespace', $wgXhtmlDefaultNamespace );
+ $tpl->set( 'xhtmldefaultnamespace', 'http://www.w3.org/1999/xhtml' );
$tpl->set( 'xhtmlnamespaces', $wgXhtmlNamespaces );
$tpl->set( 'html5version', $wgHtml5Version );
$tpl->set( 'headlinks', $out->getHeadLinks() );
@@ -219,7 +283,7 @@ class SkinTemplate extends Skin {
if ( $subpagestr !== '' ) {
$subpagestr = '<span class="subpages">' . $subpagestr . '</span>';
}
- $tpl->set( 'subtitle', $subpagestr . $out->getSubtitle() );
+ $tpl->set( 'subtitle', $subpagestr . $out->getSubtitle() );
$undelete = $this->getUndeleteLink();
if ( $undelete === '' ) {
@@ -229,10 +293,11 @@ class SkinTemplate extends Skin {
}
$tpl->set( 'catlinks', $this->getCategories() );
- if( $out->isSyndicated() ) {
+ if ( $out->isSyndicated() ) {
$feeds = array();
- foreach( $out->getSyndicationLinks() as $format => $link ) {
+ foreach ( $out->getSyndicationLinks() as $format => $link ) {
$feeds[$format] = array(
+ // Messages: feed-atom, feed-rss
'text' => $this->msg( "feed-$format" )->text(),
'href' => $link
);
@@ -262,7 +327,7 @@ class SkinTemplate extends Skin {
$tpl->set( 'helppage', $this->msg( 'helppage' )->text() );
*/
$tpl->set( 'searchaction', $this->escapeSearchLink() );
- $tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBKey() );
+ $tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey() );
$tpl->set( 'search', trim( $request->getVal( 'search' ) ) );
$tpl->setRef( 'stylepath', $wgStylePath );
$tpl->setRef( 'articlepath', $wgArticlePath );
@@ -273,14 +338,14 @@ class SkinTemplate extends Skin {
$userLang = $this->getLanguage();
$userLangCode = $userLang->getHtmlCode();
- $userLangDir = $userLang->getDir();
+ $userLangDir = $userLang->getDir();
$tpl->set( 'lang', $userLangCode );
$tpl->set( 'dir', $userLangDir );
$tpl->set( 'rtl', $userLang->isRTL() );
$tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
- $tpl->set( 'showjumplinks', $user->getOption( 'showjumplinks' ) );
+ $tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed
$tpl->set( 'username', $this->loggedin ? $this->username : null );
$tpl->setRef( 'userpage', $this->userpage );
$tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] );
@@ -398,44 +463,18 @@ class SkinTemplate extends Skin {
# not for special pages or file pages AND only when viewing AND if the page exists
# (or is in MW namespace, because that has default content)
if ( !in_array( $title->getNamespace(), array( NS_SPECIAL, NS_FILE ) ) &&
- in_array( $request->getVal( 'action', 'view' ), array( 'view', 'historysubmit' ) ) &&
+ Action::getActionName( $this ) === 'view' &&
( $title->exists() || $title->getNamespace() == NS_MEDIAWIKI ) ) {
$pageLang = $title->getPageViewLanguage();
$realBodyAttribs['lang'] = $pageLang->getHtmlCode();
$realBodyAttribs['dir'] = $pageLang->getDir();
- $realBodyAttribs['class'] = 'mw-content-'.$pageLang->getDir();
+ $realBodyAttribs['class'] = 'mw-content-' . $pageLang->getDir();
}
$out->mBodytext = Html::rawElement( 'div', $realBodyAttribs, $out->mBodytext );
$tpl->setRef( 'bodytext', $out->mBodytext );
- # Language links
- $language_urls = array();
-
- if ( !$wgHideInterlanguageLinks ) {
- foreach( $out->getLanguageLinks() as $l ) {
- $tmp = explode( ':', $l, 2 );
- $class = 'interwiki-' . $tmp[0];
- unset( $tmp );
- $nt = Title::newFromText( $l );
- if ( $nt ) {
- $ilLangName = Language::fetchLanguageName( $nt->getInterwiki() );
- if ( strval( $ilLangName ) === '' ) {
- $ilLangName = $l;
- } else {
- $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
- }
- $language_urls[] = array(
- 'href' => $nt->getFullURL(),
- 'text' => $ilLangName,
- 'title' => $nt->getText(),
- 'class' => $class,
- 'lang' => $nt->getInterwiki(),
- 'hreflang' => $nt->getInterwiki(),
- );
- }
- }
- }
+ $language_urls = $this->getLanguages();
if ( count( $language_urls ) ) {
$tpl->setRef( 'language_urls', $language_urls );
} else {
@@ -466,7 +505,7 @@ class SkinTemplate extends Skin {
$tpl->set( 'reporttime', wfReportTime() );
// original version by hansm
- if( !wfRunHooks( 'SkinTemplateOutputPageBeforeExec', array( &$this, &$tpl ) ) ) {
+ if ( !wfRunHooks( 'SkinTemplateOutputPageBeforeExec', array( &$this, &$tpl ) ) ) {
wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" );
}
@@ -499,6 +538,32 @@ class SkinTemplate extends Skin {
}
/**
+ * Get the HTML for the p-personal list
+ * @return string
+ */
+ public function getPersonalToolsList() {
+ $tpl = $this->setupTemplateForOutput();
+ $tpl->set( 'personal_urls', $this->buildPersonalUrls() );
+ $html = '';
+ foreach ( $tpl->getPersonalTools() as $key => $item ) {
+ $html .= $tpl->makeListItem( $key, $item );
+ }
+ return $html;
+ }
+
+ /**
+ * Format language name for use in sidebar interlanguage links list.
+ * By default it is capitalized.
+ *
+ * @param string $name Language name, e.g. "English" or "español"
+ * @return string
+ * @private
+ */
+ function formatLanguageName( $name ) {
+ return $this->getLanguage()->ucfirst( $name );
+ }
+
+ /**
* Output the string, or print error message if it's
* an error object of the appropriate type.
* For the base class, assume strings all around.
@@ -511,7 +576,7 @@ class SkinTemplate extends Skin {
}
/**
- * Output a boolean indiciating if buildPersonalUrls should output separate
+ * Output a boolean indicating if buildPersonalUrls should output separate
* login and create account links or output a combined link
* By default we simply return a global config setting that affects most skins
* This is setup as a method so that like with $wgLogo and getLogo() a skin
@@ -541,18 +606,23 @@ class SkinTemplate extends Skin {
# $this->getTitle() will just give Special:Badtitle, which is
# not especially useful as a returnto parameter. Use the title
# from the request instead, if there was one.
- $page = Title::newFromURL( $request->getVal( 'title', '' ) );
+ if ( $this->getUser()->isAllowed( 'read' ) ) {
+ $page = $this->getTitle();
+ } else {
+ $page = Title::newFromText( $request->getVal( 'title', '' ) );
+ }
$page = $request->getVal( 'returnto', $page );
$a = array();
if ( strval( $page ) !== '' ) {
$a['returnto'] = $page;
$query = $request->getVal( 'returntoquery', $this->thisquery );
- if( $query != '' ) {
+ if ( $query != '' ) {
$a['returntoquery'] = $query;
}
}
- $returnto = wfArrayToCGI( $a );
- if( $this->loggedin ) {
+
+ $returnto = wfArrayToCgi( $a );
+ if ( $this->loggedin ) {
$personal_urls['userpage'] = array(
'text' => $this->username,
'href' => &$this->userpageUrlDetails['href'],
@@ -573,12 +643,15 @@ class SkinTemplate extends Skin {
'href' => $href,
'active' => ( $href == $pageurl )
);
- $href = self::makeSpecialUrl( 'Watchlist' );
- $personal_urls['watchlist'] = array(
- 'text' => $this->msg( 'mywatchlist' )->text(),
- 'href' => $href,
- 'active' => ( $href == $pageurl )
- );
+
+ if ( $this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
+ $href = self::makeSpecialUrl( 'Watchlist' );
+ $personal_urls['watchlist'] = array(
+ 'text' => $this->msg( 'mywatchlist' )->text(),
+ 'href' => $href,
+ 'active' => ( $href == $pageurl )
+ );
+ }
# We need to do an explicit check for Special:Contributions, as we
# have to match both the title, and the target, which could come
@@ -588,7 +661,7 @@ class SkinTemplate extends Skin {
# thickens, because the Title object is altered for special pages,
# so it doesn't contain the original alias-with-subpage.
$origTitle = Title::newFromText( $request->getText( 'title' ) );
- if( $origTitle instanceof Title && $origTitle->isSpecialPage() ) {
+ if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) {
list( $spName, $spPar ) = SpecialPageFactory::resolveAlias( $origTitle->getText() );
$active = $spName == 'Contributions'
&& ( ( $spPar && $spPar == $this->username )
@@ -619,25 +692,19 @@ class SkinTemplate extends Skin {
: 'login';
$is_signup = $request->getText( 'type' ) == 'signup';
- # anonlogin & login are the same
- global $wgSecureLogin;
- $proto = $wgSecureLogin ? PROTO_HTTPS : null;
-
$login_id = $this->showIPinHeader() ? 'anonlogin' : 'login';
$login_url = array(
'text' => $this->msg( $loginlink )->text(),
- 'href' => self::makeSpecialUrl( 'Userlogin', $returnto, $proto ),
+ 'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
'active' => $title->isSpecial( 'Userlogin' ) && ( $loginlink == 'nav-login-createaccount' || !$is_signup ),
- 'class' => $wgSecureLogin ? 'link-https' : ''
);
$createaccount_url = array(
'text' => $this->msg( 'createaccount' )->text(),
- 'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup", $proto ),
+ 'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ),
'active' => $title->isSpecial( 'Userlogin' ) && $is_signup,
- 'class' => $wgSecureLogin ? 'link-https' : ''
);
- if( $this->showIPinHeader() ) {
+ if ( $this->showIPinHeader() ) {
$href = &$this->userpageUrlDetails['href'];
$personal_urls['anonuserpage'] = array(
'text' => $this->username,
@@ -668,22 +735,28 @@ class SkinTemplate extends Skin {
}
/**
- * TODO document
- * @param $title Title
- * @param $message String message key
- * @param $selected Bool
- * @param $query String
- * @param $checkEdit Bool
+ * Builds an array with tab definition
+ *
+ * @param Title $title page where the tab links to
+ * @param string|array $message message key or an array of message keys (will fall back)
+ * @param boolean $selected display the tab as selected
+ * @param string $query query string attached to tab URL
+ * @param boolean $checkEdit check if $title exists and mark with .new if one doesn't
+ *
* @return array
*/
function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) {
$classes = array();
- if( $selected ) {
+ if ( $selected ) {
$classes[] = 'selected';
}
- if( $checkEdit && !$title->isKnown() ) {
+ if ( $checkEdit && !$title->isKnown() ) {
$classes[] = 'new';
- $query = 'action=edit&redlink=1';
+ if ( $query !== '' ) {
+ $query = 'action=edit&redlink=1&' . $query;
+ } else {
+ $query = 'action=edit&redlink=1';
+ }
}
// wfMessageFallback will nicely accept $message as an array of fallbacks
@@ -702,7 +775,7 @@ class SkinTemplate extends Skin {
}
$result = array();
- if( !wfRunHooks( 'SkinTemplateTabAction', array( &$this,
+ if ( !wfRunHooks( 'SkinTemplateTabAction', array( &$this,
$title, $message, $selected, $checkEdit,
&$classes, &$query, &$text, &$result ) ) ) {
return $result;
@@ -711,13 +784,13 @@ class SkinTemplate extends Skin {
return array(
'class' => implode( ' ', $classes ),
'text' => $text,
- 'href' => $title->getLocalUrl( $query ),
+ 'href' => $title->getLocalURL( $query ),
'primary' => true );
}
function makeTalkUrlDetails( $name, $urlaction = '' ) {
$title = Title::newFromText( $name );
- if( !is_object( $title ) ) {
+ if ( !is_object( $title ) ) {
throw new MWException( __METHOD__ . " given invalid pagename $name" );
}
$title = $title->getTalkPage();
@@ -730,7 +803,7 @@ class SkinTemplate extends Skin {
function makeArticleUrlDetails( $name, $urlaction = '' ) {
$title = Title::newFromText( $name );
- $title= $title->getSubjectPage();
+ $title = $title->getSubjectPage();
self::checkTitle( $title, $name );
return array(
'href' => $title->getLocalURL( $urlaction ),
@@ -748,7 +821,7 @@ class SkinTemplate extends Skin {
* variants: Used to list the language variants for the page
*
* Each section's value is a key/value array of links for that section.
- * The links themseves have these common keys:
+ * The links themselves have these common keys:
* - class: The css classes to apply to the tab
* - text: The text to display on the tab
* - href: The href for the tab to point to
@@ -800,7 +873,7 @@ class SkinTemplate extends Skin {
wfRunHooks( 'SkinTemplatePreventOtherActiveTabs', array( &$this, &$preventActiveTabs ) );
// Checks if page is some kind of content
- if( $title->canExist() ) {
+ if ( $title->canExist() ) {
// Gets page objects for the related namespaces
$subjectPage = $title->getSubjectPage();
$talkPage = $title->getTalkPage();
@@ -935,7 +1008,7 @@ class SkinTemplate extends Skin {
}
}
- if ( $title->getNamespace() !== NS_MEDIAWIKI && $title->quickUserCan( 'protect', $user ) ) {
+ if ( $title->getNamespace() !== NS_MEDIAWIKI && $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() ) {
$mode = $title->isProtected() ? 'unprotect' : 'protect';
$content_navigation['actions'][$mode] = array(
'class' => ( $onPage && $action == $mode ) ? 'selected' : false,
@@ -947,7 +1020,7 @@ class SkinTemplate extends Skin {
wfProfileOut( __METHOD__ . '-live' );
// Checks if the user is logged in
- if ( $this->loggedin ) {
+ if ( $this->loggedin && $user->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) ) {
/**
* The following actions use messages which, if made particular to
* the any specific skins, would break the Ajax code which makes this
@@ -980,22 +1053,23 @@ class SkinTemplate extends Skin {
// Gets preferred variant (note that user preference is
// only possible for wiki content language variant)
$preferred = $pageLang->getPreferredVariant();
+ if ( Action::getActionName( $this ) === 'view' ) {
+ $params = $request->getQueryValues();
+ unset( $params['title'] );
+ } else {
+ $params = array();
+ }
// Loops over each variant
- foreach( $variants as $code ) {
+ foreach ( $variants as $code ) {
// Gets variant name from language code
$varname = $pageLang->getVariantname( $code );
- // Checks if the variant is marked as disabled
- if( $varname == 'disable' ) {
- // Skips this variant
- continue;
- }
// Appends variant link
$content_navigation['variants'][] = array(
'class' => ( $code == $preferred ) ? 'selected' : false,
'text' => $varname,
- 'href' => $title->getLocalURL( array( 'variant' => $code ) ),
- 'lang' => $code,
- 'hreflang' => $code
+ 'href' => $title->getLocalURL( array( 'variant' => $code ) + $params ),
+ 'lang' => wfBCP47( $code ),
+ 'hreflang' => wfBCP47( $code ),
);
}
}
@@ -1014,7 +1088,7 @@ class SkinTemplate extends Skin {
}
// Equiv to SkinTemplateContentActions
- wfRunHooks( 'SkinTemplateNavigation::Universal', array( &$this, &$content_navigation ) );
+ wfRunHooks( 'SkinTemplateNavigation::Universal', array( &$this, &$content_navigation ) );
// Setup xml ids and tooltip info
foreach ( $content_navigation as $section => &$links ) {
@@ -1036,8 +1110,8 @@ class SkinTemplate extends Skin {
# 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 su-
- # perfluous and conflicts with an accesskey (Ctrl-E) often
+ # 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' ) ) ) {
if ( isset( $content_navigation['views']['edit'] ) ) {
@@ -1119,9 +1193,9 @@ class SkinTemplate extends Skin {
$nav_urls = array();
$nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() );
- if( $wgUploadNavigationUrl ) {
+ if ( $wgUploadNavigationUrl ) {
$nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl );
- } elseif( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
+ } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
$nav_urls['upload'] = array( 'href' => self::makeSpecialUrl( 'Upload' ) );
} else {
$nav_urls['upload'] = false;
@@ -1130,24 +1204,26 @@ class SkinTemplate extends Skin {
$nav_urls['print'] = false;
$nav_urls['permalink'] = false;
+ $nav_urls['info'] = false;
$nav_urls['whatlinkshere'] = false;
$nav_urls['recentchangeslinked'] = false;
$nav_urls['contributions'] = false;
$nav_urls['log'] = false;
$nav_urls['blockip'] = false;
$nav_urls['emailuser'] = false;
+ $nav_urls['userrights'] = false;
// A print stylesheet is attached to all pages, but nobody ever
// figures that out. :) Add a link...
- if ( $out->isArticle() ) {
- if ( !$out->isPrintable() ) {
- $nav_urls['print'] = array(
- 'text' => $this->msg( 'printableversion' )->text(),
- 'href' => $this->getTitle()->getLocalURL(
- $request->appendQueryValue( 'printable', 'yes', true ) )
- );
- }
+ if ( !$out->isPrintable() && ( $out->isArticle() || $this->getTitle()->isSpecialPage() ) ) {
+ $nav_urls['print'] = array(
+ 'text' => $this->msg( 'printableversion' )->text(),
+ 'href' => $this->getTitle()->getLocalURL(
+ $request->appendQueryValue( 'printable', 'yes', true ) )
+ );
+ }
+ if ( $out->isArticle() ) {
// Also add a "permalink" while we're at it
$revid = $this->getRevisionId();
if ( $revid ) {
@@ -1164,11 +1240,17 @@ class SkinTemplate extends Skin {
if ( $out->isArticleRelated() ) {
$nav_urls['whatlinkshere'] = array(
- 'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalUrl()
+ 'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalURL()
+ );
+
+ $nav_urls['info'] = array(
+ 'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
+ 'href' => $this->getTitle()->getLocalURL( "action=info" )
);
+
if ( $this->getTitle()->getArticleID() ) {
$nav_urls['recentchangeslinked'] = array(
- 'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalUrl()
+ 'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalURL()
);
}
}
@@ -1178,6 +1260,7 @@ class SkinTemplate extends Skin {
$rootUser = $user->getName();
$nav_urls['contributions'] = array(
+ 'text' => $this->msg( 'contributions', $rootUser )->text(),
'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser )
);
@@ -1196,6 +1279,16 @@ class SkinTemplate extends Skin {
'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser )
);
}
+
+ if ( !$user->isAnon() ) {
+ $sur = new UserrightsPage;
+ $sur->setContext( $this->getContext() );
+ if ( $sur->userCanExecute( $this->getUser() ) ) {
+ $nav_urls['userrights'] = array(
+ 'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
+ );
+ }
+ }
}
wfProfileOut( __METHOD__ );
@@ -1210,10 +1303,6 @@ class SkinTemplate extends Skin {
function getNameSpaceKey() {
return $this->getTitle()->getNamespaceKey();
}
-
- public function commonPrintStylesheet() {
- return false;
- }
}
/**
@@ -1225,7 +1314,7 @@ abstract class QuickTemplate {
/**
* Constructor
*/
- public function QuickTemplate() {
+ function __construct() {
$this->data = array();
$this->translator = new MediaWiki_I18N();
}
@@ -1240,6 +1329,21 @@ abstract class QuickTemplate {
}
/**
+ * Gets the template data requested
+ * @since 1.22
+ * @param string $name Key for the data
+ * @param mixed $default Optional default (or null)
+ * @return mixed The value of the data requested or the deafult
+ */
+ public function get( $name, $default = null ) {
+ if ( isset( $this->data[$name] ) ) {
+ return $this->data[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ /**
* @param $name
* @param $value
*/
@@ -1269,8 +1373,10 @@ abstract class QuickTemplate {
/**
* @private
+ * @deprecated since 1.21; use Xml::encodeJsVar() or Xml::encodeJsCall() instead
*/
function jstext( $str ) {
+ wfDeprecated( __METHOD__, '1.21' );
echo Xml::escapeJsString( $this->data[$str] );
}
@@ -1344,7 +1450,7 @@ abstract class BaseTemplate extends QuickTemplate {
/**
* Get a Message object with its context set
*
- * @param $name string message name
+ * @param string $name message name
* @return Message
*/
public function getMsg( $name ) {
@@ -1366,7 +1472,7 @@ abstract class BaseTemplate extends QuickTemplate {
/**
* Create an array of common toolbox items from the data in the quicktemplate
* stored by SkinTemplate.
- * The resulting array is built acording to a format intended to be passed
+ * The resulting array is built according to a format intended to be passed
* through makeListItem to generate the html.
* @return array
*/
@@ -1394,7 +1500,7 @@ abstract class BaseTemplate extends QuickTemplate {
$toolbox['feeds']['links'][$key]['class'] = 'feedlink';
}
}
- foreach ( array( 'contributions', 'log', 'blockip', 'emailuser', 'upload', 'specialpages' ) as $special ) {
+ foreach ( array( 'contributions', 'log', 'blockip', 'emailuser', 'userrights', 'upload', 'specialpages' ) as $special ) {
if ( isset( $this->data['nav_urls'][$special] ) && $this->data['nav_urls'][$special] ) {
$toolbox[$special] = $this->data['nav_urls'][$special];
$toolbox[$special]['id'] = "t-$special";
@@ -1417,6 +1523,11 @@ abstract class BaseTemplate extends QuickTemplate {
$toolbox['permalink']['id'] = 't-permalink';
}
}
+ if ( isset( $this->data['nav_urls']['info'] ) && $this->data['nav_urls']['info'] ) {
+ $toolbox['info'] = $this->data['nav_urls']['info'];
+ $toolbox['info']['id'] = 't-info';
+ }
+
wfRunHooks( 'BaseTemplateToolbox', array( &$this, &$toolbox ) );
wfProfileOut( __METHOD__ );
return $toolbox;
@@ -1425,7 +1536,7 @@ abstract class BaseTemplate extends QuickTemplate {
/**
* Create an array of personal tools items from the data in the quicktemplate
* stored by SkinTemplate.
- * The resulting array is built acording to a format intended to be passed
+ * The resulting array is built according to a format intended to be passed
* through makeListItem to generate the html.
* This is in reality the same list as already stored in personal_urls
* however it is reformatted so that you can just pass the individual items
@@ -1434,7 +1545,7 @@ abstract class BaseTemplate extends QuickTemplate {
*/
function getPersonalTools() {
$personal_tools = array();
- foreach ( $this->data['personal_urls'] as $key => $plink ) {
+ foreach ( $this->get( 'personal_urls' ) as $key => $plink ) {
# The class on a personal_urls item is meant to go on the <a> instead
# of the <li> so we have to use a single item "links" array instead
# of using most of the personal_url's keys directly.
@@ -1448,8 +1559,9 @@ abstract class BaseTemplate extends QuickTemplate {
$ptool['active'] = $plink['active'];
}
foreach ( array( 'href', 'class', 'text' ) as $k ) {
- if ( isset( $plink[$k] ) )
+ if ( isset( $plink[$k] ) ) {
$ptool['links'][0][$k] = $plink[$k];
+ }
}
$personal_tools[$key] = $ptool;
}
@@ -1488,39 +1600,39 @@ abstract class BaseTemplate extends QuickTemplate {
case 'SEARCH':
// Search is a special case, skins should custom implement this
$boxes[$boxName] = array(
- 'id' => 'p-search',
- 'header' => $this->getMsg( 'search' )->text(),
+ 'id' => 'p-search',
+ 'header' => $this->getMsg( 'search' )->text(),
'generated' => false,
- 'content' => true,
+ 'content' => true,
);
break;
case 'TOOLBOX':
$msgObj = $this->getMsg( 'toolbox' );
$boxes[$boxName] = array(
- 'id' => 'p-tb',
- 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
+ 'id' => 'p-tb',
+ 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
'generated' => false,
- 'content' => $this->getToolbox(),
+ 'content' => $this->getToolbox(),
);
break;
case 'LANGUAGES':
if ( $this->data['language_urls'] ) {
$msgObj = $this->getMsg( 'otherlanguages' );
$boxes[$boxName] = array(
- 'id' => 'p-lang',
- 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
+ 'id' => 'p-lang',
+ 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
'generated' => false,
- 'content' => $this->data['language_urls'],
+ 'content' => $this->data['language_urls'],
);
}
break;
default:
$msgObj = $this->getMsg( $boxName );
$boxes[$boxName] = array(
- 'id' => "p-$boxName",
- 'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
+ 'id' => "p-$boxName",
+ 'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
'generated' => true,
- 'content' => $content,
+ 'content' => $content,
);
break;
}
@@ -1560,10 +1672,10 @@ abstract class BaseTemplate extends QuickTemplate {
} else {
if ( $hookContents ) {
$boxes['TOOLBOXEND'] = array(
- 'id' => 'p-toolboxend',
- 'header' => $boxes['TOOLBOX']['header'],
+ 'id' => 'p-toolboxend',
+ 'header' => $boxes['TOOLBOX']['header'],
'generated' => false,
- 'content' => "<ul>{$hookContents}</ul>",
+ 'content' => "<ul>{$hookContents}</ul>",
);
// HACK: Make sure that TOOLBOXEND is sorted next to TOOLBOX
$boxes2 = array();
@@ -1588,9 +1700,9 @@ abstract class BaseTemplate extends QuickTemplate {
* Makes a link, usually used by makeListItem to generate a link for an item
* in a list used in navigation lists, portlets, portals, sidebars, etc...
*
- * @param $key string usually a key from the list you are generating this
+ * @param string $key usually a key from the list you are generating this
* link from.
- * @param $item array contains some of a specific set of keys.
+ * @param array $item contains some of a specific set of keys.
*
* The text of the link will be generated either from the contents of the
* "text" key in the $item array, if a "msg" key is present a message by
@@ -1605,9 +1717,13 @@ abstract class BaseTemplate extends QuickTemplate {
* on the link) is present it will be used to generate a tooltip and
* accesskey for the link.
*
+ * The keys "context" and "primary" are ignored; these keys are used
+ * internally by skins and are not supposed to be included in the HTML
+ * output.
+ *
* If you don't want an accesskey, set $item['tooltiponly'] = true;
*
- * @param $options array can be used to affect the output of a link.
+ * @param array $options can be used to affect the output of a link.
* Possible options are:
* - 'text-wrapper' key to specify a list of elements to wrap the text of
* a link in. This should be an array of arrays containing a 'tag' and
@@ -1645,7 +1761,7 @@ abstract class BaseTemplate extends QuickTemplate {
if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
$attrs = $item;
- foreach ( array( 'single-id', 'text', 'msg', 'tooltiponly' ) as $k ) {
+ foreach ( array( 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary' ) as $k ) {
unset( $attrs[$k] );
}
@@ -1720,7 +1836,7 @@ abstract class BaseTemplate extends QuickTemplate {
foreach ( array( 'id', 'class', 'active', 'tag' ) as $k ) {
unset( $link[$k] );
}
- if ( isset( $item['id'] ) ) {
+ if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
// The id goes on the <li> not on the <a> for single links
// but makeSidebarLink still needs to know what id to use when
// generating tooltips and accesskeys.
@@ -1749,14 +1865,15 @@ abstract class BaseTemplate extends QuickTemplate {
$realAttrs = array(
'type' => 'search',
'name' => 'search',
- 'value' => isset( $this->data['search'] ) ? $this->data['search'] : '',
+ 'placeholder' => wfMessage( 'searchsuggest-search' )->text(),
+ 'value' => $this->get( 'search', '' ),
);
$realAttrs = array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
return Html::element( 'input', $realAttrs );
}
function makeSearchButton( $mode, $attrs = array() ) {
- switch( $mode ) {
+ switch ( $mode ) {
case 'go':
case 'fulltext':
$realAttrs = array(
@@ -1783,11 +1900,15 @@ abstract class BaseTemplate extends QuickTemplate {
);
unset( $buttonAttrs['src'] );
unset( $buttonAttrs['alt'] );
+ unset( $buttonAttrs['width'] );
+ unset( $buttonAttrs['height'] );
$imgAttrs = array(
'src' => $attrs['src'],
'alt' => isset( $attrs['alt'] )
? $attrs['alt']
: $this->translator->translate( 'searchbutton' ),
+ 'width' => isset( $attrs['width'] ) ? $attrs['width'] : null,
+ 'height' => isset( $attrs['height'] ) ? $attrs['height'] : null,
);
return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
default:
@@ -1804,14 +1925,14 @@ abstract class BaseTemplate extends QuickTemplate {
* @return array|mixed
*/
function getFooterLinks( $option = null ) {
- $footerlinks = $this->data['footerlinks'];
+ $footerlinks = $this->get( 'footerlinks' );
// Reduce footer links down to only those which are being used
$validFooterLinks = array();
- foreach( $footerlinks as $category => $links ) {
+ foreach ( $footerlinks as $category => $links ) {
$validFooterLinks[$category] = array();
- foreach( $links as $link ) {
- if( isset( $this->data[$link] ) && $this->data[$link] ) {
+ foreach ( $links as $link ) {
+ if ( isset( $this->data[$link] ) && $this->data[$link] ) {
$validFooterLinks[$category][] = $link;
}
}
@@ -1844,7 +1965,7 @@ abstract class BaseTemplate extends QuickTemplate {
*/
function getFooterIcons( $option = null ) {
// Generate additional footer icons
- $footericons = $this->data['footericons'];
+ $footericons = $this->get( 'footericons' );
if ( $option == 'icononly' ) {
// Unset any icons which don't have an image
@@ -1877,9 +1998,10 @@ abstract class BaseTemplate extends QuickTemplate {
* body and html tags.
*/
function printTrail() { ?>
+<?php echo MWDebug::getDebugHTML( $this->getSkin()->getContext() ); ?>
<?php $this->html( 'bottomscripts' ); /* JS call to runBodyOnloadHook */ ?>
<?php $this->html( 'reporttime' ) ?>
-<?php echo MWDebug::getDebugHTML( $this->getSkin()->getContext() );
+<?php
}
}
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index 2e5e02b0..a6195fc4 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -27,7 +27,6 @@
* @ingroup SpecialPage
*/
class SpecialPage {
-
// The canonical name of this special page
// Also used for the default <h1> heading, @see getDescription()
protected $mName;
@@ -106,19 +105,6 @@ class SpecialPage {
}
/**
- * Add a page to the list of valid special pages. This used to be the preferred
- * method for adding special pages in extensions. It's now suggested that you add
- * an associative record to $wgSpecialPages. This avoids autoloading SpecialPage.
- *
- * @param $page SpecialPage
- * @deprecated since 1.7, warnings in 1.17, might be removed in 1.20
- */
- static function addPage( &$page ) {
- wfDeprecated( __METHOD__, '1.7' );
- SpecialPageFactory::getList()->{$page->mName} = $page;
- }
-
- /**
* Add a page to a certain display group for Special:SpecialPages
*
* @param $page Mixed: SpecialPage or string
@@ -148,7 +134,7 @@ class SpecialPage {
* preferred method is now to add a SpecialPage_initList hook.
* @deprecated since 1.18
*
- * @param $name String the page to remove
+ * @param string $name the page to remove
*/
static function removePage( $name ) {
wfDeprecated( __METHOD__, '1.18' );
@@ -158,7 +144,7 @@ class SpecialPage {
/**
* Check if a given name exist as a special page or as a special page alias
*
- * @param $name String: name of a special page
+ * @param string $name name of a special page
* @return Boolean: true if a special page exists with this name
* @deprecated since 1.18 call SpecialPageFactory method directly
*/
@@ -266,13 +252,15 @@ class SpecialPage {
* Get a localised Title object for a specified special page name
*
* @param $name String
- * @param $subpage String|Bool subpage string, or false to not use a subpage
+ * @param string|Bool $subpage subpage string, or false to not use a subpage
+ * @param string $fragment the link fragment (after the "#")
+ * @throws MWException
* @return Title object
*/
- public static function getTitleFor( $name, $subpage = false ) {
+ public static function getTitleFor( $name, $subpage = false, $fragment = '' ) {
$name = SpecialPageFactory::getLocalNameFor( $name, $subpage );
if ( $name ) {
- return Title::makeTitle( NS_SPECIAL, $name );
+ return Title::makeTitle( NS_SPECIAL, $name, $fragment );
} else {
throw new MWException( "Invalid special page name \"$name\"" );
}
@@ -282,7 +270,7 @@ class SpecialPage {
* Get a localised Title object for a page name with a possibly unvalidated subpage
*
* @param $name String
- * @param $subpage String|Bool subpage string, or false to not use a subpage
+ * @param string|Bool $subpage subpage string, or false to not use a subpage
* @return Title object or null if the page doesn't exist
*/
public static function getSafeTitleFor( $name, $subpage = false ) {
@@ -313,15 +301,17 @@ class SpecialPage {
* be displayed by the default execute() method, without the global function ever
* being called.
*
- * If you override execute(), you can recover the default behaviour with userCanExecute()
+ * If you override execute(), you can recover the default behavior with userCanExecute()
* and displayRestrictionError()
*
- * @param $name String: name of the special page, as seen in links and URLs
- * @param $restriction String: user right required, e.g. "block" or "delete"
- * @param $listed Bool: whether the page is listed in Special:Specialpages
- * @param $function Callback|Bool: function called by execute(). By default it is constructed from $name
- * @param $file String: file which is included by execute(). It is also constructed from $name by default
- * @param $includable Bool: whether the page can be included in normal pages
+ * @param string $name Name of the special page, as seen in links and URLs
+ * @param string $restriction User right required, e.g. "block" or "delete"
+ * @param bool $listed Whether the page is listed in Special:Specialpages
+ * @param Callback|Bool $function Function called by execute(). By default
+ * it is constructed from $name
+ * @param string $file File which is included by execute(). It is also
+ * constructed from $name by default
+ * @param bool $includable Whether the page can be included in normal pages
*/
public function __construct(
$name = '', $restriction = '', $listed = true,
@@ -333,12 +323,14 @@ class SpecialPage {
/**
* Do the real work for the constructor, mainly so __call() can intercept
* calls to SpecialPage()
- * @param $name String: name of the special page, as seen in links and URLs
- * @param $restriction String: user right required, e.g. "block" or "delete"
- * @param $listed Bool: whether the page is listed in Special:Specialpages
- * @param $function Callback|Bool: function called by execute(). By default it is constructed from $name
- * @param $file String: file which is included by execute(). It is also constructed from $name by default
- * @param $includable Bool: whether the page can be included in normal pages
+ * @param string $name Name of the special page, as seen in links and URLs
+ * @param string $restriction User right required, e.g. "block" or "delete"
+ * @param bool $listed Whether the page is listed in Special:Specialpages
+ * @param Callback|Bool $function Function called by execute(). By default
+ * it is constructed from $name
+ * @param string $file File which is included by execute(). It is also
+ * constructed from $name by default
+ * @param bool $includable Whether the page can be included in normal pages
*/
private function init( $name, $restriction, $listed, $function, $file, $includable ) {
$this->mName = $name;
@@ -361,8 +353,9 @@ class SpecialPage {
* Use PHP's magic __call handler to get calls to the old PHP4 constructor
* because PHP E_STRICT yells at you for having __construct() and SpecialPage()
*
- * @param $fName String Name of called method
- * @param $a Array Arguments to the method
+ * @param string $fName Name of called method
+ * @param array $a Arguments to the method
+ * @throws MWException
* @deprecated since 1.17, call parent::__construct()
*/
public function __call( $fName, $a ) {
@@ -455,7 +448,10 @@ class SpecialPage {
* @return Mixed
* @deprecated since 1.18
*/
- function name( $x = null ) { wfDeprecated( __METHOD__, '1.18' ); return wfSetVar( $this->mName, $x ); }
+ function name( $x = null ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return wfSetVar( $this->mName, $x );
+ }
/**
* These mutators are very evil, as the relevant variables should not mutate. So
@@ -464,7 +460,10 @@ class SpecialPage {
* @return Mixed
* @deprecated since 1.18
*/
- function restriction( $x = null ) { wfDeprecated( __METHOD__, '1.18' ); return wfSetVar( $this->mRestriction, $x ); }
+ function restriction( $x = null ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return wfSetVar( $this->mRestriction, $x );
+ }
/**
* These mutators are very evil, as the relevant variables should not mutate. So
@@ -473,7 +472,10 @@ class SpecialPage {
* @return Mixed
* @deprecated since 1.18
*/
- function func( $x = null ) { wfDeprecated( __METHOD__, '1.18' ); return wfSetVar( $this->mFunction, $x ); }
+ function func( $x = null ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return wfSetVar( $this->mFunction, $x );
+ }
/**
* These mutators are very evil, as the relevant variables should not mutate. So
@@ -482,7 +484,10 @@ class SpecialPage {
* @return Mixed
* @deprecated since 1.18
*/
- function file( $x = null ) { wfDeprecated( __METHOD__, '1.18' ); return wfSetVar( $this->mFile, $x ); }
+ function file( $x = null ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return wfSetVar( $this->mFile, $x );
+ }
/**
* These mutators are very evil, as the relevant variables should not mutate. So
@@ -491,7 +496,10 @@ class SpecialPage {
* @return Mixed
* @deprecated since 1.18
*/
- function includable( $x = null ) { wfDeprecated( __METHOD__, '1.18' ); return wfSetVar( $this->mIncludable, $x ); }
+ function includable( $x = null ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return wfSetVar( $this->mIncludable, $x );
+ }
/**
* Whether the special page is being evaluated via transclusion
@@ -525,6 +533,19 @@ class SpecialPage {
}
/**
+ * Is this page cached?
+ * Expensive pages are cached or disabled in miser mode.
+ * Used by QueryPage and subclasses, moved here so that
+ * Special:SpecialPages can safely call it for all special pages.
+ *
+ * @return Boolean
+ * @since 1.21
+ */
+ public function isCached() {
+ return false;
+ }
+
+ /**
* Can be overridden by subclasses with more complicated permissions
* schemes.
*
@@ -532,9 +553,8 @@ class SpecialPage {
* pages?
*/
public function isRestricted() {
- global $wgGroupPermissions;
- // DWIM: If all anons can do something, then it is not restricted
- return $this->mRestriction != '' && empty( $wgGroupPermissions['*'][$this->mRestriction] );
+ // DWIM: If anons can do something, then it is not restricted
+ return $this->mRestriction != '' && !User::groupHasPermission( '*', $this->mRestriction );
}
/**
@@ -596,7 +616,7 @@ class SpecialPage {
*
* @param $subPage string|null
*/
- public final function run( $subPage ) {
+ final public function run( $subPage ) {
/**
* Gets called before @see SpecialPage::execute.
*
@@ -659,7 +679,7 @@ class SpecialPage {
$func = $this->mFunction;
// only load file if the function does not exist
if ( !is_callable( $func ) && $this->mFile ) {
- require_once( $this->mFile );
+ require_once $this->mFile;
}
$this->outputHeader();
call_user_func( $func, $subPage, $this );
@@ -668,10 +688,10 @@ 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
+ * May be overridden, i.e. by extensions to stick with the naming conventions
* for message keys: 'extensionname-xxx'
*
- * @param $summaryMessageKey String: message key of the summary
+ * @param string $summaryMessageKey message key of the summary
*/
function outputHeader( $summaryMessageKey = '' ) {
global $wgContLang;
@@ -693,7 +713,7 @@ class SpecialPage {
* also the name that will be listed in Special:Specialpages
*
* Derived classes can override this, but usually it is easier to keep the
- * default behaviour. Messages can be added at run-time, see
+ * default behavior. Messages can be added at run-time, see
* MessageCache.php.
*
* @return String
@@ -732,7 +752,8 @@ class SpecialPage {
if ( $this->mContext instanceof IContextSource ) {
return $this->mContext;
} else {
- wfDebug( __METHOD__ . " called and \$mContext is null. Return RequestContext::getMain(); for sanity\n" );
+ wfDebug( __METHOD__ . " called and \$mContext is null. " .
+ "Return RequestContext::getMain(); for sanity\n" );
return RequestContext::getMain();
}
}
@@ -780,7 +801,7 @@ class SpecialPage {
/**
* Shortcut to get user's language
*
- * @deprecated 1.19 Use getLanguage instead
+ * @deprecated since 1.19 Use getLanguage instead
* @return Language
* @since 1.18
*/
@@ -825,7 +846,7 @@ class SpecialPage {
// RequestContext passes context to wfMessage, and the language is set from
// the context, but setting the language for Message class removes the
// interface message status, which breaks for example usernameless gender
- // invokations. Restore the flag when not including special page in content.
+ // invocations. Restore the flag when not including special page in content.
if ( $this->including() ) {
$message->setInterfaceMessageFlag( false );
}
@@ -840,14 +861,65 @@ class SpecialPage {
protected function addFeedLinks( $params ) {
global $wgFeedClasses;
- $feedTemplate = wfScript( 'api' ) . '?';
+ $feedTemplate = wfScript( 'api' );
foreach ( $wgFeedClasses as $format => $class ) {
$theseParams = $params + array( 'feedformat' => $format );
- $url = $feedTemplate . wfArrayToCGI( $theseParams );
+ $url = wfAppendQuery( $feedTemplate, $theseParams );
$this->getOutput()->addFeedLink( $format, $url );
}
}
+
+ /**
+ * Get the group that the special page belongs in on Special:SpecialPage
+ * Use this method, instead of getGroupName to allow customization
+ * of the group name from the wiki side
+ *
+ * @return string Group of this special page
+ * @since 1.21
+ */
+ public function getFinalGroupName() {
+ global $wgSpecialPageGroups;
+ $name = $this->getName();
+ $group = '-';
+
+ // Allow overbidding the group from the wiki side
+ $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
+ if ( !$msg->isBlank() ) {
+ $group = $msg->text();
+ } else {
+ // Than use the group from this object
+ $group = $this->getGroupName();
+
+ // Group '-' is used as default to have the chance to determine,
+ // if the special pages overrides this method,
+ // if not overridden, $wgSpecialPageGroups is checked for b/c
+ if ( $group === '-' && isset( $wgSpecialPageGroups[$name] ) ) {
+ $group = $wgSpecialPageGroups[$name];
+ }
+ }
+
+ // never give '-' back, change to 'other'
+ if ( $group === '-' ) {
+ $group = 'other';
+ }
+
+ return $group;
+ }
+
+ /**
+ * Under which header this special page is listed in Special:SpecialPages
+ * See messages 'specialpages-group-*' for valid names
+ * This method defaults to group 'other'
+ *
+ * @return string
+ * @since 1.21
+ */
+ protected function getGroupName() {
+ // '-' used here to determine, if this group is overridden or has a hardcoded 'other'
+ // Needed for b/c in getFinalGroupName
+ return '-';
+ }
}
/**
@@ -856,43 +928,77 @@ class SpecialPage {
* a new structure for SpecialPages
*/
abstract class FormSpecialPage extends SpecialPage {
+ /**
+ * The sub-page of the special page.
+ * @var string
+ */
+ protected $par = null;
/**
* Get an HTMLForm descriptor array
* @return Array
*/
- protected abstract function getFormFields();
+ abstract protected function getFormFields();
/**
- * Add pre- or post-text to the form
+ * Add pre-text to the form
* @return String HTML which will be sent to $form->addPreText()
*/
- protected function preText() { return ''; }
- protected function postText() { return ''; }
+ protected function preText() {
+ return '';
+ }
+
+ /**
+ * Add post-text to the form
+ * @return String HTML which will be sent to $form->addPostText()
+ */
+ protected function postText() {
+ return '';
+ }
/**
* Play with the HTMLForm if you need to more substantially
* @param $form HTMLForm
*/
- protected function alterForm( HTMLForm $form ) {}
+ protected function alterForm( HTMLForm $form ) {
+ }
+
+ /**
+ * Get message prefix for HTMLForm
+ *
+ * @since 1.21
+ * @return string
+ */
+ protected function getMessagePrefix() {
+ return strtolower( $this->getName() );
+ }
/**
- * Get the HTMLForm to control behaviour
+ * Get the HTMLForm to control behavior
* @return HTMLForm|null
*/
protected function getForm() {
$this->fields = $this->getFormFields();
- $form = new HTMLForm( $this->fields, $this->getContext() );
+ $form = new HTMLForm( $this->fields, $this->getContext(), $this->getMessagePrefix() );
$form->setSubmitCallback( array( $this, 'onSubmit' ) );
- $form->setWrapperLegend( $this->msg( strtolower( $this->getName() ) . '-legend' ) );
- $form->addHeaderText(
- $this->msg( strtolower( $this->getName() ) . '-text' )->parseAsBlock() );
+ // If the form is a compact vertical form, then don't output this ugly
+ // fieldset surrounding it.
+ // XXX Special pages can setDisplayFormat to 'vform' in alterForm(), but that
+ // is called after this.
+ if ( !$form->isVForm() ) {
+ $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
+ }
+
+ $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' );
+ if ( !$headerMsg->isDisabled() ) {
+ $form->addHeaderText( $headerMsg->parseAsBlock() );
+ }
// Retain query parameters (uselang etc)
$params = array_diff_key(
$this->getRequest()->getQueryValues(), array( 'title' => null ) );
- $form->addHiddenField( 'redirectparams', wfArrayToCGI( $params ) );
+ $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
$form->addPreText( $this->preText() );
$form->addPostText( $this->postText() );
@@ -909,18 +1015,20 @@ abstract class FormSpecialPage extends SpecialPage {
* @param $data Array
* @return Bool|Array true for success, false for didn't-try, array of errors on failure
*/
- public abstract function onSubmit( array $data );
+ abstract public function onSubmit( array $data );
/**
* Do something exciting on successful processing of the form, most likely to show a
* confirmation message
+ * @since 1.22 Default is to do nothing
*/
- public abstract function onSuccess();
+ public function onSuccess() {
+ }
/**
* Basic SpecialPage workflow: get a form, send it to the user; get some data back,
*
- * @param $par String Subpage string if one was specified
+ * @param string $par Subpage string if one was specified
*/
public function execute( $par ) {
$this->setParameter( $par );
@@ -937,16 +1045,18 @@ abstract class FormSpecialPage extends SpecialPage {
/**
* Maybe do something interesting with the subpage parameter
- * @param $par String
+ * @param string $par
*/
- protected function setParameter( $par ) {}
+ protected function setParameter( $par ) {
+ $this->par = $par;
+ }
/**
* Called from execute() to check if the given user can perform this action.
* Failures here must throw subclasses of ErrorPageError.
* @param $user User
+ * @throws UserBlockedError
* @return Bool true
- * @throws ErrorPageError
*/
protected function checkExecutePermissions( User $user ) {
$this->checkPermissions();
@@ -1019,7 +1129,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
// Query parameters that can be passed through redirects
protected $mAllowedRedirectParams = array();
- // Query parameteres added by redirects
+ // Query parameters added by redirects
protected $mAddedRedirectParams = array();
public function execute( $par ) {
@@ -1027,19 +1137,16 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
$query = $this->getRedirectQuery();
// Redirect to a page title with possible query parameters
if ( $redirect instanceof Title ) {
- $url = $redirect->getFullUrl( $query );
+ $url = $redirect->getFullURL( $query );
$this->getOutput()->redirect( $url );
- wfProfileOut( __METHOD__ );
return $redirect;
- // Redirect to index.php with query parameters
} elseif ( $redirect === true ) {
- global $wgScript;
- $url = $wgScript . '?' . wfArrayToCGI( $query );
+ // Redirect to index.php with query parameters
+ $url = wfAppendQuery( wfScript( 'index' ), $query );
$this->getOutput()->redirect( $url );
- wfProfileOut( __METHOD__ );
return $redirect;
} else {
- $class = __CLASS__;
+ $class = get_class( $this );
throw new MWException( "RedirectSpecialPage $class doesn't redirect!" );
}
}
@@ -1048,7 +1155,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
* If the special page is a redirect, then get the Title object it redirects to.
* False otherwise.
*
- * @param $par String Subpage string
+ * @param string $par Subpage string
* @return Title|bool
*/
abstract public function getRedirect( $par );
@@ -1079,6 +1186,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
}
abstract class SpecialRedirectToSpecial extends RedirectSpecialPage {
+ // @todo FIXME: Visibility must be declared
var $redirName, $redirSubpage;
function __construct(
@@ -1125,12 +1233,21 @@ class SpecialListBots extends SpecialRedirectToSpecial {
*/
class SpecialCreateAccount extends SpecialRedirectToSpecial {
function __construct() {
- parent::__construct( 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) );
+ parent::__construct( 'CreateAccount', 'Userlogin', 'signup', array( 'returnto', 'returntoquery', 'uselang' ) );
+ }
+
+ // No reason to hide this link on Special:Specialpages
+ public function isListed() {
+ return true;
+ }
+
+ protected function getGroupName() {
+ return 'login';
}
}
/**
* SpecialMypage, SpecialMytalk and SpecialMycontributions special pages
- * are used to get user independant links pointing to the user page, talk
+ * are used to get user independent links pointing to the user page, talk
* page and list of contributions.
* This can let us cache a single copy of any generated content for all
* users.
@@ -1155,7 +1272,7 @@ class SpecialCreateAccount extends SpecialRedirectToSpecial {
* - limit, offset: Useful for linking to history of one's own user page or
* user talk page. For example, this would be a link to "the last edit to your
* user talk page in the year 2010":
- * http://en.wikipedia.org/w/index.php?title=Special:MyPage&offset=20110000000000&limit=1&action=history
+ * http://en.wikipedia.org/wiki/Special:MyPage?offset=20110000000000&limit=1&action=history
*
* - feed: would allow linking to the current user's RSS feed for their user
* talk page:
@@ -1209,9 +1326,9 @@ abstract class RedirectSpecialArticle extends RedirectSpecialPage {
'action',
'redirect', 'rdfrom',
# Options for preloaded edits
- 'preload', 'editintro', 'preloadtitle', 'summary',
+ 'preload', 'editintro', 'preloadtitle', 'summary', 'nosummary',
# Options for overriding user settings
- 'preview', 'internaledit', 'externaledit', 'mode',
+ 'preview', 'internaledit', 'externaledit', 'mode', 'minor', 'watchthis',
# Options for history/diffs
'section', 'oldid', 'diff', 'dir',
'limit', 'offset', 'feed',
@@ -1221,7 +1338,7 @@ abstract class RedirectSpecialArticle extends RedirectSpecialPage {
'ctype', 'maxage', 'smaxage',
);
- wfRunHooks( "RedirectSpecialArticleRedirectParams", array(&$redirectParams) );
+ wfRunHooks( "RedirectSpecialArticleRedirectParams", array( &$redirectParams ) );
$this->mAllowedRedirectParams = $redirectParams;
}
}
@@ -1268,7 +1385,7 @@ class SpecialMytalk extends RedirectSpecialArticle {
*/
class SpecialMycontributions extends RedirectSpecialPage {
function __construct() {
- parent::__construct( 'Mycontributions' );
+ parent::__construct( 'Mycontributions' );
$this->mAllowedRedirectParams = array( 'limit', 'namespace', 'tagfilter',
'offset', 'dir', 'year', 'month', 'feed' );
}
@@ -1284,7 +1401,7 @@ class SpecialMycontributions extends RedirectSpecialPage {
class SpecialMyuploads extends RedirectSpecialPage {
function __construct() {
parent::__construct( 'Myuploads' );
- $this->mAllowedRedirectParams = array( 'limit' );
+ $this->mAllowedRedirectParams = array( 'limit', 'ilshowall', 'ilsearch' );
}
function getRedirect( $subpage ) {
@@ -1293,6 +1410,22 @@ class SpecialMyuploads extends RedirectSpecialPage {
}
/**
+ * Redirect Special:Listfiles?user=$wgUser&ilshowall=true
+ */
+class SpecialAllMyUploads extends RedirectSpecialPage {
+ function __construct() {
+ parent::__construct( 'AllMyUploads' );
+ $this->mAllowedRedirectParams = array( 'limit', 'ilsearch' );
+ }
+
+ function getRedirect( $subpage ) {
+ $this->mAddedRedirectParams['ilshowall'] = 1;
+ return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() );
+ }
+}
+
+
+/**
* Redirect from Special:PermanentLink/### to index.php?oldid=###
*/
class SpecialPermanentLink extends RedirectSpecialPage {
diff --git a/includes/SpecialPageFactory.php b/includes/SpecialPageFactory.php
index 95f75a8e..11edc8ac 100644
--- a/includes/SpecialPageFactory.php
+++ b/includes/SpecialPageFactory.php
@@ -78,8 +78,8 @@ class SpecialPageFactory {
'Allpages' => 'SpecialAllpages',
'Prefixindex' => 'SpecialPrefixindex',
'Categories' => 'SpecialCategories',
- 'Disambiguations' => 'DisambiguationsPage',
'Listredirects' => 'ListredirectsPage',
+ 'PagesWithProp' => 'SpecialPagesWithProp',
// Login/create account
'Userlogin' => 'LoginForm',
@@ -93,9 +93,10 @@ class SpecialPageFactory {
'PasswordReset' => 'SpecialPasswordReset',
'DeletedContributions' => 'DeletedContributionsPage',
'Preferences' => 'SpecialPreferences',
+ 'ResetTokens' => 'SpecialResetTokens',
'Contributions' => 'SpecialContributions',
'Listgrouprights' => 'SpecialListGroupRights',
- 'Listusers' => 'SpecialListUsers' ,
+ 'Listusers' => 'SpecialListUsers',
'Listadmins' => 'SpecialListAdmins',
'Listbots' => 'SpecialListBots',
'Activeusers' => 'SpecialActiveUsers',
@@ -119,7 +120,7 @@ class SpecialPageFactory {
'Upload' => 'SpecialUpload',
'UploadStash' => 'SpecialUploadStash',
- // Wiki data and tools
+ // Data and tools
'Statistics' => 'SpecialStatistics',
'Allmessages' => 'SpecialAllmessages',
'Version' => 'SpecialVersion',
@@ -129,6 +130,7 @@ class SpecialPageFactory {
// Redirecting special pages
'LinkSearch' => 'LinkSearchPage',
'Randompage' => 'Randompage',
+ 'RandomInCategory' => 'SpecialRandomInCategory',
'Randomredirect' => 'SpecialRandomredirect',
// High use pages
@@ -153,15 +155,15 @@ class SpecialPageFactory {
// Unlisted / redirects
'Blankpage' => 'SpecialBlankpage',
- 'Blockme' => 'SpecialBlockme',
'Emailuser' => 'SpecialEmailUser',
- 'JavaScriptTest' => 'SpecialJavaScriptTest',
'Movepage' => 'MovePageForm',
'Mycontributions' => 'SpecialMycontributions',
'Mypage' => 'SpecialMypage',
'Mytalk' => 'SpecialMytalk',
'Myuploads' => 'SpecialMyuploads',
+ 'AllMyUploads' => 'SpecialAllMyUploads',
'PermanentLink' => 'SpecialPermanentLink',
+ 'Redirect' => 'SpecialRedirect',
'Revisiondelete' => 'SpecialRevisionDelete',
'Specialpages' => 'SpecialSpecialpages',
'Userlogout' => 'SpecialUserlogout',
@@ -178,7 +180,7 @@ class SpecialPageFactory {
static function getList() {
global $wgSpecialPages;
global $wgDisableCounters, $wgDisableInternalSearch, $wgEmailAuthentication;
- global $wgEnableEmail;
+ global $wgEnableEmail, $wgEnableJavaScriptTest;
if ( !is_object( self::$mList ) ) {
wfProfileIn( __METHOD__ );
@@ -200,6 +202,10 @@ class SpecialPageFactory {
self::$mList['ChangeEmail'] = 'SpecialChangeEmail';
}
+ if ( $wgEnableJavaScriptTest ) {
+ self::$mList['JavaScriptTest'] = 'SpecialJavaScriptTest';
+ }
+
// Add extension special pages
self::$mList = array_merge( self::$mList, $wgSpecialPages );
@@ -218,7 +224,7 @@ class SpecialPageFactory {
/**
* Initialise and return the list of special page aliases. Returns an object with
* properties which can be accessed $obj->pagename - each property is an array of
- * aliases; the first in the array is the cannonical alias. All registered special
+ * aliases; the first in the array is the canonical alias. All registered special
* pages are guaranteed to have a property entry, and for that property array to
* contain at least one entry (English fallbacks will be added if necessary).
* @return Object
@@ -282,8 +288,11 @@ class SpecialPageFactory {
*
* @param $page Mixed: SpecialPage or string
* @param $group String
+ * @deprecated since 1.21 Override SpecialPage::getGroupName
*/
public static function setGroup( $page, $group ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
global $wgSpecialPageGroups;
$name = is_object( $page ) ? $page->getName() : $page;
$wgSpecialPageGroups[$name] = $group;
@@ -294,34 +303,18 @@ class SpecialPageFactory {
*
* @param $page SpecialPage
* @return String
+ * @deprecated since 1.21 Use SpecialPage::getFinalGroupName
*/
public static function getGroup( &$page ) {
- $name = $page->getName();
+ wfDeprecated( __METHOD__, '1.21' );
- global $wgSpecialPageGroups;
- static $specialPageGroupsCache = array();
- if ( isset( $specialPageGroupsCache[$name] ) ) {
- return $specialPageGroupsCache[$name];
- }
- $msg = wfMessage( 'specialpages-specialpagegroup-' . strtolower( $name ) );
- if ( !$msg->isBlank() ) {
- $group = $msg->text();
- } else {
- $group = isset( $wgSpecialPageGroups[$name] )
- ? $wgSpecialPageGroups[$name]
- : '-';
- }
- if ( $group == '-' ) {
- $group = 'other';
- }
- $specialPageGroupsCache[$name] = $group;
- return $group;
+ return $page->getFinalGroupName();
}
/**
* Check if a given name exist as a special page or as a special page alias
*
- * @param $name String: name of a special page
+ * @param string $name name of a special page
* @return Boolean: true if a special page exists with this name
*/
public static function exists( $name ) {
@@ -332,8 +325,8 @@ class SpecialPageFactory {
/**
* Find the object with a given name and return it (or NULL)
*
- * @param $name String Special page name, may be localised and/or an alias
- * @return SpecialPage object or null if the page doesn't exist
+ * @param string $name Special page name, may be localised and/or an alias
+ * @return SpecialPage|null SpecialPage object or null if the page doesn't exist
*/
public static function getPage( $name ) {
list( $realName, /*...*/ ) = self::resolveAlias( $name );
@@ -370,11 +363,13 @@ class SpecialPageFactory {
}
foreach ( self::getList() as $name => $rec ) {
$page = self::getPage( $name );
- if ( $page // not null
- && $page->isListed()
- && ( !$page->isRestricted() || $page->userCanExecute( $user ) )
- ) {
- $pages[$name] = $page;
+ if ( $page ) { // not null
+ $page->setContext( RequestContext::getMain() );
+ if ( $page->isListed()
+ && ( !$page->isRestricted() || $page->userCanExecute( $user ) )
+ ) {
+ $pages[$name] = $page;
+ }
}
}
return $pages;
@@ -471,9 +466,8 @@ class SpecialPageFactory {
if ( $name != $page->getLocalName() && !$context->getRequest()->wasPosted() ) {
$query = $context->getRequest()->getQueryValues();
unset( $query['title'] );
- $query = wfArrayToCGI( $query );
$title = $page->getTitle( $par );
- $url = $title->getFullUrl( $query );
+ $url = $title->getFullURL( $query );
$context->getOutput()->redirect( $url );
wfProfileOut( __METHOD__ );
return $title;
diff --git a/includes/SqlDataUpdate.php b/includes/SqlDataUpdate.php
index 52c9be00..51188d85 100644
--- a/includes/SqlDataUpdate.php
+++ b/includes/SqlDataUpdate.php
@@ -48,7 +48,7 @@ abstract class SqlDataUpdate extends DataUpdate {
public function __construct( $withTransaction = true ) {
global $wgAntiLockFlags;
- parent::__construct( );
+ parent::__construct();
if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
$this->mOptions = array();
@@ -56,7 +56,7 @@ abstract class SqlDataUpdate extends DataUpdate {
$this->mOptions = array( 'FOR UPDATE' );
}
- // @todo: get connection only when it's needed? make sure that doesn't break anything, especially transactions!
+ // @todo get connection only when it's needed? make sure that doesn't break anything, especially transactions!
$this->mDb = wfGetDB( DB_MASTER );
$this->mWithTransaction = $withTransaction;
@@ -76,7 +76,7 @@ abstract class SqlDataUpdate extends DataUpdate {
// NOTE: nested transactions are not supported, only start a transaction if none is open
if ( $this->mDb->trxLevel() === 0 ) {
- $this->mDb->begin( get_class( $this ) . '::beginTransaction' );
+ $this->mDb->begin( get_class( $this ) . '::beginTransaction' );
$this->mHasTransaction = true;
}
}
@@ -95,7 +95,7 @@ abstract class SqlDataUpdate extends DataUpdate {
* Abort the database transaction started via beginTransaction (if any).
*/
public function abortTransaction() {
- if ( $this->mHasTransaction ) {
+ if ( $this->mHasTransaction ) { //XXX: actually... maybe always?
$this->mDb->rollback( get_class( $this ) . '::abortTransaction' );
$this->mHasTransaction = false;
}
@@ -108,8 +108,8 @@ abstract class SqlDataUpdate extends DataUpdate {
* @param $namespace Integer
* @param $dbkeys Array
*/
- protected function invalidatePages( $namespace, Array $dbkeys ) {
- if ( !count( $dbkeys ) ) {
+ protected function invalidatePages( $namespace, array $dbkeys ) {
+ if ( $dbkeys === array() ) {
return;
}
@@ -127,10 +127,12 @@ abstract class SqlDataUpdate extends DataUpdate {
'page_touched < ' . $this->mDb->addQuotes( $now )
), __METHOD__
);
+
foreach ( $res as $row ) {
$ids[] = $row->page_id;
}
- if ( !count( $ids ) ) {
+
+ if ( $ids === array() ) {
return;
}
diff --git a/includes/SquidPurgeClient.php b/includes/SquidPurgeClient.php
index 8eb0f6bf..f5fd1950 100644
--- a/includes/SquidPurgeClient.php
+++ b/includes/SquidPurgeClient.php
@@ -21,9 +21,9 @@
*/
/**
- * An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
- * Uses asynchronous I/O, allowing purges to be done in a highly parallel
- * manner.
+ * An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
+ * Uses asynchronous I/O, allowing purges to be done in a highly parallel
+ * manner.
*
* Could be replaced by curl_multi_exec() or some such.
*/
@@ -123,7 +123,7 @@ class SquidPurgeClient {
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.
*/
@@ -176,11 +176,32 @@ class SquidPurgeClient {
* @param $url string
*/
public function queuePurge( $url ) {
+ global $wgSquidPurgeUseHostHeader;
$url = SquidUpdate::expand( 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";
+ $request = array();
+ if ( $wgSquidPurgeUseHostHeader ) {
+ $url = wfParseUrl( $url );
+ $host = $url['host'];
+ if ( isset( $url['port'] ) && strlen( $url['port'] ) > 0 ) {
+ $host .= ":" . $url['port'];
+ }
+ $path = $url['path'];
+ if ( isset( $url['query'] ) && is_string( $url['query'] ) ) {
+ $path = wfAppendQuery( $path, $url['query'] );
+ }
+ $request[] = "PURGE $path HTTP/1.1";
+ $request[] = "Host: $host";
+ } else {
+ $request[] = "PURGE $url HTTP/1.0";
+ }
+ $request[] = "Connection: Keep-Alive";
+ $request[] = "Proxy-Connection: Keep-Alive";
+ $request[] = "User-Agent: " . Http::userAgent() . ' ' . __CLASS__;
+ // Two ''s to create \r\n\r\n
+ $request[] = '';
+ $request[] = '';
+
+ $this->requests[] = implode( "\r\n", $request );
if ( $this->currentRequestIndex === null ) {
$this->nextRequest();
}
@@ -272,7 +293,7 @@ class SquidPurgeClient {
if ( count( $lines ) < 2 ) {
return 'done';
}
- if ( $this->readState == 'status' ) {
+ if ( $this->readState == 'status' ) {
$this->processStatusLine( $lines[0] );
} else { // header
$this->processHeaderLine( $lines[0] );
@@ -297,7 +318,7 @@ class SquidPurgeClient {
return 'done';
}
default:
- throw new MWException( __METHOD__.': unexpected state' );
+ throw new MWException( __METHOD__ . ': unexpected state' );
}
}
@@ -352,7 +373,7 @@ class SquidPurgeClient {
* @param $msg string
*/
protected function log( $msg ) {
- wfDebugLog( 'squid', __CLASS__." ($this->host): $msg\n" );
+ wfDebugLog( 'squid', __CLASS__ . " ($this->host): $msg\n" );
}
}
@@ -408,14 +429,14 @@ class SquidPurgeClientPool {
$numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout );
wfRestoreWarnings();
if ( $numReady === false ) {
- wfDebugLog( 'squid', __METHOD__.': Error in stream_select: ' .
+ 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" );
+ wfDebugLog( 'squid', __CLASS__ . ": timeout ({$this->timeout}s)\n" );
break;
} elseif ( !$numReady ) {
continue;
diff --git a/includes/StatCounter.php b/includes/StatCounter.php
new file mode 100644
index 00000000..1373f3d5
--- /dev/null
+++ b/includes/StatCounter.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * @defgroup StatCounter StatCounter
+ *
+ * StatCounter is used to increment arbitrary keys for profiling reasons.
+ * The key/values are persisted in several possible ways (see $wgStatsMethod).
+ */
+
+/**
+ * Aggregator for wfIncrStats() that batches updates per request.
+ *
+ * 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 StatCounter
+ * @author Aaron Schulz
+ */
+
+/**
+ * Aggregator for wfIncrStats() that batches updates per request.
+ * This avoids spamming the collector many times for the same key.
+ *
+ * @ingroup StatCounter
+ */
+class StatCounter {
+ /** @var Array */
+ protected $deltas = array(); // (key => count)
+
+ protected function __construct() {}
+
+ /**
+ * @return StatCounter
+ */
+ public static function singleton() {
+ static $instance = null;
+ if ( !$instance ) {
+ $instance = new self();
+ }
+ return $instance;
+ }
+
+ /**
+ * Increment a key by delta $count
+ *
+ * @param string $key
+ * @param integer $count
+ * @return void
+ */
+ public function incr( $key, $count = 1 ) {
+ $this->deltas[$key] = isset( $this->deltas[$key] ) ? $this->deltas[$key] : 0;
+ $this->deltas[$key] += $count;
+ if ( PHP_SAPI === 'cli' ) {
+ $this->flush();
+ }
+ }
+
+ /**
+ * Flush all pending deltas to persistent storage
+ *
+ * @return void
+ */
+ public function flush() {
+ global $wgStatsMethod;
+
+ $deltas = array_filter( $this->deltas ); // remove 0 valued entries
+ if ( $wgStatsMethod === 'udp' ) {
+ $this->sendDeltasUDP( $deltas );
+ } elseif ( $wgStatsMethod === 'cache' ) {
+ $this->sendDeltasMemc( $deltas );
+ } else {
+ // disabled
+ }
+ $this->deltas = array();
+ }
+
+ /**
+ * @param array $deltas
+ * @return void
+ */
+ protected function sendDeltasUDP( array $deltas ) {
+ global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgAggregateStatsID,
+ $wgStatsFormatString;
+
+ $id = strlen( $wgAggregateStatsID ) ? $wgAggregateStatsID : wfWikiID();
+
+ $lines = array();
+ foreach ( $deltas as $key => $count ) {
+ $lines[] = sprintf( $wgStatsFormatString, $id, $count, $key );
+ }
+
+ if ( count( $lines ) ) {
+ static $socket = null;
+ if ( !$socket ) {
+ $socket = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
+ }
+ $packet = '';
+ $packets = array();
+ foreach ( $lines as $line ) {
+ if ( ( strlen( $packet ) + strlen( $line ) ) > 1450 ) {
+ $packets[] = $packet;
+ $packet = '';
+ }
+ $packet .= $line;
+ }
+ if ( $packet != '' ) {
+ $packets[] = $packet;
+ }
+ foreach ( $packets as $packet ) {
+ wfSuppressWarnings();
+ socket_sendto(
+ $socket,
+ $packet,
+ strlen( $packet ),
+ 0,
+ $wgUDPProfilerHost,
+ $wgUDPProfilerPort
+ );
+ wfRestoreWarnings();
+ }
+ }
+ }
+
+ /**
+ * @param array $deltas
+ * @return void
+ */
+ protected function sendDeltasMemc( array $deltas ) {
+ global $wgMemc;
+
+ foreach ( $deltas as $key => $count ) {
+ $ckey = wfMemcKey( 'stats', $key );
+ if ( $wgMemc->incr( $ckey, $count ) === null ) {
+ $wgMemc->add( $ckey, $count );
+ }
+ }
+ }
+}
diff --git a/includes/Status.php b/includes/Status.php
index 10dfb516..928f8ebd 100644
--- a/includes/Status.php
+++ b/includes/Status.php
@@ -31,6 +31,11 @@
* An operation which is not OK should have errors so that the user can be
* informed as to what went wrong. Calling the fatal() function sets an error
* message and simultaneously switches off the OK flag.
+ *
+ * The recommended pattern for Status objects is to return a Status object
+ * unconditionally, i.e. both on success and on failure -- so that the
+ * developer of the calling code is reminded that the function can fail, and
+ * so that a lack of error-handling will be explicit.
*/
class Status {
var $ok = true;
@@ -47,7 +52,7 @@ class Status {
/**
* Factory function for fatal errors
*
- * @param $message String: message name
+ * @param string|Message $message message name or object
* @return Status
*/
static function newFatal( $message /*, parameters...*/ ) {
@@ -103,7 +108,7 @@ class Status {
/**
* Add a new warning
*
- * @param $message String: message name
+ * @param string|Message $message message name or object
*/
function warning( $message /*, parameters... */ ) {
$params = array_slice( func_get_args(), 1 );
@@ -117,7 +122,7 @@ class Status {
* Add an error, do not set fatal flag
* This can be used for non-fatal errors
*
- * @param $message String: message name
+ * @param string|Message $message message name or object
*/
function error( $message /*, parameters... */ ) {
$params = array_slice( func_get_args(), 1 );
@@ -131,7 +136,7 @@ class Status {
* Add an error and set OK to false, indicating that the operation
* as a whole was fatal
*
- * @param $message String: message name
+ * @param string|Message $message message name or object
*/
function fatal( $message /*, parameters... */ ) {
$params = array_slice( func_get_args(), 1 );
@@ -167,31 +172,34 @@ class Status {
/**
* Get the error list as a wikitext formatted list
*
- * @param $shortContext String: a short enclosing context message name, to
+ * @param string $shortContext a short enclosing context message name, to
* be used when there is a single error
- * @param $longContext String: a long enclosing context message name, for a list
+ * @param string $longContext a long enclosing context message name, for a list
* @return String
*/
function getWikiText( $shortContext = false, $longContext = false ) {
if ( count( $this->errors ) == 0 ) {
if ( $this->ok ) {
$this->fatal( 'internalerror_info',
- __METHOD__." called for a good result, this is incorrect\n" );
+ __METHOD__ . " called for a good result, this is incorrect\n" );
} else {
$this->fatal( 'internalerror_info',
- __METHOD__.": Invalid result object: no error text but not OK\n" );
+ __METHOD__ . ": Invalid result object: no error text but not OK\n" );
}
}
if ( count( $this->errors ) == 1 ) {
- $s = $this->getWikiTextForError( $this->errors[0], $this->errors[0] );
+ $s = $this->getErrorMessage( $this->errors[0] )->plain();
if ( $shortContext ) {
$s = wfMessage( $shortContext, $s )->plain();
} elseif ( $longContext ) {
$s = wfMessage( $longContext, "* $s\n" )->plain();
}
} else {
- $s = '* '. implode("\n* ",
- $this->getWikiTextArray( $this->errors ) ) . "\n";
+ $errors = $this->getErrorMessageArray( $this->errors );
+ foreach ( $errors as &$error ) {
+ $error = $error->plain();
+ }
+ $s = '* ' . implode( "\n* ", $errors ) . "\n";
if ( $longContext ) {
$s = wfMessage( $longContext, $s )->plain();
} elseif ( $shortContext ) {
@@ -202,7 +210,57 @@ class Status {
}
/**
- * Return the wiki text for a single error.
+ * Get the error list as a Message object
+ *
+ * @param string $shortContext a short enclosing context message name, to
+ * be used when there is a single error
+ * @param string $longContext a long enclosing context message name, for a list
+ * @return Message
+ */
+ function getMessage( $shortContext = false, $longContext = false ) {
+ if ( count( $this->errors ) == 0 ) {
+ if ( $this->ok ) {
+ $this->fatal( 'internalerror_info',
+ __METHOD__ . " called for a good result, this is incorrect\n" );
+ } else {
+ $this->fatal( 'internalerror_info',
+ __METHOD__ . ": Invalid result object: no error text but not OK\n" );
+ }
+ }
+ if ( count( $this->errors ) == 1 ) {
+ $s = $this->getErrorMessage( $this->errors[0] );
+ if ( $shortContext ) {
+ $s = wfMessage( $shortContext, $s );
+ } elseif ( $longContext ) {
+ $wrapper = new RawMessage( "* \$1\n" );
+ $wrapper->params( $s )->parse();
+ $s = wfMessage( $longContext, $wrapper );
+ }
+ } else {
+ $msgs = $this->getErrorMessageArray( $this->errors );
+ $msgCount = count( $msgs );
+
+ if ( $shortContext ) {
+ $msgCount++;
+ }
+
+ $wrapper = new RawMessage( '* $' . implode( "\n* \$", range( 1, $msgCount ) ) );
+ $s = $wrapper->params( $msgs )->parse();
+
+ if ( $longContext ) {
+ $s = wfMessage( $longContext, $wrapper );
+ } elseif ( $shortContext ) {
+ $wrapper = new RawMessage( "\n\$1\n", $wrapper );
+ $wrapper->parse();
+ $s = wfMessage( $shortContext, $wrapper );
+ }
+ }
+
+ return $s;
+ }
+
+ /**
+ * Return the message for a single error.
* @param $error Mixed With an array & two values keyed by
* 'message' and 'params', use those keys-value pairs.
* Otherwise, if its an array, just use the first value as the
@@ -210,19 +268,35 @@ class Status {
*
* @return String
*/
- protected function getWikiTextForError( $error ) {
+ protected function getErrorMessage( $error ) {
if ( is_array( $error ) ) {
- if ( isset( $error['message'] ) && isset( $error['params'] ) ) {
- return wfMessage( $error['message'],
- array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) )->plain();
+ if ( isset( $error['message'] ) && $error['message'] instanceof Message ) {
+ $msg = $error['message'];
+ } elseif ( isset( $error['message'] ) && isset( $error['params'] ) ) {
+ $msg = wfMessage( $error['message'],
+ array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) );
} else {
- $message = array_shift($error);
- return wfMessage( $message,
- array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) )->plain();
+ $msgName = array_shift( $error );
+ $msg = wfMessage( $msgName,
+ array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) );
}
} else {
- return wfMessage( $error )->plain();
+ $msg = wfMessage( $error );
}
+ return $msg;
+ }
+
+ /**
+ * Get the error message as HTML. This is done by parsing the wikitext error
+ * message.
+ *
+ * @note: this does not perform a full wikitext to HTML conversion, it merely applies
+ * a message transformation.
+ * @todo figure out whether that is actually The Right Thing.
+ */
+ public function getHTML( $shortContext = false, $longContext = false ) {
+ $text = $this->getWikiText( $shortContext, $longContext );
+ return MessageCache::singleton()->transform( $text, true );
}
/**
@@ -230,8 +304,8 @@ class Status {
* @param $errors Array
* @return Array
*/
- function getWikiTextArray( $errors ) {
- return array_map( array( $this, 'getWikiTextForError' ), $errors );
+ protected function getErrorMessageArray( $errors ) {
+ return array_map( array( $this, 'getErrorMessage' ), $errors );
}
/**
@@ -253,7 +327,8 @@ class Status {
/**
* Get the list of errors (but not warnings)
*
- * @return Array
+ * @return array A list in which each entry is an array with a message key as its first element.
+ * The remaining array elements are the message parameters.
*/
function getErrorsArray() {
return $this->getStatusArray( "error" );
@@ -262,7 +337,8 @@ class Status {
/**
* Get the list of warnings (but not errors)
*
- * @return Array
+ * @return array A list in which each entry is an array with a message key as its first element.
+ * The remaining array elements are the message parameters.
*/
function getWarningsArray() {
return $this->getStatusArray( "warning" );
@@ -278,7 +354,9 @@ class Status {
$result = array();
foreach ( $this->errors as $error ) {
if ( $error['type'] === $type ) {
- if( $error['params'] ) {
+ if ( $error['message'] instanceof Message ) {
+ $result[] = array_merge( array( $error['message']->getKey() ), $error['message']->getParams() );
+ } elseif ( $error['params'] ) {
$result[] = array_merge( array( $error['message'] ), $error['params'] );
} else {
$result[] = array( $error['message'] );
@@ -309,7 +387,10 @@ class Status {
/**
* Returns true if the specified message is present as a warning or error
*
- * @param $msg String: message name
+ * Note, due to the lack of tools for comparing Message objects, this
+ * function will not work when using a Message object as a parameter.
+ *
+ * @param string $msg message name
* @return Boolean
*/
function hasMessage( $msg ) {
@@ -325,9 +406,12 @@ class Status {
* If the specified source message exists, replace it with the specified
* destination message, but keep the same parameters as in the original error.
*
- * Return true if the replacement was done, false otherwise.
+ * Note, due to the lack of tools for comparing Message objects, this
+ * function will not work when using a Message object as the search parameter.
*
- * @return bool
+ * @param $source Message|String: Message key or object to search for
+ * @param $dest Message|String: Replacement message key or object
+ * @return bool Return true if the replacement was done, false otherwise.
*/
function replaceMessage( $source, $dest ) {
$replaced = false;
@@ -341,15 +425,6 @@ class Status {
}
/**
- * Backward compatibility function for WikiError -> Status migration
- *
- * @return String
- */
- public function getMessage() {
- return $this->getWikiText();
- }
-
- /**
* @return mixed
*/
public function getValue() {
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
index 95c69a20..1ad643ac 100644
--- a/includes/StreamFile.php
+++ b/includes/StreamFile.php
@@ -32,15 +32,17 @@ class StreamFile {
* Headers sent include: Content-type, Content-Length, Last-Modified,
* and Content-Disposition.
*
- * @param $fname string Full name and path of the file to stream
- * @param $headers array Any additional headers to send
- * @param $sendErrors bool Send error messages if errors occur (like 404)
+ * @param string $fname Full name and path of the file to stream
+ * @param array $headers Any additional headers to send
+ * @param bool $sendErrors Send error messages if errors occur (like 404)
+ * @throws MWException
* @return bool Success
*/
public static function stream( $fname, $headers = array(), $sendErrors = true ) {
wfProfileIn( __METHOD__ );
if ( FileBackend::isStoragePath( $fname ) ) { // sanity
+ wfProfileOut( __METHOD__ );
throw new MWException( __FUNCTION__ . " given storage path '$fname'." );
}
@@ -70,10 +72,10 @@ class StreamFile {
* (b) cancels any PHP output buffering and automatic gzipping of output
* (c) sends Content-Length header based on HTTP_IF_MODIFIED_SINCE check
*
- * @param $path string Storage path or file system path
- * @param $info Array|bool File stat info with 'mtime' and 'size' fields
- * @param $headers Array Additional headers to send
- * @param $sendErrors bool Send error messages if errors occur (like 404)
+ * @param string $path Storage path or file system path
+ * @param array|bool $info File stat info with 'mtime' and 'size' fields
+ * @param array $headers Additional headers to send
+ * @param bool $sendErrors Send error messages if errors occur (like 404)
* @return int|bool READY_STREAM, NOT_MODIFIED, or false on failure
*/
public static function prepareForStream(
@@ -142,8 +144,8 @@ class StreamFile {
/**
* Determine the file type of a file based on the path
*
- * @param $filename string Storage path or file system path
- * @param $safe bool Whether to do retroactive upload blacklist checks
+ * @param string $filename Storage path or file system path
+ * @param bool $safe Whether to do retroactive upload blacklist checks
* @return null|string
*/
public static function contentTypeFromPath( $filename, $safe = true ) {
diff --git a/includes/StringUtils.php b/includes/StringUtils.php
index 43275a66..c1545e6e 100644
--- a/includes/StringUtils.php
+++ b/includes/StringUtils.php
@@ -24,6 +24,92 @@
* A collection of static methods to play with strings.
*/
class StringUtils {
+
+ /**
+ * Test whether a string is valid UTF-8.
+ *
+ * The function check for invalid byte sequences, overlong encoding but
+ * not for different normalisations.
+ *
+ * This relies internally on the mbstring function mb_check_encoding()
+ * hardcoded to check against UTF-8. Whenever the function is not available
+ * we fallback to a pure PHP implementation. Setting $disableMbstring to
+ * true will skip the use of mb_check_encoding, this is mostly intended for
+ * unit testing our internal implementation.
+ *
+ * @since 1.21
+ * @note In MediaWiki 1.21, this function did not provide proper UTF-8 validation.
+ * In particular, the pure PHP code path did not in fact check for overlong forms.
+ * Beware of this when backporting code to that version of MediaWiki.
+ *
+ * @param string $value String to check
+ * @param boolean $disableMbstring Whether to use the pure PHP
+ * implementation instead of trying mb_check_encoding. Intended for unit
+ * testing. Default: false
+ *
+ * @return boolean Whether the given $value is a valid UTF-8 encoded string
+ */
+ static function isUtf8( $value, $disableMbstring = false ) {
+ $value = (string)$value;
+
+ // If the mbstring extension is loaded, use it. However, before PHP 5.4, values above
+ // U+10FFFF are incorrectly allowed, so we have to check for them separately.
+ if ( !$disableMbstring && function_exists( 'mb_check_encoding' ) ) {
+ static $newPHP;
+ if ( $newPHP === null ) {
+ $newPHP = !mb_check_encoding( "\xf4\x90\x80\x80", 'UTF-8' );
+ }
+
+ return mb_check_encoding( $value, 'UTF-8' ) &&
+ ( $newPHP || preg_match( "/\xf4[\x90-\xbf]|[\xf5-\xff]/S", $value ) === 0 );
+ }
+
+ if ( preg_match( "/[\x80-\xff]/S", $value ) === 0 ) {
+ // String contains only ASCII characters, has to be valid
+ return true;
+ }
+
+ // PCRE implements repetition using recursion; to avoid a stack overflow (and segfault)
+ // for large input, we check for invalid sequences (<= 5 bytes) rather than valid
+ // sequences, which can be as long as the input string is. Multiple short regexes are
+ // used rather than a single long regex for performance.
+ static $regexes;
+ if ( $regexes === null ) {
+ $cont = "[\x80-\xbf]";
+ $after = "(?!$cont)"; // "(?:[^\x80-\xbf]|$)" would work here
+ $regexes = array(
+ // Continuation byte at the start
+ "/^$cont/",
+
+ // ASCII byte followed by a continuation byte
+ "/[\\x00-\x7f]$cont/S",
+
+ // Illegal byte
+ "/[\xc0\xc1\xf5-\xff]/S",
+
+ // Invalid 2-byte sequence, or valid one then an extra continuation byte
+ "/[\xc2-\xdf](?!$cont$after)/S",
+
+ // Invalid 3-byte sequence, or valid one then an extra continuation byte
+ "/\xe0(?![\xa0-\xbf]$cont$after)/",
+ "/[\xe1-\xec\xee\xef](?!$cont{2}$after)/S",
+ "/\xed(?![\x80-\x9f]$cont$after)/",
+
+ // Invalid 4-byte sequence, or valid one then an extra continuation byte
+ "/\xf0(?![\x90-\xbf]$cont{2}$after)/",
+ "/[\xf1-\xf3](?!$cont{3}$after)/S",
+ "/\xf4(?![\x80-\x8f]$cont{2}$after)/",
+ );
+ }
+
+ foreach ( $regexes as $regex ) {
+ if ( preg_match( $regex, $value ) !== 0 ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Perform an operation equivalent to
*
@@ -65,16 +151,16 @@ class StringUtils {
* memory. The delimiters are literal strings, not regular expressions.
*
* If the start delimiter ends with an initial substring of the end delimiter,
- * e.g. in the case of C-style comments, the behaviour differs from the model
+ * e.g. in the case of C-style comments, the behavior differs from the model
* regex. In this implementation, the end must share no characters with the
* start, so e.g. /*\/ is not considered to be both the start and end of a
* comment. /*\/xy/*\/ is considered to be a single comment with contents /xy/.
*
- * @param $startDelim String: start delimiter
- * @param $endDelim String: end delimiter
+ * @param string $startDelim start delimiter
+ * @param string $endDelim end delimiter
* @param $callback Callback: function to call on each match
* @param $subject String
- * @param $flags String: regular expression flags
+ * @param string $flags regular expression flags
* @throws MWException
* @return string
*/
@@ -90,12 +176,12 @@ class StringUtils {
$m = array();
while ( $inputPos < strlen( $subject ) &&
- preg_match( "!($encStart)|($encEnd)!S$flags", $subject, $m, PREG_OFFSET_CAPTURE, $inputPos ) )
+ preg_match( "!($encStart)|($encEnd)!S$flags", $subject, $m, PREG_OFFSET_CAPTURE, $inputPos ) )
{
$tokenOffset = $m[0][1];
if ( $m[1][0] != '' ) {
if ( $foundStart &&
- $strcmp( $endDelim, substr( $subject, $tokenOffset, $endLength ) ) == 0 )
+ $strcmp( $endDelim, substr( $subject, $tokenOffset, $endLength ) ) == 0 )
{
# An end match is present at the same location
$tokenType = 'end';
@@ -155,12 +241,12 @@ class StringUtils {
*
* preg_replace( "!$startDelim(.*)$endDelim!$flags", $replace, $subject )
*
- * @param $startDelim String: start delimiter regular expression
- * @param $endDelim String: end delimiter regular expression
- * @param $replace String: replacement string. May contain $1, which will be
+ * @param string $startDelim start delimiter regular expression
+ * @param string $endDelim end delimiter regular expression
+ * @param string $replace replacement string. May contain $1, which will be
* replaced by the text between the delimiters
- * @param $subject String to search
- * @param $flags String: regular expression flags
+ * @param string $subject to search
+ * @param string $flags regular expression flags
* @return String: The string with the matches replaced
*/
static function delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags = '' ) {
@@ -172,8 +258,8 @@ class StringUtils {
/**
* More or less "markup-safe" explode()
* Ignores any instances of the separator inside <...>
- * @param $separator String
- * @param $text String
+ * @param string $separator
+ * @param string $text
* @return array
*/
static function explodeMarkup( $separator, $text ) {
@@ -188,7 +274,7 @@ class StringUtils {
// Explode, then put the replaced separators back in
$items = explode( $separator, $cleaned );
- foreach( $items as $i => $str ) {
+ foreach ( $items as $i => $str ) {
$items[$i] = str_replace( $placeholder, $separator, $str );
}
@@ -199,8 +285,8 @@ class StringUtils {
* Escape a string to make it suitable for inclusion in a preg_replace()
* replacement parameter.
*
- * @param $string String
- * @return String
+ * @param string $string
+ * @return string
*/
static function escapeRegexReplacement( $string ) {
$string = str_replace( '\\', '\\\\', $string );
@@ -211,8 +297,8 @@ class StringUtils {
/**
* Workalike for explode() with limited memory usage.
* Returns an Iterator
- * @param $separator
- * @param $subject
+ * @param string $separator
+ * @param string $subject
* @return ArrayIterator|ExplodeIterator
*/
static function explode( $separator, $subject ) {
@@ -245,14 +331,14 @@ class RegexlikeReplacer extends Replacer {
var $r;
/**
- * @param $r string
+ * @param string $r
*/
function __construct( $r ) {
$this->r = $r;
}
/**
- * @param $matches array
+ * @param array $matches
* @return string
*/
function replace( $matches ) {
@@ -273,7 +359,7 @@ class DoubleReplacer extends Replacer {
/**
* @param $from
* @param $to
- * @param $index int
+ * @param int $index
*/
function __construct( $from, $to, $index = 0 ) {
$this->from = $from;
@@ -282,7 +368,7 @@ class DoubleReplacer extends Replacer {
}
/**
- * @param $matches array
+ * @param array $matches
* @return mixed
*/
function replace( $matches ) {
@@ -298,7 +384,7 @@ class HashtableReplacer extends Replacer {
/**
* @param $table
- * @param $index int
+ * @param int $index
*/
function __construct( $table, $index = 0 ) {
$this->table = $table;
@@ -306,7 +392,7 @@ class HashtableReplacer extends Replacer {
}
/**
- * @param $matches array
+ * @param array $matches
* @return mixed
*/
function replace( $matches ) {
@@ -344,6 +430,7 @@ class ReplacementArray {
/**
* Set the whole replacement array at once
+ * @param array $data
*/
function setArray( $data ) {
$this->data = $data;
@@ -359,8 +446,8 @@ class ReplacementArray {
/**
* Set an element of the replacement array
- * @param $from string
- * @param $to stromg
+ * @param string $from
+ * @param string $to
*/
function setPair( $from, $to ) {
$this->data[$from] = $to;
@@ -368,7 +455,7 @@ class ReplacementArray {
}
/**
- * @param $data array
+ * @param array $data
*/
function mergeArray( $data ) {
$this->data = array_merge( $this->data, $data );
@@ -376,7 +463,7 @@ class ReplacementArray {
}
/**
- * @param $other
+ * @param ReplacementArray $other
*/
function merge( $other ) {
$this->data = array_merge( $this->data, $other->data );
@@ -384,39 +471,39 @@ class ReplacementArray {
}
/**
- * @param $from string
+ * @param string $from
*/
function removePair( $from ) {
- unset($this->data[$from]);
+ unset( $this->data[$from] );
$this->fss = false;
}
/**
- * @param $data array
+ * @param array $data
*/
function removeArray( $data ) {
- foreach( $data as $from => $to ) {
+ foreach ( $data as $from => $to ) {
$this->removePair( $from );
}
$this->fss = false;
}
/**
- * @param $subject string
+ * @param string $subject
* @return string
*/
function replace( $subject ) {
if ( function_exists( 'fss_prep_replace' ) ) {
- wfProfileIn( __METHOD__.'-fss' );
+ wfProfileIn( __METHOD__ . '-fss' );
if ( $this->fss === false ) {
$this->fss = fss_prep_replace( $this->data );
}
$result = fss_exec_replace( $this->fss, $subject );
- wfProfileOut( __METHOD__.'-fss' );
+ wfProfileOut( __METHOD__ . '-fss' );
} else {
- wfProfileIn( __METHOD__.'-strtr' );
+ wfProfileIn( __METHOD__ . '-strtr' );
$result = strtr( $subject, $this->data );
- wfProfileOut( __METHOD__.'-strtr' );
+ wfProfileOut( __METHOD__ . '-strtr' );
}
return $result;
}
@@ -424,7 +511,7 @@ class ReplacementArray {
/**
* An iterator which works exactly like:
- *
+ *
* foreach ( explode( $delim, $s ) as $element ) {
* ...
* }
@@ -449,15 +536,15 @@ class ExplodeIterator implements Iterator {
/**
* Construct a DelimIterator
- * @param $delim string
- * @param $s string
+ * @param string $delim
+ * @param string $subject
*/
- function __construct( $delim, $s ) {
- $this->subject = $s;
+ function __construct( $delim, $subject ) {
+ $this->subject = $subject;
$this->delim = $delim;
// Micro-optimisation (theoretical)
- $this->subjectLength = strlen( $s );
+ $this->subjectLength = strlen( $subject );
$this->delimLength = strlen( $delim );
$this->rewind();
@@ -485,6 +572,9 @@ class ExplodeIterator implements Iterator {
return $this->current;
}
+ /**
+ * @return int|bool Current position or boolean false if invalid
+ */
function key() {
return $this->curPos;
}
diff --git a/includes/StubObject.php b/includes/StubObject.php
index 615bcb5f..a3970f34 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -25,6 +25,12 @@
* their associated module code by deferring initialisation until the first
* method call.
*
+ * Note on reference parameters:
+ *
+ * If the called method takes any parameters by reference, the __call magic
+ * here won't work correctly. The solution is to unstub the object before
+ * calling the method.
+ *
* Note on unstub loops:
*
* Unstub loops (infinite recursion) sometimes occur when a constructor calls
@@ -41,9 +47,9 @@ class StubObject {
/**
* Constructor.
*
- * @param $global String: name of the global variable.
- * @param $class String: name of the class of the real object.
- * @param $params Array: parameters to pass to contructor of the real
+ * @param string $global name of the global variable.
+ * @param string $class name of the class of the real object.
+ * @param array $params parameters to pass to constructor of the real
* object.
*/
function __construct( $global = null, $class = null, $params = array() ) {
@@ -53,14 +59,28 @@ class StubObject {
}
/**
- * Returns a bool value whetever $obj is a stub object. Can be used to break
+ * Returns a bool value whenever $obj is a stub object. Can be used to break
* a infinite loop when unstubbing an object.
*
* @param $obj Object to check.
* @return Boolean: true if $obj is not an instance of StubObject class.
*/
static function isRealObject( $obj ) {
- return is_object( $obj ) && !($obj instanceof StubObject);
+ return is_object( $obj ) && !$obj instanceof StubObject;
+ }
+
+ /**
+ * Unstubs an object, if it is a stub object. Can be used to break a
+ * infinite loop when unstubbing an object or to avoid reference parameter
+ * breakage.
+ *
+ * @param $obj Object to check.
+ * @return void
+ */
+ static function unstub( $obj ) {
+ if ( $obj instanceof StubObject ) {
+ $obj->_unstub( 'unstub', 3 );
+ }
}
/**
@@ -70,8 +90,8 @@ class StubObject {
* This function will also call the function with the same name in the real
* object.
*
- * @param $name String: name of the function called
- * @param $args Array: arguments
+ * @param string $name name of the function called
+ * @param array $args arguments
* @return mixed
*/
function _call( $name, $args ) {
@@ -91,8 +111,8 @@ class StubObject {
* Function called by PHP if no function with that name exists in this
* object.
*
- * @param $name String: name of the function called
- * @param $args Array: arguments
+ * @param string $name name of the function called
+ * @param array $args arguments
* @return mixed
*/
function __call( $name, $args ) {
@@ -105,22 +125,24 @@ class StubObject {
* This is public, for the convenience of external callers wishing to access
* properties, e.g. eval.php
*
- * @param $name String: name of the method called in this object.
- * @param $level Integer: level to go in the stact trace to get the function
+ * @param string $name name of the method called in this object.
+ * @param $level Integer: level to go in the stack trace to get the function
* who called this function.
+ * @throws MWException
*/
function _unstub( $name = '_unstub', $level = 2 ) {
static $recursionLevel = 0;
- if ( !($GLOBALS[$this->mGlobal] instanceof StubObject) ) {
+ if ( !$GLOBALS[$this->mGlobal] instanceof StubObject ) {
return $GLOBALS[$this->mGlobal]; // already unstubbed.
}
if ( get_class( $GLOBALS[$this->mGlobal] ) != $this->mClass ) {
- $fname = __METHOD__.'-'.$this->mGlobal;
+ $fname = __METHOD__ . '-' . $this->mGlobal;
wfProfileIn( $fname );
$caller = wfGetCaller( $level );
if ( ++$recursionLevel > 2 ) {
+ wfProfileOut( $fname );
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" );
diff --git a/includes/Timestamp.php b/includes/Timestamp.php
index c9ba8d91..edcd6a88 100644
--- a/includes/Timestamp.php
+++ b/includes/Timestamp.php
@@ -42,34 +42,21 @@ class MWTimestamp {
TS_RFC2822 => 'D, d M Y H:i:s',
TS_ORACLE => 'd-m-Y H:i:s.000000', // Was 'd-M-y h.i.s A' . ' +00:00' before r51500
TS_POSTGRES => 'Y-m-d H:i:s',
- TS_DB2 => 'Y-m-d H:i:s',
);
/**
- * Different units for human readable timestamps.
- * @see MWTimestamp::getHumanTimestamp
+ * The actual timestamp being wrapped (DateTime object).
+ * @var DateTime
*/
- private static $units = array(
- "milliseconds" => 1,
- "seconds" => 1000, // 1000 milliseconds per second
- "minutes" => 60, // 60 seconds per minute
- "hours" => 60, // 60 minutes per hour
- "days" => 24 // 24 hours per day
- );
-
- /**
- * The actual timestamp being wrapped. Either a DateTime
- * object or a string with a Unix timestamp depending on
- * PHP.
- * @var string|DateTime
- */
- private $timestamp;
+ public $timestamp;
/**
* Make a new timestamp and set it to the specified time,
* or the current time if unspecified.
*
- * @param $timestamp bool|string Timestamp to set, or false for current time
+ * @since 1.20
+ *
+ * @param bool|string $timestamp Timestamp to set, or false for current time
*/
public function __construct( $timestamp = false ) {
$this->setTimestamp( $timestamp );
@@ -81,7 +68,9 @@ class MWTimestamp {
* Parse the given timestamp into either a DateTime object or a Unix timestamp,
* and then store it.
*
- * @param $ts string|bool Timestamp to store, or false for now
+ * @since 1.20
+ *
+ * @param string|bool $ts Timestamp to store, or false for now
* @throws TimestampException
*/
public function setTimestamp( $ts = false ) {
@@ -112,8 +101,6 @@ class MWTimestamp {
# TS_POSTGRES
} elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/', $ts, $da ) ) {
# TS_POSTGRES
- } elseif (preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.\d\d\d$/', $ts, $da ) ) {
- # TS_DB2
} elseif ( preg_match( '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' . # Day of week
'\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' . # dd Mon yyyy
'[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S', $ts ) ) { # hh:mm:ss
@@ -127,27 +114,23 @@ class MWTimestamp {
# asctime
$strtime = $ts;
} else {
- throw new TimestampException( __METHOD__ . " : Invalid timestamp - $ts" );
+ throw new TimestampException( __METHOD__ . ": Invalid timestamp - $ts" );
}
- if( !$strtime ) {
+ if ( !$strtime ) {
$da = array_map( 'intval', $da );
$da[0] = "%04d-%02d-%02dT%02d:%02d:%02d.00+00:00";
$strtime = call_user_func_array( "sprintf", $da );
}
- if( function_exists( "date_create" ) ) {
- try {
- $final = new DateTime( $strtime, new DateTimeZone( 'GMT' ) );
- } catch(Exception $e) {
- throw new TimestampException( __METHOD__ . ' Invalid timestamp format.' );
- }
- } else {
- $final = strtotime( $strtime );
+ try {
+ $final = new DateTime( $strtime, new DateTimeZone( 'GMT' ) );
+ } catch ( Exception $e ) {
+ throw new TimestampException( __METHOD__ . ': Invalid timestamp format.', $e->getCode(), $e );
}
- if( $final === false ) {
- throw new TimestampException( __METHOD__ . ' Invalid timestamp format.' );
+ if ( $final === false ) {
+ throw new TimestampException( __METHOD__ . ': Invalid timestamp format.' );
}
$this->timestamp = $final;
}
@@ -158,25 +141,18 @@ class MWTimestamp {
* Convert the internal timestamp to the specified format and then
* return it.
*
- * @param $style int Constant Output format for timestamp
+ * @since 1.20
+ *
+ * @param int $style Constant Output format for timestamp
* @throws TimestampException
* @return string The formatted timestamp
*/
public function getTimestamp( $style = TS_UNIX ) {
- if( !isset( self::$formats[$style] ) ) {
- throw new TimestampException( __METHOD__ . ' : Illegal timestamp output type.' );
+ if ( !isset( self::$formats[$style] ) ) {
+ throw new TimestampException( __METHOD__ . ': Illegal timestamp output type.' );
}
- if( is_object( $this->timestamp ) ) {
- // DateTime object was used, call DateTime::format.
- $output = $this->timestamp->format( self::$formats[$style] );
- } elseif( TS_UNIX == $style ) {
- // Unix timestamp was used and is wanted, just return it.
- $output = $this->timestamp;
- } else {
- // Unix timestamp was used, use gmdate().
- $output = gmdate( self::$formats[$style], $this->timestamp );
- }
+ $output = $this->timestamp->format( self::$formats[$style] );
if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) {
$output .= ' GMT';
@@ -192,38 +168,229 @@ class MWTimestamp {
* generate a readable timestamp by returning "<N> <units> ago", where the
* largest possible unit is used.
*
+ * @since 1.20
+ * @since 1.22 Uses Language::getHumanTimestamp to produce the timestamp
+ *
+ * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
+ * @param User|null $user User the timestamp is being generated for (or null to use main context's user)
+ * @param Language|null $lang Language to use to make the human timestamp (or null to use main context's language)
* @return string Formatted timestamp
*/
- public function getHumanTimestamp() {
- $then = $this->getTimestamp( TS_UNIX );
- $now = time();
- $timeago = ($now - $then) * 1000;
- $message = false;
-
- foreach( self::$units as $unit => $factor ) {
- $next = $timeago / $factor;
- if( $next < 1 ) {
- break;
+ public function getHumanTimestamp( MWTimestamp $relativeTo = null, User $user = null, Language $lang = null ) {
+ if ( $relativeTo === null ) {
+ $relativeTo = new self();
+ }
+ if ( $user === null ) {
+ $user = RequestContext::getMain()->getUser();
+ }
+ if ( $lang === null ) {
+ $lang = RequestContext::getMain()->getLanguage();
+ }
+
+ // Adjust for the user's timezone.
+ $offsetThis = $this->offsetForUser( $user );
+ $offsetRel = $relativeTo->offsetForUser( $user );
+
+ $ts = '';
+ if ( wfRunHooks( 'GetHumanTimestamp', array( &$ts, $this, $relativeTo, $user, $lang ) ) ) {
+ $ts = $lang->getHumanTimestamp( $this, $relativeTo, $user );
+ }
+
+ // Reset the timezone on the objects.
+ $this->timestamp->sub( $offsetThis );
+ $relativeTo->timestamp->sub( $offsetRel );
+
+ return $ts;
+ }
+
+ /**
+ * Adjust the timestamp depending on the given user's preferences.
+ *
+ * @since 1.22
+ *
+ * @param User $user User to take preferences from
+ * @param[out] MWTimestamp $ts Timestamp to adjust
+ * @return DateInterval Offset that was applied to the timestamp
+ */
+ public function offsetForUser( User $user ) {
+ global $wgLocalTZoffset;
+
+ $option = $user->getOption( 'timecorrection' );
+ $data = explode( '|', $option, 3 );
+
+ // First handle the case of an actual timezone being specified.
+ if ( $data[0] == 'ZoneInfo' ) {
+ try {
+ $tz = new DateTimeZone( $data[2] );
+ } catch ( Exception $e ) {
+ $tz = false;
+ }
+
+ if ( $tz ) {
+ $this->timestamp->setTimezone( $tz );
+ return new DateInterval( 'P0Y' );
} else {
- $timeago = $next;
- $message = array( $unit, floor( $timeago ) );
+ $data[0] = 'Offset';
}
}
- if( $message ) {
- $initial = call_user_func_array( 'wfMessage', $message );
- return wfMessage( 'ago', $initial );
+ $diff = 0;
+ // If $option is in fact a pipe-separated value, check the
+ // first value.
+ if ( $data[0] == 'System' ) {
+ // First value is System, so use the system offset.
+ if ( isset( $wgLocalTZoffset ) ) {
+ $diff = $wgLocalTZoffset;
+ }
+ } elseif ( $data[0] == 'Offset' ) {
+ // First value is Offset, so use the specified offset
+ $diff = (int)$data[1];
} else {
- return wfMessage( 'just-now' );
+ // $option actually isn't a pipe separated value, but instead
+ // a comma separated value. Isn't MediaWiki fun?
+ $data = explode( ':', $option );
+ if ( count( $data ) >= 2 ) {
+ // Combination hours and minutes.
+ $diff = abs( (int)$data[0] ) * 60 + (int)$data[1];
+ if ( (int)$data[0] < 0 ) {
+ $diff *= -1;
+ }
+ } else {
+ // Just hours.
+ $diff = (int)$data[0] * 60;
+ }
+ }
+
+ $interval = new DateInterval( 'PT' . abs( $diff ) . 'M' );
+ if ( $diff < 1 ) {
+ $interval->invert = 1;
+ }
+
+ $this->timestamp->add( $interval );
+ return $interval;
+ }
+
+ /**
+ * Generate a purely relative timestamp, i.e., represent the time elapsed between
+ * the given base timestamp and this object.
+ *
+ * @param MWTimestamp $relativeTo Relative base timestamp (defaults to now)
+ * @param User $user Use to use offset for
+ * @param Language $lang Language to use
+ * @param array $chosenIntervals Intervals to use to represent it
+ * @return string Relative timestamp
+ */
+ public function getRelativeTimestamp(
+ MWTimestamp $relativeTo = null,
+ User $user = null,
+ Language $lang = null,
+ array $chosenIntervals = array()
+ ) {
+ if ( $relativeTo === null ) {
+ $relativeTo = new self;
+ }
+ if ( $user === null ) {
+ $user = RequestContext::getMain()->getUser();
+ }
+ if ( $lang === null ) {
+ $lang = RequestContext::getMain()->getLanguage();
}
+
+ $ts = '';
+ $diff = $this->diff( $relativeTo );
+ if ( wfRunHooks( 'GetRelativeTimestamp', array( &$ts, &$diff, $this, $relativeTo, $user, $lang ) ) ) {
+ $seconds = ( ( ( $diff->days * 24 + $diff->h ) * 60 + $diff->i ) * 60 + $diff->s );
+ $ts = wfMessage( 'ago', $lang->formatDuration( $seconds, $chosenIntervals ) )
+ ->inLanguage( $lang )
+ ->text();
+ }
+
+ return $ts;
}
/**
+ * @since 1.20
+ *
* @return string
*/
public function __toString() {
return $this->getTimestamp();
}
+
+ /**
+ * Calculate the difference between two MWTimestamp objects.
+ *
+ * @since 1.22
+ * @param MWTimestamp $relativeTo Base time to calculate difference from
+ * @return DateInterval|bool The DateInterval object representing the difference between the two dates or false on failure
+ */
+ public function diff( MWTimestamp $relativeTo ) {
+ return $this->timestamp->diff( $relativeTo->timestamp );
+ }
+
+ /**
+ * Set the timezone of this timestamp to the specified timezone.
+ *
+ * @since 1.22
+ * @param String $timezone Timezone to set
+ * @throws TimestampException
+ */
+ public function setTimezone( $timezone ) {
+ try {
+ $this->timestamp->setTimezone( new DateTimeZone( $timezone ) );
+ } catch ( Exception $e ) {
+ throw new TimestampException( __METHOD__ . ': Invalid timezone.', $e->getCode(), $e );
+ }
+ }
+
+ /**
+ * Get the timezone of this timestamp.
+ *
+ * @since 1.22
+ * @return DateTimeZone The timezone
+ */
+ public function getTimezone() {
+ return $this->timestamp->getTimezone();
+ }
+
+ /**
+ * Format the timestamp in a given format.
+ *
+ * @since 1.22
+ * @param string $format Pattern to format in
+ * @return string The formatted timestamp
+ */
+ public function format( $format ) {
+ return $this->timestamp->format( $format );
+ }
+
+ /**
+ * Get a timestamp instance in the server local timezone ($wgLocaltimezone)
+ *
+ * @since 1.22
+ * @param bool|string $ts Timestamp to set, or false for current time
+ * @return MWTimestamp the local instance
+ */
+ public static function getLocalInstance( $ts = false ) {
+ global $wgLocaltimezone;
+ $timestamp = new self( $ts );
+ $timestamp->setTimezone( $wgLocaltimezone );
+ return $timestamp;
+ }
+
+ /**
+ * Get a timestamp instance in GMT
+ *
+ * @since 1.22
+ * @param bool|string $ts Timestamp to set, or false for current time
+ * @return MWTimestamp the instance
+ */
+ public static function getInstance( $ts = false ) {
+ return new self( $ts );
+ }
}
+/**
+ * @since 1.20
+ */
class TimestampException extends MWException {}
diff --git a/includes/Title.php b/includes/Title.php
index 1b5e21d2..21872e45 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -65,6 +65,7 @@ class Title {
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 $mContentModel = false; // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
private $mEstimateRevisions; // /< Estimated number of revisions; null of not loaded
var $mRestrictions = array(); // /< Array of groups allowed to edit this article
var $mOldRestrictions = false;
@@ -87,7 +88,6 @@ class Title {
var $mHasSubpage; // /< Whether a page has any subpages
// @}
-
/**
* Constructor
*/
@@ -96,7 +96,7 @@ class Title {
/**
* Create a new Title from a prefixed DB key
*
- * @param $key String the database key, which has underscores
+ * @param string $key the database key, which has underscores
* instead of spaces, possibly including namespace and
* interwiki prefixes
* @return Title, or NULL on an error
@@ -115,10 +115,10 @@ class Title {
* Create a new Title from text, such as what one would find in a link. De-
* codes any HTML entities in the text.
*
- * @param $text String the link text; spaces, prefixes, and an
+ * @param string $text the link text; spaces, prefixes, and an
* initial ':' indicating the main namespace are accepted.
- * @param $defaultNamespace Int the namespace to use if none is speci-
- * fied by a prefix. If you want to force a specific namespace even if
+ * @param int $defaultNamespace the namespace to use if none is specified
+ * by a prefix. If you want to force a specific namespace even if
* $text might begin with a namespace prefix, use makeTitle() or
* makeTitleSafe().
* @throws MWException
@@ -148,7 +148,7 @@ class Title {
$t->mDbkeyform = str_replace( ' ', '_', $filteredText );
$t->mDefaultNamespace = $defaultNamespace;
- static $cachedcount = 0 ;
+ static $cachedcount = 0;
if ( $t->secureAndSplit() ) {
if ( $defaultNamespace == NS_MAIN ) {
if ( $cachedcount >= self::CACHE_MAX ) {
@@ -178,7 +178,7 @@ class Title {
* Create a new Title from URL-encoded text. Ensures that
* the given title's length does not exceed the maximum.
*
- * @param $url String the title, as might be taken from a URL
+ * @param string $url the title, as might be taken from a URL
* @return Title the new object, or NULL on an error
*/
public static function newFromURL( $url ) {
@@ -200,20 +200,38 @@ class Title {
}
/**
+ * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
+ * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
+ *
+ * @return array
+ */
+ protected static function getSelectFields() {
+ global $wgContentHandlerUseDB;
+
+ $fields = array(
+ 'page_namespace', 'page_title', 'page_id',
+ 'page_len', 'page_is_redirect', 'page_latest',
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'page_content_model';
+ }
+
+ return $fields;
+ }
+
+ /**
* Create a new Title from an article ID
*
- * @param $id Int the page_id corresponding to the Title to create
- * @param $flags Int use Title::GAID_FOR_UPDATE to use master
- * @return Title the new object, or NULL on an error
+ * @param int $id the page_id corresponding to the Title to create
+ * @param int $flags use Title::GAID_FOR_UPDATE to use master
+ * @return Title|null the new object, or NULL on an error
*/
public static function newFromID( $id, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
$row = $db->selectRow(
'page',
- array(
- 'page_namespace', 'page_title', 'page_id',
- 'page_len', 'page_is_redirect', 'page_latest',
- ),
+ self::getSelectFields(),
array( 'page_id' => $id ),
__METHOD__
);
@@ -228,7 +246,7 @@ class Title {
/**
* Make an array of titles from an array of IDs
*
- * @param $ids Array of Int Array of IDs
+ * @param array $ids of Int Array of IDs
* @return Array of Titles
*/
public static function newFromIDs( $ids ) {
@@ -239,10 +257,7 @@ class Title {
$res = $dbr->select(
'page',
- array(
- 'page_namespace', 'page_title', 'page_id',
- 'page_len', 'page_is_redirect', 'page_latest',
- ),
+ self::getSelectFields(),
array( 'page_id' => $ids ),
__METHOD__
);
@@ -274,19 +289,29 @@ class Title {
*/
public function loadFromRow( $row ) {
if ( $row ) { // page found
- if ( isset( $row->page_id ) )
+ if ( isset( $row->page_id ) ) {
$this->mArticleID = (int)$row->page_id;
- if ( isset( $row->page_len ) )
+ }
+ if ( isset( $row->page_len ) ) {
$this->mLength = (int)$row->page_len;
- if ( isset( $row->page_is_redirect ) )
+ }
+ if ( isset( $row->page_is_redirect ) ) {
$this->mRedirect = (bool)$row->page_is_redirect;
- if ( isset( $row->page_latest ) )
+ }
+ if ( isset( $row->page_latest ) ) {
$this->mLatestID = (int)$row->page_latest;
+ }
+ if ( isset( $row->page_content_model ) ) {
+ $this->mContentModel = strval( $row->page_content_model );
+ } else {
+ $this->mContentModel = false; # initialized lazily in getContentModel()
+ }
} else { // page not found
$this->mArticleID = 0;
$this->mLength = 0;
$this->mRedirect = false;
$this->mLatestID = 0;
+ $this->mContentModel = false; # initialized lazily in getContentModel()
}
}
@@ -297,10 +322,10 @@ class Title {
* For convenience, spaces are converted to underscores so that
* eg user_text fields can be used directly.
*
- * @param $ns Int the namespace of the article
- * @param $title String the unprefixed database key form
- * @param $fragment String the link fragment (after the "#")
- * @param $interwiki String the interwiki prefix
+ * @param int $ns the namespace of the article
+ * @param string $title the unprefixed database key form
+ * @param string $fragment the link fragment (after the "#")
+ * @param string $interwiki the interwiki prefix
* @return Title the new object
*/
public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
@@ -312,6 +337,7 @@ class Title {
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
$t->mTextform = str_replace( '_', ' ', $title );
+ $t->mContentModel = false; # initialized lazily in getContentModel()
return $t;
}
@@ -320,10 +346,10 @@ class Title {
* The parameters will be checked for validity, which is a bit slower
* than makeTitle() but safer for user-provided data.
*
- * @param $ns Int the namespace of the article
- * @param $title String database key form
- * @param $fragment String the link fragment (after the "#")
- * @param $interwiki String interwiki prefix
+ * @param int $ns the namespace of the article
+ * @param string $title database key form
+ * @param string $fragment the link fragment (after the "#")
+ * @param string $interwiki interwiki prefix
* @return Title the new object, or NULL on an error
*/
public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
@@ -360,11 +386,15 @@ class Title {
* This will only return the very next target, useful for
* the redirect table and other checks that don't need full recursion
*
- * @param $text String: Text with possible redirect
+ * @param string $text Text with possible redirect
* @return Title: The corresponding Title
+ * @deprecated since 1.21, use Content::getRedirectTarget instead.
*/
public static function newFromRedirect( $text ) {
- return self::newFromRedirectInternal( $text );
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+ return $content->getRedirectTarget();
}
/**
@@ -373,12 +403,15 @@ class Title {
* This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
* in order to provide (hopefully) the Title of the final destination instead of another redirect
*
- * @param $text String Text with possible redirect
+ * @param string $text Text with possible redirect
* @return Title
+ * @deprecated since 1.21, use Content::getUltimateRedirectTarget instead.
*/
public static function newFromRedirectRecurse( $text ) {
- $titles = self::newFromRedirectArray( $text );
- return $titles ? array_pop( $titles ) : null;
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+ return $content->getUltimateRedirectTarget();
}
/**
@@ -387,79 +420,21 @@ class Title {
* The last element in the array is the final destination after all redirects
* have been resolved (up to $wgMaxRedirects times)
*
- * @param $text String Text with possible redirect
+ * @param string $text Text with possible redirect
* @return Array of Titles, with the destination last
+ * @deprecated since 1.21, use Content::getRedirectChain instead.
*/
public static function newFromRedirectArray( $text ) {
- global $wgMaxRedirects;
- $title = self::newFromRedirectInternal( $text );
- if ( is_null( $title ) ) {
- return null;
- }
- // recursive check to follow double redirects
- $recurse = $wgMaxRedirects;
- $titles = array( $title );
- while ( --$recurse > 0 ) {
- if ( $title->isRedirect() ) {
- $page = WikiPage::factory( $title );
- $newtitle = $page->getRedirectTarget();
- } else {
- break;
- }
- // Redirects to some special pages are not permitted
- if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
- // the new title passes the checks, so make that our current title so that further recursion can be checked
- $title = $newtitle;
- $titles[] = $newtitle;
- } else {
- break;
- }
- }
- return $titles;
- }
+ ContentHandler::deprecated( __METHOD__, '1.21' );
- /**
- * Really extract the redirect destination
- * Do not call this function directly, use one of the newFromRedirect* functions above
- *
- * @param $text String Text with possible redirect
- * @return Title
- */
- protected static function newFromRedirectInternal( $text ) {
- global $wgMaxRedirects;
- if ( $wgMaxRedirects < 1 ) {
- //redirects are disabled, so quit early
- return null;
- }
- $redir = MagicWord::get( 'redirect' );
- $text = trim( $text );
- if ( $redir->matchStartAndRemove( $text ) ) {
- // Extract the first link and see if it's usable
- // Ensure that it really does come directly after #REDIRECT
- // Some older redirects included a colon, so don't freak about that!
- $m = array();
- if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
- // Strip preceding colon used to "escape" categories, etc.
- // and URL-decode links
- if ( strpos( $m[1], '%' ) !== false ) {
- // Match behavior of inline link parsing here;
- $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
- }
- $title = Title::newFromText( $m[1] );
- // If the title is a redirect to bad special pages or is invalid, return null
- if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
- return null;
- }
- return $title;
- }
- }
- return null;
+ $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+ return $content->getRedirectChain();
}
/**
* Get the prefixed DB key associated with an ID
*
- * @param $id Int the page_id of the article
+ * @param int $id the page_id of the article
* @return Title an object representing the article, or NULL if no such article was found
*/
public static function nameOf( $id ) {
@@ -517,11 +492,113 @@ class Title {
}
/**
+ * Utility method for converting a character sequence from bytes to Unicode.
+ *
+ * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
+ * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
+ *
+ * @param string $byteClass
+ * @return string
+ */
+ public static function convertByteClassToUnicodeClass( $byteClass ) {
+ $length = strlen( $byteClass );
+ // Input token queue
+ $x0 = $x1 = $x2 = '';
+ // Decoded queue
+ $d0 = $d1 = $d2 = '';
+ // Decoded integer codepoints
+ $ord0 = $ord1 = $ord2 = 0;
+ // Re-encoded queue
+ $r0 = $r1 = $r2 = '';
+ // Output
+ $out = '';
+ // Flags
+ $allowUnicode = false;
+ for ( $pos = 0; $pos < $length; $pos++ ) {
+ // Shift the queues down
+ $x2 = $x1;
+ $x1 = $x0;
+ $d2 = $d1;
+ $d1 = $d0;
+ $ord2 = $ord1;
+ $ord1 = $ord0;
+ $r2 = $r1;
+ $r1 = $r0;
+ // Load the current input token and decoded values
+ $inChar = $byteClass[$pos];
+ if ( $inChar == '\\' ) {
+ if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
+ $x0 = $inChar . $m[0];
+ $d0 = chr( hexdec( $m[1] ) );
+ $pos += strlen( $m[0] );
+ } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
+ $x0 = $inChar . $m[0];
+ $d0 = chr( octdec( $m[0] ) );
+ $pos += strlen( $m[0] );
+ } elseif ( $pos + 1 >= $length ) {
+ $x0 = $d0 = '\\';
+ } else {
+ $d0 = $byteClass[$pos + 1];
+ $x0 = $inChar . $d0;
+ $pos += 1;
+ }
+ } else {
+ $x0 = $d0 = $inChar;
+ }
+ $ord0 = ord( $d0 );
+ // Load the current re-encoded value
+ if ( $ord0 < 32 || $ord0 == 0x7f ) {
+ $r0 = sprintf( '\x%02x', $ord0 );
+ } elseif ( $ord0 >= 0x80 ) {
+ // Allow unicode if a single high-bit character appears
+ $r0 = sprintf( '\x%02x', $ord0 );
+ $allowUnicode = true;
+ } elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
+ $r0 = '\\' . $d0;
+ } else {
+ $r0 = $d0;
+ }
+ // Do the output
+ if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
+ // Range
+ if ( $ord2 > $ord0 ) {
+ // Empty range
+ } elseif ( $ord0 >= 0x80 ) {
+ // Unicode range
+ $allowUnicode = true;
+ if ( $ord2 < 0x80 ) {
+ // Keep the non-unicode section of the range
+ $out .= "$r2-\\x7F";
+ }
+ } else {
+ // Normal range
+ $out .= "$r2-$r0";
+ }
+ // Reset state to the initial value
+ $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
+ } elseif ( $ord2 < 0x80 ) {
+ // ASCII character
+ $out .= $r2;
+ }
+ }
+ if ( $ord1 < 0x80 ) {
+ $out .= $r1;
+ }
+ if ( $ord0 < 0x80 ) {
+ $out .= $r0;
+ }
+ if ( $allowUnicode ) {
+ $out .= '\u0080-\uFFFF';
+ }
+ return $out;
+ }
+
+ /**
* Get a string representation of a title suitable for
* including in a search index
*
- * @param $ns Int a namespace index
- * @param $title String text-form main part
+ * @param int $ns a namespace index
+ * @param string $title text-form main part
* @return String a stripped-down title string ready for the search index
*/
public static function indexTitle( $ns, $title ) {
@@ -547,10 +624,10 @@ class Title {
/**
* Make a prefixed DB key from a DB key and a namespace index
*
- * @param $ns Int numerical representation of the namespace
- * @param $title String the DB key form the title
- * @param $fragment String The link fragment (after the "#")
- * @param $interwiki String The interwiki prefix
+ * @param int $ns numerical representation of the namespace
+ * @param string $title the DB key form the title
+ * @param string $fragment The link fragment (after the "#")
+ * @param string $interwiki The interwiki prefix
* @return String the prefixed form of the title
*/
public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) {
@@ -570,7 +647,7 @@ class Title {
/**
* Escape a text fragment, say from a link, for a URL
*
- * @param $fragment string containing a URL or link fragment (after the "#")
+ * @param string $fragment containing a URL or link fragment (after the "#")
* @return String: escaped string
*/
static function escapeFragmentForURL( $fragment ) {
@@ -605,10 +682,12 @@ class Title {
*/
public function isLocal() {
if ( $this->mInterwiki != '' ) {
- return Interwiki::fetch( $this->mInterwiki )->isLocal();
- } else {
- return true;
+ $iw = Interwiki::fetch( $this->mInterwiki );
+ if ( $iw ) {
+ return $iw->isLocal();
+ }
}
+ return true;
}
/**
@@ -702,6 +781,39 @@ class Title {
}
/**
+ * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
+ *
+ * @throws MWException
+ * @return String: Content model id
+ */
+ public function getContentModel() {
+ if ( !$this->mContentModel ) {
+ $linkCache = LinkCache::singleton();
+ $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+ }
+
+ if ( !$this->mContentModel ) {
+ $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+ }
+
+ if ( !$this->mContentModel ) {
+ throw new MWException( 'Failed to determine content model!' );
+ }
+
+ return $this->mContentModel;
+ }
+
+ /**
+ * Convenience method for checking a title's content model name
+ *
+ * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
+ * @return Boolean true if $this->getContentModel() == $id
+ */
+ public function hasContentModel( $id ) {
+ return $this->getContentModel() == $id;
+ }
+
+ /**
* Get the namespace text
*
* @return String: Namespace text
@@ -747,7 +859,7 @@ class Title {
*/
public function getTalkNsText() {
global $wgContLang;
- return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
+ return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
}
/**
@@ -756,7 +868,7 @@ class Title {
* @return Bool TRUE or FALSE
*/
public function canTalk() {
- return( MWNamespace::canTalk( $this->mNamespace ) );
+ return MWNamespace::canTalk( $this->mNamespace );
}
/**
@@ -790,7 +902,7 @@ class Title {
/**
* Returns true if this title resolves to the named special page
*
- * @param $name String The special page name
+ * @param string $name The special page name
* @return boolean
*/
public function isSpecial( $name ) {
@@ -828,7 +940,7 @@ class Title {
* Please make use of this instead of comparing to getNamespace()
* This function is much more resistant to changes we may make
* to namespaces than code that makes direct comparisons.
- * @param $ns int The namespace
+ * @param int $ns The namespace
* @return bool
* @since 1.19
*/
@@ -865,7 +977,7 @@ class Title {
* is either NS_USER or NS_USER_TALK since both of them have NS_USER
* as their subject namespace.
*
- * This is MUCH simpler than individually testing for equivilance
+ * This is MUCH simpler than individually testing for equivalence
* against both NS_USER and NS_USER_TALK, and is also forward compatible.
* @since 1.19
* @param $ns int
@@ -905,9 +1017,9 @@ class Title {
/**
* Is this the mainpage?
- * @note Title::newFromText seams to be sufficiently optimized by the title
+ * @note Title::newFromText seems to be sufficiently optimized by the title
* cache that we don't need to over-optimize by doing direct comparisons and
- * acidentally creating new bugs where $title->equals( Title::newFromText() )
+ * accidentally creating new bugs where $title->equals( Title::newFromText() )
* ends up reporting something differently than $title->isMainPage();
*
* @since 1.18
@@ -934,6 +1046,8 @@ class Title {
* @return Bool
*/
public function isConversionTable() {
+ // @todo ConversionTable should become a separate content model.
+
return $this->getNamespace() == NS_MEDIAWIKI &&
strpos( $this->getText(), 'Conversiontable/' ) === 0;
}
@@ -944,22 +1058,31 @@ class Title {
* @return Bool
*/
public function isWikitextPage() {
- $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
- wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
- return $retval;
+ return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
}
/**
- * Could this page contain custom CSS or JavaScript, based
- * on the title?
+ * Could this page contain custom CSS or JavaScript for the global UI.
+ * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
+ * or CONTENT_MODEL_JAVASCRIPT.
+ *
+ * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
+ *
+ * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
*
* @return Bool
*/
public function isCssOrJsPage() {
- $retval = $this->mNamespace == NS_MEDIAWIKI
- && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
- wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
- return $retval;
+ $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
+ && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
+
+ #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
+ # hook functions can force this method to return true even outside the mediawiki namespace.
+
+ wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
+
+ return $isCssOrJsPage;
}
/**
@@ -967,7 +1090,9 @@ class Title {
* @return Bool
*/
public function isCssJsSubpage() {
- return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
}
/**
@@ -977,10 +1102,11 @@ class Title {
*/
public function getSkinFromCssJsSubpage() {
$subpage = explode( '/', $this->mTextform );
- $subpage = $subpage[ count( $subpage ) - 1 ];
+ $subpage = $subpage[count( $subpage ) - 1];
$lastdot = strrpos( $subpage, '.' );
- if ( $lastdot === false )
+ if ( $lastdot === false ) {
return $subpage; # Never happens: only called for names ending in '.css' or '.js'
+ }
return substr( $subpage, 0, $lastdot );
}
@@ -990,7 +1116,8 @@ class Title {
* @return Bool
*/
public function isCssSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_CSS ) );
}
/**
@@ -999,7 +1126,8 @@ class Title {
* @return Bool
*/
public function isJsSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
}
/**
@@ -1083,7 +1211,7 @@ class Title {
* Deprecated for public use, use Title::makeTitle() with fragment parameter.
* Still in active use privately.
*
- * @param $fragment String text
+ * @param string $fragment text
*/
public function setFragment( $fragment ) {
$this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
@@ -1093,7 +1221,7 @@ class Title {
* Prefix some arbitrary text with the namespace or interwiki prefix
* of this object
*
- * @param $name String the text
+ * @param string $name the text
* @return String the prefixed text
* @private
*/
@@ -1161,7 +1289,49 @@ class Title {
}
/**
- * Get the base page name, i.e. the leftmost part before any slashes
+ * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
+ *
+ * @par Example:
+ * @code
+ * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
+ * # returns: 'Foo'
+ * @endcode
+ *
+ * @return String Root name
+ * @since 1.20
+ */
+ public function getRootText() {
+ if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ return $this->getText();
+ }
+
+ return strtok( $this->getText(), '/' );
+ }
+
+ /**
+ * Get the root page name title, i.e. the leftmost part before any slashes
+ *
+ * @par Example:
+ * @code
+ * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
+ * # returns: Title{User:Foo}
+ * @endcode
+ *
+ * @return Title Root title
+ * @since 1.20
+ */
+ public function getRootTitle() {
+ return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
+ }
+
+ /**
+ * Get the base page name without a namespace, i.e. the part before the subpage name
+ *
+ * @par Example:
+ * @code
+ * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
+ * # returns: 'Foo/Bar'
+ * @endcode
*
* @return String Base name
*/
@@ -1179,16 +1349,55 @@ class Title {
}
/**
+ * Get the base page name title, i.e. the part before the subpage name
+ *
+ * @par Example:
+ * @code
+ * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
+ * # returns: Title{User:Foo/Bar}
+ * @endcode
+ *
+ * @return Title Base title
+ * @since 1.20
+ */
+ public function getBaseTitle() {
+ return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
+ }
+
+ /**
* Get the lowest-level subpage name, i.e. the rightmost part after any slashes
*
+ * @par Example:
+ * @code
+ * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
+ * # returns: "Baz"
+ * @endcode
+ *
* @return String Subpage name
*/
public function getSubpageText() {
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
- return( $this->mTextform );
+ return $this->mTextform;
}
$parts = explode( '/', $this->mTextform );
- return( $parts[count( $parts ) - 1] );
+ return $parts[count( $parts ) - 1];
+ }
+
+ /**
+ * Get the title for a subpage of the current page
+ *
+ * @par Example:
+ * @code
+ * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
+ * # returns: Title{User:Foo/Bar/Baz/Asdf}
+ * @endcode
+ *
+ * @param string $text The subpage name to add to the title
+ * @return Title Subpage title
+ * @since 1.20
+ */
+ public function getSubpage( $text ) {
+ return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
}
/**
@@ -1196,6 +1405,7 @@ class Title {
* Used for the title field in <a> tags.
*
* @return String the text, including any prefixes
+ * @deprecated since 1.19
*/
public function getEscapedText() {
wfDeprecated( __METHOD__, '1.19' );
@@ -1210,7 +1420,7 @@ class Title {
public function getSubpageUrlForm() {
$text = $this->getSubpageText();
$text = wfUrlencode( str_replace( ' ', '_', $text ) );
- return( $text );
+ return $text;
}
/**
@@ -1225,12 +1435,12 @@ class Title {
}
/**
- * Helper to fix up the get{Local,Full,Link,Canonical}URL args
- * get{Canonical,Full,Link,Local}URL methods accepted an optional
+ * Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
+ * get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
* second argument named variant. This was deprecated in favor
* of passing an array of option with a "variant" key
* Once $query2 is removed for good, this helper can be dropped
- * andthe wfArrayToCGI moved to getLocalURL();
+ * and the wfArrayToCgi moved to getLocalURL();
*
* @since 1.19 (r105919)
* @param $query
@@ -1238,19 +1448,21 @@ class Title {
* @return String
*/
private static function fixUrlQueryArgs( $query, $query2 = false ) {
- if( $query2 !== false ) {
- wfDeprecated( "Title::get{Canonical,Full,Link,Local} method called with a second parameter is deprecated. Add your parameter to an array passed as the first parameter.", "1.19" );
+ if ( $query2 !== false ) {
+ wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
+ "method called with a second parameter is deprecated. Add your " .
+ "parameter to an array passed as the first parameter.", "1.19" );
}
if ( is_array( $query ) ) {
- $query = wfArrayToCGI( $query );
+ $query = wfArrayToCgi( $query );
}
if ( $query2 ) {
if ( is_string( $query2 ) ) {
// $query2 is a string, we will consider this to be
// a deprecated $variant argument and add it to the query
- $query2 = wfArrayToCGI( array( 'variant' => $query2 ) );
+ $query2 = wfArrayToCgi( array( 'variant' => $query2 ) );
} else {
- $query2 = wfArrayToCGI( $query2 );
+ $query2 = wfArrayToCgi( $query2 );
}
// If we have $query content add a & to it first
if ( $query ) {
@@ -1270,6 +1482,8 @@ class Title {
*
* @see self::getLocalURL
* @see wfExpandUrl
+ * @param $query
+ * @param $query2 bool
* @param $proto Protocol type to use in URL
* @return String the URL
*/
@@ -1295,9 +1509,8 @@ class Title {
* Get a URL with no fragment or server name. If this page is generated
* with action=render, $wgServer is prepended.
*
-
- * @param $query string|array an optional query string,
- * not used for interwiki links. Can be specified as an associative array as well,
+ * @param string|array $query an optional query string,
+ * not used for interwiki links. Can be specified as an associative array as well,
* e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
* Some query patterns will trigger various shorturl path replacements.
* @param $query2 Mixed: An optional secondary query array. This one MUST
@@ -1330,7 +1543,7 @@ class Title {
$url = str_replace( '$1', $dbkey, $wgArticlePath );
wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
} else {
- global $wgVariantArticlePath, $wgActionPaths;
+ global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
$url = false;
$matches = array();
@@ -1352,6 +1565,7 @@ class Title {
if ( $url === false &&
$wgVariantArticlePath &&
+ $wgContLang->getCode() === $this->getPageLanguage()->getCode() &&
$this->getPageLanguage()->hasVariants() &&
preg_match( '/^variant=([^&]*)$/', $query, $matches ) )
{
@@ -1396,13 +1610,16 @@ class Title {
*
* See getLocalURL for the arguments.
*
+ * @param $query
+ * @param $query2 bool
+ * @param $proto Protocol to use; setting this will cause a full URL to be used
* @see self::getLocalURL
* @return String the URL
*/
- public function getLinkURL( $query = '', $query2 = false ) {
+ public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
wfProfileIn( __METHOD__ );
- if ( $this->isExternal() ) {
- $ret = $this->getFullURL( $query, $query2 );
+ if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
+ $ret = $this->getFullURL( $query, $query2, $proto );
} elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
$ret = $this->getFragmentForURL();
} else {
@@ -1422,6 +1639,7 @@ class Title {
* @param $query string
* @param $query2 bool|string
* @return String the URL
+ * @deprecated since 1.19
*/
public function escapeLocalURL( $query = '', $query2 = false ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -1436,6 +1654,7 @@ class Title {
*
* @see self::getLocalURL
* @return String the URL
+ * @deprecated since 1.19
*/
public function escapeFullURL( $query = '', $query2 = false ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -1493,6 +1712,7 @@ class Title {
* @see self::getLocalURL
* @since 1.18
* @return string
+ * @deprecated since 1.19
*/
public function escapeCanonicalURL( $query = '', $query2 = false ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -1555,7 +1775,7 @@ class Title {
*
* May provide false positives, but should never provide a false negative.
*
- * @param $action String action that permission needs to be checked for
+ * @param string $action action that permission needs to be checked for
* @param $user User to check (since 1.19); $wgUser will be used if not
* provided.
* @return Bool
@@ -1567,10 +1787,10 @@ class Title {
/**
* Can $user perform $action on this page?
*
- * @param $action String action that permission needs to be checked for
+ * @param string $action action that permission needs to be checked for
* @param $user User to check (since 1.19); $wgUser will be used if not
* provided.
- * @param $doExpensiveQueries Bool Set this to false to avoid doing
+ * @param bool $doExpensiveQueries Set this to false to avoid doing
* unnecessary queries.
* @return Bool
*/
@@ -1587,11 +1807,11 @@ class Title {
*
* @todo FIXME: This *does not* check throttles (User::pingLimiter()).
*
- * @param $action String action that permission needs to be checked for
+ * @param string $action action that permission needs to be checked for
* @param $user User to check
- * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary
+ * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary
* queries by skipping checks for cascading protections and user blocks.
- * @param $ignoreErrors Array of Strings Set this to a list of message keys
+ * @param array $ignoreErrors of Strings Set this to a list of message keys
* whose corresponding errors may be ignored.
* @return Array of arguments to wfMessage to explain permissions problems.
*/
@@ -1613,18 +1833,24 @@ class Title {
/**
* Permissions checks that fail most often, and which are easiest to test.
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User user to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
* @return Array list of errors
*/
private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+ if ( !wfRunHooks( 'TitleQuickPermissions', array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) ) ) {
+ return $errors;
+ }
+
if ( $action == 'create' ) {
- if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
- ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
+ if (
+ ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
+ ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
+ ) {
$errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' );
}
} elseif ( $action == 'move' ) {
@@ -1641,15 +1867,8 @@ class Title {
if ( !$user->isAllowed( 'move' ) ) {
// User can't move anything
- global $wgGroupPermissions;
- $userCanMove = false;
- if ( isset( $wgGroupPermissions['user']['move'] ) ) {
- $userCanMove = $wgGroupPermissions['user']['move'];
- }
- $autoconfirmedCanMove = false;
- if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
- $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
- }
+ $userCanMove = User::groupHasPermission( 'user', 'move' );
+ $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
// custom message if logged-in users without any special rights can move
$errors[] = array( 'movenologintext' );
@@ -1676,7 +1895,7 @@ class Title {
/**
* Add the resulting error code to the errors array
*
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $result Mixed result of errors
*
* @return Array list of errors
@@ -1701,9 +1920,9 @@ class Title {
/**
* Check various permission hooks
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User user to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
@@ -1720,8 +1939,11 @@ class Title {
$errors = $this->resultToError( $errors, $result );
}
// Check getUserPermissionsErrorsExpensive hook
- if ( $doExpensiveQueries && !( $short && count( $errors ) > 0 ) &&
- !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) {
+ if (
+ $doExpensiveQueries
+ && !( $short && count( $errors ) > 0 )
+ && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) )
+ ) {
$errors = $this->resultToError( $errors, $result );
}
@@ -1731,19 +1953,18 @@ class Title {
/**
* Check permissions on special pages & namespaces
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User user to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
* @return Array list of errors
*/
private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
- # Only 'createaccount' and 'execute' can be performed on
- # special pages, which don't actually exist in the DB.
- $specialOKActions = array( 'createaccount', 'execute', 'read' );
- if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) {
+ # Only 'createaccount' can be performed on special pages,
+ # which don't actually exist in the DB.
+ if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
$errors[] = array( 'ns-specialprotected' );
}
@@ -1752,7 +1973,7 @@ class Title {
$ns = $this->mNamespace == NS_MAIN ?
wfMessage( 'nstab-main' )->text() : $this->getNsText();
$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
- array( 'protectedinterface' ) : array( 'namespaceprotected', $ns );
+ array( 'protectedinterface' ) : array( 'namespaceprotected', $ns );
}
return $errors;
@@ -1761,9 +1982,9 @@ class Title {
/**
* Check CSS/JS sub-page permissions
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User user to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
@@ -1773,12 +1994,19 @@ class Title {
# Protect css/js subpages of user pages
# XXX: this might be better using restrictions
# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
- if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' )
- && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
- if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
- $errors[] = array( 'customcssprotected' );
- } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
- $errors[] = array( 'customjsprotected' );
+ if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
+ if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
+ if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
+ $errors[] = array( 'mycustomcssprotected' );
+ } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
+ $errors[] = array( 'mycustomjsprotected' );
+ }
+ } else {
+ if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
+ $errors[] = array( 'customcssprotected' );
+ } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
+ $errors[] = array( 'customjsprotected' );
+ }
}
}
@@ -1790,9 +2018,9 @@ class Title {
* page. The user must possess all required rights for this
* action.
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User user to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
@@ -1800,18 +2028,21 @@ class Title {
*/
private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
foreach ( $this->getRestrictions( $action ) as $right ) {
- // Backwards compatibility, rewrite sysop -> protect
+ // Backwards compatibility, rewrite sysop -> editprotected
if ( $right == 'sysop' ) {
- $right = 'protect';
+ $right = 'editprotected';
}
- if ( $right != '' && !$user->isAllowed( $right ) ) {
- // Users with 'editprotected' permission can edit protected pages
- // without cascading option turned on.
- if ( $action != 'edit' || !$user->isAllowed( 'editprotected' )
- || $this->mCascadeRestriction )
- {
- $errors[] = array( 'protectedpagetext', $right );
- }
+ // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected';
+ }
+ if ( $right == '' ) {
+ continue;
+ }
+ if ( !$user->isAllowed( $right ) ) {
+ $errors[] = array( 'protectedpagetext', $right );
+ } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
+ $errors[] = array( 'protectedpagetext', 'protect' );
}
}
@@ -1821,9 +2052,9 @@ class Title {
/**
* Check restrictions on cascading pages.
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
@@ -1843,11 +2074,19 @@ class Title {
# This is only for protection restrictions, not for all actions
if ( isset( $restrictions[$action] ) ) {
foreach ( $restrictions[$action] as $right ) {
- $right = ( $right == 'sysop' ) ? 'protect' : $right;
- if ( $right != '' && !$user->isAllowed( $right ) ) {
+ // Backwards compatibility, rewrite sysop -> editprotected
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected';
+ }
+ // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected';
+ }
+ if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
$pages = '';
- foreach ( $cascadingSources as $page )
+ foreach ( $cascadingSources as $page ) {
$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
+ }
$errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages );
}
}
@@ -1860,9 +2099,9 @@ class Title {
/**
* Check action permissions not already checked in checkQuickPermissions
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
@@ -1878,11 +2117,14 @@ class Title {
}
} elseif ( $action == 'create' ) {
$title_protection = $this->getTitleProtection();
- if( $title_protection ) {
- if( $title_protection['pt_create_perm'] == 'sysop' ) {
- $title_protection['pt_create_perm'] = 'protect'; // B/C
+ if ( $title_protection ) {
+ if ( $title_protection['pt_create_perm'] == 'sysop' ) {
+ $title_protection['pt_create_perm'] = 'editprotected'; // B/C
}
- if( $title_protection['pt_create_perm'] == '' ||
+ if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) {
+ $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C
+ }
+ if ( $title_protection['pt_create_perm'] == '' ||
!$user->isAllowed( $title_protection['pt_create_perm'] ) )
{
$errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] );
@@ -1914,11 +2156,11 @@ class Title {
}
/**
- * Check that the user isn't blocked from editting.
+ * Check that the user isn't blocked from editing.
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
@@ -1927,11 +2169,11 @@ class Title {
private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
// Account creation blocks handled at userlogin.
// Unblocking handled in SpecialUnblock
- if( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
+ if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
return $errors;
}
- global $wgContLang, $wgLang, $wgEmailConfirmToEdit;
+ global $wgEmailConfirmToEdit;
if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
$errors[] = array( 'confirmedittext' );
@@ -1940,39 +2182,9 @@ class Title {
if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) {
// Don't block the user from editing their own talk page unless they've been
// explicitly blocked from that too.
- } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) {
- $block = $user->getBlock();
-
- // This is from OutputPage::blockedPage
- // Copied at r23888 by werdna
-
- $id = $user->blockedBy();
- $reason = $user->blockedFor();
- if ( $reason == '' ) {
- $reason = wfMessage( 'blockednoreason' )->text();
- }
- $ip = $user->getRequest()->getIP();
-
- if ( is_numeric( $id ) ) {
- $name = User::whoIs( $id );
- } else {
- $name = $id;
- }
-
- $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
- $blockid = $block->getId();
- $blockExpiry = $block->getExpiry();
- $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true );
- if ( $blockExpiry == 'infinity' ) {
- $blockExpiry = wfMessage( 'infiniteblock' )->text();
- } else {
- $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
- }
-
- $intended = strval( $block->getTarget() );
-
- $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
- $blockid, $blockExpiry, $intended, $blockTimestamp );
+ } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) {
+ // @todo FIXME: Pass the relevant context into this function.
+ $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
}
return $errors;
@@ -1981,43 +2193,19 @@ class Title {
/**
* Check that the user is allowed to read this page.
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $user User to check
- * @param $errors Array list of current errors
+ * @param array $errors list of current errors
* @param $doExpensiveQueries Boolean whether or not to perform expensive queries
* @param $short Boolean short circuit on first error
*
* @return Array list of errors
*/
private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
- global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions;
- static $useShortcut = null;
-
- # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
- if ( is_null( $useShortcut ) ) {
- $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;
- }
- }
- }
- }
+ global $wgWhitelistRead, $wgWhitelistReadRegexp;
$whitelisted = false;
- if ( $useShortcut ) {
+ if ( User::isEveryoneAllowed( 'read' ) ) {
# Shortcut for public wikis, allows skipping quite a bit of code
$whitelisted = true;
} elseif ( $user->isAllowed( 'read' ) ) {
@@ -2034,7 +2222,7 @@ class Title {
# Time to check the whitelist
# Only do these checks is there's something to check against
$name = $this->getPrefixedText();
- $dbName = $this->getPrefixedDBKey();
+ $dbName = $this->getPrefixedDBkey();
// Check for explicit whitelisting with and without underscores
if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
@@ -2049,7 +2237,7 @@ class Title {
# If it's a special page, ditch the subpage bit and check again
$name = $this->getDBkey();
list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
- if ( $name !== false ) {
+ if ( $name ) {
$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
if ( in_array( $pure, $wgWhitelistRead, true ) ) {
$whitelisted = true;
@@ -2058,6 +2246,17 @@ class Title {
}
}
+ if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
+ $name = $this->getPrefixedText();
+ // Check for regex whitelisting
+ foreach ( $wgWhitelistReadRegexp as $listItem ) {
+ if ( preg_match( $listItem, $name ) ) {
+ $whitelisted = true;
+ break;
+ }
+ }
+ }
+
if ( !$whitelisted ) {
# If the title is not whitelisted, give extensions a chance to do so...
wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) );
@@ -2073,7 +2272,7 @@ class Title {
* Get a description array when the user doesn't have the right to perform
* $action (i.e. when User::isAllowed() returns false)
*
- * @param $action String the action to check
+ * @param string $action the action to check
* @param $short Boolean short circuit on first error
* @return Array list of errors
*/
@@ -2103,10 +2302,10 @@ class Title {
* which checks ONLY that previously checked by userCan (i.e. it leaves out
* checks on wfReadOnly() and blocks)
*
- * @param $action String action that permission needs to be checked for
+ * @param string $action action that permission needs to be checked for
* @param $user User to check
- * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries.
- * @param $short Bool Set this to true to stop after the first permission error.
+ * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
+ * @param bool $short Set this to true to stop after the first permission error.
* @return Array of arrays of the arguments to wfMessage to explain permissions problems.
*/
protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
@@ -2132,7 +2331,7 @@ class Title {
}
$errors = array();
- while( count( $checks ) > 0 &&
+ while ( count( $checks ) > 0 &&
!( $short && count( $errors ) > 0 ) ) {
$method = array_shift( $checks );
$errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
@@ -2143,34 +2342,6 @@ class Title {
}
/**
- * Protect css subpages of user pages: can $wgUser edit
- * this page?
- *
- * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
- * @return Bool
- */
- public function userCanEditCssSubpage() {
- global $wgUser;
- wfDeprecated( __METHOD__, '1.19' );
- return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
- || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
- }
-
- /**
- * Protect js subpages of user pages: can $wgUser edit
- * this page?
- *
- * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
- * @return Bool
- */
- public function userCanEditJsSubpage() {
- global $wgUser;
- wfDeprecated( __METHOD__, '1.19' );
- return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
- || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
- }
-
- /**
* Get a filtered list of all restriction types supported by this wiki.
* @param bool $exists True to get all restriction types that apply to
* titles that do exist, False for all restriction types that apply to
@@ -2235,9 +2406,12 @@ class Title {
if ( !isset( $this->mTitleProtection ) ) {
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'protected_titles', '*',
+ $res = $dbr->select(
+ 'protected_titles',
+ array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ),
array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
- __METHOD__ );
+ __METHOD__
+ );
// fetchRow returns false if there are no rows.
$this->mTitleProtection = $dbr->fetchRow( $res );
@@ -2248,10 +2422,10 @@ class Title {
/**
* Update the title protection status
*
- * @deprecated in 1.19; will be removed in 1.20. Use WikiPage::doUpdateRestrictions() instead.
+ * @deprecated in 1.19; use WikiPage::doUpdateRestrictions() instead.
* @param $create_perm String Permission required for creation
- * @param $reason String Reason for protection
- * @param $expiry String Expiry timestamp
+ * @param string $reason Reason for protection
+ * @param string $expiry Expiry timestamp
* @return boolean true
*/
public function updateTitleProtection( $create_perm, $reason, $expiry ) {
@@ -2263,7 +2437,8 @@ class Title {
$expiry = array( 'create' => $expiry );
$page = WikiPage::factory( $this );
- $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser );
+ $cascade = false;
+ $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $wgUser );
return $status->isOK();
}
@@ -2283,35 +2458,37 @@ class Title {
}
/**
- * Is this page "semi-protected" - the *only* protection is autoconfirm?
+ * Is this page "semi-protected" - the *only* protection levels are listed
+ * in $wgSemiprotectedRestrictionLevels?
*
- * @param $action String Action to check (default: edit)
+ * @param string $action Action to check (default: edit)
* @return Bool
*/
public function isSemiProtected( $action = 'edit' ) {
- if ( $this->exists() ) {
- $restrictions = $this->getRestrictions( $action );
- if ( count( $restrictions ) > 0 ) {
- foreach ( $restrictions as $restriction ) {
- if ( strtolower( $restriction ) != 'autoconfirmed' ) {
- return false;
- }
- }
- } else {
- # Not protected
- return false;
- }
- return true;
- } else {
- # If it doesn't exist, it can't be protected
+ global $wgSemiprotectedRestrictionLevels;
+
+ $restrictions = $this->getRestrictions( $action );
+ $semi = $wgSemiprotectedRestrictionLevels;
+ if ( !$restrictions || !$semi ) {
+ // Not protected, or all protection is full protection
return false;
}
+
+ // Remap autoconfirmed to editsemiprotected for BC
+ foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
+ $semi[$key] = 'editsemiprotected';
+ }
+ foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
+ $restrictions[$key] = 'editsemiprotected';
+ }
+
+ return !array_diff( $restrictions, $semi );
}
/**
* Does the title correspond to a protected article?
*
- * @param $action String the action the page is protected from,
+ * @param string $action the action the page is protected from,
* by default checks all actions.
* @return Bool
*/
@@ -2321,7 +2498,7 @@ class Title {
$restrictionTypes = $this->getRestrictionTypes();
# Special pages have inherent protection
- if( $this->isSpecialPage() ) {
+ if ( $this->isSpecialPage() ) {
return true;
}
@@ -2373,7 +2550,7 @@ class Title {
/**
* Cascading protection: Get the source of any cascading restrictions on this page.
*
- * @param $getPages Bool Whether or not to retrieve the actual pages
+ * @param bool $getPages Whether or not to retrieve the actual pages
* that the restrictions have come from.
* @return Mixed Array of Title objects of the pages from which cascading restrictions
* have come, false for none, or true if such restrictions exist, but $getPages
@@ -2413,7 +2590,7 @@ class Title {
if ( $getPages ) {
$cols = array( 'pr_page', 'page_namespace', 'page_title',
- 'pr_expiry', 'pr_type', 'pr_level' );
+ 'pr_expiry', 'pr_type', 'pr_level' );
$where_clauses[] = 'page_id=pr_page';
$tables[] = 'page';
} else {
@@ -2441,8 +2618,10 @@ class Title {
$pagerestrictions[$row->pr_type] = array();
}
- if ( isset( $pagerestrictions[$row->pr_type] ) &&
- !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) ) {
+ if (
+ isset( $pagerestrictions[$row->pr_type] )
+ && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
+ ) {
$pagerestrictions[$row->pr_type][] = $row->pr_level;
}
} else {
@@ -2471,7 +2650,7 @@ class Title {
/**
* Accessor/initialisation for mRestrictions
*
- * @param $action String action that permission needs to be checked for
+ * @param string $action action that permission needs to be checked for
* @return Array of Strings the array of groups allowed to edit this article
*/
public function getRestrictions( $action ) {
@@ -2514,7 +2693,7 @@ class Title {
* Loads a string into mRestrictions array
*
* @param $res Resource restrictions as an SQL result.
- * @param $oldFashionedRestrictions String comma-separated list of page
+ * @param string $oldFashionedRestrictions comma-separated list of page
* restrictions from page table (pre 1.10)
*/
private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
@@ -2532,8 +2711,8 @@ class Title {
* and page_restrictions table for this existing page.
* Public for usage by LiquidThreads.
*
- * @param $rows array of db result objects
- * @param $oldFashionedRestrictions string comma-separated list of page
+ * @param array $rows of db result objects
+ * @param string $oldFashionedRestrictions comma-separated list of page
* restrictions from page table (pre 1.10)
*/
public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
@@ -2566,7 +2745,7 @@ class Title {
$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
} else {
$restriction = trim( $temp[1] );
- if( $restriction != '' ) { //some old entries are empty
+ if ( $restriction != '' ) { //some old entries are empty
$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
}
}
@@ -2585,8 +2764,9 @@ class Title {
foreach ( $rows as $row ) {
// Don't take care of restrictions types that aren't allowed
- if ( !in_array( $row->pr_type, $restrictionTypes ) )
+ if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
continue;
+ }
// This code should be refactored, now that it's being used more generally,
// But I don't really see any harm in leaving it in Block for now -werdna
@@ -2615,7 +2795,7 @@ class Title {
/**
* Load restrictions from the page_restrictions table
*
- * @param $oldFashionedRestrictions String comma-separated list of page
+ * @param string $oldFashionedRestrictions comma-separated list of page
* restrictions from page table (pre 1.10)
*/
public function loadRestrictions( $oldFashionedRestrictions = null ) {
@@ -2626,7 +2806,7 @@ class Title {
$res = $dbr->select(
'page_restrictions',
- '*',
+ array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ),
array( 'pr_page' => $this->getArticleID() ),
__METHOD__
);
@@ -2668,18 +2848,24 @@ class Title {
* Purge expired restrictions from the page_restrictions table
*/
static function purgeExpiredRestrictions() {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete(
- 'page_restrictions',
- array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
- __METHOD__
- );
+ if ( wfReadOnly() ) {
+ return;
+ }
- $dbw->delete(
- 'protected_titles',
- array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
- __METHOD__
- );
+ $method = __METHOD__;
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->onTransactionIdle( function() use ( $dbw, $method ) {
+ $dbw->delete(
+ 'page_restrictions',
+ array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
+ $method
+ );
+ $dbw->delete(
+ 'protected_titles',
+ array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
+ $method
+ );
+ } );
}
/**
@@ -2711,7 +2897,7 @@ class Title {
/**
* Get all subpages of this page.
*
- * @param $limit Int maximum number of subpages to fetch; -1 for no limit
+ * @param int $limit maximum number of subpages to fetch; -1 for no limit
* @return mixed TitleArray, or empty array if this page's namespace
* doesn't allow subpages
*/
@@ -2789,7 +2975,7 @@ class Title {
* Get the article ID for this Title from the link cache,
* adding it if necessary
*
- * @param $flags Int a bit field; may be Title::GAID_FOR_UPDATE to select
+ * @param int $flags a bit field; may be Title::GAID_FOR_UPDATE to select
* for update
* @return Int the ID
*/
@@ -2815,7 +3001,7 @@ class Title {
* Is this an article that is a redirect page?
* Uses link cache, adding it if necessary
*
- * @param $flags Int a bit field; may be Title::GAID_FOR_UPDATE to select for update
+ * @param int $flags a bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return Bool
*/
public function isRedirect( $flags = 0 ) {
@@ -2826,8 +3012,20 @@ class Title {
if ( !$this->getArticleID( $flags ) ) {
return $this->mRedirect = false;
}
+
$linkCache = LinkCache::singleton();
- $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+ $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+ if ( $cached === null ) {
+ # Trust LinkCache's state over our own
+ # LinkCache is telling us that the page doesn't exist, despite there being cached
+ # data relating to an existing page in $this->mArticleID. Updaters should clear
+ # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
+ # set, then LinkCache will definitely be up to date here, since getArticleID() forces
+ # LinkCache to refresh its data from the master.
+ return $this->mRedirect = false;
+ }
+
+ $this->mRedirect = (bool)$cached;
return $this->mRedirect;
}
@@ -2836,7 +3034,7 @@ class Title {
* What is the length of this page?
* Uses link cache, adding it if necessary
*
- * @param $flags Int a bit field; may be Title::GAID_FOR_UPDATE to select for update
+ * @param int $flags a bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return Int
*/
public function getLength( $flags = 0 ) {
@@ -2848,7 +3046,13 @@ class Title {
return $this->mLength = 0;
}
$linkCache = LinkCache::singleton();
- $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
+ $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
+ if ( $cached === null ) {
+ # Trust LinkCache's state over our own, as for isRedirect()
+ return $this->mLength = 0;
+ }
+
+ $this->mLength = intval( $cached );
return $this->mLength;
}
@@ -2856,7 +3060,7 @@ class Title {
/**
* What is the page_latest field for this page?
*
- * @param $flags Int a bit field; may be Title::GAID_FOR_UPDATE to select for update
+ * @param int $flags a bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return Int or 0 if the page doesn't exist
*/
public function getLatestRevID( $flags = 0 ) {
@@ -2868,7 +3072,14 @@ class Title {
return $this->mLatestID = 0;
}
$linkCache = LinkCache::singleton();
- $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
+ $linkCache->addLinkObj( $this );
+ $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
+ if ( $cached === null ) {
+ # Trust LinkCache's state over our own, as for isRedirect()
+ return $this->mLatestID = 0;
+ }
+
+ $this->mLatestID = intval( $cached );
return $this->mLatestID;
}
@@ -2881,7 +3092,7 @@ class Title {
* loading of the new page_id. It's also called from
* WikiPage::doDeleteArticleReal()
*
- * @param $newid Int the new Article ID
+ * @param int $newid the new Article ID
*/
public function resetArticleID( $newid ) {
$linkCache = LinkCache::singleton();
@@ -2897,14 +3108,15 @@ class Title {
$this->mRedirect = null;
$this->mLength = -1;
$this->mLatestID = false;
+ $this->mContentModel = false;
$this->mEstimateRevisions = null;
}
/**
* Capitalize a text string for a title if it belongs to a namespace that capitalizes
*
- * @param $text String containing title to capitalize
- * @param $ns int namespace index, defaults to NS_MAIN
+ * @param string $text containing title to capitalize
+ * @param int $ns namespace index, defaults to NS_MAIN
* @return String containing capitalized title
*/
public static function capitalize( $text, $ns = NS_MAIN ) {
@@ -2953,7 +3165,7 @@ class Title {
return false;
}
- if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) {
+ if ( strpos( $dbkey, UTF8_REPLACEMENT ) !== false ) {
# Contained illegal UTF-8 sequences or forbidden Unicode chars.
return false;
}
@@ -3049,15 +3261,18 @@ class Title {
# Pages with "/./" or "/../" appearing in the URLs will often be un-
# reachable due to the way web browsers deal with 'relative' URLs.
# Also, they conflict with subpage syntax. Forbid them explicitly.
- if ( strpos( $dbkey, '.' ) !== false &&
- ( $dbkey === '.' || $dbkey === '..' ||
- strpos( $dbkey, './' ) === 0 ||
- strpos( $dbkey, '../' ) === 0 ||
- strpos( $dbkey, '/./' ) !== false ||
- strpos( $dbkey, '/../' ) !== false ||
- substr( $dbkey, -2 ) == '/.' ||
- substr( $dbkey, -3 ) == '/..' ) )
- {
+ if (
+ strpos( $dbkey, '.' ) !== false &&
+ (
+ $dbkey === '.' || $dbkey === '..' ||
+ strpos( $dbkey, './' ) === 0 ||
+ strpos( $dbkey, '../' ) === 0 ||
+ strpos( $dbkey, '/./' ) !== false ||
+ strpos( $dbkey, '/../' ) !== false ||
+ substr( $dbkey, -2 ) == '/.' ||
+ substr( $dbkey, -3 ) == '/..'
+ )
+ ) {
return false;
}
@@ -3070,9 +3285,10 @@ class Title {
# underlying database field. We make an exception for special pages, which
# don't need to be stored in the database, and may edge over 255 bytes due
# to subpage syntax for long titles, e.g. [[Special:Block/Long name]]
- if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) ||
- strlen( $dbkey ) > 512 )
- {
+ if (
+ ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 )
+ || strlen( $dbkey ) > 512
+ ) {
return false;
}
@@ -3086,6 +3302,7 @@ class Title {
# Can't make a link to a namespace alone... "empty" local links can only be
# self-links with a fragment identifier.
+ # TODO: Why do we exclude NS_MAIN (bug 54044)
if ( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) {
return false;
}
@@ -3121,9 +3338,9 @@ class Title {
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
- * @param $options Array: may be FOR UPDATE
- * @param $table String: table name
- * @param $prefix String: fields prefix
+ * @param array $options may be FOR UPDATE
+ * @param string $table table name
+ * @param string $prefix fields prefix
* @return Array of Title objects linking here
*/
public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
@@ -3135,11 +3352,11 @@ class Title {
$res = $db->select(
array( 'page', $table ),
- array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+ self::getSelectFields(),
array(
"{$prefix}_from=page_id",
"{$prefix}_namespace" => $this->getNamespace(),
- "{$prefix}_title" => $this->getDBkey() ),
+ "{$prefix}_title" => $this->getDBkey() ),
__METHOD__,
$options
);
@@ -3165,7 +3382,7 @@ class Title {
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
- * @param $options Array: may be FOR UPDATE
+ * @param array $options may be FOR UPDATE
* @return Array of Title the Title objects linking here
*/
public function getTemplateLinksTo( $options = array() ) {
@@ -3179,12 +3396,14 @@ class Title {
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
- * @param $options Array: may be FOR UPDATE
- * @param $table String: table name
- * @param $prefix String: fields prefix
+ * @param array $options may be FOR UPDATE
+ * @param string $table table name
+ * @param string $prefix fields prefix
* @return Array of Title objects linking here
*/
public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+ global $wgContentHandlerUseDB;
+
$id = $this->getArticleID();
# If the page doesn't exist; there can't be any link from this page
@@ -3201,9 +3420,14 @@ class Title {
$namespaceFiled = "{$prefix}_namespace";
$titleField = "{$prefix}_title";
+ $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'page_content_model';
+ }
+
$res = $db->select(
array( $table, 'page' ),
- array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+ $fields,
array( "{$prefix}_from" => $id ),
__METHOD__,
$options,
@@ -3235,7 +3459,7 @@ class Title {
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
- * @param $options Array: may be FOR UPDATE
+ * @param array $options may be FOR UPDATE
* @return Array of Title the Title objects used here
*/
public function getTemplateLinksFrom( $options = array() ) {
@@ -3278,7 +3502,6 @@ class Title {
return $retVal;
}
-
/**
* Get a list of URLs to purge from the Squid cache when this
* page changes
@@ -3299,6 +3522,7 @@ class Title {
}
}
+ wfRunHooks( 'TitleSquidURLs', array( $this, &$urls ) );
return $urls;
}
@@ -3329,13 +3553,13 @@ class Title {
* Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
*
* @param $nt Title the new title
- * @param $auth Bool indicates whether $wgUser's permissions
+ * @param bool $auth indicates whether $wgUser's permissions
* should be checked
- * @param $reason String is the log summary of the move, used for spam checking
+ * @param string $reason is the log summary of the move, used for spam checking
* @return Mixed True on success, getUserPermissionsErrors()-like array on failure
*/
public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
- global $wgUser;
+ global $wgUser, $wgContentHandlerUseDB;
$errors = array();
if ( !$nt ) {
@@ -3362,12 +3586,25 @@ class Title {
if ( strlen( $nt->getDBkey() ) < 1 ) {
$errors[] = array( 'articleexists' );
}
- if ( ( $this->getDBkey() == '' ) ||
- ( !$oldid ) ||
- ( $nt->getDBkey() == '' ) ) {
+ if (
+ ( $this->getDBkey() == '' ) ||
+ ( !$oldid ) ||
+ ( $nt->getDBkey() == '' )
+ ) {
$errors[] = array( 'badarticleerror' );
}
+ // Content model checks
+ if ( !$wgContentHandlerUseDB &&
+ $this->getContentModel() !== $nt->getContentModel() ) {
+ // can't move a page if that would change the page's content model
+ $errors[] = array(
+ 'bad-target-model',
+ ContentHandler::getLocalizedName( $this->getContentModel() ),
+ ContentHandler::getLocalizedName( $nt->getContentModel() )
+ );
+ }
+
// Image-specific checks
if ( $this->getNamespace() == NS_FILE ) {
$errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) );
@@ -3406,7 +3643,13 @@ class Title {
}
} else {
$tp = $nt->getTitleProtection();
- $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm'];
+ $right = $tp['pt_create_perm'];
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // B/C
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // B/C
+ }
if ( $tp and !$wgUser->isAllowed( $right ) ) {
$errors[] = array( 'cantmove-titleprotected' );
}
@@ -3460,10 +3703,10 @@ class Title {
* Move a title to a new location
*
* @param $nt Title the new title
- * @param $auth Bool indicates whether $wgUser's permissions
+ * @param bool $auth indicates whether $wgUser's permissions
* should be checked
- * @param $reason String the reason for the move
- * @param $createRedirect Bool Whether to create a redirect from the old title to the new title.
+ * @param string $reason the reason for the move
+ * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
* Ignored if the user doesn't have the suppressredirect right.
* @return Mixed true on success, getUserPermissionsErrors()-like array on failure
*/
@@ -3480,6 +3723,8 @@ class Title {
$createRedirect = true;
}
+ wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) );
+
// 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 );
@@ -3532,12 +3777,12 @@ class Title {
# Protect the redirect title as the title used to be...
$dbw->insertSelect( 'page_restrictions', 'page_restrictions',
array(
- 'pr_page' => $redirid,
- 'pr_type' => 'pr_type',
- 'pr_level' => 'pr_level',
+ 'pr_page' => $redirid,
+ 'pr_type' => 'pr_type',
+ 'pr_level' => 'pr_level',
'pr_cascade' => 'pr_cascade',
- 'pr_user' => 'pr_user',
- 'pr_expiry' => 'pr_expiry'
+ 'pr_user' => 'pr_user',
+ 'pr_expiry' => 'pr_expiry'
),
array( 'pr_page' => $pageid ),
__METHOD__,
@@ -3554,12 +3799,12 @@ class Title {
$comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
}
// @todo FIXME: $params?
- $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) );
+ $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ), $wgUser );
}
# Update watchlists
- $oldnamespace = $this->getNamespace() & ~1;
- $newnamespace = $nt->getNamespace() & ~1;
+ $oldnamespace = MWNamespace::getSubject( $this->getNamespace() );
+ $newnamespace = MWNamespace::getSubject( $nt->getNamespace() );
$oldtitle = $this->getDBkey();
$newtitle = $nt->getDBkey();
@@ -3578,8 +3823,8 @@ class Title {
* source page or nonexistent
*
* @param $nt Title the page to move to, which should be a redirect or nonexistent
- * @param $reason String The reason for the move
- * @param $createRedirect Bool Whether to leave a redirect at the old title. Does not check
+ * @param string $reason The reason for the move
+ * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
* if the user has the suppressredirect right
* @throws MWException
*/
@@ -3594,7 +3839,15 @@ class Title {
$logType = 'move';
}
- $redirectSuppressed = !$createRedirect;
+ if ( $createRedirect ) {
+ $contentHandler = ContentHandler::getForTitle( $this );
+ $redirectContent = $contentHandler->makeRedirectContent( $nt,
+ wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
+
+ // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
+ } else {
+ $redirectContent = null;
+ }
$logEntry = new ManualLogEntry( 'move', $logType );
$logEntry->setPerformer( $wgUser );
@@ -3602,7 +3855,7 @@ class Title {
$logEntry->setComment( $reason );
$logEntry->setParameters( array(
'4::target' => $nt->getPrefixedText(),
- '5::noredir' => $redirectSuppressed ? '1': '0',
+ '5::noredir' => $redirectContent ? '0': '1',
) );
$formatter = LogFormatter::newFromEntry( $logEntry );
@@ -3637,20 +3890,27 @@ class Title {
if ( !is_object( $nullRevision ) ) {
throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
}
- $nullRevId = $nullRevision->insertOn( $dbw );
+
+ $nullRevision->insertOn( $dbw );
# Change the name of the target page:
$dbw->update( 'page',
/* SET */ array(
'page_namespace' => $nt->getNamespace(),
- 'page_title' => $nt->getDBkey(),
+ 'page_title' => $nt->getDBkey(),
),
/* WHERE */ array( 'page_id' => $oldid ),
__METHOD__
);
- $this->resetArticleID( 0 );
+ // clean up the old title before reset article id - bug 45348
+ if ( !$redirectContent ) {
+ WikiPage::onArticleDelete( $this );
+ }
+
+ $this->resetArticleID( 0 ); // 0 == non existing
$nt->resetArticleID( $oldid );
+ $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397
$newpage->updateRevisionOn( $dbw, $nullRevision );
@@ -3664,18 +3924,17 @@ class Title {
}
# Recreate the redirect, this time in the other direction.
- if ( $redirectSuppressed ) {
- WikiPage::onArticleDelete( $this );
- } else {
- $mwRedir = MagicWord::get( 'redirect' );
- $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
+ if ( $redirectContent ) {
$redirectArticle = WikiPage::factory( $this );
+ $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397
$newid = $redirectArticle->insertOn( $dbw );
if ( $newid ) { // sanity
+ $this->resetArticleID( $newid );
$redirectRevision = new Revision( array(
- 'page' => $newid,
+ 'title' => $this, // for determining the default content model
+ 'page' => $newid,
'comment' => $comment,
- 'text' => $redirectText ) );
+ 'content' => $redirectContent ) );
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
@@ -3695,9 +3954,9 @@ class Title {
* Move this page's subpages to be subpages of $nt
*
* @param $nt Title Move target
- * @param $auth bool Whether $wgUser's permissions should be checked
- * @param $reason string The reason for the move
- * @param $createRedirect bool Whether to create redirects from the old subpages to
+ * @param bool $auth Whether $wgUser's permissions should be checked
+ * @param string $reason The reason for the move
+ * @param bool $createRedirect Whether to create redirects from the old subpages to
* the new ones Ignored if the user doesn't have the 'suppressredirect' right
* @return mixed array with old page titles as keys, and strings (new page titles) or
* arrays (errors) as values, or an error array with numeric indices if no pages
@@ -3771,10 +4030,18 @@ class Title {
* @return Bool
*/
public function isSingleRevRedirect() {
+ global $wgContentHandlerUseDB;
+
$dbw = wfGetDB( DB_MASTER );
+
# Is it a redirect?
+ $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'page_content_model';
+ }
+
$row = $dbw->selectRow( 'page',
- array( 'page_is_redirect', 'page_latest', 'page_id' ),
+ $fields,
$this->pageCond(),
__METHOD__,
array( 'FOR UPDATE' )
@@ -3783,6 +4050,7 @@ class Title {
$this->mArticleID = $row ? intval( $row->page_id ) : 0;
$this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
$this->mLatestID = $row ? intval( $row->page_latest ) : false;
+ $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false;
if ( !$this->mRedirect ) {
return false;
}
@@ -3824,27 +4092,28 @@ class Title {
}
# Get the article text
$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
- if( !is_object( $rev ) ){
+ if ( !is_object( $rev ) ) {
return false;
}
- $text = $rev->getText();
+ $content = $rev->getContent();
# Does the redirect point to the source?
# Or is it a broken self-redirect, usually caused by namespace collisions?
- $m = array();
- if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
- $redirTitle = Title::newFromText( $m[1] );
- if ( !is_object( $redirTitle ) ||
- ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
- $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
+ $redirTitle = $content ? $content->getRedirectTarget() : null;
+
+ if ( $redirTitle ) {
+ if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
+ $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
wfDebug( __METHOD__ . ": redirect points to other page\n" );
return false;
+ } else {
+ return true;
}
} else {
- # Fail safe
- wfDebug( __METHOD__ . ": failsafe\n" );
+ # Fail safe (not a redirect after all. strange.)
+ wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
+ " is a redirect, but it doesn't contain a valid redirect.\n" );
return false;
}
- return true;
}
/**
@@ -3867,18 +4136,17 @@ class Title {
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'categorylinks', '*',
- array(
- 'cl_from' => $titleKey,
- ),
- __METHOD__,
- array()
+ $res = $dbr->select(
+ 'categorylinks',
+ 'cl_to',
+ array( 'cl_from' => $titleKey ),
+ __METHOD__
);
- if ( $dbr->numRows( $res ) > 0 ) {
+ if ( $res->numRows() > 0 ) {
foreach ( $res as $row ) {
- // $data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to);
- $data[$wgContLang->getNSText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
+ // $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
+ $data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
}
}
return $data;
@@ -3887,7 +4155,7 @@ class Title {
/**
* Get a tree of parent categories
*
- * @param $children Array with the children in the keys, to check for circular refs
+ * @param array $children with the children in the keys, to check for circular refs
* @return Array Tree of parent categories
*/
public function getParentCategoryTree( $children = array() ) {
@@ -3929,8 +4197,8 @@ class Title {
/**
* Get the revision ID of the previous revision
*
- * @param $revId Int Revision ID. Get the revision that was before this one.
- * @param $flags Int Title::GAID_FOR_UPDATE
+ * @param int $revId Revision ID. Get the revision that was before this one.
+ * @param int $flags Title::GAID_FOR_UPDATE
* @return Int|Bool Old revision ID, or FALSE if none exists
*/
public function getPreviousRevisionID( $revId, $flags = 0 ) {
@@ -3954,8 +4222,8 @@ class Title {
/**
* Get the revision ID of the next revision
*
- * @param $revId Int Revision ID. Get the revision that was after this one.
- * @param $flags Int Title::GAID_FOR_UPDATE
+ * @param int $revId Revision ID. Get the revision that was after this one.
+ * @param int $flags Title::GAID_FOR_UPDATE
* @return Int|Bool Next revision ID, or FALSE if none exists
*/
public function getNextRevisionID( $revId, $flags = 0 ) {
@@ -3979,7 +4247,7 @@ class Title {
/**
* Get the first revision of the page
*
- * @param $flags Int Title::GAID_FOR_UPDATE
+ * @param int $flags Title::GAID_FOR_UPDATE
* @return Revision|Null if page doesn't exist
*/
public function getFirstRevision( $flags = 0 ) {
@@ -4001,7 +4269,7 @@ class Title {
/**
* Get the oldest revision timestamp of this page
*
- * @param $flags Int Title::GAID_FOR_UPDATE
+ * @param int $flags Title::GAID_FOR_UPDATE
* @return String: MW timestamp
*/
public function getEarliestRevTime( $flags = 0 ) {
@@ -4058,8 +4326,8 @@ class Title {
* Get the number of revisions between the given revision.
* Used for diffs and other things that really need it.
*
- * @param $old int|Revision Old revision or rev ID (first before range)
- * @param $new int|Revision New revision or rev ID (first after range)
+ * @param int|Revision $old Old revision or rev ID (first before range)
+ * @param int|Revision $new New revision or rev ID (first after range)
* @return Int Number of revisions between these revisions.
*/
public function countRevisionsBetween( $old, $new ) {
@@ -4087,10 +4355,10 @@ class Title {
* Get the number of authors between the given revisions or revision IDs.
* Used for diffs and other things that really need it.
*
- * @param $old int|Revision Old revision or rev ID (first before range by default)
- * @param $new int|Revision New revision or rev ID (first after range by default)
- * @param $limit int Maximum number of authors
- * @param $options string|array (Optional): Single option, or an array of options:
+ * @param int|Revision $old Old revision or rev ID (first before range by default)
+ * @param int|Revision $new New revision or rev ID (first after range by default)
+ * @param int $limit Maximum number of authors
+ * @param string|array $options (Optional): Single option, or an array of options:
* 'include_old' Include $old in the range; $new is excluded.
* 'include_new' Include $new in the range; $old is excluded.
* 'include_both' Include both $old and $new in the range.
@@ -4112,7 +4380,7 @@ class Title {
}
$old_cmp = '>';
$new_cmp = '<';
- $options = (array) $options;
+ $options = (array)$options;
if ( in_array( 'include_old', $options ) ) {
$old_cmp = '>=';
}
@@ -4126,7 +4394,7 @@ class Title {
// No DB query needed if $old and $new are the same or successive revisions:
if ( $old->getId() === $new->getId() ) {
return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1;
- } else if ( $old->getId() === $new->getParentId() ) {
+ } elseif ( $old->getId() === $new->getParentId() ) {
if ( $old_cmp === '>' || $new_cmp === '<' ) {
return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1;
}
@@ -4202,7 +4470,7 @@ class Title {
$isKnown = null;
/**
- * Allows overriding default behaviour for determining if a page exists.
+ * Allows overriding default behavior for determining if a page exists.
* If $isKnown is kept as null, regular checks happen. If it's
* a boolean, this value is returned by the isKnown method.
*
@@ -4221,7 +4489,7 @@ class Title {
return true; // any interwiki link might be viewable, for all we know
}
- switch( $this->mNamespace ) {
+ switch ( $this->mNamespace ) {
case NS_MEDIA:
case NS_FILE:
// file exists, possibly in a foreign repo
@@ -4271,7 +4539,7 @@ class Title {
// Use always content language to avoid loading hundreds of languages
// to get the link color.
global $wgContLang;
- list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) );
+ list( $name, ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) );
$message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
return $message->exists();
}
@@ -4304,26 +4572,31 @@ class Title {
/**
* Updates page_touched for this page; called from LinksUpdate.php
*
- * @return Bool true if the update succeded
+ * @return Bool true if the update succeeded
*/
public function invalidateCache() {
if ( wfReadOnly() ) {
return false;
}
+
+ $method = __METHOD__;
$dbw = wfGetDB( DB_MASTER );
- $success = $dbw->update(
- 'page',
- array( 'page_touched' => $dbw->timestamp() ),
- $this->pageCond(),
- __METHOD__
- );
- HTMLFileCache::clearFileCache( $this );
- return $success;
+ $conds = $this->pageCond();
+ $dbw->onTransactionIdle( function() use ( $dbw, $conds, $method ) {
+ $dbw->update(
+ 'page',
+ array( 'page_touched' => $dbw->timestamp() ),
+ $conds,
+ $method
+ );
+ } );
+
+ return true;
}
/**
* Update page_touched timestamps and send squid purge messages for
- * pages linking to this title. May be sent to the job queue depending
+ * pages linking to this title. May be sent to the job queue depending
* on the number of links. Typically called on create and delete.
*/
public function touchLinks() {
@@ -4366,7 +4639,7 @@ class Title {
if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
return $this->mNotificationTimestamp[$uid];
}
- if ( !$uid || !$wgShowUpdatedMarker ) {
+ if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) {
return $this->mNotificationTimestamp[$uid] = false;
}
// Don't cache too much!
@@ -4389,14 +4662,14 @@ class Title {
/**
* Generate strings used for xml 'id' names in monobook tabs
*
- * @param $prepend string defaults to 'nstab-'
+ * @param string $prepend defaults to 'nstab-'
* @return String XML 'id' name
*/
public function getNamespaceKey( $prepend = 'nstab-' ) {
global $wgContLang;
// Gets the subject namespace if this title
$namespace = MWNamespace::getSubject( $this->getNamespace() );
- // Checks if cononical namespace name exists for namespace
+ // Checks if canonical namespace name exists for namespace
if ( MWNamespace::exists( $this->getNamespace() ) ) {
// Uses canonical namespace name
$namespaceKey = MWNamespace::getCanonicalName( $namespace );
@@ -4420,7 +4693,7 @@ class Title {
/**
* Get all extant redirects to this Title
*
- * @param $ns Int|Null Single namespace to consider; NULL to consider all namespaces
+ * @param int|Null $ns Single namespace to consider; NULL to consider all namespaces
* @return Array of Title redirects to this title
*/
public function getRedirectsHere( $ns = null ) {
@@ -4462,7 +4735,7 @@ class Title {
public function isValidRedirectTarget() {
global $wgInvalidRedirectTargets;
- // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here
+ // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
if ( $this->isSpecial( 'Userlogout' ) ) {
return false;
}
@@ -4506,7 +4779,7 @@ class Title {
* prefix. This will be fed to Collation::getSortKey() to get a
* binary sortkey that can be used for actual sorting.
*
- * @param $prefix string The prefix to be used, specified using
+ * @param string $prefix The prefix to be used, specified using
* {{defaultsort:}} or like [[Category:Foo|prefix]]. Empty for no
* prefix.
* @return string
@@ -4543,19 +4816,13 @@ class Title {
if ( $this->isSpecialPage() ) {
// special pages are in the user language
return $wgLang;
- } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
- // css/js should always be LTR and is, in fact, English
- return wfGetLangObj( 'en' );
- } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
- // Parse mediawiki messages with correct target language
- list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
- return wfGetLangObj( $lang );
}
- global $wgContLang;
- // If nothing special, it should be in the wiki content language
- $pageLang = $wgContLang;
- // Hook at the end because we don't want to override the above stuff
- wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
+
+ //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request.
+ //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
+ $contentHandler = ContentHandler::getForTitle( $this );
+ $pageLang = $contentHandler->getPageLanguage( $this );
+
return wfGetLangObj( $pageLang );
}
@@ -4568,19 +4835,65 @@ class Title {
* @return Language
*/
public function getPageViewLanguage() {
- $pageLang = $this->getPageLanguage();
- // If this is nothing special (so the content is converted when viewed)
- if ( !$this->isSpecialPage()
- && !$this->isCssOrJsPage() && !$this->isCssJsSubpage()
- && $this->getNamespace() !== NS_MEDIAWIKI
- ) {
+ global $wgLang;
+
+ if ( $this->isSpecialPage() ) {
// If the user chooses a variant, the content is actually
// in a language whose code is the variant code.
- $variant = $pageLang->getPreferredVariant();
- if ( $pageLang->getCode() !== $variant ) {
- $pageLang = Language::factory( $variant );
+ $variant = $wgLang->getPreferredVariant();
+ if ( $wgLang->getCode() !== $variant ) {
+ return Language::factory( $variant );
}
+
+ return $wgLang;
}
+
+ //NOTE: can't be cached persistently, depends on user settings
+ //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language!
+ $contentHandler = ContentHandler::getForTitle( $this );
+ $pageLang = $contentHandler->getPageViewLanguage( $this );
return $pageLang;
}
+
+ /**
+ * Get a list of rendered edit notices for this page.
+ *
+ * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
+ * they will already be wrapped in paragraphs.
+ *
+ * @since 1.21
+ * @param int oldid Revision ID that's being edited
+ * @return Array
+ */
+ public function getEditNotices( $oldid = 0 ) {
+ $notices = array();
+
+ # Optional notices on a per-namespace and per-page basis
+ $editnotice_ns = 'editnotice-' . $this->getNamespace();
+ $editnotice_ns_message = wfMessage( $editnotice_ns );
+ if ( $editnotice_ns_message->exists() ) {
+ $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock();
+ }
+ if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
+ $parts = explode( '/', $this->getDBkey() );
+ $editnotice_base = $editnotice_ns;
+ while ( count( $parts ) > 0 ) {
+ $editnotice_base .= '-' . array_shift( $parts );
+ $editnotice_base_msg = wfMessage( $editnotice_base );
+ if ( $editnotice_base_msg->exists() ) {
+ $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock();
+ }
+ }
+ } else {
+ # Even if there are no subpages in namespace, we still don't want / in MW ns.
+ $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() );
+ $editnoticeMsg = wfMessage( $editnoticeText );
+ if ( $editnoticeMsg->exists() ) {
+ $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock();
+ }
+ }
+
+ wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) );
+ return $notices;
+ }
}
diff --git a/includes/TitleArray.php b/includes/TitleArray.php
index 5cdec16d..90fb861a 100644
--- a/includes/TitleArray.php
+++ b/includes/TitleArray.php
@@ -3,8 +3,8 @@
* Classes to walk into a list of Title objects.
*
* Note: this entire file is a byte-for-byte copy of UserArray.php with
- * s/User/Title/. If anyone can figure out how to do this nicely with inheri-
- * tance or something, please do so.
+ * s/User/Title/. If anyone can figure out how to do this nicely with
+ * inheritance or something, please do so.
*
* 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
diff --git a/includes/UIDGenerator.php b/includes/UIDGenerator.php
new file mode 100644
index 00000000..963e51a4
--- /dev/null
+++ b/includes/UIDGenerator.php
@@ -0,0 +1,337 @@
+<?php
+/**
+ * This file deals with UID generation.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class for getting statistically unique IDs
+ *
+ * @since 1.21
+ */
+class UIDGenerator {
+ /** @var UIDGenerator */
+ protected static $instance = null;
+
+ protected $nodeId32; // string; node ID in binary (32 bits)
+ protected $nodeId48; // string; node ID in binary (48 bits)
+
+ protected $lockFile88; // string; local file path
+ protected $lockFile128; // string; local file path
+
+ /** @var Array */
+ protected $fileHandles = array(); // cache file handles
+
+ const QUICK_RAND = 1; // get randomness from fast and insecure sources
+
+ protected function __construct() {
+ $idFile = wfTempDir() . '/mw-' . __CLASS__ . '-UID-nodeid';
+ $nodeId = is_file( $idFile ) ? file_get_contents( $idFile ) : '';
+ // Try to get some ID that uniquely identifies this machine (RFC 4122)...
+ if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
+ wfSuppressWarnings();
+ if ( wfIsWindows() ) {
+ // http://technet.microsoft.com/en-us/library/bb490913.aspx
+ $csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) );
+ $line = substr( $csv, 0, strcspn( $csv, "\n" ) );
+ $info = str_getcsv( $line );
+ $nodeId = isset( $info[0] ) ? str_replace( '-', '', $info[0] ) : '';
+ } elseif ( is_executable( '/sbin/ifconfig' ) ) { // Linux/BSD/Solaris/OS X
+ // See http://linux.die.net/man/8/ifconfig
+ $m = array();
+ preg_match( '/\s([0-9a-f]{2}(:[0-9a-f]{2}){5})\s/',
+ wfShellExec( '/sbin/ifconfig -a' ), $m );
+ $nodeId = isset( $m[1] ) ? str_replace( ':', '', $m[1] ) : '';
+ }
+ wfRestoreWarnings();
+ if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
+ $nodeId = MWCryptRand::generateHex( 12, true );
+ $nodeId[1] = dechex( hexdec( $nodeId[1] ) | 0x1 ); // set multicast bit
+ }
+ file_put_contents( $idFile, $nodeId ); // cache
+ }
+ $this->nodeId32 = wfBaseConvert( substr( sha1( $nodeId ), 0, 8 ), 16, 2, 32 );
+ $this->nodeId48 = wfBaseConvert( $nodeId, 16, 2, 48 );
+ // If different processes run as different users, they may have different temp dirs.
+ // This is dealt with by initializing the clock sequence number and counters randomly.
+ $this->lockFile88 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-88';
+ $this->lockFile128 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-128';
+ }
+
+ /**
+ * @return UIDGenerator
+ */
+ protected static function singleton() {
+ if ( self::$instance === null ) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Get a statistically unique 88-bit unsigned integer ID string.
+ * The bits of the UID are prefixed with the time (down to the millisecond).
+ *
+ * These IDs are suitable as values for the shard key of distributed data.
+ * If a column uses these as values, it should be declared UNIQUE to handle collisions.
+ * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
+ * They can also be stored "DECIMAL(27) UNSIGNED" or BINARY(11) in MySQL.
+ *
+ * UID generation is serialized on each server (as the node ID is for the whole machine).
+ *
+ * @param $base integer Specifies a base other than 10
+ * @return string Number
+ * @throws MWException
+ */
+ public static function newTimestampedUID88( $base = 10 ) {
+ if ( !is_integer( $base ) || $base > 36 || $base < 2 ) {
+ throw new MWException( "Base must an integer be between 2 and 36" );
+ }
+ $gen = self::singleton();
+ $time = $gen->getTimestampAndDelay( 'lockFile88', 1, 1024 );
+ return wfBaseConvert( $gen->getTimestampedID88( $time ), 2, $base );
+ }
+
+ /**
+ * @param array $time (UIDGenerator::millitime(), clock sequence)
+ * @return string 88 bits
+ */
+ protected function getTimestampedID88( array $info ) {
+ list( $time, $counter ) = $info;
+ // Take the 46 MSBs of "milliseconds since epoch"
+ $id_bin = $this->millisecondsSinceEpochBinary( $time );
+ // Add a 10 bit counter resulting in 56 bits total
+ $id_bin .= str_pad( decbin( $counter ), 10, '0', STR_PAD_LEFT );
+ // Add the 32 bit node ID resulting in 88 bits total
+ $id_bin .= $this->nodeId32;
+ // Convert to a 1-27 digit integer string
+ if ( strlen( $id_bin ) !== 88 ) {
+ throw new MWException( "Detected overflow for millisecond timestamp." );
+ }
+ return $id_bin;
+ }
+
+ /**
+ * Get a statistically unique 128-bit unsigned integer ID string.
+ * The bits of the UID are prefixed with the time (down to the millisecond).
+ *
+ * These IDs are suitable as globally unique IDs, without any enforced uniqueness.
+ * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
+ * They can also be stored as "DECIMAL(39) UNSIGNED" or BINARY(16) in MySQL.
+ *
+ * UID generation is serialized on each server (as the node ID is for the whole machine).
+ *
+ * @param $base integer Specifies a base other than 10
+ * @return string Number
+ * @throws MWException
+ */
+ public static function newTimestampedUID128( $base = 10 ) {
+ if ( !is_integer( $base ) || $base > 36 || $base < 2 ) {
+ throw new MWException( "Base must be an integer between 2 and 36" );
+ }
+ $gen = self::singleton();
+ $time = $gen->getTimestampAndDelay( 'lockFile128', 16384, 1048576 );
+ return wfBaseConvert( $gen->getTimestampedID128( $time ), 2, $base );
+ }
+
+ /**
+ * @param array $info (UIDGenerator::millitime(), counter, clock sequence)
+ * @return string 128 bits
+ */
+ protected function getTimestampedID128( array $info ) {
+ list( $time, $counter, $clkSeq ) = $info;
+ // Take the 46 MSBs of "milliseconds since epoch"
+ $id_bin = $this->millisecondsSinceEpochBinary( $time );
+ // Add a 20 bit counter resulting in 66 bits total
+ $id_bin .= str_pad( decbin( $counter ), 20, '0', STR_PAD_LEFT );
+ // Add a 14 bit clock sequence number resulting in 80 bits total
+ $id_bin .= str_pad( decbin( $clkSeq ), 14, '0', STR_PAD_LEFT );
+ // Add the 48 bit node ID resulting in 128 bits total
+ $id_bin .= $this->nodeId48;
+ // Convert to a 1-39 digit integer string
+ if ( strlen( $id_bin ) !== 128 ) {
+ throw new MWException( "Detected overflow for millisecond timestamp." );
+ }
+ return $id_bin;
+ }
+
+ /**
+ * Return an RFC4122 compliant v4 UUID
+ *
+ * @param $flags integer Bitfield (supports UIDGenerator::QUICK_RAND)
+ * @return string
+ * @throws MWException
+ */
+ public static function newUUIDv4( $flags = 0 ) {
+ $hex = ( $flags & self::QUICK_RAND )
+ ? wfRandomString( 31 )
+ : MWCryptRand::generateHex( 31 );
+
+ return sprintf( '%s-%s-%s-%s-%s',
+ // "time_low" (32 bits)
+ substr( $hex, 0, 8 ),
+ // "time_mid" (16 bits)
+ substr( $hex, 8, 4 ),
+ // "time_hi_and_version" (16 bits)
+ '4' . substr( $hex, 12, 3 ),
+ // "clk_seq_hi_res (8 bits, variant is binary 10x) and "clk_seq_low" (8 bits)
+ dechex( 0x8 | ( hexdec( $hex[15] ) & 0x3 ) ) . $hex[16] . substr( $hex, 17, 2 ),
+ // "node" (48 bits)
+ substr( $hex, 19, 12 )
+ );
+ }
+
+ /**
+ * Return an RFC4122 compliant v4 UUID
+ *
+ * @param $flags integer Bitfield (supports UIDGenerator::QUICK_RAND)
+ * @return string 32 hex characters with no hyphens
+ * @throws MWException
+ */
+ public static function newRawUUIDv4( $flags = 0 ) {
+ return str_replace( '-', '', self::newUUIDv4( $flags ) );
+ }
+
+ /**
+ * Get a (time,counter,clock sequence) where (time,counter) is higher
+ * than any previous (time,counter) value for the given clock sequence.
+ * This is useful for making UIDs sequential on a per-node bases.
+ *
+ * @param string $lockFile Name of a local lock file
+ * @param $clockSeqSize integer The number of possible clock sequence values
+ * @param $counterSize integer The number of possible counter values
+ * @return Array (result of UIDGenerator::millitime(), counter, clock sequence)
+ * @throws MWException
+ */
+ protected function getTimestampAndDelay( $lockFile, $clockSeqSize, $counterSize ) {
+ // Get the UID lock file handle
+ if ( isset( $this->fileHandles[$lockFile] ) ) {
+ $handle = $this->fileHandles[$lockFile];
+ } else {
+ $handle = fopen( $this->$lockFile, 'cb+' );
+ $this->fileHandles[$lockFile] = $handle ?: null; // cache
+ }
+ // Acquire the UID lock file
+ if ( $handle === false ) {
+ throw new MWException( "Could not open '{$this->$lockFile}'." );
+ } elseif ( !flock( $handle, LOCK_EX ) ) {
+ throw new MWException( "Could not acquire '{$this->$lockFile}'." );
+ }
+ // Get the current timestamp, clock sequence number, last time, and counter
+ rewind( $handle );
+ $data = explode( ' ', fgets( $handle ) ); // "<clk seq> <sec> <msec> <counter> <offset>"
+ $clockChanged = false; // clock set back significantly?
+ if ( count( $data ) == 5 ) { // last UID info already initialized
+ $clkSeq = (int)$data[0] % $clockSeqSize;
+ $prevTime = array( (int)$data[1], (int)$data[2] );
+ $offset = (int)$data[4] % $counterSize; // random counter offset
+ $counter = 0; // counter for UIDs with the same timestamp
+ // Delay until the clock reaches the time of the last ID.
+ // This detects any microtime() drift among processes.
+ $time = $this->timeWaitUntil( $prevTime );
+ if ( !$time ) { // too long to delay?
+ $clockChanged = true; // bump clock sequence number
+ $time = self::millitime();
+ } elseif ( $time == $prevTime ) {
+ // Bump the counter if there are timestamp collisions
+ $counter = (int)$data[3] % $counterSize;
+ if ( ++$counter >= $counterSize ) { // sanity (starts at 0)
+ flock( $handle, LOCK_UN ); // abort
+ throw new MWException( "Counter overflow for timestamp value." );
+ }
+ }
+ } else { // last UID info not initialized
+ $clkSeq = mt_rand( 0, $clockSeqSize - 1 );
+ $counter = 0;
+ $offset = mt_rand( 0, $counterSize - 1 );
+ $time = self::millitime();
+ }
+ // microtime() and gettimeofday() can drift from time() at least on Windows.
+ // The drift is immediate for processes running while the system clock changes.
+ // time() does not have this problem. See https://bugs.php.net/bug.php?id=42659.
+ if ( abs( time() - $time[0] ) >= 2 ) {
+ // We don't want processes using too high or low timestamps to avoid duplicate
+ // UIDs and clock sequence number churn. This process should just be restarted.
+ flock( $handle, LOCK_UN ); // abort
+ throw new MWException( "Process clock is outdated or drifted." );
+ }
+ // If microtime() is synced and a clock change was detected, then the clock went back
+ if ( $clockChanged ) {
+ // Bump the clock sequence number and also randomize the counter offset,
+ // which is useful for UIDs that do not include the clock sequence number.
+ $clkSeq = ( $clkSeq + 1 ) % $clockSeqSize;
+ $offset = mt_rand( 0, $counterSize - 1 );
+ trigger_error( "Clock was set back; sequence number incremented." );
+ }
+ // Update the (clock sequence number, timestamp, counter)
+ ftruncate( $handle, 0 );
+ rewind( $handle );
+ fwrite( $handle, "{$clkSeq} {$time[0]} {$time[1]} {$counter} {$offset}" );
+ fflush( $handle );
+ // Release the UID lock file
+ flock( $handle, LOCK_UN );
+
+ return array( $time, ( $counter + $offset ) % $counterSize, $clkSeq );
+ }
+
+ /**
+ * Wait till the current timestamp reaches $time and return the current
+ * timestamp. This returns false if it would have to wait more than 10ms.
+ *
+ * @param array $time Result of UIDGenerator::millitime()
+ * @return Array|bool UIDGenerator::millitime() result or false
+ */
+ protected function timeWaitUntil( array $time ) {
+ do {
+ $ct = self::millitime();
+ if ( $ct >= $time ) { // http://php.net/manual/en/language.operators.comparison.php
+ return $ct; // current timestamp is higher than $time
+ }
+ } while ( ( ( $time[0] - $ct[0] ) * 1000 + ( $time[1] - $ct[1] ) ) <= 10 );
+
+ return false;
+ }
+
+ /**
+ * @param array $time Result of UIDGenerator::millitime()
+ * @return string 46 MSBs of "milliseconds since epoch" in binary (rolls over in 4201)
+ */
+ protected function millisecondsSinceEpochBinary( array $time ) {
+ list( $sec, $msec ) = $time;
+ $ts = 1000 * $sec + $msec;
+ if ( $ts > pow( 2, 52 ) ) {
+ throw new MWException( __METHOD__ .
+ ': sorry, this function doesn\'t work after the year 144680' );
+ }
+ return substr( wfBaseConvert( $ts, 10, 2, 46 ), -46 );
+ }
+
+ /**
+ * @return Array (current time in seconds, milliseconds since then)
+ */
+ protected static function millitime() {
+ list( $msec, $sec ) = explode( ' ', microtime() );
+ return array( (int)$sec, (int)( $msec * 1000 ) );
+ }
+
+ function __destruct() {
+ array_map( 'fclose', $this->fileHandles );
+ }
+}
diff --git a/includes/User.php b/includes/User.php
index 0a3db4c0..12912e1c 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -124,6 +124,12 @@ class User {
'edit',
'editinterface',
'editprotected',
+ 'editmyoptions',
+ 'editmyprivateinfo',
+ 'editmyusercss',
+ 'editmyuserjs',
+ 'editmywatchlist',
+ 'editsemiprotected',
'editusercssjs', #deprecated
'editusercss',
'edituserjs',
@@ -164,6 +170,8 @@ class User {
'upload_by_url',
'userrights',
'userrights-interwiki',
+ 'viewmyprivateinfo',
+ 'viewmywatchlist',
'writeapi',
);
/**
@@ -251,9 +259,9 @@ class User {
}
/**
- * @return String
+ * @return string
*/
- function __toString(){
+ function __toString() {
return $this->getName();
}
@@ -266,7 +274,7 @@ class User {
}
wfProfileIn( __METHOD__ );
- # Set it now to avoid infinite recursion in accessors
+ // Set it now to avoid infinite recursion in accessors
$this->mLoadedItems = true;
switch ( $this->mFrom ) {
@@ -276,7 +284,7 @@ class User {
case 'name':
$this->mId = self::idFromName( $this->mName );
if ( !$this->mId ) {
- # Nonexistent user placeholder object
+ // Nonexistent user placeholder object
$this->loadDefaults( $this->mName );
} else {
$this->loadFromId();
@@ -286,10 +294,14 @@ class User {
$this->loadFromId();
break;
case 'session':
- $this->loadFromSession();
+ if ( !$this->loadFromSession() ) {
+ // Loading from session failed. Load defaults.
+ $this->loadDefaults();
+ }
wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
break;
default:
+ wfProfileOut( __METHOD__ );
throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
}
wfProfileOut( __METHOD__ );
@@ -297,7 +309,7 @@ class User {
/**
* Load user table data, given mId has already been set.
- * @return Bool false if the ID does not exist, true otherwise
+ * @return bool false if the ID does not exist, true otherwise
*/
public function loadFromId() {
global $wgMemc;
@@ -306,29 +318,32 @@ class User {
return false;
}
- # Try cache
+ // Try cache
$key = wfMemcKey( 'user', 'id', $this->mId );
$data = $wgMemc->get( $key );
if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
- # Object is expired, load from DB
+ // Object is expired, load from DB
$data = false;
}
if ( !$data ) {
wfDebug( "User: cache miss for user {$this->mId}\n" );
- # Load from DB
+ // Load from DB
if ( !$this->loadFromDatabase() ) {
- # Can't load from ID, user is anonymous
+ // Can't load from ID, user is anonymous
return false;
}
$this->saveToCache();
} else {
wfDebug( "User: got user {$this->mId} from cache\n" );
- # Restore from cache
+ // Restore from cache
foreach ( self::$mCacheVars as $name ) {
$this->$name = $data[$name];
}
}
+
+ $this->mLoadedItems = true;
+
return true;
}
@@ -362,15 +377,15 @@ class User {
* This is slightly less efficient than newFromId(), so use newFromId() if
* you have both an ID and a name handy.
*
- * @param $name String Username, validated by Title::newFromText()
- * @param $validate String|Bool Validate username. Takes the same parameters as
- * User::getCanonicalName(), except that true is accepted as an alias
- * for 'valid', for BC.
+ * @param string $name Username, validated by Title::newFromText()
+ * @param string|bool $validate Validate username. Takes the same parameters as
+ * User::getCanonicalName(), except that true is accepted as an alias
+ * for 'valid', for BC.
*
- * @return User object, or 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.
+ * @return User|bool 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.
*/
public static function newFromName( $name, $validate = 'valid' ) {
if ( $validate === true ) {
@@ -380,7 +395,7 @@ class User {
if ( $name === false ) {
return false;
} else {
- # Create unloaded user object
+ // Create unloaded user object
$u = new User;
$u->mName = $name;
$u->mFrom = 'name';
@@ -392,7 +407,7 @@ class User {
/**
* Static factory method for creation from a given user ID.
*
- * @param $id Int Valid user ID
+ * @param int $id Valid user ID
* @return User The corresponding User object
*/
public static function newFromId( $id ) {
@@ -410,8 +425,8 @@ class User {
*
* If the code is invalid or has expired, returns NULL.
*
- * @param $code String Confirmation code
- * @return User object, or null
+ * @param string $code Confirmation code
+ * @return User|null
*/
public static function newFromConfirmationCode( $code ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -419,7 +434,7 @@ class User {
'user_email_token' => md5( $code ),
'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
) );
- if( $id !== false ) {
+ if ( $id !== false ) {
return User::newFromId( $id );
} else {
return null;
@@ -430,8 +445,7 @@ class User {
* Create a new user object using data from session or cookies. If the
* login credentials are invalid, the result is an anonymous user.
*
- * @param $request WebRequest object to use; $wgRequest will be used if
- * ommited.
+ * @param WebRequest $request Object to use; $wgRequest will be used if omitted.
* @return User object
*/
public static function newFromSession( WebRequest $request = null ) {
@@ -451,12 +465,13 @@ class User {
* user_name and user_real_name are not provided because the whole row
* will be loaded once more from the database when accessing them.
*
- * @param $row Array A row from the user table
+ * @param array $row A row from the user table
+ * @param array $data Further data to load into the object (see User::loadFromRow for valid keys)
* @return User
*/
- public static function newFromRow( $row ) {
+ public static function newFromRow( $row, $data = null ) {
$user = new User;
- $user->loadFromRow( $row );
+ $user->loadFromRow( $row, $data );
return $user;
}
@@ -464,8 +479,8 @@ class User {
/**
* Get the username corresponding to a given user ID
- * @param $id Int User ID
- * @return String|bool The corresponding username
+ * @param int $id User ID
+ * @return string|bool The corresponding username
*/
public static function whoIs( $id ) {
return UserCache::singleton()->getProp( $id, 'name' );
@@ -474,8 +489,8 @@ class User {
/**
* Get the real name of a user given their user ID
*
- * @param $id Int User ID
- * @return String|bool The corresponding user's real name
+ * @param int $id User ID
+ * @return string|bool The corresponding user's real name
*/
public static function whoIsReal( $id ) {
return UserCache::singleton()->getProp( $id, 'real_name' );
@@ -483,13 +498,13 @@ class User {
/**
* Get database id given a user name
- * @param $name String Username
- * @return Int|Null The corresponding user's ID, or null if user is nonexistent
+ * @param string $name Username
+ * @return int|null The corresponding user's ID, or null if user is nonexistent
*/
public static function idFromName( $name ) {
$nt = Title::makeTitleSafe( NS_USER, $name );
- if( is_null( $nt ) ) {
- # Illegal name
+ if ( is_null( $nt ) ) {
+ // Illegal name
return null;
}
@@ -535,23 +550,23 @@ class User {
* addresses like this, if we allowed accounts like this to be created
* new users could get the old edits of these anonymous users.
*
- * @param $name String to match
- * @return Bool
+ * @param string $name Name to match
+ * @return bool
*/
public static function isIP( $name ) {
- return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
+ return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) || IP::isIPv6( $name );
}
/**
* Is the input a valid username?
*
* Checks if the input is a valid username, we don't want an empty string,
- * an IP address, anything that containins slashes (would mess up subpages),
+ * an IP address, anything that contains slashes (would mess up subpages),
* is longer than the maximum allowed username size or doesn't begin with
* a capital letter.
*
- * @param $name String to match
- * @return Bool
+ * @param string $name Name to match
+ * @return bool
*/
public static function isValidUserName( $name ) {
global $wgContLang, $wgMaxNameChars;
@@ -566,11 +581,10 @@ class User {
return false;
}
-
// Ensure that the name can't be misresolved as a different title,
// such as with extra namespace keys at the start.
$parsed = Title::newFromText( $name );
- if( is_null( $parsed )
+ if ( is_null( $parsed )
|| $parsed->getNamespace()
|| strcmp( $name, $parsed->getPrefixedText() ) ) {
wfDebugLog( 'username', __METHOD__ .
@@ -588,7 +602,7 @@ class User {
'\x{3000}' . # ideographic space
'\x{e000}-\x{f8ff}' . # private use
']/u';
- if( preg_match( $unicodeBlacklist, $name ) ) {
+ if ( preg_match( $unicodeBlacklist, $name ) ) {
wfDebugLog( 'username', __METHOD__ .
": '$name' invalid due to blacklisted characters" );
return false;
@@ -605,8 +619,8 @@ class User {
* If an account already exists in this form, login will be blocked
* by a failure to pass this function.
*
- * @param $name String to match
- * @return Bool
+ * @param string $name Name to match
+ * @return bool
*/
public static function isUsableName( $name ) {
global $wgReservedUsernames;
@@ -642,8 +656,8 @@ class User {
* Additional blacklisting may be added here rather than in
* isValidUserName() to avoid disrupting existing accounts.
*
- * @param $name String to match
- * @return Bool
+ * @param string $name to match
+ * @return bool
*/
public static function isCreatableName( $name ) {
global $wgInvalidUsernameCharacters;
@@ -651,15 +665,15 @@ class User {
// Ensure that the username isn't longer than 235 bytes, so that
// (at least for the builtin skins) user javascript and css files
// will work. (bug 23080)
- if( strlen( $name ) > 235 ) {
+ if ( strlen( $name ) > 235 ) {
wfDebugLog( 'username', __METHOD__ .
": '$name' invalid due to length" );
return false;
}
// Preg yells if you try to give it an empty string
- if( $wgInvalidUsernameCharacters !== '' ) {
- if( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
+ if ( $wgInvalidUsernameCharacters !== '' ) {
+ if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
wfDebugLog( 'username', __METHOD__ .
": '$name' invalid due to wgInvalidUsernameCharacters" );
return false;
@@ -672,8 +686,8 @@ class User {
/**
* Is the input a valid password for this user?
*
- * @param $password String Desired password
- * @return Bool
+ * @param string $password Desired password
+ * @return bool
*/
public function isValidPassword( $password ) {
//simple boolean wrapper for getPasswordValidity
@@ -683,7 +697,7 @@ class User {
/**
* Given unvalidated password input, return error message on failure.
*
- * @param $password String Desired password
+ * @param string $password Desired password
* @return mixed: true on success, string or array of error message on failure
*/
public function getPasswordValidity( $password ) {
@@ -696,15 +710,16 @@ class User {
$result = false; //init $result to false for the internal checks
- if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
+ if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) {
return $result;
+ }
if ( $result === false ) {
- if( strlen( $password ) < $wgMinimalPasswordLength ) {
+ if ( strlen( $password ) < $wgMinimalPasswordLength ) {
return 'passwordtooshort';
} elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
return 'password-name-match';
- } elseif ( isset( $blockedLogins[ $this->getName() ] ) && $password == $blockedLogins[ $this->getName() ] ) {
+ } elseif ( isset( $blockedLogins[$this->getName()] ) && $password == $blockedLogins[$this->getName()] ) {
return 'password-login-forbidden';
} else {
//it seems weird returning true here, but this is because of the
@@ -713,7 +728,7 @@ class User {
//a valid password.
return true;
}
- } elseif( $result === true ) {
+ } elseif ( $result === true ) {
return true;
} else {
return $result; //the isValidPassword hook set a string $result and returned true
@@ -724,7 +739,7 @@ class User {
* Does a string look like an e-mail address?
*
* This validates an email address using an HTML5 specification found at:
- * http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
+ * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
* Which as of 2011-01-24 says:
*
* A valid e-mail address is a string that matches the ABNF production
@@ -743,8 +758,8 @@ class User {
* to be liberal enough for wide use. Some invalid addresses will still
* pass validation here.
*
- * @param $addr String E-mail address
- * @return Bool
+ * @param string $addr E-mail address
+ * @return bool
* @deprecated since 1.18 call Sanitizer::isValidEmail() directly
*/
public static function isValidEmailAddr( $addr ) {
@@ -755,35 +770,37 @@ class User {
/**
* Given unvalidated user input, return a canonical username, or false if
* the username is invalid.
- * @param $name String User input
- * @param $validate String|Bool type of validation to use:
+ * @param string $name User input
+ * @param string|bool $validate type of validation to use:
* - false No validation
* - 'valid' Valid for batch processes
* - 'usable' Valid for batch processes and login
* - 'creatable' Valid for batch processes, login and account creation
*
+ * @throws MWException
* @return bool|string
*/
public static function getCanonicalName( $name, $validate = 'valid' ) {
- # Force usernames to capital
+ // Force usernames to capital
global $wgContLang;
$name = $wgContLang->ucfirst( $name );
# Reject names containing '#'; these will be cleaned up
# with title normalisation, but then it's too late to
# check elsewhere
- if( strpos( $name, '#' ) !== false )
+ if ( strpos( $name, '#' ) !== false ) {
return false;
+ }
- # Clean up name according to title rules
+ // Clean up name according to title rules
$t = ( $validate === 'valid' ) ?
Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
- # Check for invalid titles
- if( is_null( $t ) ) {
+ // Check for invalid titles
+ if ( is_null( $t ) ) {
return false;
}
- # Reject various classes of invalid names
+ // Reject various classes of invalid names
global $wgAuth;
$name = $wgAuth->getCanonicalName( $t->getText() );
@@ -813,45 +830,22 @@ class User {
/**
* Count the number of edits of a user
- * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
*
- * @param $uid Int User ID to check
- * @return Int the user's edit count
+ * @param int $uid User ID to check
+ * @return int The user's edit count
+ *
+ * @deprecated since 1.21 in favour of User::getEditCount
*/
public static function edits( $uid ) {
- wfProfileIn( __METHOD__ );
- $dbr = wfGetDB( DB_SLAVE );
- // check if the user_editcount field has been initialized
- $field = $dbr->selectField(
- 'user', 'user_editcount',
- array( 'user_id' => $uid ),
- __METHOD__
- );
-
- if( $field === null ) { // it has not been initialized. do so.
- $dbw = wfGetDB( DB_MASTER );
- $count = $dbr->selectField(
- 'revision', 'count(*)',
- array( 'rev_user' => $uid ),
- __METHOD__
- );
- $dbw->update(
- 'user',
- array( 'user_editcount' => $count ),
- array( 'user_id' => $uid ),
- __METHOD__
- );
- } else {
- $count = $field;
- }
- wfProfileOut( __METHOD__ );
- return $count;
+ wfDeprecated( __METHOD__, '1.21' );
+ $user = self::newFromId( $uid );
+ return $user->getEditCount();
}
/**
* Return a random password.
*
- * @return String new random password
+ * @return string New random password
*/
public static function randomPassword() {
global $wgMinimalPasswordLength;
@@ -871,7 +865,7 @@ class User {
* @note This no longer clears uncached lazy-initialised properties;
* the constructor does that instead.
*
- * @param $name string
+ * @param $name string|bool
*/
public function loadDefaults( $name = false ) {
wfProfileIn( __METHOD__ );
@@ -886,10 +880,10 @@ class User {
$this->mOptionsLoaded = false;
$loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
- if( $loggedOut !== null ) {
+ if ( $loggedOut !== null ) {
$this->mTouched = wfTimestamp( TS_MW, $loggedOut );
} else {
- $this->mTouched = '0'; # Allow any pages to be cached
+ $this->mTouched = '1'; # Allow any pages to be cached
}
$this->mToken = null; // Don't run cryptographic functions till we need a token
@@ -907,14 +901,14 @@ class User {
/**
* Return whether an item has been loaded.
*
- * @param $item String: item to check. Current possibilities:
+ * @param string $item item to check. Current possibilities:
* - id
* - name
* - realname
- * @param $all String: 'all' to check if the whole object has been loaded
+ * @param string $all 'all' to check if the whole object has been loaded
* or any other string to check if only the item is available (e.g.
* for optimisation)
- * @return Boolean
+ * @return boolean
*/
public function isItemLoaded( $item, $all = 'all' ) {
return ( $this->mLoadedItems === true && $all === 'all' ) ||
@@ -924,36 +918,25 @@ class User {
/**
* Set that an item has been loaded
*
- * @param $item String
+ * @param string $item
*/
- private function setItemLoaded( $item ) {
+ protected function setItemLoaded( $item ) {
if ( is_array( $this->mLoadedItems ) ) {
$this->mLoadedItems[$item] = true;
}
}
/**
- * Load user data from the session or login cookie. If there are no valid
- * credentials, initialises the user as an anonymous user.
- * @return Bool True if the user is logged in, false otherwise.
+ * Load user data from the session or login cookie.
+ * @return bool True if the user is logged in, false otherwise.
*/
private function loadFromSession() {
- global $wgExternalAuthType, $wgAutocreatePolicy;
-
$result = null;
wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
if ( $result !== null ) {
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)
- }
- }
-
$request = $this->getRequest();
$cookieId = $request->getCookie( 'UserID' );
@@ -961,8 +944,7 @@ class User {
if ( $cookieId !== null ) {
$sId = intval( $cookieId );
- if( $sessId !== null && $cookieId != $sessId ) {
- $this->loadDefaults(); // Possible collision!
+ if ( $sessId !== null && $cookieId != $sessId ) {
wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
cookie user ID ($sId) don't match!" );
return false;
@@ -971,7 +953,6 @@ class User {
} elseif ( $sessId !== null && $sessId != 0 ) {
$sId = $sessId;
} else {
- $this->loadDefaults();
return false;
}
@@ -981,33 +962,32 @@ class User {
$sName = $request->getCookie( 'UserName' );
$request->setSessionData( 'wsUserName', $sName );
} else {
- $this->loadDefaults();
return false;
}
$proposedUser = User::newFromId( $sId );
if ( !$proposedUser->isLoggedIn() ) {
- # Not a valid ID
- $this->loadDefaults();
+ // Not a valid ID
return false;
}
global $wgBlockDisablesLogin;
- if( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
- # User blocked and we've disabled blocked user logins
- $this->loadDefaults();
+ if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
+ // User blocked and we've disabled blocked user logins
return false;
}
if ( $request->getSessionData( 'wsToken' ) ) {
- $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' );
+ $passwordCorrect = ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
$from = 'session';
} elseif ( $request->getCookie( 'Token' ) ) {
- $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' );
+ # Get the token from DB/cache and clean it up to remove garbage padding.
+ # This deals with historical problems with bugs and the default column value.
+ $token = rtrim( $proposedUser->getToken( false ) ); // correct token
+ $passwordCorrect = ( strlen( $token ) && $token === $request->getCookie( 'Token' ) );
$from = 'cookie';
} else {
- # No session or persistent login cookie
- $this->loadDefaults();
+ // No session or persistent login cookie
return false;
}
@@ -1017,9 +997,8 @@ class User {
wfDebug( "User: logged in from $from\n" );
return true;
} else {
- # Invalid credentials
+ // Invalid credentials
wfDebug( "User: can't log in from $from, invalid credentials\n" );
- $this->loadDefaults();
return false;
}
}
@@ -1028,14 +1007,14 @@ class User {
* Load user and user_group data from the database.
* $this->mId must be set, this is how the user is identified.
*
- * @return Bool True if the user exists, false if the user is anonymous
+ * @return bool True if the user exists, false if the user is anonymous
*/
public function loadFromDatabase() {
- # Paranoia
+ // Paranoia
$this->mId = intval( $this->mId );
- /** Anonymous user */
- if( !$this->mId ) {
+ // Anonymous user
+ if ( !$this->mId ) {
$this->loadDefaults();
return false;
}
@@ -1046,13 +1025,13 @@ class User {
wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
if ( $s !== false ) {
- # Initialise user table data
+ // Initialise user table data
$this->loadFromRow( $s );
$this->mGroups = null; // deferred
$this->getEditCount(); // revalidation for nulls
return true;
} else {
- # Invalid user_id
+ // Invalid user_id
$this->mId = 0;
$this->loadDefaults();
return false;
@@ -1062,9 +1041,13 @@ class User {
/**
* Initialize this object from a row from the user table.
*
- * @param $row Array Row from the user table to load.
+ * @param array $row Row from the user table to load.
+ * @param array $data Further user data to load into the object
+ *
+ * user_groups Array with groups out of the user_groups table
+ * user_properties Array with properties out of the user_properties table
*/
- public function loadFromRow( $row ) {
+ public function loadFromRow( $row, $data = null ) {
$all = true;
$this->mGroups = null; // deferred
@@ -1122,6 +1105,15 @@ class User {
if ( $all ) {
$this->mLoadedItems = true;
}
+
+ if ( is_array( $data ) ) {
+ if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
+ $this->mGroups = $data['user_groups'];
+ }
+ if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
+ $this->loadOptions( $data['user_properties'] );
+ }
+ }
}
/**
@@ -1163,40 +1155,50 @@ class User {
* will not be re-added automatically. The user will also not lose the
* group if they no longer meet the criteria.
*
- * @param $event String key in $wgAutopromoteOnce (each one has groups/criteria)
+ * @param string $event key in $wgAutopromoteOnce (each one has groups/criteria)
*
* @return array Array of groups the user has been promoted to.
*
* @see $wgAutopromoteOnce
*/
public function addAutopromoteOnceGroups( $event ) {
- global $wgAutopromoteOnceLogInRC;
+ global $wgAutopromoteOnceLogInRC, $wgAuth;
$toPromote = array();
if ( $this->getId() ) {
$toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
if ( count( $toPromote ) ) {
$oldGroups = $this->getGroups(); // previous groups
+
foreach ( $toPromote as $group ) {
$this->addGroup( $group );
}
+ // update groups in external authentication database
+ $wgAuth->updateExternalDBGroups( $this, $toPromote );
+
$newGroups = array_merge( $oldGroups, $toPromote ); // all groups
- $log = new LogPage( 'rights', $wgAutopromoteOnceLogInRC /* in RC? */ );
- $log->addEntry( 'autopromote',
- $this->getUserPage(),
- '', // no comment
- // These group names are "list to texted"-ed in class LogPage.
- array( implode( ', ', $oldGroups ), implode( ', ', $newGroups ) )
- );
+ $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
+ $logEntry->setPerformer( $this );
+ $logEntry->setTarget( $this->getUserPage() );
+ $logEntry->setParameters( array(
+ '4::oldgroups' => $oldGroups,
+ '5::newgroups' => $newGroups,
+ ) );
+ $logid = $logEntry->insert();
+ if ( $wgAutopromoteOnceLogInRC ) {
+ $logEntry->publish( $logid );
+ }
}
}
return $toPromote;
}
/**
- * Clear various cached data stored in this object.
- * @param $reloadFrom bool|String Reload user and user_groups table data from a
+ * Clear various cached data stored in this object. The cache of the user table
+ * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
+ *
+ * @param bool|string $reloadFrom Reload user and user_groups table data from a
* given source. May be "name", "id", "defaults", "session", or false for
* no reload.
*/
@@ -1208,7 +1210,10 @@ class User {
$this->mRights = null;
$this->mEffectiveGroups = null;
$this->mImplicitGroups = null;
+ $this->mGroups = null;
$this->mOptions = null;
+ $this->mOptionsLoaded = false;
+ $this->mEditCount = null;
if ( $reloadFrom ) {
$this->mLoadedItems = array();
@@ -1225,22 +1230,25 @@ class User {
public static function getDefaultOptions() {
global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
+ static $defOpt = null;
+ if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
+ // Disabling this for the unit tests, as they rely on being able to change $wgContLang
+ // mid-request and see that change reflected in the return value of this function.
+ // Which is insane and would never happen during normal MW operation
+ return $defOpt;
+ }
+
$defOpt = $wgDefaultUserOptions;
- # default language setting
- $variant = $wgContLang->getDefaultVariant();
- $defOpt['variant'] = $variant;
- $defOpt['language'] = $variant;
- foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
- $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
+ // Default language setting
+ $defOpt['language'] = $wgContLang->getCode();
+ foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
+ $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
+ }
+ foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
+ $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
}
$defOpt['skin'] = $wgDefaultSkin;
- // FIXME: Ideally we'd cache the results of this function so the hook is only run once,
- // but that breaks the parser tests because they rely on being able to change $wgContLang
- // mid-request and see that change reflected in the return value of this function.
- // Which is insane and would never happen during normal MW operation, but is also not
- // likely to get fixed unless and until we context-ify everything.
- // See also https://www.mediawiki.org/wiki/Special:Code/MediaWiki/101488#c25275
wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
return $defOpt;
@@ -1249,35 +1257,34 @@ class User {
/**
* Get a given default option value.
*
- * @param $opt String Name of option to retrieve
- * @return String Default option value
+ * @param string $opt Name of option to retrieve
+ * @return string Default option value
*/
public static function getDefaultOption( $opt ) {
$defOpts = self::getDefaultOptions();
- if( isset( $defOpts[$opt] ) ) {
+ if ( isset( $defOpts[$opt] ) ) {
return $defOpts[$opt];
} else {
return null;
}
}
-
/**
* Get blocking information
- * @param $bFromSlave Bool Whether to check the slave database first. To
+ * @param bool $bFromSlave Whether to check the slave database first. To
* improve performance, non-critical checks are done
* against slaves. Check when actually saving should be
* done against master.
*/
private function getBlockedStatus( $bFromSlave = true ) {
- global $wgProxyWhitelist, $wgUser;
+ global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
if ( -1 != $this->mBlockedby ) {
return;
}
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__.": checking...\n" );
+ wfDebug( __METHOD__ . ": checking...\n" );
// Initialize data...
// Otherwise something ends up stomping on $this->mBlockedby when
@@ -1295,14 +1302,14 @@ class User {
$ip = null;
}
- # User/IP blocking
+ // User/IP blocking
$block = Block::newFromTarget( $this, $ip, !$bFromSlave );
- # Proxy blocking
+ // Proxy blocking
if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
&& !in_array( $ip, $wgProxyWhitelist ) )
{
- # Local list
+ // Local list
if ( self::isLocallyBlockedProxy( $ip ) ) {
$block = new Block;
$block->setBlocker( wfMessage( 'proxyblocker' )->text() );
@@ -1316,6 +1323,25 @@ class User {
}
}
+ // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled
+ if ( !$block instanceof Block
+ && $wgApplyIpBlocksToXff
+ && $ip !== null
+ && !$this->isAllowed( 'proxyunbannable' )
+ && !in_array( $ip, $wgProxyWhitelist )
+ ) {
+ $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
+ $xff = array_map( 'trim', explode( ',', $xff ) );
+ $xff = array_diff( $xff, array( $ip ) );
+ $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
+ $block = Block::chooseBlock( $xffblocks, $xff );
+ if ( $block instanceof Block ) {
+ # Mangle the reason to alert the user that the block
+ # originated from matching the X-Forwarded-For header.
+ $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
+ }
+ }
+
if ( $block instanceof Block ) {
wfDebug( __METHOD__ . ": Found block.\n" );
$this->mBlock = $block;
@@ -1329,7 +1355,7 @@ class User {
$this->mAllowUsertalk = false;
}
- # Extensions
+ // Extensions
wfRunHooks( 'GetBlockedStatus', array( &$this ) );
wfProfileOut( __METHOD__ );
@@ -1338,19 +1364,21 @@ class User {
/**
* Whether the given IP is in a DNS blacklist.
*
- * @param $ip String IP to check
- * @param $checkWhitelist Bool: whether to check the whitelist first
- * @return Bool True if blacklisted.
+ * @param string $ip IP to check
+ * @param bool $checkWhitelist whether to check the whitelist first
+ * @return bool True if blacklisted.
*/
public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
global $wgEnableSorbs, $wgEnableDnsBlacklist,
$wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
- if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
+ if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) {
return false;
+ }
- if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
+ if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
return false;
+ }
$urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
return $this->inDnsBlacklist( $ip, $urls );
@@ -1359,25 +1387,25 @@ class User {
/**
* Whether the given IP is in a given DNS blacklist.
*
- * @param $ip String IP to check
- * @param $bases String|Array of Strings: URL of the DNS blacklist
- * @return Bool True if blacklisted.
+ * @param string $ip IP to check
+ * @param string|array $bases of Strings: URL of the DNS blacklist
+ * @return bool True if blacklisted.
*/
public function inDnsBlacklist( $ip, $bases ) {
wfProfileIn( __METHOD__ );
$found = false;
// @todo FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
- if( IP::isIPv4( $ip ) ) {
- # Reverse IP, bug 21255
+ if ( IP::isIPv4( $ip ) ) {
+ // Reverse IP, bug 21255
$ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
- foreach( (array)$bases as $base ) {
- # Make hostname
- # If we have an access key, use that too (ProjectHoneypot, etc.)
- if( is_array( $base ) ) {
- if( count( $base ) >= 2 ) {
- # Access key is 1, base URL is 0
+ foreach ( (array)$bases as $base ) {
+ // Make hostname
+ // If we have an access key, use that too (ProjectHoneypot, etc.)
+ if ( is_array( $base ) ) {
+ if ( count( $base ) >= 2 ) {
+ // Access key is 1, base URL is 0
$host = "{$base[1]}.$ipReversed.{$base[0]}";
} else {
$host = "$ipReversed.{$base[0]}";
@@ -1386,10 +1414,10 @@ class User {
$host = "$ipReversed.$base";
}
- # Send query
+ // Send query
$ipList = gethostbynamel( $host );
- if( $ipList ) {
+ if ( $ipList ) {
wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
$found = true;
break;
@@ -1419,7 +1447,7 @@ class User {
wfProfileIn( __METHOD__ );
if ( !is_array( $wgProxyList ) ) {
- # Load from the specified file
+ // Load from the specified file
$wgProxyList = array_map( 'trim', file( $wgProxyList ) );
}
@@ -1428,7 +1456,7 @@ class User {
} elseif ( array_search( $ip, $wgProxyList ) !== false ) {
$ret = true;
} elseif ( array_key_exists( $ip, $wgProxyList ) ) {
- # Old-style flipped proxy list
+ // Old-style flipped proxy list
$ret = true;
} else {
$ret = false;
@@ -1440,17 +1468,17 @@ class User {
/**
* Is this user subject to rate limiting?
*
- * @return Bool True if rate limited
+ * @return bool True if rate limited
*/
public function isPingLimitable() {
global $wgRateLimitsExcludedIPs;
- if( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
+ if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
// No other good way currently to disable rate limits
// for specific IPs. :P
// But this is a crappy hack and should die.
return false;
}
- return !$this->isAllowed('noratelimit');
+ return !$this->isAllowed( 'noratelimit' );
}
/**
@@ -1460,24 +1488,26 @@ class User {
* @note When using a shared cache like memcached, IP-address
* last-hit counters will be shared across wikis.
*
- * @param $action String Action to enforce; 'edit' if unspecified
- * @return Bool True if a rate limiter was tripped
+ * @param string $action Action to enforce; 'edit' if unspecified
+ * @param integer $incrBy Positive amount to increment counter by [defaults to 1]
+ * @return bool True if a rate limiter was tripped
*/
- public function pingLimiter( $action = 'edit' ) {
- # Call the 'PingLimiter' hook
+ public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
+ // Call the 'PingLimiter' hook
$result = false;
- if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
+ if ( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) {
return $result;
}
global $wgRateLimits;
- if( !isset( $wgRateLimits[$action] ) ) {
+ if ( !isset( $wgRateLimits[$action] ) ) {
return false;
}
- # Some groups shouldn't trigger the ping limiter, ever
- if( !$this->isPingLimitable() )
+ // Some groups shouldn't trigger the ping limiter, ever
+ if ( !$this->isPingLimitable() ) {
return false;
+ }
global $wgMemc, $wgRateLimitLog;
wfProfileIn( __METHOD__ );
@@ -1485,27 +1515,37 @@ class User {
$limits = $wgRateLimits[$action];
$keys = array();
$id = $this->getId();
- $ip = $this->getRequest()->getIP();
$userLimit = false;
- if( isset( $limits['anon'] ) && $id == 0 ) {
+ if ( isset( $limits['anon'] ) && $id == 0 ) {
$keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
}
- if( isset( $limits['user'] ) && $id != 0 ) {
+ if ( isset( $limits['user'] ) && $id != 0 ) {
$userLimit = $limits['user'];
}
- if( $this->isNewbie() ) {
- if( isset( $limits['newbie'] ) && $id != 0 ) {
+ if ( $this->isNewbie() ) {
+ if ( isset( $limits['newbie'] ) && $id != 0 ) {
$keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
}
- if( isset( $limits['ip'] ) ) {
+ if ( isset( $limits['ip'] ) ) {
+ $ip = $this->getRequest()->getIP();
$keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
}
- $matches = array();
- if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
- $subnet = $matches[1];
- $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
+ if ( isset( $limits['subnet'] ) ) {
+ $ip = $this->getRequest()->getIP();
+ $matches = array();
+ $subnet = false;
+ if ( IP::isIPv6( $ip ) ) {
+ $parts = IP::parseRange( "$ip/64" );
+ $subnet = $parts[0];
+ } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
+ // IPv4
+ $subnet = $matches[1];
+ }
+ if ( $subnet !== false ) {
+ $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
+ }
}
}
// Check for group-specific permissions
@@ -1519,20 +1559,21 @@ class User {
}
// Set the user limit key
if ( $userLimit !== false ) {
- wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
- $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
+ list( $max, $period ) = $userLimit;
+ wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
+ $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
}
$triggered = false;
- foreach( $keys as $key => $limit ) {
+ foreach ( $keys as $key => $limit ) {
list( $max, $period ) = $limit;
$summary = "(limit $max in {$period}s)";
$count = $wgMemc->get( $key );
// Already pinged?
- if( $count ) {
- if( $count >= $max ) {
+ if ( $count ) {
+ if ( $count >= $max ) {
wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
- if( $wgRateLimitLog ) {
+ if ( $wgRateLimitLog ) {
wfSuppressWarnings();
file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
wfRestoreWarnings();
@@ -1543,9 +1584,13 @@ class User {
}
} else {
wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
- $wgMemc->add( $key, 0, intval( $period ) ); // first ping
+ if ( $incrBy > 0 ) {
+ $wgMemc->add( $key, 0, intval( $period ) ); // first ping
+ }
+ }
+ if ( $incrBy > 0 ) {
+ $wgMemc->incr( $key, $incrBy );
}
- $wgMemc->incr( $key );
}
wfProfileOut( __METHOD__ );
@@ -1555,8 +1600,8 @@ 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
+ * @param bool $bFromSlave Whether to check the slave database instead of the master
+ * @return bool True if blocked, false otherwise
*/
public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
@@ -1565,10 +1610,10 @@ class User {
/**
* Get the block affecting the user, or null if the user is not blocked
*
- * @param $bFromSlave Bool Whether to check the slave database instead of the master
+ * @param bool $bFromSlave Whether to check the slave database instead of the master
* @return Block|null
*/
- public function getBlock( $bFromSlave = true ){
+ public function getBlock( $bFromSlave = true ) {
$this->getBlockedStatus( $bFromSlave );
return $this->mBlock instanceof Block ? $this->mBlock : null;
}
@@ -1576,9 +1621,9 @@ class User {
/**
* Check if user is blocked from editing a particular article
*
- * @param $title Title to check
- * @param $bFromSlave Bool whether to check the slave database instead of the master
- * @return Bool
+ * @param Title $title Title to check
+ * @param bool $bFromSlave whether to check the slave database instead of the master
+ * @return bool
*/
function isBlockedFrom( $title, $bFromSlave = false ) {
global $wgBlockAllowsUTEdit;
@@ -1586,7 +1631,7 @@ class User {
$blocked = $this->isBlocked( $bFromSlave );
$allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
- # If a user's name is suppressed, they cannot make edits anywhere
+ // 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;
@@ -1601,7 +1646,7 @@ class User {
/**
* If user is blocked, return the name of the user who placed the block
- * @return String name of blocker
+ * @return string Name of blocker
*/
public function blockedBy() {
$this->getBlockedStatus();
@@ -1610,7 +1655,7 @@ class User {
/**
* If user is blocked, return the specified reason for the block
- * @return String Blocking reason
+ * @return string Blocking reason
*/
public function blockedFor() {
$this->getBlockedStatus();
@@ -1619,7 +1664,7 @@ class User {
/**
* If user is blocked, return the ID for the block
- * @return Int Block ID
+ * @return int Block ID
*/
public function getBlockId() {
$this->getBlockedStatus();
@@ -1629,19 +1674,19 @@ class User {
/**
* Check if user is blocked on all wikis.
* Do not use for actual edit permission checks!
- * This is intented for quick UI checks.
+ * This is intended for quick UI checks.
*
- * @param $ip String IP address, uses current client if none given
- * @return Bool True if blocked, false otherwise
+ * @param string $ip IP address, uses current client if none given
+ * @return bool True if blocked, false otherwise
*/
public function isBlockedGlobally( $ip = '' ) {
- if( $this->mBlockedGlobally !== null ) {
+ if ( $this->mBlockedGlobally !== null ) {
return $this->mBlockedGlobally;
}
// User is already an IP?
- if( IP::isIPAddress( $this->getName() ) ) {
+ if ( IP::isIPAddress( $this->getName() ) ) {
$ip = $this->getName();
- } elseif( !$ip ) {
+ } elseif ( !$ip ) {
$ip = $this->getRequest()->getIP();
}
$blocked = false;
@@ -1653,13 +1698,14 @@ class User {
/**
* Check if user account is locked
*
- * @return Bool True if locked, false otherwise
+ * @return bool True if locked, false otherwise
*/
public function isLocked() {
- if( $this->mLocked !== null ) {
+ if ( $this->mLocked !== null ) {
return $this->mLocked;
}
global $wgAuth;
+ StubObject::unstub( $wgAuth );
$authUser = $wgAuth->getUserInstance( $this );
$this->mLocked = (bool)$authUser->isLocked();
return $this->mLocked;
@@ -1668,15 +1714,16 @@ class User {
/**
* Check if user account is hidden
*
- * @return Bool True if hidden, false otherwise
+ * @return bool True if hidden, false otherwise
*/
public function isHidden() {
- if( $this->mHideName !== null ) {
+ if ( $this->mHideName !== null ) {
return $this->mHideName;
}
$this->getBlockedStatus();
- if( !$this->mHideName ) {
+ if ( !$this->mHideName ) {
global $wgAuth;
+ StubObject::unstub( $wgAuth );
$authUser = $wgAuth->getUserInstance( $this );
$this->mHideName = (bool)$authUser->isHidden();
}
@@ -1685,14 +1732,13 @@ class User {
/**
* Get the user's ID.
- * @return Int The user's ID; 0 if the user is anonymous or nonexistent
+ * @return int The user's ID; 0 if the user is anonymous or nonexistent
*/
public function getId() {
- if( $this->mId === null && $this->mName !== null
- && User::isIP( $this->mName ) ) {
+ if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) {
// Special case, we know the user is anonymous
return 0;
- } elseif( !$this->isItemLoaded( 'id' ) ) {
+ } elseif ( !$this->isItemLoaded( 'id' ) ) {
// Don't load if this was initialized from an ID
$this->load();
}
@@ -1701,7 +1747,7 @@ class User {
/**
* Set the user and reload all fields according to a given ID
- * @param $v Int User ID to reload
+ * @param int $v User ID to reload
*/
public function setId( $v ) {
$this->mId = $v;
@@ -1710,16 +1756,16 @@ class User {
/**
* Get the user name, or the IP of an anonymous user
- * @return String User's name or IP address
+ * @return string User's name or IP address
*/
public function getName() {
if ( $this->isItemLoaded( 'name', 'only' ) ) {
- # Special case optimisation
+ // Special case optimisation
return $this->mName;
} else {
$this->load();
if ( $this->mName === false ) {
- # Clean up IPs
+ // Clean up IPs
$this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
}
return $this->mName;
@@ -1735,9 +1781,9 @@ class User {
* address for an anonymous user to something other than the current
* remote IP.
*
- * @note User::newFromName() has rougly the same function, when the named user
+ * @note User::newFromName() has roughly the same function, when the named user
* does not exist.
- * @param $str String New user name to set
+ * @param string $str New user name to set
*/
public function setName( $str ) {
$this->load();
@@ -1746,7 +1792,7 @@ class User {
/**
* Get the user's name escaped by underscores.
- * @return String Username escaped by underscores.
+ * @return string Username escaped by underscores.
*/
public function getTitleKey() {
return str_replace( ' ', '_', $this->getName() );
@@ -1754,27 +1800,27 @@ class User {
/**
* Check if the user has new messages.
- * @return Bool True if the user has new messages
+ * @return bool True if the user has new messages
*/
public function getNewtalk() {
$this->load();
- # Load the newtalk status if it is unloaded (mNewtalk=-1)
- if( $this->mNewtalk === -1 ) {
+ // Load the newtalk status if it is unloaded (mNewtalk=-1)
+ if ( $this->mNewtalk === -1 ) {
$this->mNewtalk = false; # reset talk page status
- # Check memcached separately for anons, who have no
- # entire User object stored in there.
- if( !$this->mId ) {
+ // Check memcached separately for anons, who have no
+ // entire User object stored in there.
+ if ( !$this->mId ) {
global $wgDisableAnonTalk;
- if( $wgDisableAnonTalk ) {
+ if ( $wgDisableAnonTalk ) {
// Anon newtalk disabled by configuration.
$this->mNewtalk = false;
} else {
global $wgMemc;
$key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
$newtalk = $wgMemc->get( $key );
- if( strval( $newtalk ) !== '' ) {
+ if ( strval( $newtalk ) !== '' ) {
$this->mNewtalk = (bool)$newtalk;
} else {
// Since we are caching this, make sure it is up to date by getting it
@@ -1792,14 +1838,23 @@ class User {
}
/**
- * Return the talk page(s) this user has new messages on.
- * @return Array of String page URLs
+ * Return the data needed to construct links for new talk page message
+ * alerts. If there are new messages, this will return an associative array
+ * with the following data:
+ * wiki: The database name of the wiki
+ * link: Root-relative link to the user's talk page
+ * rev: The last talk page revision that the user has seen or null. This
+ * is useful for building diff links.
+ * If there are no new messages, it returns an empty array.
+ * @note This function was designed to accomodate multiple talk pages, but
+ * currently only returns a single link and revision.
+ * @return Array
*/
public function getNewMessageLinks() {
$talks = array();
- if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
+ if ( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
return $talks;
- } elseif( !$this->getNewtalk() ) {
+ } elseif ( !$this->getNewtalk() ) {
return array();
}
$utp = $this->getTalkPage();
@@ -1814,13 +1869,36 @@ class User {
}
/**
+ * Get the revision ID for the last talk page revision viewed by the talk
+ * page owner.
+ * @return int|null Revision ID or null
+ */
+ public function getNewMessageRevisionId() {
+ $newMessageRevisionId = null;
+ $newMessageLinks = $this->getNewMessageLinks();
+ if ( $newMessageLinks ) {
+ // Note: getNewMessageLinks() never returns more than a single link
+ // and it is always for the same wiki, but we double-check here in
+ // case that changes some time in the future.
+ if ( count( $newMessageLinks ) === 1
+ && $newMessageLinks[0]['wiki'] === wfWikiID()
+ && $newMessageLinks[0]['rev']
+ ) {
+ $newMessageRevision = $newMessageLinks[0]['rev'];
+ $newMessageRevisionId = $newMessageRevision->getId();
+ }
+ }
+ return $newMessageRevisionId;
+ }
+
+ /**
* Internal uncached check for new messages
*
* @see getNewtalk()
- * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
- * @param $id String|Int User's IP address for anonymous users, User ID otherwise
- * @param $fromMaster Bool true to fetch from the master, false for a slave
- * @return Bool True if the user has new messages
+ * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
+ * @param string|int $id User's IP address for anonymous users, User ID otherwise
+ * @param bool $fromMaster true to fetch from the master, false for a slave
+ * @return bool True if the user has new messages
*/
protected function checkNewtalk( $field, $id, $fromMaster = false ) {
if ( $fromMaster ) {
@@ -1835,10 +1913,10 @@ class User {
/**
* Add or update the new messages flag
- * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
- * @param $id String|Int User's IP address for anonymous users, User ID otherwise
+ * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
+ * @param string|int $id User's IP address for anonymous users, User ID otherwise
* @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null.
- * @return Bool True if successful, false otherwise
+ * @return bool True if successful, false otherwise
*/
protected function updateNewtalk( $field, $id, $curRev = null ) {
// Get timestamp of the talk page revision prior to the current one
@@ -1861,9 +1939,9 @@ class User {
/**
* Clear the new messages flag for the given user
- * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
- * @param $id String|Int User's IP address for anonymous users, User ID otherwise
- * @return Bool True if successful, false otherwise
+ * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
+ * @param string|int $id User's IP address for anonymous users, User ID otherwise
+ * @return bool True if successful, false otherwise
*/
protected function deleteNewtalk( $field, $id ) {
$dbw = wfGetDB( DB_MASTER );
@@ -1881,18 +1959,18 @@ class User {
/**
* Update the 'You have new messages!' status.
- * @param $val Bool Whether the user has new messages
+ * @param bool $val Whether the user has new messages
* @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null or !$val.
*/
public function setNewtalk( $val, $curRev = null ) {
- if( wfReadOnly() ) {
+ if ( wfReadOnly() ) {
return;
}
$this->load();
$this->mNewtalk = $val;
- if( $this->isAnon() ) {
+ if ( $this->isAnon() ) {
$field = 'user_ip';
$id = $this->getName();
} else {
@@ -1901,13 +1979,13 @@ class User {
}
global $wgMemc;
- if( $val ) {
+ if ( $val ) {
$changed = $this->updateNewtalk( $field, $id, $curRev );
} else {
$changed = $this->deleteNewtalk( $field, $id );
}
- if( $this->isAnon() ) {
+ if ( $this->isAnon() ) {
// Anons have a separate memcached space, since
// user records aren't kept for them.
$key = wfMemcKey( 'newtalk', 'ip', $id );
@@ -1921,7 +1999,7 @@ class User {
/**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
- * @return String Timestamp in TS_MW format
+ * @return string Timestamp in TS_MW format
*/
private static function newTouchedTimestamp() {
global $wgClockSkewFudge;
@@ -1937,7 +2015,7 @@ class User {
*/
private function clearSharedCache() {
$this->load();
- if( $this->mId ) {
+ if ( $this->mId ) {
global $wgMemc;
$wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
}
@@ -1949,36 +2027,37 @@ class User {
* for reload on the next hit.
*/
public function invalidateCache() {
- if( wfReadOnly() ) {
+ if ( wfReadOnly() ) {
return;
}
$this->load();
- if( $this->mId ) {
+ if ( $this->mId ) {
$this->mTouched = self::newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
-
- // Prevent contention slams by checking user_touched first
- $now = $dbw->timestamp( $this->mTouched );
- $needsPurge = $dbw->selectField( 'user', '1',
- array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) )
- );
- if ( $needsPurge ) {
- $dbw->update( 'user',
- array( 'user_touched' => $now ),
- array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ),
- __METHOD__
- );
- }
-
+ $userid = $this->mId;
+ $touched = $this->mTouched;
+ $method = __METHOD__;
+ $dbw->onTransactionIdle( function() use ( $dbw, $userid, $touched, $method ) {
+ // Prevent contention slams by checking user_touched first
+ $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) );
+ $needsPurge = $dbw->selectField( 'user', '1',
+ array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) );
+ if ( $needsPurge ) {
+ $dbw->update( 'user',
+ array( 'user_touched' => $dbw->timestamp( $touched ) ),
+ array( 'user_id' => $userid, 'user_touched < ' . $encTouched ),
+ $method
+ );
+ }
+ } );
$this->clearSharedCache();
}
}
/**
* Validate the cache for this account.
- * @param $timestamp String A timestamp in TS_MW format
- *
+ * @param string $timestamp A timestamp in TS_MW format
* @return bool
*/
public function validateCache( $timestamp ) {
@@ -1988,7 +2067,7 @@ class User {
/**
* Get the user touched timestamp
- * @return String timestamp
+ * @return string timestamp
*/
public function getTouched() {
$this->load();
@@ -2006,7 +2085,7 @@ class User {
* wipes it, so the account cannot be logged in until
* a new password is set, for instance via e-mail.
*
- * @param $str String New password to set
+ * @param string $str New password to set
* @throws PasswordError on failure
*
* @return bool
@@ -2014,12 +2093,12 @@ class User {
public function setPassword( $str ) {
global $wgAuth;
- if( $str !== null ) {
- if( !$wgAuth->allowPasswordChange() ) {
+ if ( $str !== null ) {
+ if ( !$wgAuth->allowPasswordChange() ) {
throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
}
- if( !$this->isValidPassword( $str ) ) {
+ if ( !$this->isValidPassword( $str ) ) {
global $wgMinimalPasswordLength;
$valid = $this->getPasswordValidity( $str );
if ( is_array( $valid ) ) {
@@ -2033,7 +2112,7 @@ class User {
}
}
- if( !$wgAuth->setPassword( $this, $str ) ) {
+ if ( !$wgAuth->setPassword( $this, $str ) ) {
throw new PasswordError( wfMessage( 'externaldberror' )->text() );
}
@@ -2045,13 +2124,15 @@ class User {
/**
* Set the password and reset the random token unconditionally.
*
- * @param $str String New password to set
+ * @param string|null $str New password to set or null to set an invalid
+ * password hash meaning that the user will not be able to log in
+ * through the web interface.
*/
public function setInternalPassword( $str ) {
$this->load();
$this->setToken();
- if( $str === null ) {
+ if ( $str === null ) {
// Save an invalid hash...
$this->mPassword = '';
} else {
@@ -2063,8 +2144,8 @@ class User {
/**
* Get the user's current token.
- * @param $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
- * @return String Token
+ * @param bool $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
+ * @return string Token
*/
public function getToken( $forceCreation = true ) {
$this->load();
@@ -2078,7 +2159,7 @@ class User {
* Set the random token (used for persistent authentication)
* Called from loadDefaults() among other places.
*
- * @param $token String|bool If specified, set the token to this value
+ * @param string|bool $token If specified, set the token to this value
*/
public function setToken( $token = false ) {
$this->load();
@@ -2092,8 +2173,8 @@ class User {
/**
* Set the password for a password reminder or new account email
*
- * @param $str String New password to set
- * @param $throttle Bool If true, reset the throttle timestamp to the present
+ * @param string $str New password to set
+ * @param bool $throttle If true, reset the throttle timestamp to the present
*/
public function setNewpassword( $str, $throttle = true ) {
$this->load();
@@ -2106,7 +2187,7 @@ class User {
/**
* Has password reminder email been sent within the last
* $wgPasswordReminderResendTime hours?
- * @return Bool
+ * @return bool
*/
public function isPasswordReminderThrottled() {
global $wgPasswordReminderResendTime;
@@ -2120,7 +2201,7 @@ class User {
/**
* Get the user's e-mail address
- * @return String User's email address
+ * @return string User's email address
*/
public function getEmail() {
$this->load();
@@ -2130,7 +2211,7 @@ class User {
/**
* Get the timestamp of the user's e-mail authentication
- * @return String TS_MW timestamp
+ * @return string TS_MW timestamp
*/
public function getEmailAuthenticationTimestamp() {
$this->load();
@@ -2140,11 +2221,11 @@ class User {
/**
* Set the user's e-mail address
- * @param $str String New e-mail address
+ * @param string $str New e-mail address
*/
public function setEmail( $str ) {
$this->load();
- if( $str == $this->mEmail ) {
+ if ( $str == $this->mEmail ) {
return;
}
$this->mEmail = $str;
@@ -2156,7 +2237,7 @@ class User {
* Set the user's e-mail address and a confirmation mail if needed.
*
* @since 1.20
- * @param $str String New e-mail address
+ * @param string $str New e-mail address
* @return Status
*/
public function setEmailWithConfirmation( $str ) {
@@ -2174,11 +2255,11 @@ class User {
$this->setEmail( $str );
if ( $str !== '' && $wgEmailAuthentication ) {
- # Send a confirmation request to the new address if needed
+ // Send a confirmation request to the new address if needed
$type = $oldaddr != '' ? 'changed' : 'set';
$result = $this->sendConfirmationMail( $type );
if ( $result->isGood() ) {
- # Say the the caller that a confirmation mail has been sent
+ // Say the the caller that a confirmation mail has been sent
$result->value = 'eauth';
}
} else {
@@ -2190,7 +2271,7 @@ class User {
/**
* Get the user's real name
- * @return String User's real name
+ * @return string User's real name
*/
public function getRealName() {
if ( !$this->isItemLoaded( 'realname' ) ) {
@@ -2202,7 +2283,7 @@ class User {
/**
* Set the user's real name
- * @param $str String New real name
+ * @param string $str New real name
*/
public function setRealName( $str ) {
$this->load();
@@ -2212,10 +2293,10 @@ class User {
/**
* Get the user's current setting for a given option.
*
- * @param $oname String The option to check
- * @param $defaultOverride String A default value returned if the option does not exist
- * @param $ignoreHidden Bool = whether to ignore the effects of $wgHiddenPrefs
- * @return String User's current value for the option
+ * @param string $oname The option to check
+ * @param string $defaultOverride A default value returned if the option does not exist
+ * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
+ * @return string User's current value for the option
* @see getBoolOption()
* @see getIntOption()
*/
@@ -2223,19 +2304,12 @@ class User {
global $wgHiddenPrefs;
$this->loadOptions();
- if ( is_null( $this->mOptions ) ) {
- if($defaultOverride != '') {
- return $defaultOverride;
- }
- $this->mOptions = User::getDefaultOptions();
- }
-
# We want 'disabled' preferences to always behave as the default value for
# users, even if they have set the option explicitly in their settings (ie they
# set it, and then it was disabled removing their ability to change it). But
# we don't want to erase the preferences in the database in case the preference
# is re-enabled again. So don't touch $mOptions, just override the returned value
- if( in_array( $oname, $wgHiddenPrefs ) && !$ignoreHidden ){
+ if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
return self::getDefaultOption( $oname );
}
@@ -2261,9 +2335,9 @@ class User {
# set it, and then it was disabled removing their ability to change it). But
# we don't want to erase the preferences in the database in case the preference
# is re-enabled again. So don't touch $mOptions, just override the returned value
- foreach( $wgHiddenPrefs as $pref ){
+ foreach ( $wgHiddenPrefs as $pref ) {
$default = self::getDefaultOption( $pref );
- if( $default !== null ){
+ if ( $default !== null ) {
$options[$pref] = $default;
}
}
@@ -2274,8 +2348,8 @@ class User {
/**
* Get the user's current setting for a given option, as a boolean value.
*
- * @param $oname String The option to check
- * @return Bool User's current value for the option
+ * @param string $oname The option to check
+ * @return bool User's current value for the option
* @see getOption()
*/
public function getBoolOption( $oname ) {
@@ -2283,16 +2357,16 @@ class User {
}
/**
- * Get the user's current setting for a given option, as a boolean value.
+ * Get the user's current setting for a given option, as an integer value.
*
- * @param $oname String The option to check
- * @param $defaultOverride Int A default value returned if the option does not exist
- * @return Int User's current value for the option
+ * @param string $oname The option to check
+ * @param int $defaultOverride A default value returned if the option does not exist
+ * @return int User's current value for the option
* @see getOption()
*/
- public function getIntOption( $oname, $defaultOverride=0 ) {
+ public function getIntOption( $oname, $defaultOverride = 0 ) {
$val = $this->getOption( $oname );
- if( $val == '' ) {
+ if ( $val == '' ) {
$val = $defaultOverride;
}
return intval( $val );
@@ -2301,37 +2375,222 @@ class User {
/**
* Set the given option for a user.
*
- * @param $oname String The option to set
- * @param $val mixed New value to set
+ * @param string $oname The option to set
+ * @param mixed $val New value to set
*/
public function setOption( $oname, $val ) {
- $this->load();
$this->loadOptions();
// Explicitly NULL values should refer to defaults
- if( is_null( $val ) ) {
- $defaultOption = self::getDefaultOption( $oname );
- if( !is_null( $defaultOption ) ) {
- $val = $defaultOption;
- }
+ if ( is_null( $val ) ) {
+ $val = self::getDefaultOption( $oname );
}
$this->mOptions[$oname] = $val;
}
/**
- * Reset all options to the site defaults
+ * Get a token stored in the preferences (like the watchlist one),
+ * resetting it if it's empty (and saving changes).
+ *
+ * @param string $oname The option name to retrieve the token from
+ * @return string|bool User's current value for the option, or false if this option is disabled.
+ * @see resetTokenFromOption()
+ * @see getOption()
*/
- public function resetOptions() {
+ public function getTokenFromOption( $oname ) {
+ global $wgHiddenPrefs;
+ if ( in_array( $oname, $wgHiddenPrefs ) ) {
+ return false;
+ }
+
+ $token = $this->getOption( $oname );
+ if ( !$token ) {
+ $token = $this->resetTokenFromOption( $oname );
+ $this->saveSettings();
+ }
+ return $token;
+ }
+
+ /**
+ * Reset a token stored in the preferences (like the watchlist one).
+ * *Does not* save user's preferences (similarly to setOption()).
+ *
+ * @param string $oname The option name to reset the token in
+ * @return string|bool New token value, or false if this option is disabled.
+ * @see getTokenFromOption()
+ * @see setOption()
+ */
+ public function resetTokenFromOption( $oname ) {
+ global $wgHiddenPrefs;
+ if ( in_array( $oname, $wgHiddenPrefs ) ) {
+ return false;
+ }
+
+ $token = MWCryptRand::generateHex( 40 );
+ $this->setOption( $oname, $token );
+ return $token;
+ }
+
+ /**
+ * Return a list of the types of user options currently returned by
+ * User::getOptionKinds().
+ *
+ * Currently, the option kinds are:
+ * - 'registered' - preferences which are registered in core MediaWiki or
+ * by extensions using the UserGetDefaultOptions hook.
+ * - 'registered-multiselect' - as above, using the 'multiselect' type.
+ * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
+ * - 'userjs' - preferences with names starting with 'userjs-', intended to
+ * be used by user scripts.
+ * - 'unused' - preferences about which MediaWiki doesn't know anything.
+ * These are usually legacy options, removed in newer versions.
+ *
+ * The API (and possibly others) use this function to determine the possible
+ * option types for validation purposes, so make sure to update this when a
+ * new option kind is added.
+ *
+ * @see User::getOptionKinds
+ * @return array Option kinds
+ */
+ public static function listOptionKinds() {
+ return array(
+ 'registered',
+ 'registered-multiselect',
+ 'registered-checkmatrix',
+ 'userjs',
+ 'unused'
+ );
+ }
+
+ /**
+ * Return an associative array mapping preferences keys to the kind of a preference they're
+ * used for. Different kinds are handled differently when setting or reading preferences.
+ *
+ * See User::listOptionKinds for the list of valid option types that can be provided.
+ *
+ * @see User::listOptionKinds
+ * @param $context IContextSource
+ * @param array $options assoc. array with options keys to check as keys. Defaults to $this->mOptions.
+ * @return array the key => kind mapping data
+ */
+ public function getOptionKinds( IContextSource $context, $options = null ) {
+ $this->loadOptions();
+ if ( $options === null ) {
+ $options = $this->mOptions;
+ }
+
+ $prefs = Preferences::getPreferences( $this, $context );
+ $mapping = array();
+
+ // Multiselect and checkmatrix options are stored in the database with
+ // one key per option, each having a boolean value. Extract those keys.
+ $multiselectOptions = array();
+ foreach ( $prefs as $name => $info ) {
+ if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
+ ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
+ $opts = HTMLFormField::flattenOptions( $info['options'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+
+ foreach ( $opts as $value ) {
+ $multiselectOptions["$prefix$value"] = true;
+ }
+
+ unset( $prefs[$name] );
+ }
+ }
+ $checkmatrixOptions = array();
+ foreach ( $prefs as $name => $info ) {
+ if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
+ ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
+ $columns = HTMLFormField::flattenOptions( $info['columns'] );
+ $rows = HTMLFormField::flattenOptions( $info['rows'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+
+ foreach ( $columns as $column ) {
+ foreach ( $rows as $row ) {
+ $checkmatrixOptions["$prefix-$column-$row"] = true;
+ }
+ }
+
+ unset( $prefs[$name] );
+ }
+ }
+
+ // $value is ignored
+ foreach ( $options as $key => $value ) {
+ if ( isset( $prefs[$key] ) ) {
+ $mapping[$key] = 'registered';
+ } elseif ( isset( $multiselectOptions[$key] ) ) {
+ $mapping[$key] = 'registered-multiselect';
+ } elseif ( isset( $checkmatrixOptions[$key] ) ) {
+ $mapping[$key] = 'registered-checkmatrix';
+ } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
+ $mapping[$key] = 'userjs';
+ } else {
+ $mapping[$key] = 'unused';
+ }
+ }
+
+ return $mapping;
+ }
+
+ /**
+ * Reset certain (or all) options to the site defaults
+ *
+ * The optional parameter determines which kinds of preferences will be reset.
+ * Supported values are everything that can be reported by getOptionKinds()
+ * and 'all', which forces a reset of *all* preferences and overrides everything else.
+ *
+ * @param array|string $resetKinds which kinds of preferences to reset. Defaults to
+ * array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
+ * for backwards-compatibility.
+ * @param $context IContextSource|null context source used when $resetKinds
+ * does not contain 'all', passed to getOptionKinds().
+ * Defaults to RequestContext::getMain() when null.
+ */
+ public function resetOptions(
+ $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
+ IContextSource $context = null
+ ) {
$this->load();
+ $defaultOptions = self::getDefaultOptions();
- $this->mOptions = self::getDefaultOptions();
+ if ( !is_array( $resetKinds ) ) {
+ $resetKinds = array( $resetKinds );
+ }
+
+ if ( in_array( 'all', $resetKinds ) ) {
+ $newOptions = $defaultOptions;
+ } else {
+ if ( $context === null ) {
+ $context = RequestContext::getMain();
+ }
+
+ $optionKinds = $this->getOptionKinds( $context );
+ $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
+ $newOptions = array();
+
+ // Use default values for the options that should be deleted, and
+ // copy old values for the ones that shouldn't.
+ foreach ( $this->mOptions as $key => $value ) {
+ if ( in_array( $optionKinds[$key], $resetKinds ) ) {
+ if ( array_key_exists( $key, $defaultOptions ) ) {
+ $newOptions[$key] = $defaultOptions[$key];
+ }
+ } else {
+ $newOptions[$key] = $value;
+ }
+ }
+ }
+
+ $this->mOptions = $newOptions;
$this->mOptionsLoaded = true;
}
/**
* Get the user's preferred date format.
- * @return String User's preferred date format
+ * @return string User's preferred date format
*/
public function getDatePreference() {
// Important migration for old data rows
@@ -2348,16 +2607,36 @@ class User {
}
/**
+ * Determine based on the wiki configuration and the user's options,
+ * whether this user must be over HTTPS no matter what.
+ *
+ * @return bool
+ */
+ public function requiresHTTPS() {
+ global $wgSecureLogin;
+ if ( !$wgSecureLogin ) {
+ return false;
+ } else {
+ $https = $this->getBoolOption( 'prefershttps' );
+ wfRunHooks( 'UserRequiresHTTPS', array( $this, &$https ) );
+ if ( $https ) {
+ $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
+ }
+ return $https;
+ }
+ }
+
+ /**
* Get the user preferred stub threshold
*
* @return int
*/
public function getStubThreshold() {
global $wgMaxArticleSize; # Maximum article size, in Kb
- $threshold = intval( $this->getOption( 'stubthreshold' ) );
+ $threshold = $this->getIntOption( 'stubthreshold' );
if ( $threshold > $wgMaxArticleSize * 1024 ) {
- # If they have set an impossible value, disable the preference
- # so we can use the parser cache again.
+ // If they have set an impossible value, disable the preference
+ // so we can use the parser cache again.
$threshold = 0;
}
return $threshold;
@@ -2372,7 +2651,7 @@ class User {
$this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
// Force reindexation of rights when a hook has unset one of them
- $this->mRights = array_values( $this->mRights );
+ $this->mRights = array_values( array_unique( $this->mRights ) );
}
return $this->mRights;
}
@@ -2392,7 +2671,7 @@ class User {
* Get the list of implicit group memberships this user has.
* This includes all explicit groups, plus 'user' if logged in,
* '*' for all accounts, and autopromoted groups
- * @param $recache Bool Whether to avoid the cache
+ * @param bool $recache Whether to avoid the cache
* @return Array of String internal group names
*/
public function getEffectiveGroups( $recache = false ) {
@@ -2402,8 +2681,10 @@ class User {
$this->getGroups(), // explicit groups
$this->getAutomaticGroups( $recache ) // implicit groups
) );
- # Hook for additional groups
+ // Hook for additional groups
wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
+ // Force reindexation of groups when a hook has unset one of them
+ $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
wfProfileOut( __METHOD__ );
}
return $this->mEffectiveGroups;
@@ -2413,7 +2694,7 @@ class User {
* Get the list of implicit group memberships this user has.
* This includes 'user' if logged in, '*' for all accounts,
* and autopromoted groups
- * @param $recache Bool Whether to avoid the cache
+ * @param bool $recache Whether to avoid the cache
* @return Array of String internal group names
*/
public function getAutomaticGroups( $recache = false ) {
@@ -2429,8 +2710,8 @@ class User {
) );
}
if ( $recache ) {
- # Assure data consistency with rights/groups,
- # as getEffectiveGroups() depends on this function
+ // Assure data consistency with rights/groups,
+ // as getEffectiveGroups() depends on this function
$this->mEffectiveGroups = null;
}
wfProfileOut( __METHOD__ );
@@ -2448,14 +2729,14 @@ class User {
* @return array Names of the groups the user has belonged to.
*/
public function getFormerGroups() {
- if( is_null( $this->mFormerGroups ) ) {
+ if ( is_null( $this->mFormerGroups ) ) {
$dbr = wfGetDB( DB_MASTER );
$res = $dbr->select( 'user_former_groups',
array( 'ufg_group' ),
array( 'ufg_user' => $this->mId ),
__METHOD__ );
$this->mFormerGroups = array();
- foreach( $res as $row ) {
+ foreach ( $res as $row ) {
$this->mFormerGroups[] = $row->ufg_group;
}
}
@@ -2464,33 +2745,46 @@ class User {
/**
* Get the user's edit count.
- * @return Int
+ * @return int, null for anonymous users
*/
public function getEditCount() {
- if( $this->getId() ) {
- if ( !isset( $this->mEditCount ) ) {
- /* Populate the count, if it has not been populated yet */
- $this->mEditCount = User::edits( $this->mId );
- }
- return $this->mEditCount;
- } else {
- /* nil */
+ if ( !$this->getId() ) {
return null;
}
+
+ if ( !isset( $this->mEditCount ) ) {
+ /* Populate the count, if it has not been populated yet */
+ wfProfileIn( __METHOD__ );
+ $dbr = wfGetDB( DB_SLAVE );
+ // check if the user_editcount field has been initialized
+ $count = $dbr->selectField(
+ 'user', 'user_editcount',
+ array( 'user_id' => $this->mId ),
+ __METHOD__
+ );
+
+ if ( $count === null ) {
+ // it has not been initialized. do so.
+ $count = $this->initEditCount();
+ }
+ $this->mEditCount = $count;
+ wfProfileOut( __METHOD__ );
+ }
+ return (int)$this->mEditCount;
}
/**
* Add the user to the given group.
* This takes immediate effect.
- * @param $group String Name of the group to add
+ * @param string $group Name of the group to add
*/
public function addGroup( $group ) {
- if( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
+ if ( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
$dbw = wfGetDB( DB_MASTER );
- if( $this->getId() ) {
+ if ( $this->getId() ) {
$dbw->insert( 'user_groups',
array(
- 'ug_user' => $this->getID(),
+ 'ug_user' => $this->getID(),
'ug_group' => $group,
),
__METHOD__,
@@ -2499,7 +2793,14 @@ class User {
}
$this->loadGroups();
$this->mGroups[] = $group;
- $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
+ // In case loadGroups was not called before, we now have the right twice.
+ // Get rid of the duplicate.
+ $this->mGroups = array_unique( $this->mGroups );
+
+ // Refresh the groups caches, and clear the rights cache so it will be
+ // refreshed on the next call to $this->getRights().
+ $this->getEffectiveGroups( true );
+ $this->mRights = null;
$this->invalidateCache();
}
@@ -2507,21 +2808,21 @@ class User {
/**
* Remove the user from the given group.
* This takes immediate effect.
- * @param $group String Name of the group to remove
+ * @param string $group Name of the group to remove
*/
public function removeGroup( $group ) {
$this->load();
- if( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
+ if ( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'user_groups',
array(
- 'ug_user' => $this->getID(),
+ 'ug_user' => $this->getID(),
'ug_group' => $group,
), __METHOD__ );
// Remember that the user was in this group
$dbw->insert( 'user_former_groups',
array(
- 'ufg_user' => $this->getID(),
+ 'ufg_user' => $this->getID(),
'ufg_group' => $group,
),
__METHOD__,
@@ -2529,14 +2830,18 @@ class User {
}
$this->loadGroups();
$this->mGroups = array_diff( $this->mGroups, array( $group ) );
- $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
+
+ // Refresh the groups caches, and clear the rights cache so it will be
+ // refreshed on the next call to $this->getRights().
+ $this->getEffectiveGroups( true );
+ $this->mRights = null;
$this->invalidateCache();
}
/**
* Get whether the user is logged in
- * @return Bool
+ * @return bool
*/
public function isLoggedIn() {
return $this->getID() != 0;
@@ -2544,7 +2849,7 @@ class User {
/**
* Get whether the user is anonymous
- * @return Bool
+ * @return bool
*/
public function isAnon() {
return !$this->isLoggedIn();
@@ -2554,14 +2859,14 @@ class User {
* Check if user is allowed to access a feature / make an action
*
* @internal param \String $varargs permissions to test
- * @return Boolean: True if user is allowed to perform *any* of the given actions
+ * @return boolean: True if user is allowed to perform *any* of the given actions
*
* @return bool
*/
- public function isAllowedAny( /*...*/ ){
+ public function isAllowedAny( /*...*/ ) {
$permissions = func_get_args();
- foreach( $permissions as $permission ){
- if( $this->isAllowed( $permission ) ){
+ foreach ( $permissions as $permission ) {
+ if ( $this->isAllowed( $permission ) ) {
return true;
}
}
@@ -2573,10 +2878,10 @@ class User {
* @internal param $varargs string
* @return bool True if the user is allowed to perform *all* of the given actions
*/
- public function isAllowedAll( /*...*/ ){
+ public function isAllowedAll( /*...*/ ) {
$permissions = func_get_args();
- foreach( $permissions as $permission ){
- if( !$this->isAllowed( $permission ) ){
+ foreach ( $permissions as $permission ) {
+ if ( !$this->isAllowed( $permission ) ) {
return false;
}
}
@@ -2585,27 +2890,28 @@ class User {
/**
* Internal mechanics of testing a permission
- * @param $action String
+ * @param string $action
* @return bool
*/
public function isAllowed( $action = '' ) {
if ( $action === '' ) {
return true; // In the spirit of DWIM
}
- # Patrolling may not be enabled
- if( $action === 'patrol' || $action === 'autopatrol' ) {
+ // Patrolling may not be enabled
+ if ( $action === 'patrol' || $action === 'autopatrol' ) {
global $wgUseRCPatrol, $wgUseNPPatrol;
- if( !$wgUseRCPatrol && !$wgUseNPPatrol )
+ if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
return false;
+ }
}
- # Use strict parameter to avoid matching numeric 0 accidentally inserted
- # by misconfiguration: 0 == 'foo'
+ // 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 Boolean: True or false
+ * @return boolean: True or false
*/
public function useRCPatrol() {
global $wgUseRCPatrol;
@@ -2614,11 +2920,14 @@ class User {
/**
* Check whether to enable new pages patrol features for this user
- * @return Bool True or false
+ * @return bool True or false
*/
public function useNPPatrol() {
global $wgUseRCPatrol, $wgUseNPPatrol;
- return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) );
+ return (
+ ( $wgUseRCPatrol || $wgUseNPPatrol )
+ && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
+ );
}
/**
@@ -2649,11 +2958,14 @@ class User {
/**
* Get a WatchedItem for this user and $title.
*
+ * @since 1.22 $checkRights parameter added
* @param $title Title
+ * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
* @return WatchedItem
*/
- public function getWatchedItem( $title ) {
- $key = $title->getNamespace() . ':' . $title->getDBkey();
+ public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+ $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
if ( isset( $this->mWatchedItems[$key] ) ) {
return $this->mWatchedItems[$key];
@@ -2663,34 +2975,43 @@ class User {
$this->mWatchedItems = array();
}
- $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title );
+ $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
return $this->mWatchedItems[$key];
}
/**
* Check the watched status of an article.
+ * @since 1.22 $checkRights parameter added
* @param $title Title of the article to look at
- * @return Bool
+ * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+ * @return bool
*/
- public function isWatched( $title ) {
- return $this->getWatchedItem( $title )->isWatched();
+ public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+ return $this->getWatchedItem( $title, $checkRights )->isWatched();
}
/**
* Watch an article.
+ * @since 1.22 $checkRights parameter added
* @param $title Title of the article to look at
+ * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
*/
- public function addWatch( $title ) {
- $this->getWatchedItem( $title )->addWatch();
+ public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+ $this->getWatchedItem( $title, $checkRights )->addWatch();
$this->invalidateCache();
}
/**
* Stop watching an article.
+ * @since 1.22 $checkRights parameter added
* @param $title Title of the article to look at
+ * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
*/
- public function removeWatch( $title ) {
- $this->getWatchedItem( $title )->removeWatch();
+ public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+ $this->getWatchedItem( $title, $checkRights )->removeWatch();
$this->invalidateCache();
}
@@ -2698,28 +3019,35 @@ class User {
* Clear the user's notification timestamp for the given title.
* If e-notif e-mails are on, they will receive notification mails on
* the next change of the page if it's watched etc.
+ * @note If the user doesn't have 'editmywatchlist', this will do nothing.
* @param $title Title of the article to look at
*/
public function clearNotification( &$title ) {
global $wgUseEnotif, $wgShowUpdatedMarker;
- # Do nothing if the database is locked to writes
- if( wfReadOnly() ) {
+ // Do nothing if the database is locked to writes
+ if ( wfReadOnly() ) {
return;
}
- if( $title->getNamespace() == NS_USER_TALK &&
+ // Do nothing if not allowed to edit the watchlist
+ if ( !$this->isAllowed( 'editmywatchlist' ) ) {
+ return;
+ }
+
+ if ( $title->getNamespace() == NS_USER_TALK &&
$title->getText() == $this->getName() ) {
- if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
+ if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) ) {
return;
+ }
$this->setNewtalk( false );
}
- if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
+ if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
return;
}
- if( $this->isAnon() ) {
+ if ( $this->isAnon() ) {
// Nothing else to do...
return;
}
@@ -2742,15 +3070,25 @@ class User {
* Resets all of the given user's page-change notification timestamps.
* If e-notif e-mails are on, they will receive notification mails on
* the next change of any watched page.
+ * @note If the user doesn't have 'editmywatchlist', this will do nothing.
*/
public function clearAllNotifications() {
+ if ( wfReadOnly() ) {
+ return;
+ }
+
+ // Do nothing if not allowed to edit the watchlist
+ if ( !$this->isAllowed( 'editmywatchlist' ) ) {
+ return;
+ }
+
global $wgUseEnotif, $wgShowUpdatedMarker;
if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
$this->setNewtalk( false );
return;
}
$id = $this->getId();
- if( $id != 0 ) {
+ if ( $id != 0 ) {
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'watchlist',
array( /* SET */
@@ -2766,14 +3104,15 @@ class User {
/**
* Set this user's options from an encoded string
- * @param $str String Encoded options to import
+ * @param string $str Encoded options to import
*
* @deprecated in 1.19 due to removal of user_options from the user table
*/
private function decodeOptions( $str ) {
wfDeprecated( __METHOD__, '1.19' );
- if( !$str )
+ if ( !$str ) {
return;
+ }
$this->mOptionsLoaded = true;
$this->mOptionOverrides = array();
@@ -2794,21 +3133,32 @@ class User {
/**
* Set a cookie on the user's client. Wrapper for
* WebResponse::setCookie
- * @param $name String Name of the cookie to set
- * @param $value String Value to set
- * @param $exp Int Expiration time, as a UNIX time value;
+ * @param string $name Name of the cookie to set
+ * @param string $value Value to set
+ * @param int $exp Expiration time, as a UNIX time value;
* if 0 or not specified, use the default $wgCookieExpiration
+ * @param bool $secure
+ * true: Force setting the secure attribute when setting the cookie
+ * false: Force NOT setting the secure attribute when setting the cookie
+ * null (default): Use the default ($wgCookieSecure) to set the secure attribute
+ * @param array $params Array of options sent passed to WebResponse::setcookie()
*/
- protected function setCookie( $name, $value, $exp = 0 ) {
- $this->getRequest()->response()->setcookie( $name, $value, $exp );
+ protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array() ) {
+ $params['secure'] = $secure;
+ $this->getRequest()->response()->setcookie( $name, $value, $exp, $params );
}
/**
* Clear a cookie on the user's client
- * @param $name String Name of the cookie to clear
+ * @param string $name Name of the cookie to clear
+ * @param bool $secure
+ * true: Force setting the secure attribute when setting the cookie
+ * false: Force NOT setting the secure attribute when setting the cookie
+ * null (default): Use the default ($wgCookieSecure) to set the secure attribute
+ * @param array $params Array of options sent passed to WebResponse::setcookie()
*/
- protected function clearCookie( $name ) {
- $this->setCookie( $name, '', time() - 86400 );
+ protected function clearCookie( $name, $secure = null, $params = array() ) {
+ $this->setCookie( $name, '', time() - 86400, $secure, $params );
}
/**
@@ -2816,14 +3166,17 @@ class User {
*
* @param $request WebRequest object to use; $wgRequest will be used if null
* is passed.
+ * @param bool $secure Whether to force secure/insecure cookies or use default
*/
- public function setCookies( $request = null ) {
+ public function setCookies( $request = null, $secure = null ) {
if ( $request === null ) {
$request = $this->getRequest();
}
$this->load();
- if ( 0 == $this->mId ) return;
+ if ( 0 == $this->mId ) {
+ return;
+ }
if ( !$this->mToken ) {
// When token is empty or NULL generate a new one and then save it to the database
// This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
@@ -2856,8 +3209,29 @@ class User {
if ( $value === false ) {
$this->clearCookie( $name );
} else {
- $this->setCookie( $name, $value );
+ $this->setCookie( $name, $value, 0, $secure );
+ }
+ }
+
+ /**
+ * If wpStickHTTPS was selected, also set an insecure cookie that
+ * will cause the site to redirect the user to HTTPS, if they access
+ * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
+ * as the one set by centralauth (bug 53538). Also set it to session, or
+ * standard time setting, based on if rememberme was set.
+ */
+ if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
+ $time = null;
+ if ( ( 1 == $this->getOption( 'rememberpassword' ) ) ) {
+ $time = 0; // set to $wgCookieExpiration
}
+ $this->setCookie(
+ 'forceHTTPS',
+ 'true',
+ $time,
+ false,
+ array( 'prefix' => '' ) // no prefix
+ );
}
}
@@ -2865,7 +3239,7 @@ class User {
* Log this user out.
*/
public function logout() {
- if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
+ if ( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
$this->doLogout();
}
}
@@ -2881,9 +3255,10 @@ class User {
$this->clearCookie( 'UserID' );
$this->clearCookie( 'Token' );
+ $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
- # Remember when user logged out, to prevent seeing cached pages
- $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
+ // Remember when user logged out, to prevent seeing cached pages
+ $this->setCookie( 'LoggedOut', time(), time() + 86400 );
}
/**
@@ -2894,8 +3269,12 @@ class User {
global $wgAuth;
$this->load();
- if ( wfReadOnly() ) { return; }
- if ( 0 == $this->mId ) { return; }
+ if ( wfReadOnly() ) {
+ return;
+ }
+ if ( 0 == $this->mId ) {
+ return;
+ }
$this->mTouched = self::newTouchedTimestamp();
if ( !$wgAuth->allowSetLocalPassword() ) {
@@ -2930,11 +3309,13 @@ class User {
/**
* If only this user's username is known, and it exists, return the user ID.
- * @return Int
+ * @return int
*/
public function idForName() {
$s = trim( $this->getName() );
- if ( $s === '' ) return 0;
+ if ( $s === '' ) {
+ return 0;
+ }
$dbr = wfGetDB( DB_SLAVE );
$id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
@@ -2947,8 +3328,8 @@ class User {
/**
* Add a user to the database, return the user object
*
- * @param $name String Username to add
- * @param $params Array of Strings Non-default parameters to save to the database as user_* fields:
+ * @param string $name Username to add
+ * @param array $params of Strings Non-default parameters to save to the database as user_* fields:
* - password The user's password hash. Password logins will be disabled if this is omitted.
* - newpassword Hash for a temporary password that has been mailed to the user
* - email The user's email address
@@ -2963,6 +3344,7 @@ class User {
public static function createNew( $name, $params = array() ) {
$user = new User;
$user->load();
+ $user->setToken(); // init token
if ( isset( $params['options'] ) ) {
$user->mOptions = $params['options'] + (array)$user->mOptions;
unset( $params['options'] );
@@ -2997,14 +3379,41 @@ class User {
}
/**
- * Add this existing user object to the database
+ * Add this existing user object to the database. If the user already
+ * exists, a fatal status object is returned, and the user object is
+ * initialised with the data from the database.
+ *
+ * Previously, this function generated a DB error due to a key conflict
+ * if the user already existed. Many extension callers use this function
+ * in code along the lines of:
+ *
+ * $user = User::newFromName( $name );
+ * if ( !$user->isLoggedIn() ) {
+ * $user->addToDatabase();
+ * }
+ * // do something with $user...
+ *
+ * However, this was vulnerable to a race condition (bug 16020). By
+ * initialising the user object if the user exists, we aim to support this
+ * calling sequence as far as possible.
+ *
+ * Note that if the user exists, this function will acquire a write lock,
+ * so it is still advisable to make the call conditional on isLoggedIn(),
+ * and to commit the transaction after calling.
+ *
+ * @throws MWException
+ * @return Status
*/
public function addToDatabase() {
$this->load();
+ if ( !$this->mToken ) {
+ $this->setToken(); // init token
+ }
$this->mTouched = self::newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
+ $inWrite = $dbw->writesOrCallbacksPending();
$seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
$dbw->insert( 'user',
array(
@@ -3020,14 +3429,37 @@ class User {
'user_registration' => $dbw->timestamp( $this->mRegistration ),
'user_editcount' => 0,
'user_touched' => $dbw->timestamp( $this->mTouched ),
- ), __METHOD__
+ ), __METHOD__,
+ array( 'IGNORE' )
);
+ if ( !$dbw->affectedRows() ) {
+ if ( !$inWrite ) {
+ // XXX: Get out of REPEATABLE-READ so the SELECT below works.
+ // Often this case happens early in views before any writes.
+ // This shows up at least with CentralAuth.
+ $dbw->commit( __METHOD__, 'flush' );
+ }
+ $this->mId = $dbw->selectField( 'user', 'user_id',
+ array( 'user_name' => $this->mName ), __METHOD__ );
+ $loaded = false;
+ if ( $this->mId ) {
+ if ( $this->loadFromDatabase() ) {
+ $loaded = true;
+ }
+ }
+ if ( !$loaded ) {
+ throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
+ "to insert user '{$this->mName}' row, but it was not present in select!" );
+ }
+ return Status::newFatal( 'userexists' );
+ }
$this->mId = $dbw->insertId();
// Clear instance cache other than user table data, which is already accurate
$this->clearInstanceCache();
$this->saveOptions();
+ return Status::newGood();
}
/**
@@ -3074,13 +3506,13 @@ class User {
* settings.
*
* @deprecated since 1.17 use the ParserOptions object to get the relevant options
- * @return String Page rendering hash
+ * @return string Page rendering hash
*/
public function getPageRenderingHash() {
wfDeprecated( __METHOD__, '1.17' );
- global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
- if( $this->mHash ){
+ global $wgRenderHashAppend, $wgLang, $wgContLang;
+ if ( $this->mHash ) {
return $this->mHash;
}
@@ -3088,11 +3520,8 @@ class User {
// since it disables the parser cache, its value will always
// be 0 when this function is called by parsercache.
- $confstr = $this->getOption( 'math' );
+ $confstr = $this->getOption( 'math' );
$confstr .= '!' . $this->getStubThreshold();
- if ( $wgUseDynamicDates ) { # This is wrong (bug 24714)
- $confstr .= '!' . $this->getDatePreference();
- }
$confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
$confstr .= '!' . $wgLang->getCode();
$confstr .= '!' . $this->getOption( 'thumbsize' );
@@ -3117,18 +3546,18 @@ class User {
/**
* Get whether the user is explicitly blocked from account creation.
- * @return Bool|Block
+ * @return bool|Block
*/
public function isBlockedFromCreateAccount() {
$this->getBlockedStatus();
- if( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ){
+ if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
return $this->mBlock;
}
# bug 13611: if the IP address the user is trying to create an account from is
# blocked with createaccount disabled, prevent new account creation there even
# when the user is logged in
- if( $this->mBlockedFromCreateAccount === false ){
+ if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
$this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
}
return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
@@ -3138,7 +3567,7 @@ class User {
/**
* Get whether the user is blocked from using Special:Emailuser.
- * @return Bool
+ * @return bool
*/
public function isBlockedFromEmailuser() {
$this->getBlockedStatus();
@@ -3147,7 +3576,7 @@ class User {
/**
* Get whether the user is allowed to create an account.
- * @return Bool
+ * @return bool
*/
function isAllowedToCreateAccount() {
return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
@@ -3175,7 +3604,7 @@ class User {
/**
* Determine whether the user is a newbie. Newbies are either
* anonymous IPs, or the most recently created accounts.
- * @return Bool
+ * @return bool
*/
public function isNewbie() {
return !$this->isAllowed( 'autoconfirmed' );
@@ -3183,8 +3612,8 @@ class User {
/**
* Check to see if the given clear-text password is one of the accepted passwords
- * @param $password String: user password.
- * @return Boolean: True if the given password is correct, otherwise False.
+ * @param string $password user password.
+ * @return boolean: True if the given password is correct, otherwise False.
*/
public function checkPassword( $password ) {
global $wgAuth, $wgLegacyEncoding;
@@ -3195,24 +3624,24 @@ class User {
// to. Certain authentication plugins do NOT want to save
// domain passwords in a mysql database, so we should
// check this (in case $wgAuth->strict() is false).
- if( !$this->isValidPassword( $password ) ) {
+ if ( !$this->isValidPassword( $password ) ) {
return false;
}
- if( $wgAuth->authenticate( $this->getName(), $password ) ) {
+ if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
return true;
- } elseif( $wgAuth->strict() ) {
- /* Auth plugin doesn't allow local authentication */
+ } elseif ( $wgAuth->strict() ) {
+ // Auth plugin doesn't allow local authentication
return false;
- } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
- /* Auth plugin doesn't allow local authentication for this user name */
+ } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
+ // Auth plugin doesn't allow local authentication for this user name
return false;
}
if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
return true;
} elseif ( $wgLegacyEncoding ) {
- # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
- # Check for this with iconv
+ // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+ // Check for this with iconv
$cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
if ( $cp1252Password != $password &&
self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) )
@@ -3229,13 +3658,13 @@ class User {
*
* @param $plaintext string
*
- * @return Boolean: True if matches, false otherwise
+ * @return boolean: True if matches, false otherwise
*/
public function checkTemporaryPassword( $plaintext ) {
global $wgNewPasswordExpiry;
$this->load();
- if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
+ if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
if ( is_null( $this->mNewpassTime ) ) {
return true;
}
@@ -3250,9 +3679,9 @@ class User {
* Alias for getEditToken.
* @deprecated since 1.19, use getEditToken instead.
*
- * @param $salt String|Array of Strings Optional function-specific data for hashing
+ * @param string|array $salt of Strings Optional function-specific data for hashing
* @param $request WebRequest object to use or null to use $wgRequest
- * @return String The new edit token
+ * @return string The new edit token
*/
public function editToken( $salt = '', $request = null ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -3267,9 +3696,9 @@ class User {
*
* @since 1.19
*
- * @param $salt String|Array of Strings Optional function-specific data for hashing
+ * @param string|array $salt of Strings Optional function-specific data for hashing
* @param $request WebRequest object to use or null to use $wgRequest
- * @return String The new edit token
+ * @return string The new edit token
*/
public function getEditToken( $salt = '', $request = null ) {
if ( $request == null ) {
@@ -3284,7 +3713,7 @@ class User {
$token = MWCryptRand::generateHex( 32 );
$request->setSessionData( 'wsEditToken', $token );
}
- if( is_array( $salt ) ) {
+ if ( is_array( $salt ) ) {
$salt = implode( '|', $salt );
}
return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
@@ -3294,11 +3723,10 @@ class User {
/**
* Generate a looking random token for various uses.
*
- * @param $salt String Optional salt value
- * @return String The new random token
- * @deprecated since 1.20; Use MWCryptRand for secure purposes or wfRandomString for pesudo-randomness
+ * @return string The new random token
+ * @deprecated since 1.20: Use MWCryptRand for secure purposes or wfRandomString for pseudo-randomness
*/
- public static function generateToken( $salt = '' ) {
+ public static function generateToken() {
return MWCryptRand::generateHex( 32 );
}
@@ -3308,10 +3736,10 @@ class User {
* user's own login session, not a form submission from a third-party
* site.
*
- * @param $val String Input value to compare
- * @param $salt String Optional function-specific data for hashing
- * @param $request WebRequest object to use or null to use $wgRequest
- * @return Boolean: Whether the token matches
+ * @param string $val Input value to compare
+ * @param string $salt Optional function-specific data for hashing
+ * @param WebRequest $request Object to use or null to use $wgRequest
+ * @return boolean: Whether the token matches
*/
public function matchEditToken( $val, $salt = '', $request = null ) {
$sessionToken = $this->getEditToken( $salt, $request );
@@ -3325,10 +3753,10 @@ class User {
* Check given value against the token value stored in the session,
* ignoring the suffix.
*
- * @param $val String Input value to compare
- * @param $salt String Optional function-specific data for hashing
- * @param $request WebRequest object to use or null to use $wgRequest
- * @return Boolean: Whether the token matches
+ * @param string $val Input value to compare
+ * @param string $salt Optional function-specific data for hashing
+ * @param WebRequest $request object to use or null to use $wgRequest
+ * @return boolean: Whether the token matches
*/
public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
$sessionToken = $this->getEditToken( $salt, $request );
@@ -3339,7 +3767,7 @@ class User {
* Generate a new e-mail confirmation token and send a confirmation/invalidation
* mail to the user's given address.
*
- * @param $type String: message to send, either "created", "changed" or "set"
+ * @param string $type message to send, either "created", "changed" or "set"
* @return Status object
*/
public function sendConfirmationMail( $type = 'created' ) {
@@ -3355,6 +3783,7 @@ class User {
} elseif ( $type === true ) {
$message = 'confirmemail_body_changed';
} else {
+ // Messages: confirmemail_body_changed, confirmemail_body_set
$message = 'confirmemail_body_' . $type;
}
@@ -3373,14 +3802,14 @@ class User {
* Send an e-mail to this user's account. Does not check for
* confirmed status or validity.
*
- * @param $subject String Message subject
- * @param $body String Message body
- * @param $from String Optional From address; if unspecified, default $wgPasswordSender will be used
- * @param $replyto String Reply-To address
+ * @param string $subject Message subject
+ * @param string $body Message body
+ * @param string $from Optional From address; if unspecified, default $wgPasswordSender will be used
+ * @param string $replyto Reply-To address
* @return Status
*/
public function sendMail( $subject, $body, $from = null, $replyto = null ) {
- if( is_null( $from ) ) {
+ if ( is_null( $from ) ) {
global $wgPasswordSender, $wgPasswordSenderName;
$sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
} else {
@@ -3399,9 +3828,9 @@ class User {
* this change to the database.
*
* @param &$expiration \mixed Accepts the expiration time
- * @return String New token
+ * @return string New token
*/
- private function confirmationToken( &$expiration ) {
+ protected function confirmationToken( &$expiration ) {
global $wgUserEmailConfirmationTokenExpiry;
$now = time();
$expires = $now + $wgUserEmailConfirmationTokenExpiry;
@@ -3415,20 +3844,20 @@ class User {
}
/**
- * Return a URL the user can use to confirm their email address.
- * @param $token String Accepts the email confirmation token
- * @return String New token URL
+ * Return a URL the user can use to confirm their email address.
+ * @param string $token Accepts the email confirmation token
+ * @return string New token URL
*/
- private function confirmationTokenUrl( $token ) {
+ protected 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
- * @return String New token URL
+ * @param string $token Accepts the email confirmation token
+ * @return string New token URL
*/
- private function invalidationTokenUrl( $token ) {
+ protected function invalidationTokenUrl( $token ) {
return $this->getTokenUrl( 'InvalidateEmail', $token );
}
@@ -3442,14 +3871,14 @@ class User {
* also sometimes can get corrupted in some browsers/mailers
* (bug 6957 with Gmail and Internet Explorer).
*
- * @param $page String Special page
- * @param $token String Token
- * @return String Formatted URL
+ * @param string $page Special page
+ * @param string $token Token
+ * @return string Formatted URL
*/
protected function getTokenUrl( $page, $token ) {
// Hack to bypass localization of 'Special:'
$title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
- return $title->getCanonicalUrl();
+ return $title->getCanonicalURL();
}
/**
@@ -3460,8 +3889,12 @@ class User {
* @return bool
*/
public function confirmEmail() {
- $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
- wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
+ // Check if it's already confirmed, so we don't touch the database
+ // and fire the ConfirmEmailComplete hook on redundant confirmations.
+ if ( !$this->isEmailConfirmed() ) {
+ $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
+ wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
+ }
return true;
}
@@ -3483,7 +3916,7 @@ class User {
/**
* Set the e-mail authentication timestamp.
- * @param $timestamp String TS_MW timestamp
+ * @param string $timestamp TS_MW timestamp
*/
function setEmailAuthenticationTimestamp( $timestamp ) {
$this->load();
@@ -3494,11 +3927,11 @@ class User {
/**
* Is this user allowed to send e-mails within limits of current
* site configuration?
- * @return Bool
+ * @return bool
*/
public function canSendEmail() {
global $wgEnableEmail, $wgEnableUserEmail;
- if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
+ if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
return false;
}
$canSend = $this->isEmailConfirmed();
@@ -3509,7 +3942,7 @@ class User {
/**
* Is this user allowed to receive e-mails within limits of current
* site configuration?
- * @return Bool
+ * @return bool
*/
public function canReceiveEmail() {
return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
@@ -3523,20 +3956,20 @@ class User {
* confirmed their address by returning a code or using a password
* sent to the address from the wiki.
*
- * @return Bool
+ * @return bool
*/
public function isEmailConfirmed() {
global $wgEmailAuthentication;
$this->load();
$confirmed = true;
- if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
- if( $this->isAnon() ) {
+ if ( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
+ if ( $this->isAnon() ) {
return false;
}
- if( !Sanitizer::validateEmail( $this->mEmail ) ) {
+ if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
return false;
}
- if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
+ if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
return false;
}
return true;
@@ -3547,7 +3980,7 @@ class User {
/**
* Check whether there is an outstanding request for e-mail confirmation.
- * @return Bool
+ * @return bool
*/
public function isEmailConfirmationPending() {
global $wgEmailAuthentication;
@@ -3560,8 +3993,9 @@ class User {
/**
* Get the timestamp of account creation.
*
- * @return String|Bool Timestamp of account creation, or false for
- * non-existent/anonymous user accounts.
+ * @return string|bool|null Timestamp of account creation, false for
+ * non-existent/anonymous user accounts, or null if existing account
+ * but information is not in database.
*/
public function getRegistration() {
if ( $this->isAnon() ) {
@@ -3574,11 +4008,11 @@ class User {
/**
* Get the timestamp of the first edit
*
- * @return String|Bool Timestamp of first edit, or false for
- * non-existent/anonymous user accounts.
+ * @return string|bool Timestamp of first edit, or false for
+ * non-existent/anonymous user accounts.
*/
public function getFirstEditTimestamp() {
- if( $this->getId() == 0 ) {
+ if ( $this->getId() == 0 ) {
return false; // anons
}
$dbr = wfGetDB( DB_SLAVE );
@@ -3587,7 +4021,7 @@ class User {
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp ASC' )
);
- if( !$time ) {
+ if ( !$time ) {
return false; // no edits
}
return wfTimestamp( TS_MW, $time );
@@ -3596,23 +4030,23 @@ class User {
/**
* Get the permissions associated with a given list of groups
*
- * @param $groups Array of Strings List of internal group names
+ * @param array $groups of Strings List of internal group names
* @return Array of Strings List of permission key names for given groups combined
*/
public static function getGroupPermissions( $groups ) {
global $wgGroupPermissions, $wgRevokePermissions;
$rights = array();
// grant every granted permission first
- foreach( $groups as $group ) {
- if( isset( $wgGroupPermissions[$group] ) ) {
+ foreach ( $groups as $group ) {
+ if ( isset( $wgGroupPermissions[$group] ) ) {
$rights = array_merge( $rights,
// array_filter removes empty items
array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
}
}
// now revoke the revoked permissions
- foreach( $groups as $group ) {
- if( isset( $wgRevokePermissions[$group] ) ) {
+ foreach ( $groups as $group ) {
+ if ( isset( $wgRevokePermissions[$group] ) ) {
$rights = array_diff( $rights,
array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
}
@@ -3623,14 +4057,14 @@ class User {
/**
* Get all the groups who have a given permission
*
- * @param $role String Role to check
+ * @param string $role Role to check
* @return Array of Strings List of internal group names with the given permission
*/
public static function getGroupsWithPermission( $role ) {
global $wgGroupPermissions;
$allowedGroups = array();
- foreach ( $wgGroupPermissions as $group => $rights ) {
- if ( isset( $rights[$role] ) && $rights[$role] ) {
+ foreach ( array_keys( $wgGroupPermissions ) as $group ) {
+ if ( self::groupHasPermission( $group, $role ) ) {
$allowedGroups[] = $group;
}
}
@@ -3638,10 +4072,68 @@ class User {
}
/**
+ * Check, if the given group has the given permission
+ *
+ * If you're wanting to check whether all users have a permission, use
+ * User::isEveryoneAllowed() instead. That properly checks if it's revoked
+ * from anyone.
+ *
+ * @since 1.21
+ * @param string $group Group to check
+ * @param string $role Role to check
+ * @return bool
+ */
+ public static function groupHasPermission( $group, $role ) {
+ global $wgGroupPermissions, $wgRevokePermissions;
+ return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
+ && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
+ }
+
+ /**
+ * Check if all users have the given permission
+ *
+ * @since 1.22
+ * @param string $right Right to check
+ * @return bool
+ */
+ public static function isEveryoneAllowed( $right ) {
+ global $wgGroupPermissions, $wgRevokePermissions;
+ static $cache = array();
+
+ // Use the cached results, except in unit tests which rely on
+ // being able change the permission mid-request
+ if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
+ return $cache[$right];
+ }
+
+ if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
+ $cache[$right] = false;
+ return false;
+ }
+
+ // If it's revoked anywhere, then everyone doesn't have it
+ foreach ( $wgRevokePermissions as $rights ) {
+ if ( isset( $rights[$right] ) && $rights[$right] ) {
+ $cache[$right] = false;
+ return false;
+ }
+ }
+
+ // Allow extensions (e.g. OAuth) to say false
+ if ( !wfRunHooks( 'UserIsEveryoneAllowed', array( $right ) ) ) {
+ $cache[$right] = false;
+ return false;
+ }
+
+ $cache[$right] = true;
+ return true;
+ }
+
+ /**
* Get the localized descriptive name for a group, if it exists
*
- * @param $group String Internal group name
- * @return String Localized descriptive group name
+ * @param string $group Internal group name
+ * @return string Localized descriptive group name
*/
public static function getGroupName( $group ) {
$msg = wfMessage( "group-$group" );
@@ -3651,9 +4143,9 @@ class User {
/**
* Get the localized descriptive name for a member of a group, if it exists
*
- * @param $group String Internal group name
- * @param $username String Username for gender (since 1.19)
- * @return String Localized name for group member
+ * @param string $group Internal group name
+ * @param string $username Username for gender (since 1.19)
+ * @return string Localized name for group member
*/
public static function getGroupMember( $group, $username = '#' ) {
$msg = wfMessage( "group-$group-member", $username );
@@ -3705,15 +4197,16 @@ class User {
/**
* Get the title of a page describing a particular group
*
- * @param $group String Internal group name
- * @return Title|Bool Title of the page if it exists, false otherwise
+ * @param string $group Internal group name
+ * @return Title|bool Title of the page if it exists, false otherwise
*/
public static function getGroupPage( $group ) {
$msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
- if( $msg->exists() ) {
+ if ( $msg->exists() ) {
$title = Title::newFromText( $msg->text() );
- if( is_object( $title ) )
+ if ( is_object( $title ) ) {
return $title;
+ }
}
return false;
}
@@ -3722,16 +4215,16 @@ class User {
* Create a link to the group in HTML, if available;
* else return the group name.
*
- * @param $group String Internal name of the group
- * @param $text String The text of the link
- * @return String HTML link to the group
+ * @param string $group Internal name of the group
+ * @param string $text The text of the link
+ * @return string HTML link to the group
*/
public static function makeGroupLinkHTML( $group, $text = '' ) {
- if( $text == '' ) {
+ if ( $text == '' ) {
$text = self::getGroupName( $group );
}
$title = self::getGroupPage( $group );
- if( $title ) {
+ if ( $title ) {
return Linker::link( $title, htmlspecialchars( $text ) );
} else {
return $text;
@@ -3742,16 +4235,16 @@ class User {
* Create a link to the group in Wikitext, if available;
* else return the group name.
*
- * @param $group String Internal name of the group
- * @param $text String The text of the link
- * @return String Wikilink to the group
+ * @param string $group Internal name of the group
+ * @param string $text The text of the link
+ * @return string Wikilink to the group
*/
public static function makeGroupLinkWiki( $group, $text = '' ) {
- if( $text == '' ) {
+ if ( $text == '' ) {
$text = self::getGroupName( $group );
}
$title = self::getGroupPage( $group );
- if( $title ) {
+ if ( $title ) {
$page = $title->getPrefixedText();
return "[[$page|$text]]";
} else {
@@ -3762,7 +4255,7 @@ 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
+ * @param string $group 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),
@@ -3772,53 +4265,53 @@ class User {
global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
$groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
- if( empty( $wgAddGroups[$group] ) ) {
+ if ( empty( $wgAddGroups[$group] ) ) {
// Don't add anything to $groups
- } elseif( $wgAddGroups[$group] === true ) {
+ } elseif ( $wgAddGroups[$group] === true ) {
// You get everything
$groups['add'] = self::getAllGroups();
- } elseif( is_array( $wgAddGroups[$group] ) ) {
+ } elseif ( is_array( $wgAddGroups[$group] ) ) {
$groups['add'] = $wgAddGroups[$group];
}
// Same thing for remove
- if( empty( $wgRemoveGroups[$group] ) ) {
- } elseif( $wgRemoveGroups[$group] === true ) {
+ if ( empty( $wgRemoveGroups[$group] ) ) {
+ } elseif ( $wgRemoveGroups[$group] === true ) {
$groups['remove'] = self::getAllGroups();
- } elseif( is_array( $wgRemoveGroups[$group] ) ) {
+ } 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 ) ) {
+ 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 ) ) {
+ 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 ) {
+ 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] ) ) {
+ } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
$groups['add-self'] = $wgGroupsAddToSelf[$group];
}
- if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
- } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
+ if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
+ } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
$groups['remove-self'] = User::getAllGroups();
- } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
+ } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
$groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
}
@@ -3833,7 +4326,7 @@ class User {
* 'remove-self' => array( removable groups from self) )
*/
public function changeableGroups() {
- if( $this->isAllowed( 'userrights' ) ) {
+ if ( $this->isAllowed( 'userrights' ) ) {
// This group gives the right to modify everything (reverse-
// compatibility with old "userrights lets you change
// everything")
@@ -3856,11 +4349,11 @@ class User {
);
$addergroups = $this->getEffectiveGroups();
- foreach( $addergroups as $addergroup ) {
+ foreach ( $addergroups as $addergroup ) {
$groups = array_merge_recursive(
$groups, $this->changeableByGroup( $addergroup )
);
- $groups['add'] = array_unique( $groups['add'] );
+ $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'] );
@@ -3873,39 +4366,30 @@ class User {
* Will have no effect for anonymous users.
*/
public function incEditCount() {
- if( !$this->isAnon() ) {
+ if ( !$this->isAnon() ) {
$dbw = wfGetDB( DB_MASTER );
- $dbw->update( 'user',
+ $dbw->update(
+ 'user',
array( 'user_editcount=user_editcount+1' ),
array( 'user_id' => $this->getId() ),
- __METHOD__ );
+ __METHOD__
+ );
// Lazy initialization check...
- if( $dbw->affectedRows() == 0 ) {
- // Pull from a slave to be less cruel to servers
- // Accuracy isn't the point anyway here
- $dbr = wfGetDB( DB_SLAVE );
- $count = $dbr->selectField( 'revision',
- 'COUNT(rev_user)',
- array( 'rev_user' => $this->getId() ),
- __METHOD__ );
-
+ if ( $dbw->affectedRows() == 0 ) {
// Now here's a goddamn hack...
- if( $dbr !== $dbw ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ if ( $dbr !== $dbw ) {
// If we actually have a slave server, the count is
// at least one behind because the current transaction
// has not been committed and replicated.
- $count++;
+ $this->initEditCount( 1 );
} else {
// But if DB_SLAVE is selecting the master, then the
// count we just read includes the revision that was
// just added in the working transaction.
+ $this->initEditCount();
}
-
- $dbw->update( 'user',
- array( 'user_editcount' => $count ),
- array( 'user_id' => $this->getId() ),
- __METHOD__ );
}
}
// edit count in user cache too
@@ -3913,10 +4397,39 @@ class User {
}
/**
+ * Initialize user_editcount from data out of the revision table
+ *
+ * @param $add Integer Edits to add to the count from the revision table
+ * @return integer Number of edits
+ */
+ protected function initEditCount( $add = 0 ) {
+ // Pull from a slave to be less cruel to servers
+ // Accuracy isn't the point anyway here
+ $dbr = wfGetDB( DB_SLAVE );
+ $count = (int)$dbr->selectField(
+ 'revision',
+ 'COUNT(rev_user)',
+ array( 'rev_user' => $this->getId() ),
+ __METHOD__
+ );
+ $count = $count + $add;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'user',
+ array( 'user_editcount' => $count ),
+ array( 'user_id' => $this->getId() ),
+ __METHOD__
+ );
+
+ return $count;
+ }
+
+ /**
* Get the description of a given right
*
- * @param $right String Right to query
- * @return String Localized description of the right
+ * @param string $right Right to query
+ * @return string Localized description of the right
*/
public static function getRightDescription( $right ) {
$key = "right-$right";
@@ -3927,9 +4440,9 @@ class User {
/**
* Make an old-style password hash
*
- * @param $password String Plain-text password
- * @param $userId String User ID
- * @return String Password hash
+ * @param string $password Plain-text password
+ * @param string $userId User ID
+ * @return string Password hash
*/
public static function oldCrypt( $password, $userId ) {
global $wgPasswordSalt;
@@ -3943,21 +4456,20 @@ class User {
/**
* Make a new-style password hash
*
- * @param $password String Plain-text password
+ * @param string $password Plain-text password
* @param bool|string $salt Optional salt, may be random or the user ID.
-
- * If unspecified or false, will generate one automatically
- * @return String Password hash
+ * If unspecified or false, will generate one automatically
+ * @return string Password hash
*/
public static function crypt( $password, $salt = false ) {
global $wgPasswordSalt;
$hash = '';
- if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
+ if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
return $hash;
}
- if( $wgPasswordSalt ) {
+ if ( $wgPasswordSalt ) {
if ( $salt === false ) {
$salt = MWCryptRand::generateHex( 8 );
}
@@ -3971,117 +4483,161 @@ class User {
* Compare a password hash with a plain-text password. Requires the user
* ID if there's a chance that the hash is an old-style hash.
*
- * @param $hash String Password hash
- * @param $password String Plain-text password to compare
- * @param $userId String|bool User ID for old-style password salt
+ * @param string $hash Password hash
+ * @param string $password Plain-text password to compare
+ * @param string|bool $userId User ID for old-style password salt
*
- * @return Boolean
+ * @return boolean
*/
public static function comparePasswords( $hash, $password, $userId = false ) {
$type = substr( $hash, 0, 3 );
$result = false;
- if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
+ if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
return $result;
}
if ( $type == ':A:' ) {
- # Unsalted
+ // Unsalted
return md5( $password ) === substr( $hash, 3 );
} elseif ( $type == ':B:' ) {
- # Salted
+ // Salted
list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
- return md5( $salt.'-'.md5( $password ) ) === $realHash;
+ return md5( $salt . '-' . md5( $password ) ) === $realHash;
} else {
- # Old-style
+ // Old-style
return self::oldCrypt( $password, $userId ) === $hash;
}
}
/**
- * Add a newuser log entry for this user. Before 1.19 the return value was always true.
+ * Add a newuser log entry for this user.
+ * Before 1.19 the return value was always true.
+ *
+ * @param string|bool $action account creation type.
+ * - String, one of the following values:
+ * - 'create' for an anonymous user creating an account for himself.
+ * This will force the action's performer to be the created user itself,
+ * no matter the value of $wgUser
+ * - 'create2' for a logged in user creating an account for someone else
+ * - 'byemail' when the created user will receive its password by e-mail
+ * - 'autocreate' when the user is automatically created (such as by CentralAuth).
+ * - Boolean means whether the account was created by e-mail (deprecated):
+ * - true will be converted to 'byemail'
+ * - false will be converted to 'create' if this object is the same as
+ * $wgUser and to 'create2' otherwise
*
- * @param $byEmail Boolean: account made by email?
- * @param $reason String: user supplied reason
+ * @param string $reason user supplied reason
*
* @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
*/
- public function addNewUserLogEntry( $byEmail = false, $reason = '' ) {
- global $wgUser, $wgContLang, $wgNewUserLog;
- if( empty( $wgNewUserLog ) ) {
+ public function addNewUserLogEntry( $action = false, $reason = '' ) {
+ global $wgUser, $wgNewUserLog;
+ if ( empty( $wgNewUserLog ) ) {
return true; // disabled
}
- if( $this->getName() == $wgUser->getName() ) {
- $action = 'create';
- } else {
- $action = 'create2';
- if ( $byEmail ) {
- if ( $reason === '' ) {
- $reason = wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text();
- } else {
- $reason = $wgContLang->commaList( array(
- $reason, wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text() ) );
- }
+ if ( $action === true ) {
+ $action = 'byemail';
+ } elseif ( $action === false ) {
+ if ( $this->getName() == $wgUser->getName() ) {
+ $action = 'create';
+ } else {
+ $action = 'create2';
}
}
- $log = new LogPage( 'newusers' );
- return (int)$log->addEntry(
- $action,
- $this->getUserPage(),
- $reason,
- array( $this->getId() )
- );
+
+ if ( $action === 'create' || $action === 'autocreate' ) {
+ $performer = $this;
+ } else {
+ $performer = $wgUser;
+ }
+
+ $logEntry = new ManualLogEntry( 'newusers', $action );
+ $logEntry->setPerformer( $performer );
+ $logEntry->setTarget( $this->getUserPage() );
+ $logEntry->setComment( $reason );
+ $logEntry->setParameters( array(
+ '4::userid' => $this->getId(),
+ ) );
+ $logid = $logEntry->insert();
+
+ if ( $action !== 'autocreate' ) {
+ $logEntry->publish( $logid );
+ }
+
+ return (int)$logid;
}
/**
* Add an autocreate newuser log entry for this user
* Used by things like CentralAuth and perhaps other authplugins.
+ * Consider calling addNewUserLogEntry() directly instead.
*
* @return bool
*/
public function addNewUserLogEntryAutoCreate() {
- global $wgNewUserLog;
- if( !$wgNewUserLog ) {
- return true; // disabled
- }
- $log = new LogPage( 'newusers', false );
- $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ), $this );
+ $this->addNewUserLogEntry( 'autocreate' );
+
return true;
}
/**
- * @todo document
+ * Load the user options either from cache, the database or an array
+ *
+ * @param array $data Rows for the current user out of the user_properties table
*/
- protected function loadOptions() {
+ protected function loadOptions( $data = null ) {
+ global $wgContLang;
+
$this->load();
- if ( $this->mOptionsLoaded || !$this->getId() )
+
+ if ( $this->mOptionsLoaded ) {
return;
+ }
$this->mOptions = self::getDefaultOptions();
+ if ( !$this->getId() ) {
+ // For unlogged-in users, load language/variant options from request.
+ // There's no need to do it for logged-in users: they can set preferences,
+ // and handling of page content is done by $pageLang->getPreferredVariant() and such,
+ // so don't override user's choice (especially when the user chooses site default).
+ $variant = $wgContLang->getDefaultVariant();
+ $this->mOptions['variant'] = $variant;
+ $this->mOptions['language'] = $variant;
+ $this->mOptionsLoaded = true;
+ return;
+ }
+
// Maybe load from the object
if ( !is_null( $this->mOptionOverrides ) ) {
wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
- foreach( $this->mOptionOverrides as $key => $value ) {
+ foreach ( $this->mOptionOverrides as $key => $value ) {
$this->mOptions[$key] = $value;
}
} else {
- wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
- // Load from database
- $dbr = wfGetDB( DB_SLAVE );
+ if ( !is_array( $data ) ) {
+ wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
+ // Load from database
+ $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select(
- 'user_properties',
- array( 'up_property', 'up_value' ),
- array( 'up_user' => $this->getId() ),
- __METHOD__
- );
+ $res = $dbr->select(
+ 'user_properties',
+ array( 'up_property', 'up_value' ),
+ array( 'up_user' => $this->getId() ),
+ __METHOD__
+ );
- $this->mOptionOverrides = array();
- foreach ( $res as $row ) {
- $this->mOptionOverrides[$row->up_property] = $row->up_value;
- $this->mOptions[$row->up_property] = $row->up_value;
+ $this->mOptionOverrides = array();
+ $data = array();
+ foreach ( $res as $row ) {
+ $data[$row->up_property] = $row->up_value;
+ }
+ }
+ foreach ( $data as $property => $value ) {
+ $this->mOptionOverrides[$property] = $value;
+ $this->mOptions[$property] = $value;
}
}
@@ -4094,8 +4650,6 @@ class User {
* @todo document
*/
protected function saveOptions() {
- global $wgAllowPrefChange;
-
$this->loadOptions();
// Not using getOptions(), to keep hidden preferences in database
@@ -4103,15 +4657,14 @@ class User {
// 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 ) ) ) {
+ if ( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
return;
}
- $extuser = ExternalUser::newFromUser( $this );
$userId = $this->getId();
$insert_rows = array();
- foreach( $saveOptions as $key => $value ) {
- # Don't bother storing default values
+ foreach ( $saveOptions as $key => $value ) {
+ // Don't bother storing default values
$defaultOption = self::getDefaultOption( $key );
if ( ( is_null( $defaultOption ) &&
!( $value === false || is_null( $value ) ) ) ||
@@ -4122,21 +4675,21 @@ class User {
'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 = wfGetDB( DB_MASTER );
- $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
- $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
+ $hasRows = $dbw->selectField( 'user_properties', '1',
+ array( 'up_user' => $userId ), __METHOD__ );
+
+ if ( $hasRows ) {
+ // Only do this delete if there is something there. A very large portion of
+ // calls to this function are for setting 'rememberpassword' for new accounts.
+ // Doing this delete for new accounts with no rows in the table rougly causes
+ // gap locks on [max user ID,+infinity) which causes high contention since many
+ // updates will pile up on each other since they are for higher (newer) user IDs.
+ $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
+ }
+ $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
}
/**
@@ -4160,8 +4713,7 @@ class User {
*
* @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.)
+ * That will get confused by the boolean attribute syntax used.)
*/
public static function passwordChangeInputAttribs() {
global $wgMinimalPasswordLength;
@@ -4221,4 +4773,26 @@ class User {
'user_editcount',
);
}
+
+ /**
+ * Factory function for fatal permission-denied errors
+ *
+ * @since 1.22
+ * @param string $permission User right required
+ * @return Status
+ */
+ static function newFatalPermissionDeniedStatus( $permission ) {
+ global $wgLang;
+
+ $groups = array_map(
+ array( 'User', 'makeGroupLinkWiki' ),
+ User::getGroupsWithPermission( $permission )
+ );
+
+ if ( $groups ) {
+ return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
+ } else {
+ return Status::newFatal( 'badaccess-group0' );
+ }
+ }
}
diff --git a/includes/UserArray.php b/includes/UserArray.php
index 3b8f5c10..1f55ef35 100644
--- a/includes/UserArray.php
+++ b/includes/UserArray.php
@@ -44,7 +44,7 @@ abstract class UserArray implements Iterator {
$ids = array_map( 'intval', (array)$ids ); // paranoia
if ( !$ids ) {
// Database::select() doesn't like empty arrays
- return new ArrayIterator(array());
+ return new ArrayIterator( array() );
}
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'user', '*', array( 'user_id' => $ids ),
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
index 01e7132d..8ab10b2d 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -21,9 +21,9 @@
* @author <brion@pobox.com>
* @author <mail@tgries.de>
* @author Tim Starling
+ * @author Luke Welling lwelling@wikimedia.org
*/
-
/**
* Stores a single person's name and email address.
* These are passed in via the constructor, and will be returned in SMTP
@@ -31,9 +31,9 @@
*/
class MailAddress {
/**
- * @param $address string|User string with an email address, or a User object
- * @param $name String: human-readable name if a string address is given
- * @param $realName String: human-readable real name if a string address is given
+ * @param string|User $address string with an email address, or a User object
+ * @param string $name human-readable name if a string address is given
+ * @param string $realName human-readable real name if a string address is given
*/
function __construct( $address, $name = null, $realName = null ) {
if ( is_object( $address ) && $address instanceof User ) {
@@ -77,7 +77,6 @@ class MailAddress {
}
}
-
/**
* Collection of static functions for sending mail
*/
@@ -109,14 +108,18 @@ class UserMailer {
/**
* Creates a single string from an associative array
*
- * @param $headers array Associative Array: keys are header field names,
+ * @param array $headers Associative Array: keys are header field names,
* values are ... values.
- * @param $endl String: The end of line character. Defaults to "\n"
+ * @param string $endl The end of line character. Defaults to "\n"
+ *
+ * Note RFC2822 says newlines must be CRLF (\r\n)
+ * but php mail naively "corrects" it and requires \n for the "correction" to work
+ *
* @return String
*/
static function arrayToHeaderString( $headers, $endl = "\n" ) {
$strings = array();
- foreach( $headers as $name => $value ) {
+ foreach ( $headers as $name => $value ) {
$strings[] = "$name: $value";
}
return implode( $endl, $strings );
@@ -131,10 +134,10 @@ class UserMailer {
global $wgSMTP, $wgServer;
$msgid = uniqid( wfWikiID() . ".", true ); /* true required for cygwin */
- if ( is_array($wgSMTP) && isset($wgSMTP['IDHost']) && $wgSMTP['IDHost'] ) {
+ if ( is_array( $wgSMTP ) && isset( $wgSMTP['IDHost'] ) && $wgSMTP['IDHost'] ) {
$domain = $wgSMTP['IDHost'];
} else {
- $url = wfParseUrl($wgServer);
+ $url = wfParseUrl( $wgServer );
$domain = $url['host'];
}
return "<$msgid@$domain>";
@@ -148,19 +151,48 @@ class UserMailer {
*
* @param $to MailAddress: recipient's email (or an array of them)
* @param $from MailAddress: sender's email
- * @param $subject String: email's subject.
- * @param $body String: email's text.
+ * @param string $subject email's subject.
+ * @param string $body email's text or Array of two strings to be the text and html bodies
* @param $replyto MailAddress: optional reply-to email (default: null).
- * @param $contentType String: optional custom Content-Type (default: text/plain; charset=UTF-8)
+ * @param string $contentType optional custom Content-Type (default: text/plain; charset=UTF-8)
+ * @throws MWException
* @return Status object
*/
public static function send( $to, $from, $subject, $body, $replyto = null, $contentType = 'text/plain; charset=UTF-8' ) {
- global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams;
-
+ global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail;
+ $mime = null;
if ( !is_array( $to ) ) {
$to = array( $to );
}
+ // mail body must have some content
+ $minBodyLen = 10;
+ // arbitrary but longer than Array or Object to detect casting error
+
+ // body must either be a string or an array with text and body
+ if (
+ !(
+ !is_array( $body ) &&
+ strlen( $body ) >= $minBodyLen
+ )
+ &&
+ !(
+ is_array( $body ) &&
+ isset( $body['text'] ) &&
+ isset( $body['html'] ) &&
+ strlen( $body['text'] ) >= $minBodyLen &&
+ strlen( $body['html'] ) >= $minBodyLen
+ )
+ ) {
+ // if it is neither we have a problem
+ return Status::newFatal( 'user-mail-no-body' );
+ }
+
+ if ( !$wgAllowHTMLEmail && is_array( $body ) ) {
+ // HTML not wanted. Dump it.
+ $body = $body['text'];
+ }
+
wfDebug( __METHOD__ . ': sending mail to ' . implode( ', ', $to ) . "\n" );
# Make sure we have at least one address
@@ -194,7 +226,7 @@ class UserMailer {
# NOTE: To: is for presentation, the actual recipient is specified
# by the mailer using the Rcpt-To: header.
#
- # Subject:
+ # Subject:
# PHP mail() second argument to pass the subject, passing a Subject
# as an additional header will result in a duplicate header.
#
@@ -209,36 +241,68 @@ class UserMailer {
$headers['Reply-To'] = $replyto->toString();
}
- $headers['Date'] = date( 'r' );
- $headers['MIME-Version'] = '1.0';
- $headers['Content-type'] = ( is_null( $contentType ) ?
- 'text/plain; charset=UTF-8' : $contentType );
- $headers['Content-transfer-encoding'] = '8bit';
-
+ $headers['Date'] = MWTimestamp::getLocalInstance()->format( 'r' );
$headers['Message-ID'] = self::makeMsgId();
$headers['X-Mailer'] = 'MediaWiki mailer';
+ # Line endings need to be different on Unix and Windows due to
+ # the bug described at http://trac.wordpress.org/ticket/2603
+ if ( wfIsWindows() ) {
+ $endl = "\r\n";
+ } else {
+ $endl = "\n";
+ }
+
+ if ( is_array( $body ) ) {
+ // we are sending a multipart message
+ wfDebug( "Assembling multipart mime email\n" );
+ if ( !stream_resolve_include_path( 'Mail/mime.php' ) ) {
+ wfDebug( "PEAR Mail_Mime package is not installed. Falling back to text email.\n" );
+ // remove the html body for text email fall back
+ $body = $body['text'];
+ }
+ else {
+ require_once 'Mail/mime.php';
+ if ( wfIsWindows() ) {
+ $body['text'] = str_replace( "\n", "\r\n", $body['text'] );
+ $body['html'] = str_replace( "\n", "\r\n", $body['html'] );
+ }
+ $mime = new Mail_mime( array( 'eol' => $endl, 'text_charset' => 'UTF-8', 'html_charset' => 'UTF-8' ) );
+ $mime->setTXTBody( $body['text'] );
+ $mime->setHTMLBody( $body['html'] );
+ $body = $mime->get(); // must call get() before headers()
+ $headers = $mime->headers( $headers );
+ }
+ }
+ if ( !isset( $mime ) ) {
+ // sending text only, either deliberately or as a fallback
+ if ( wfIsWindows() ) {
+ $body = str_replace( "\n", "\r\n", $body );
+ }
+ $headers['MIME-Version'] = '1.0';
+ $headers['Content-type'] = ( is_null( $contentType ) ?
+ 'text/plain; charset=UTF-8' : $contentType );
+ $headers['Content-transfer-encoding'] = '8bit';
+ }
+
$ret = wfRunHooks( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) );
if ( $ret === false ) {
+ // the hook implementation will return false to skip regular mail sending
return Status::newGood();
} elseif ( $ret !== true ) {
+ // the hook implementation will return a string to pass an error message
return Status::newFatal( 'php-mail-error', $ret );
}
if ( is_array( $wgSMTP ) ) {
#
# PEAR MAILER
- #
+ #
- if ( function_exists( 'stream_resolve_include_path' ) ) {
- $found = stream_resolve_include_path( 'Mail.php' );
- } else {
- $found = Fallback::stream_resolve_include_path( 'Mail.php' );
- }
- if ( !$found ) {
+ if ( !stream_resolve_include_path( 'Mail.php' ) ) {
throw new MWException( 'PEAR mail package is not installed' );
}
- require_once( 'Mail.php' );
+ require_once 'Mail.php';
wfSuppressWarnings();
@@ -260,7 +324,7 @@ class UserMailer {
}
# Split jobs since SMTP servers tends to limit the maximum
- # number of possible recipients.
+ # number of possible recipients.
$chunks = array_chunk( $to, $wgEnotifMaxRecips );
foreach ( $chunks as $chunk ) {
$status = self::sendWithPear( $mail_object, $chunk, $headers, $body );
@@ -272,21 +336,11 @@ class UserMailer {
}
wfRestoreWarnings();
return Status::newGood();
- } else {
- #
+ } else {
+ #
# PHP mail()
#
-
- # Line endings need to be different on Unix and Windows due to
- # the bug described at http://trac.wordpress.org/ticket/2603
- if ( wfIsWindows() ) {
- $body = str_replace( "\n", "\r\n", $body );
- $endl = "\r\n";
- } else {
- $endl = "\n";
- }
-
- if( count($to) > 1 ) {
+ if ( count( $to ) > 1 ) {
$headers['To'] = 'undisclosed-recipients:;';
}
$headers = self::arrayToHeaderString( $headers, $endl );
@@ -299,6 +353,7 @@ class UserMailer {
set_error_handler( 'UserMailer::errorHandler' );
$safeMode = wfIniGetBool( 'safe_mode' );
+
foreach ( $to as $recip ) {
if ( $safeMode ) {
$sent = mail( $recip, self::quotedPrintable( $subject ), $body, $headers );
@@ -327,7 +382,7 @@ class UserMailer {
* Set the mail error message in self::$mErrorString
*
* @param $code Integer: error number
- * @param $string String: error message
+ * @param string $string error message
*/
static function errorHandler( $code, $string ) {
self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string );
@@ -346,11 +401,17 @@ class UserMailer {
/**
* Converts a string into quoted-printable format
* @since 1.17
+ *
+ * From PHP5.3 there is a built in function quoted_printable_encode()
+ * This method does not duplicate that.
+ * This method is doing Q encoding inside encoded-words as defined by RFC 2047
+ * This is for email headers.
+ * The built in quoted_printable_encode() is for email bodies
* @return string
*/
public static function quotedPrintable( $string, $charset = '' ) {
# Probably incomplete; see RFC 2045
- if( empty( $charset ) ) {
+ if ( empty( $charset ) ) {
$charset = 'UTF-8';
}
$charset = strtoupper( $charset );
@@ -358,7 +419,7 @@ class UserMailer {
$illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff=';
$replace = $illegal . '\t ?_';
- if( !preg_match( "/[$illegal]/", $string ) ) {
+ if ( !preg_match( "/[$illegal]/", $string ) ) {
return $string;
}
$out = "=?$charset?Q?";
@@ -395,7 +456,7 @@ class UserMailer {
*/
class EmailNotification {
protected $subject, $body, $replyto, $from;
- protected $timestamp, $summary, $minorEdit, $oldid, $composed_common;
+ protected $timestamp, $summary, $minorEdit, $oldid, $composed_common, $pageStatus;
protected $mailTargets = array();
/**
@@ -420,8 +481,9 @@ class EmailNotification {
* @param $summary
* @param $minorEdit
* @param $oldid (default: false)
+ * @param $pageStatus (default: 'changed')
*/
- public function notifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid = false ) {
+ public function notifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid = false, $pageStatus = 'changed' ) {
global $wgEnotifUseJobQ, $wgEnotifWatchlist, $wgShowUpdatedMarker, $wgEnotifMinorEdits,
$wgUsersNotifiedOnAllChanges, $wgEnotifUserTalk;
@@ -429,7 +491,7 @@ class EmailNotification {
return;
}
- // Build a list of users to notfiy
+ // Build a list of users to notify
$watchers = array();
if ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) {
$dbw = wfGetDB( DB_MASTER );
@@ -446,19 +508,23 @@ class EmailNotification {
$watchers[] = intval( $row->wl_user );
}
if ( $watchers ) {
- // Update wl_notificationtimestamp for all watching users except
- // the editor
- $dbw->begin( __METHOD__ );
- $dbw->update( 'watchlist',
- array( /* SET */
- 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
- ), array( /* WHERE */
- 'wl_user' => $watchers,
- 'wl_namespace' => $title->getNamespace(),
- 'wl_title' => $title->getDBkey(),
- ), __METHOD__
+ // Update wl_notificationtimestamp for all watching users except the editor
+ $fname = __METHOD__;
+ $dbw->onTransactionIdle(
+ function() use ( $dbw, $timestamp, $watchers, $title, $fname ) {
+ $dbw->begin( $fname );
+ $dbw->update( 'watchlist',
+ array( /* SET */
+ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
+ ), array( /* WHERE */
+ 'wl_user' => $watchers,
+ 'wl_namespace' => $title->getNamespace(),
+ 'wl_title' => $title->getDBkey(),
+ ), $fname
+ );
+ $dbw->commit( $fname );
+ }
);
- $dbw->commit( __METHOD__ );
}
}
@@ -488,12 +554,13 @@ class EmailNotification {
'summary' => $summary,
'minorEdit' => $minorEdit,
'oldid' => $oldid,
- 'watchers' => $watchers
+ 'watchers' => $watchers,
+ 'pageStatus' => $pageStatus
);
$job = new EnotifNotifyJob( $title, $params );
- $job->insert();
+ JobQueueGroup::singleton()->push( $job );
} else {
- $this->actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers );
+ $this->actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus );
}
}
@@ -505,13 +572,16 @@ class EmailNotification {
*
* @param $editor User object
* @param $title Title object
- * @param $timestamp string Edit timestamp
- * @param $summary string Edit summary
+ * @param string $timestamp Edit timestamp
+ * @param string $summary Edit summary
* @param $minorEdit bool
- * @param $oldid int Revision ID
- * @param $watchers array of user IDs
+ * @param int $oldid Revision ID
+ * @param array $watchers of user IDs
+ * @param string $pageStatus
+ * @throws MWException
*/
- public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers ) {
+ public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit,
+ $oldid, $watchers, $pageStatus = 'changed' ) {
# we use $wgPasswordSender as sender's address
global $wgEnotifWatchlist;
global $wgEnotifMinorEdits, $wgEnotifUserTalk;
@@ -531,6 +601,15 @@ class EmailNotification {
$this->oldid = $oldid;
$this->editor = $editor;
$this->composed_common = false;
+ $this->pageStatus = $pageStatus;
+
+ $formattedPageStatus = array( 'deleted', 'created', 'moved', 'restored', 'changed' );
+
+ wfRunHooks( 'UpdateUserMailerFormattedPageStatus', array( &$formattedPageStatus ) );
+ if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( 'Not a valid page status!' );
+ }
$userTalkId = false;
@@ -591,11 +670,13 @@ class EmailNotification {
} elseif ( $targetUser->getOption( 'enotifusertalkpages' ) &&
( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) ) )
{
- if ( $targetUser->isEmailConfirmed() ) {
+ if ( !$targetUser->isEmailConfirmed() ) {
+ wfDebug( __METHOD__ . ": talk page owner doesn't have validated email\n" );
+ } elseif ( !wfRunHooks( 'AbortTalkPageEmailNotification', array( $targetUser, $title ) ) ) {
+ wfDebug( __METHOD__ . ": talk page update notification is aborted for this user\n" );
+ } else {
wfDebug( __METHOD__ . ": sending talk page update notification\n" );
return true;
- } else {
- wfDebug( __METHOD__ . ": talk page owner doesn't have validated email\n" );
}
} else {
wfDebug( __METHOD__ . ": talk page owner doesn't want notifications\n" );
@@ -620,56 +701,70 @@ class EmailNotification {
$keys = array();
$postTransformKeys = array();
+ $pageTitleUrl = $this->title->getCanonicalURL();
+ $pageTitle = $this->title->getPrefixedText();
if ( $this->oldid ) {
// Always show a link to the diff which triggered the mail. See bug 32210.
- $keys['$NEWPAGE'] = wfMessage( 'enotif_lastdiff',
- $this->title->getCanonicalUrl( 'diff=next&oldid=' . $this->oldid ) )
+ $keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff',
+ $this->title->getCanonicalURL( array( 'diff' => 'next', 'oldid' => $this->oldid ) ) )
->inContentLanguage()->text();
+
if ( !$wgEnotifImpersonal ) {
// For personal mail, also show a link to the diff of all changes
// since last visited.
- $keys['$NEWPAGE'] .= " \n" . wfMessage( 'enotif_lastvisited',
- $this->title->getCanonicalUrl( 'diff=0&oldid=' . $this->oldid ) )
+ $keys['$NEWPAGE'] .= "\n\n" . wfMessage( 'enotif_lastvisited',
+ $this->title->getCanonicalURL( array( 'diff' => '0', 'oldid' => $this->oldid ) ) )
->inContentLanguage()->text();
}
- $keys['$OLDID'] = $this->oldid;
+ $keys['$OLDID'] = $this->oldid;
+ // @deprecated Remove in MediaWiki 1.23.
$keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
} else {
- $keys['$NEWPAGE'] = wfMessage( 'enotif_newpagetext' )->inContentLanguage()->text();
# clear $OLDID placeholder in the message template
- $keys['$OLDID'] = '';
+ $keys['$OLDID'] = '';
+ $keys['$NEWPAGE'] = '';
+ // @deprecated Remove in MediaWiki 1.23.
$keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
}
$keys['$PAGETITLE'] = $this->title->getPrefixedText();
- $keys['$PAGETITLE_URL'] = $this->title->getCanonicalUrl();
+ $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
$keys['$PAGEMINOREDIT'] = $this->minorEdit ?
wfMessage( 'minoredit' )->inContentLanguage()->text() : '';
- $keys['$UNWATCHURL'] = $this->title->getCanonicalUrl( 'action=unwatch' );
+ $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
if ( $this->editor->isAnon() ) {
# real anon (user:xxx.xxx.xxx.xxx)
$keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
->inContentLanguage()->text();
$keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
+
} else {
$keys['$PAGEEDITOR'] = $wgEnotifUseRealName ? $this->editor->getRealName() : $this->editor->getName();
$emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
- $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalUrl();
+ $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
}
- $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalUrl();
+ $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
# Replace this after transforming the message, bug 35019
$postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
- # Now build message's subject and body
+ // Now build message's subject and body
+
+ // Messages:
+ // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
+ // enotif_subject_restored, enotif_subject_changed
+ $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
+ ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
- $subject = wfMessage( 'enotif_subject' )->inContentLanguage()->plain();
- $subject = strtr( $subject, $keys );
- $subject = MessageCache::singleton()->transform( $subject, false, null, $this->title );
- $this->subject = strtr( $subject, $postTransformKeys );
+ // Messages:
+ // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
+ // enotif_body_intro_restored, enotif_body_intro_changed
+ $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
+ ->inContentLanguage()->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
+ ->text();
$body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
$body = strtr( $body, $keys );
@@ -686,13 +781,13 @@ class EmailNotification {
{
$editorAddress = new MailAddress( $this->editor );
if ( $wgEnotifFromEditor ) {
- $this->from = $editorAddress;
+ $this->from = $editorAddress;
} else {
- $this->from = $adminAddress;
+ $this->from = $adminAddress;
$this->replyto = $editorAddress;
}
} else {
- $this->from = $adminAddress;
+ $this->from = $adminAddress;
$this->replyto = new MailAddress( $wgNoReplyAddress );
}
}
@@ -707,8 +802,9 @@ class EmailNotification {
function compose( $user ) {
global $wgEnotifImpersonal;
- if ( !$this->composed_common )
+ if ( !$this->composed_common ) {
$this->composeCommonMailtext();
+ }
if ( $wgEnotifImpersonal ) {
$this->mailTargets[] = new MailAddress( $user );
@@ -761,13 +857,15 @@ class EmailNotification {
/**
* Same as sendPersonalised but does impersonal mail suitable for bulk
* mailing. Takes an array of MailAddress objects.
- * @return Status
+ * @param $addresses array
+ * @return Status|null
*/
function sendImpersonal( $addresses ) {
global $wgContLang;
- if ( empty( $addresses ) )
- return;
+ if ( empty( $addresses ) ) {
+ return null;
+ }
$body = str_replace(
array( '$WATCHINGUSERNAME',
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
index 26ac3dcd..a8a22be7 100644
--- a/includes/UserRightsProxy.php
+++ b/includes/UserRightsProxy.php
@@ -32,8 +32,8 @@ class UserRightsProxy {
* @see newFromId()
* @see newFromName()
* @param $db DatabaseBase: db connection
- * @param $database String: database name
- * @param $name String: user name
+ * @param string $database database name
+ * @param string $name user name
* @param $id Integer: user ID
*/
private function __construct( $db, $database, $name, $id ) {
@@ -56,7 +56,7 @@ class UserRightsProxy {
/**
* Confirm the selected database name is a valid local interwiki database name.
*
- * @param $database String: database name
+ * @param string $database database name
* @return Boolean
*/
public static function validDatabase( $database ) {
@@ -67,14 +67,14 @@ class UserRightsProxy {
/**
* Same as User::whoIs()
*
- * @param $database String: database name
+ * @param string $database database name
* @param $id Integer: user ID
* @param $ignoreInvalidDB Boolean: if true, don't check if $database is in $wgLocalDatabases
* @return String: user name or false if the user doesn't exist
*/
public static function whoIs( $database, $id, $ignoreInvalidDB = false ) {
$user = self::newFromId( $database, $id, $ignoreInvalidDB );
- if( $user ) {
+ if ( $user ) {
return $user->name;
} else {
return false;
@@ -84,7 +84,7 @@ class UserRightsProxy {
/**
* Factory function; get a remote user entry by ID number.
*
- * @param $database String: database name
+ * @param string $database database name
* @param $id Integer: user ID
* @param $ignoreInvalidDB Boolean: if true, don't check if $database is in $wgLocalDatabases
* @return UserRightsProxy or null if doesn't exist
@@ -96,8 +96,8 @@ class UserRightsProxy {
/**
* Factory function; get a remote user entry by name.
*
- * @param $database String: database name
- * @param $name String: user name
+ * @param string $database database name
+ * @param string $name user name
* @param $ignoreInvalidDB Boolean: if true, don't check if $database is in $wgLocalDatabases
* @return UserRightsProxy or null if doesn't exist
*/
@@ -114,12 +114,12 @@ class UserRightsProxy {
*/
private static function newFromLookup( $database, $field, $value, $ignoreInvalidDB = false ) {
$db = self::getDB( $database, $ignoreInvalidDB );
- if( $db ) {
+ if ( $db ) {
$row = $db->selectRow( 'user',
array( 'user_id', 'user_name' ),
array( $field => $value ),
__METHOD__ );
- if( $row !== false ) {
+ if ( $row !== false ) {
return new UserRightsProxy( $db, $database,
$row->user_name,
intval( $row->user_id ) );
@@ -138,8 +138,8 @@ class UserRightsProxy {
*/
public static function getDB( $database, $ignoreInvalidDB = false ) {
global $wgDBname;
- if( self::validDatabase( $database ) ) {
- if( $database == $wgDBname ) {
+ if ( $ignoreInvalidDB || self::validDatabase( $database ) ) {
+ if ( $database == $wgDBname ) {
// Hmm... this shouldn't happen though. :)
return wfGetDB( DB_MASTER );
} else {
diff --git a/includes/ViewCountUpdate.php b/includes/ViewCountUpdate.php
index 28ba3414..22a46493 100644
--- a/includes/ViewCountUpdate.php
+++ b/includes/ViewCountUpdate.php
@@ -53,16 +53,13 @@ class ViewCountUpdate implements DeferrableUpdate {
}
# Not important enough to warrant an error page in case of failure
- $oldignore = $dbw->ignoreErrors( true );
-
- $dbw->insert( 'hitcounter', array( 'hc_id' => $this->id ), __METHOD__ );
-
- $checkfreq = intval( $wgHitcounterUpdateFreq / 25 + 1 );
- if ( rand() % $checkfreq == 0 && $dbw->lastErrno() == 0 ) {
- $this->collect();
- }
-
- $dbw->ignoreErrors( $oldignore );
+ try {
+ $dbw->insert( 'hitcounter', array( 'hc_id' => $this->id ), __METHOD__ );
+ $checkfreq = intval( $wgHitcounterUpdateFreq / 25 + 1 );
+ if ( rand() % $checkfreq == 0 && $dbw->lastErrno() == 0 ) {
+ $this->collect();
+ }
+ } catch ( DBError $e ) {}
}
protected function collect() {
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index 932af169..1e07e7c7 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -27,38 +27,75 @@
* @ingroup Watchlist
*/
class WatchedItem {
- var $mTitle, $mUser, $id, $ns, $ti;
+ /**
+ * Constant to specify that user rights 'editmywatchlist' and
+ * 'viewmywatchlist' should not be checked.
+ * @since 1.22
+ */
+ const IGNORE_USER_RIGHTS = 0;
+
+ /**
+ * Constant to specify that user rights 'editmywatchlist' and
+ * 'viewmywatchlist' should be checked.
+ * @since 1.22
+ */
+ const CHECK_USER_RIGHTS = 1;
+
+ var $mTitle, $mUser, $mCheckRights;
private $loaded = false, $watched, $timestamp;
/**
* Create a WatchedItem object with the given user and title
+ * @since 1.22 $checkRights parameter added
* @param $user User: the user to use for (un)watching
* @param $title Title: the title we're going to (un)watch
+ * @param $checkRights int: Whether to check the 'viewmywatchlist' and 'editmywatchlist' rights.
+ * Pass either WatchedItem::IGNORE_USER_RIGHTS or WatchedItem::CHECK_USER_RIGHTS.
* @return WatchedItem object
*/
- public static function fromUserTitle( $user, $title ) {
+ public static function fromUserTitle( $user, $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
$wl = new WatchedItem;
$wl->mUser = $user;
$wl->mTitle = $title;
- $wl->id = $user->getId();
- # Patch (also) for email notification on page changes T.Gries/M.Arndt 11.09.2004
- # TG patch: here we do not consider pages and their talk pages equivalent - why should we ?
- # The change results in talk-pages not automatically included in watchlists, when their parent page is included
- # $wl->ns = $title->getNamespace() & ~1;
- $wl->ns = $title->getNamespace();
-
- $wl->ti = $title->getDBkey();
+ $wl->mCheckRights = $checkRights;
+
return $wl;
}
/**
+ * Title being watched
+ * @return Title
+ */
+ protected function getTitle() {
+ return $this->mTitle;
+ }
+
+ /** Helper to retrieve the title namespace */
+ protected function getTitleNs() {
+ return $this->getTitle()->getNamespace();
+ }
+
+ /** Helper to retrieve the title DBkey */
+ protected function getTitleDBkey() {
+ return $this->getTitle()->getDBkey();
+ }
+ /** Helper to retrieve the user id */
+ protected function getUserId() {
+ return $this->mUser->getId();
+ }
+
+ /**
* Return an array of conditions to select or update the appropriate database
* row.
*
* @return array
*/
private function dbCond() {
- return array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns, 'wl_title' => $this->ti );
+ return array(
+ 'wl_user' => $this->getUserId(),
+ 'wl_namespace' => $this->getTitleNs(),
+ 'wl_title' => $this->getTitleDBkey(),
+ );
}
/**
@@ -70,6 +107,12 @@ class WatchedItem {
}
$this->loaded = true;
+ // Only loggedin user can have a watchlist
+ if ( $this->mUser->isAnon() ) {
+ $this->watched = false;
+ return;
+ }
+
# Pages and their talk pages are considered equivalent for watching;
# remember that talk namespaces are numbered as page namespace+1.
@@ -86,10 +129,22 @@ class WatchedItem {
}
/**
+ * Check permissions
+ * @param $what string: 'viewmywatchlist' or 'editmywatchlist'
+ */
+ private function isAllowed( $what ) {
+ return !$this->mCheckRights || $this->mUser->isAllowed( $what );
+ }
+
+ /**
* Is mTitle being watched by mUser?
* @return bool
*/
public function isWatched() {
+ if ( !$this->isAllowed( 'viewmywatchlist' ) ) {
+ return false;
+ }
+
$this->load();
return $this->watched;
}
@@ -101,6 +156,10 @@ class WatchedItem {
* the wl_notificationtimestamp field otherwise
*/
public function getNotificationTimestamp() {
+ if ( !$this->isAllowed( 'viewmywatchlist' ) ) {
+ return false;
+ }
+
$this->load();
if ( $this->watched ) {
return $this->timestamp;
@@ -116,6 +175,11 @@ class WatchedItem {
* page is not watched or the notification timestamp is already NULL.
*/
public function resetNotificationTimestamp( $force = '' ) {
+ // Only loggedin user can have a watchlist
+ if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) {
+ return;
+ }
+
if ( $force != 'force' ) {
$this->load();
if ( !$this->watched || $this->timestamp === null ) {
@@ -134,31 +198,37 @@ class WatchedItem {
/**
* Given a title and user (assumes the object is setup), add the watch to the
* database.
- * @return bool (always true)
+ * @return bool
*/
public function addWatch() {
wfProfileIn( __METHOD__ );
+ // Only loggedin user can have a watchlist
+ if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
// Use INSERT IGNORE to avoid overwriting the notification timestamp
// if there's already an entry for this page
$dbw = wfGetDB( DB_MASTER );
$dbw->insert( 'watchlist',
- array(
- 'wl_user' => $this->id,
- 'wl_namespace' => MWNamespace::getSubject($this->ns),
- 'wl_title' => $this->ti,
- 'wl_notificationtimestamp' => null
- ), __METHOD__, 'IGNORE' );
+ array(
+ 'wl_user' => $this->getUserId(),
+ 'wl_namespace' => MWNamespace::getSubject( $this->getTitleNs() ),
+ 'wl_title' => $this->getTitleDBkey(),
+ 'wl_notificationtimestamp' => null
+ ), __METHOD__, 'IGNORE' );
// Every single watched page needs now to be listed in watchlist;
// namespace:page and namespace_talk:page need separate entries:
$dbw->insert( 'watchlist',
- array(
- 'wl_user' => $this->id,
- 'wl_namespace' => MWNamespace::getTalk($this->ns),
- 'wl_title' => $this->ti,
- 'wl_notificationtimestamp' => null
- ), __METHOD__, 'IGNORE' );
+ array(
+ 'wl_user' => $this->getUserId(),
+ 'wl_namespace' => MWNamespace::getTalk( $this->getTitleNs() ),
+ 'wl_title' => $this->getTitleDBkey(),
+ 'wl_notificationtimestamp' => null
+ ), __METHOD__, 'IGNORE' );
$this->watched = true;
@@ -173,28 +243,34 @@ class WatchedItem {
public function removeWatch() {
wfProfileIn( __METHOD__ );
+ // Only loggedin user can have a watchlist
+ if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
$success = false;
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'watchlist',
array(
- 'wl_user' => $this->id,
- 'wl_namespace' => MWNamespace::getSubject($this->ns),
- 'wl_title' => $this->ti
+ 'wl_user' => $this->getUserId(),
+ 'wl_namespace' => MWNamespace::getSubject( $this->getTitleNs() ),
+ 'wl_title' => $this->getTitleDBkey(),
), __METHOD__
);
if ( $dbw->affectedRows() ) {
$success = true;
}
- # the following code compensates the new behaviour, introduced by the
+ # the following code compensates the new behavior, introduced by the
# enotif patch, that every single watched page needs now to be listed
# in watchlist namespace:page and namespace_talk:page had separate
# entries: clear them
$dbw->delete( 'watchlist',
array(
- 'wl_user' => $this->id,
- 'wl_namespace' => MWNamespace::getTalk($this->ns),
- 'wl_title' => $this->ti
+ 'wl_user' => $this->getUserId(),
+ 'wl_namespace' => MWNamespace::getTalk( $this->getTitleNs() ),
+ 'wl_title' => $this->getTitleDBkey(),
), __METHOD__
);
@@ -249,7 +325,7 @@ class WatchedItem {
);
}
- if( empty( $values ) ) {
+ if ( empty( $values ) ) {
// Nothing to do
return true;
}
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index 96279fb2..b17cb9ec 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -1,6 +1,6 @@
<?php
/**
- * Deal with importing all those nasssty globals and things
+ * Deal with importing all those nasty globals and things
*
* Copyright © 2003 Brion Vibber <brion@pobox.com>
* http://www.mediawiki.org/
@@ -70,13 +70,13 @@ class WebRequest {
* If the REQUEST_URI is not provided we'll fall back on the PATH_INFO
* provided by the server if any and use that to set a 'title' parameter.
*
- * @param $want string: If this is not 'all', then the function
+ * @param string $want If this is not 'all', then the function
* will return an empty array if it determines that the URL is
* inside a rewrite path.
*
* @return Array: Any query arguments found in path matches.
*/
- static public function getPathInfo( $want = 'all' ) {
+ public static function getPathInfo( $want = 'all' ) {
global $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.
@@ -91,11 +91,11 @@ class WebRequest {
wfSuppressWarnings();
$a = parse_url( $url );
wfRestoreWarnings();
- if( $a ) {
+ if ( $a ) {
$path = isset( $a['path'] ) ? $a['path'] : '';
global $wgScript;
- if( $path == $wgScript && $want !== 'all' ) {
+ if ( $path == $wgScript && $want !== 'all' ) {
// Script inside a rewrite path?
// Abort to keep from breaking...
return $matches;
@@ -106,7 +106,7 @@ class WebRequest {
// Raw PATH_INFO style
$router->add( "$wgScript/$1" );
- if( isset( $_SERVER['SCRIPT_NAME'] )
+ if ( isset( $_SERVER['SCRIPT_NAME'] )
&& preg_match( '/\.php5?/', $_SERVER['SCRIPT_NAME'] ) )
{
# Check for SCRIPT_NAME, we handle index.php explicitly
@@ -116,19 +116,19 @@ class WebRequest {
}
global $wgArticlePath;
- if( $wgArticlePath ) {
+ if ( $wgArticlePath ) {
$router->add( $wgArticlePath );
}
global $wgActionPaths;
- if( $wgActionPaths ) {
+ if ( $wgActionPaths ) {
$router->add( $wgActionPaths, array( 'action' => '$key' ) );
}
global $wgVariantArticlePath, $wgContLang;
- if( $wgVariantArticlePath ) {
+ if ( $wgVariantArticlePath ) {
$router->add( $wgVariantArticlePath,
- array( 'variant' => '$2'),
+ array( 'variant' => '$2' ),
array( '$2' => $wgContLang->getVariants() )
);
}
@@ -144,7 +144,7 @@ class WebRequest {
// Also reported when ini_get('cgi.fix_pathinfo')==false
$matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
- } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) {
+ } elseif ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
// Regular old PATH_INFO yay
$matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
}
@@ -192,14 +192,21 @@ class WebRequest {
* @return array
*/
public static function detectProtocolAndStdPort() {
- return ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] == 'on' ) ? array( 'https', 443 ) : array( 'http', 80 );
+ if ( ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] == 'on' ) ||
+ ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
+ $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ) ) {
+ $arr = array( 'https', 443 );
+ } else {
+ $arr = array( 'http', 80 );
+ }
+ return $arr;
}
/**
* @return string
*/
public static function detectProtocol() {
- list( $proto, $stdPort ) = self::detectProtocolAndStdPort();
+ list( $proto, ) = self::detectProtocolAndStdPort();
return $proto;
}
@@ -217,7 +224,7 @@ class WebRequest {
}
$matches = self::getPathInfo( 'title' );
- foreach( $matches as $key => $val) {
+ foreach ( $matches as $key => $val ) {
$this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
}
}
@@ -226,22 +233,22 @@ class WebRequest {
* URL rewriting function; tries to extract page title and,
* optionally, one other fixed parameter value from a URL path.
*
- * @param $path string: the URL path given from the client
- * @param $bases array: one or more URLs, optionally with $1 at the end
- * @param $key string: if provided, the matching key in $bases will be
+ * @param string $path the URL path given from the client
+ * @param array $bases one or more URLs, optionally with $1 at the end
+ * @param string $key if provided, the matching key in $bases will be
* passed on as the value of this URL parameter
* @return array of URL variables to interpolate; empty if no match
*/
static function extractTitle( $path, $bases, $key = false ) {
- foreach( (array)$bases as $keyValue => $base ) {
+ foreach ( (array)$bases as $keyValue => $base ) {
// Find the part after $wgArticlePath
$base = str_replace( '$1', '', $base );
$baseLen = strlen( $base );
- if( substr( $path, 0, $baseLen ) == $base ) {
+ if ( substr( $path, 0, $baseLen ) == $base ) {
$raw = substr( $path, $baseLen );
- if( $raw !== '' ) {
+ if ( $raw !== '' ) {
$matches = array( 'title' => rawurldecode( $raw ) );
- if( $key ) {
+ if ( $key ) {
$matches[$key] = $keyValue;
}
return $matches;
@@ -255,8 +262,8 @@ class WebRequest {
* Recursively strips slashes from the given array;
* used for undoing the evil that is magic_quotes_gpc.
*
- * @param $arr array: will be modified
- * @param $topLevel bool Specifies if the array passed is from the top
+ * @param array $arr will be modified
+ * @param bool $topLevel Specifies if the array passed is from the top
* level of the source. In PHP5 magic_quotes only escapes the first level
* of keys that belong to an array.
* @return array the original array
@@ -264,8 +271,8 @@ class WebRequest {
*/
private function &fix_magic_quotes( &$arr, $topLevel = true ) {
$clean = array();
- foreach( $arr as $key => $val ) {
- if( is_array( $val ) ) {
+ foreach ( $arr as $key => $val ) {
+ if ( is_array( $val ) ) {
$cleanKey = $topLevel ? stripslashes( $key ) : $key;
$clean[$cleanKey] = $this->fix_magic_quotes( $arr[$key], false );
} else {
@@ -286,7 +293,7 @@ class WebRequest {
private function checkMagicQuotes() {
$mustFixQuotes = function_exists( 'get_magic_quotes_gpc' )
&& get_magic_quotes_gpc();
- if( $mustFixQuotes ) {
+ if ( $mustFixQuotes ) {
$this->fix_magic_quotes( $_COOKIE );
$this->fix_magic_quotes( $_ENV );
$this->fix_magic_quotes( $_GET );
@@ -304,8 +311,8 @@ class WebRequest {
* @private
*/
function normalizeUnicode( $data ) {
- if( is_array( $data ) ) {
- foreach( $data as $key => $val ) {
+ if ( is_array( $data ) ) {
+ foreach ( $data as $key => $val ) {
$data[$key] = $this->normalizeUnicode( $val );
}
} else {
@@ -328,19 +335,18 @@ class WebRequest {
# 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] ) ) {
+ if ( isset( $arr[$name] ) ) {
global $wgContLang;
$data = $arr[$name];
- if( isset( $_GET[$name] ) && !is_array( $data ) ) {
+ if ( isset( $_GET[$name] ) && !is_array( $data ) ) {
# Check for alternate/legacy character encoding.
- if( isset( $wgContLang ) ) {
+ if ( isset( $wgContLang ) ) {
$data = $wgContLang->checkTitleEncoding( $data );
}
}
$data = $this->normalizeUnicode( $data );
return $data;
} else {
- taint( $default );
return $default;
}
}
@@ -352,15 +358,15 @@ class WebRequest {
* selected by a drop-down menu). For freeform input, see getText().
*
* @param $name String
- * @param $default String: optional default (or NULL)
+ * @param string $default optional default (or NULL)
* @return String
*/
public function getVal( $name, $default = null ) {
$val = $this->getGPCVal( $this->data, $name, $default );
- if( is_array( $val ) ) {
+ if ( is_array( $val ) ) {
$val = $default;
}
- if( is_null( $val ) ) {
+ if ( is_null( $val ) ) {
return $val;
} else {
return (string)$val;
@@ -370,7 +376,7 @@ class WebRequest {
/**
* Set an arbitrary value into our get/post data.
*
- * @param $key String: key name to use
+ * @param string $key key name to use
* @param $value Mixed: value to set
* @return Mixed: old value if one was present, null otherwise
*/
@@ -380,11 +386,10 @@ class WebRequest {
return $ret;
}
-
/**
* Unset an arbitrary value from our get/post data.
- *
- * @param $key String: key name to use
+ *
+ * @param string $key key name to use
* @return Mixed: old value if one was present, null otherwise
*/
public function unsetVal( $key ) {
@@ -403,12 +408,12 @@ class WebRequest {
* If no source and no default, returns NULL.
*
* @param $name String
- * @param $default Array: optional default (or NULL)
+ * @param array $default optional default (or NULL)
* @return Array
*/
public function getArray( $name, $default = null ) {
$val = $this->getGPCVal( $this->data, $name, $default );
- if( is_null( $val ) ) {
+ if ( is_null( $val ) ) {
return null;
} else {
return (array)$val;
@@ -422,12 +427,12 @@ class WebRequest {
* If an array is returned, contents are guaranteed to be integers.
*
* @param $name String
- * @param $default Array: option default (or NULL)
+ * @param array $default option default (or NULL)
* @return Array of ints
*/
public function getIntArray( $name, $default = null ) {
$val = $this->getArray( $name, $default );
- if( is_array( $val ) ) {
+ if ( is_array( $val ) ) {
$val = array_map( 'intval', $val );
}
return $val;
@@ -497,7 +502,7 @@ class WebRequest {
*/
public function getCheck( $name ) {
# Checkboxes and buttons are only present when clicked
- # Presence connotes truth, abscense false
+ # Presence connotes truth, absence false
return $this->getVal( $name, null ) !== null;
}
@@ -510,7 +515,7 @@ class WebRequest {
* be required - e.g. Esperanto x-coding).
*
* @param $name String
- * @param $default String: optional
+ * @param string $default optional
* @return String
*/
public function getText( $name, $default = '' ) {
@@ -559,9 +564,47 @@ class WebRequest {
*
* @return Array
*/
- public function getQueryValues() {
+ public function getQueryValues() {
return $_GET;
- }
+ }
+
+ /**
+ * Return the contents of the Query with no decoding. Use when you need to
+ * know exactly what was sent, e.g. for an OAuth signature over the elements.
+ *
+ * @return String
+ */
+ public function getRawQueryString() {
+ return $_SERVER['QUERY_STRING'];
+ }
+
+ /**
+ * Return the contents of the POST with no decoding. Use when you need to
+ * know exactly what was sent, e.g. for an OAuth signature over the elements.
+ *
+ * @return String
+ */
+ public function getRawPostString() {
+ if ( !$this->wasPosted() ) {
+ return '';
+ }
+ return $this->getRawInput();
+ }
+
+ /**
+ * Return the raw request body, with no processing. Cached since some methods
+ * disallow reading the stream more than once. As stated in the php docs, this
+ * does not work with enctype="multipart/form-data".
+ *
+ * @return String
+ */
+ public function getRawInput() {
+ static $input = false;
+ if ( $input === false ) {
+ $input = file_get_contents( 'php://input' );
+ }
+ return $input;
+ }
/**
* Get the HTTP method used for this request.
@@ -597,40 +640,41 @@ class WebRequest {
* @return Boolean
*/
public function checkSessionCookie() {
- return isset( $_COOKIE[ session_name() ] );
+ return isset( $_COOKIE[session_name()] );
}
/**
* Get a cookie from the $_COOKIE jar
*
- * @param $key String: the name of the cookie
- * @param $prefix String: a prefix to use for the cookie name, if not $wgCookiePrefix
+ * @param string $key the name of the cookie
+ * @param string $prefix a prefix to use for the cookie name, if not $wgCookiePrefix
* @param $default Mixed: what to return if the value isn't found
* @return Mixed: cookie value or $default if the cookie not set
*/
public function getCookie( $key, $prefix = null, $default = null ) {
- if( $prefix === null ) {
+ if ( $prefix === null ) {
global $wgCookiePrefix;
$prefix = $wgCookiePrefix;
}
- return $this->getGPCVal( $_COOKIE, $prefix . $key , $default );
+ return $this->getGPCVal( $_COOKIE, $prefix . $key, $default );
}
/**
* Return the path and query string portion of the request URI.
* This will be suitable for use as a relative link in HTML output.
*
+ * @throws MWException
* @return String
*/
public function getRequestURL() {
- if( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
+ if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
$base = $_SERVER['REQUEST_URI'];
} elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] ) && strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] ) ) {
// Probably IIS; doesn't set REQUEST_URI
$base = $_SERVER['HTTP_X_ORIGINAL_URL'];
- } elseif( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
$base = $_SERVER['SCRIPT_NAME'];
- if( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
+ if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
$base .= '?' . $_SERVER['QUERY_STRING'];
}
} else {
@@ -644,14 +688,16 @@ class WebRequest {
// need to strip it or we get false-positive redirect loops
// or weird output URLs
$hash = strpos( $base, '#' );
- if( $hash !== false ) {
+ if ( $hash !== false ) {
$base = substr( $base, 0, $hash );
}
- if( $base[0] == '/' ) {
- return $base;
+
+ if ( $base[0] == '/' ) {
+ // More than one slash will look like it is protocol relative
+ return preg_replace( '!^/+!', '/', $base );
} else {
// We may get paths with a host prepended; strip it.
- return preg_replace( '!^[^:]+://[^/]+/!', '/', $base );
+ return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base );
}
}
@@ -671,7 +717,7 @@ class WebRequest {
/**
* Take an arbitrary query and rewrite the present URL to include it
- * @param $query String: query string fragment; do not include initial '?'
+ * @param string $query query string fragment; do not include initial '?'
*
* @return String
*/
@@ -683,7 +729,7 @@ class WebRequest {
* HTML-safe version of appendQuery().
* @deprecated: Deprecated in 1.20, warnings in 1.21, remove in 1.22.
*
- * @param $query String: query string fragment; do not include initial '?'
+ * @param string $query query string fragment; do not include initial '?'
* @return String
*/
public function escapeAppendQuery( $query ) {
@@ -703,8 +749,8 @@ class WebRequest {
/**
* Appends or replaces value of query variables.
*
- * @param $array Array of values to replace/add to query
- * @param $onlyquery Bool: whether to only return the query string and not
+ * @param array $array of values to replace/add to query
+ * @param bool $onlyquery whether to only return the query string and not
* the complete URL
* @return String
*/
@@ -713,7 +759,7 @@ class WebRequest {
$newquery = $this->getQueryValues();
unset( $newquery['title'] );
$newquery = array_merge( $newquery, $array );
- $query = wfArrayToCGI( $newquery );
+ $query = wfArrayToCgi( $newquery );
return $onlyquery ? $query : $wgTitle->getLocalURL( $query );
}
@@ -723,28 +769,28 @@ class WebRequest {
* Offset must be positive but is not capped.
*
* @param $deflimit Integer: limit to use if no input and the user hasn't set the option.
- * @param $optionname String: to specify an option other than rclimit to pull from.
+ * @param string $optionname to specify an option other than rclimit to pull from.
* @return array first element is limit, second is offset
*/
public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
global $wgUser;
$limit = $this->getInt( 'limit', 0 );
- if( $limit < 0 ) {
+ if ( $limit < 0 ) {
$limit = 0;
}
- if( ( $limit == 0 ) && ( $optionname != '' ) ) {
- $limit = (int)$wgUser->getOption( $optionname );
+ if ( ( $limit == 0 ) && ( $optionname != '' ) ) {
+ $limit = $wgUser->getIntOption( $optionname );
}
- if( $limit <= 0 ) {
+ if ( $limit <= 0 ) {
$limit = $deflimit;
}
- if( $limit > 5000 ) {
+ if ( $limit > 5000 ) {
$limit = 5000; # We have *some* limits...
}
$offset = $this->getInt( 'offset', 0 );
- if( $offset < 0 ) {
+ if ( $offset < 0 ) {
$offset = 0;
}
@@ -835,14 +881,15 @@ class WebRequest {
return;
}
- if ( function_exists( 'apache_request_headers' ) ) {
- foreach ( apache_request_headers() as $tempName => $tempValue ) {
- $this->headers[ strtoupper( $tempName ) ] = $tempValue;
+ $apacheHeaders = function_exists( 'apache_request_headers' ) ? apache_request_headers() : false;
+ if ( $apacheHeaders ) {
+ foreach ( $apacheHeaders as $tempName => $tempValue ) {
+ $this->headers[strtoupper( $tempName )] = $tempValue;
}
} else {
foreach ( $_SERVER as $name => $value ) {
if ( substr( $name, 0, 5 ) === 'HTTP_' ) {
- $name = str_replace( '_', '-', substr( $name, 5 ) );
+ $name = str_replace( '_', '-', substr( $name, 5 ) );
$this->headers[$name] = $value;
} elseif ( $name === 'CONTENT_LENGTH' ) {
$this->headers['CONTENT-LENGTH'] = $value;
@@ -863,7 +910,7 @@ class WebRequest {
/**
* Get a request header, or false if it isn't set
- * @param $name String: case-insensitive header name
+ * @param string $name case-insensitive header name
*
* @return string|bool False on failure
*/
@@ -880,11 +927,11 @@ class WebRequest {
/**
* Get data from $_SESSION
*
- * @param $key String: name of key in $_SESSION
+ * @param string $key name of key in $_SESSION
* @return Mixed
*/
public function getSessionData( $key ) {
- if( !isset( $_SESSION[$key] ) ) {
+ if ( !isset( $_SESSION[$key] ) ) {
return null;
}
return $_SESSION[$key];
@@ -893,7 +940,7 @@ class WebRequest {
/**
* Set session data
*
- * @param $key String: name of key in $_SESSION
+ * @param string $key name of key in $_SESSION
* @param $data Mixed
*/
public function setSessionData( $key, $data ) {
@@ -907,6 +954,7 @@ class WebRequest {
* false if an error message has been shown and the request should be aborted.
*
* @param $extWhitelist array
+ * @throws HttpError
* @return bool
*/
public function checkUrlExtension( $extWhitelist = array() ) {
@@ -1043,22 +1091,30 @@ HTML;
*
* @since 1.19
*
+ * @throws MWException
* @return String
*/
protected function getRawIP() {
- if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
- return IP::canonicalize( $_SERVER['REMOTE_ADDR'] );
- } else {
+ if ( !isset( $_SERVER['REMOTE_ADDR'] ) ) {
return null;
}
+
+ if ( is_array( $_SERVER['REMOTE_ADDR'] ) || strpos( $_SERVER['REMOTE_ADDR'], ',' ) !== false ) {
+ throw new MWException( __METHOD__ . " : Could not determine the remote IP address due to multiple values." );
+ } else {
+ $ipchain = $_SERVER['REMOTE_ADDR'];
+ }
+
+ return IP::canonicalize( $ipchain );
}
/**
* Work out the IP address based on various globals
* For trusted proxies, use the XFF client IP (first of the chain)
- *
+ *
* @since 1.19
*
+ * @throws MWException
* @return string
*/
public function getIP() {
@@ -1081,19 +1137,28 @@ HTML;
array_unshift( $ipchain, $ip );
}
- # Step through XFF list and find the last address in the list which is a trusted server
- # Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private)
+ # Step through XFF list and find the last address in the list which is a
+ # trusted server. Set $ip to the IP address given by that trusted server,
+ # unless the address is not sensible (e.g. private). However, prefer private
+ # IP addresses over proxy servers controlled by this site (more sensible).
foreach ( $ipchain as $i => $curIP ) {
- $curIP = IP::canonicalize( $curIP );
- if ( wfIsTrustedProxy( $curIP ) ) {
- if ( isset( $ipchain[$i + 1] ) ) {
- if ( $wgUsePrivateIPs || IP::isPublic( $ipchain[$i + 1 ] ) ) {
- $ip = $ipchain[$i + 1];
+ $curIP = IP::sanitizeIP( IP::canonicalize( $curIP ) );
+ if ( wfIsTrustedProxy( $curIP ) && isset( $ipchain[$i + 1] ) ) {
+ if ( wfIsConfiguredProxy( $curIP ) || // bug 48919; treat IP as sane
+ IP::isPublic( $ipchain[$i + 1] ) ||
+ $wgUsePrivateIPs
+ ) {
+ $nextIP = IP::canonicalize( $ipchain[$i + 1] );
+ if ( !$nextIP && wfIsConfiguredProxy( $ip ) ) {
+ // We have not yet made it past CDN/proxy servers of this site,
+ // so either they are misconfigured or there is some IP spoofing.
+ throw new MWException( "Invalid IP given in XFF '$forwardedFor'." );
}
+ $ip = $nextIP;
+ continue;
}
- } else {
- break;
}
+ break;
}
}
@@ -1101,13 +1166,22 @@ HTML;
wfRunHooks( 'GetIP', array( &$ip ) );
if ( !$ip ) {
- throw new MWException( "Unable to determine IP" );
+ throw new MWException( "Unable to determine IP." );
}
wfDebug( "IP: $ip\n" );
$this->ip = $ip;
return $ip;
}
+
+ /**
+ * @param string $ip
+ * @return void
+ * @since 1.21
+ */
+ public function setIP( $ip ) {
+ $this->ip = $ip;
+ }
}
/**
@@ -1122,7 +1196,7 @@ class WebRequestUpload {
* Constructor. Should only be called by WebRequest
*
* @param $request WebRequest The associated request
- * @param $key string Key in $_FILES array (name of form field)
+ * @param string $key Key in $_FILES array (name of form field)
*/
public function __construct( $request, $key ) {
$this->request = $request;
@@ -1234,20 +1308,22 @@ class FauxRequest extends WebRequest {
private $session = array();
/**
- * @param $data Array of *non*-urlencoded key => value pairs, the
+ * @param array $data of *non*-urlencoded key => value pairs, the
* fake GET/POST values
- * @param $wasPosted Bool: whether to treat the data as POST
+ * @param bool $wasPosted whether to treat the data as POST
* @param $session Mixed: session array or null
+ * @throws MWException
*/
public function __construct( $data = array(), $wasPosted = false, $session = null ) {
- if( is_array( $data ) ) {
+ if ( is_array( $data ) ) {
$this->data = $data;
} else {
throw new MWException( "FauxRequest() got bogus data" );
}
$this->wasPosted = $wasPosted;
- if( $session )
+ if ( $session ) {
$this->session = $session;
+ }
}
/**
@@ -1310,10 +1386,11 @@ class FauxRequest extends WebRequest {
}
/**
- * @param $name
+ * @param string $name The name of the header to get (case insensitive).
* @return bool|string
*/
public function getHeader( $name ) {
+ $name = strtoupper( $name );
return isset( $this->headers[$name] ) ? $this->headers[$name] : false;
}
@@ -1322,6 +1399,7 @@ class FauxRequest extends WebRequest {
* @param $val string
*/
public function setHeader( $name, $val ) {
+ $name = strtoupper( $name );
$this->headers[$name] = $val;
}
@@ -1330,8 +1408,10 @@ class FauxRequest extends WebRequest {
* @return mixed
*/
public function getSessionData( $key ) {
- if( isset( $this->session[$key] ) )
+ if ( isset( $this->session[$key] ) ) {
return $this->session[$key];
+ }
+ return null;
}
/**
@@ -1358,6 +1438,30 @@ class FauxRequest extends WebRequest {
}
/**
+ * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
+ * @return String
+ */
+ public function getRawQueryString() {
+ return '';
+ }
+
+ /**
+ * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
+ * @return String
+ */
+ public function getRawPostString() {
+ return '';
+ }
+
+ /**
+ * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
+ * @return String
+ */
+ public function getRawInput() {
+ return '';
+ }
+
+ /**
* @param array $extWhitelist
* @return bool
*/
diff --git a/includes/WebResponse.php b/includes/WebResponse.php
index 193101b1..ab7524c2 100644
--- a/includes/WebResponse.php
+++ b/includes/WebResponse.php
@@ -30,8 +30,8 @@ class WebResponse {
/**
* Output a HTTP header, wrapper for PHP's
* header()
- * @param $string String: header to output
- * @param $replace Bool: replace current similar header
+ * @param string $string header to output
+ * @param bool $replace replace current similar header
* @param $http_response_code null|int Forces the HTTP response code to the specified value.
*/
public function header( $string, $replace = true, $http_response_code = null ) {
@@ -40,42 +40,83 @@ class WebResponse {
/**
* Set the browser cookie
- * @param $name String: name of cookie
- * @param $value String: value to give cookie
- * @param $expire Int: number of seconds til cookie expires
- * @param $prefix String: Prefix to use, if not $wgCookiePrefix (use '' for no prefix)
- * @param @domain String: Cookie domain to use, if not $wgCookieDomain
+ * @param string $name name of cookie
+ * @param string $value value to give cookie
+ * @param int|null $expire Unix timestamp (in seconds) when the cookie should expire.
+ * 0 (the default) causes it to expire $wgCookieExpiration seconds from now.
+ * null causes it to be a session cookie.
+ * @param array $options Assoc of additional cookie options:
+ * prefix: string, name prefix ($wgCookiePrefix)
+ * domain: string, cookie domain ($wgCookieDomain)
+ * path: string, cookie path ($wgCookiePath)
+ * secure: bool, secure attribute ($wgCookieSecure)
+ * httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly)
+ * raw: bool, if true uses PHP's setrawcookie() instead of setcookie()
+ * For backwards compatability, if $options is not an array then it and
+ * the following two parameters will be interpreted as values for
+ * 'prefix', 'domain', and 'secure'
+ * @since 1.22 Replaced $prefix, $domain, and $forceSecure with $options
*/
- public function setcookie( $name, $value, $expire = 0, $prefix = null, $domain = null ) {
+ public function setcookie( $name, $value, $expire = 0, $options = null ) {
global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
- global $wgCookieSecure,$wgCookieExpiration, $wgCookieHttpOnly;
- if ( $expire == 0 ) {
+ global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly;
+
+ if ( !is_array( $options ) ) {
+ // Backwards compatability
+ $options = array( 'prefix' => $options );
+ if ( func_num_args() >= 5 ) {
+ $options['domain'] = func_get_arg( 4 );
+ }
+ if ( func_num_args() >= 6 ) {
+ $options['secure'] = func_get_arg( 5 );
+ }
+ }
+ $options = array_filter( $options, function ( $a ) {
+ return $a !== null;
+ } ) + array(
+ 'prefix' => $wgCookiePrefix,
+ 'domain' => $wgCookieDomain,
+ 'path' => $wgCookiePath,
+ 'secure' => $wgCookieSecure,
+ 'httpOnly' => $wgCookieHttpOnly,
+ 'raw' => false,
+ );
+
+ if ( $expire === null ) {
+ $expire = 0; // Session cookie
+ } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
$expire = time() + $wgCookieExpiration;
}
- if( $prefix === null ) {
- $prefix = $wgCookiePrefix;
+
+ // Don't mark the cookie as httpOnly if the requesting user-agent is
+ // known to have trouble with httpOnly cookies.
+ if ( !wfHttpOnlySafe() ) {
+ $options['httpOnly'] = false;
}
- if( $domain === null ) {
- $domain = $wgCookieDomain;
+
+ $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
+
+ if ( wfRunHooks( 'WebResponseSetCookie', array( &$name, &$value, &$expire, $options ) ) ) {
+ wfDebugLog( 'cookie',
+ $func . ': "' . implode( '", "',
+ array(
+ $options['prefix'] . $name,
+ $value,
+ $expire,
+ $options['path'],
+ $options['domain'],
+ $options['secure'],
+ $options['httpOnly'] ) ) . '"' );
+
+ call_user_func( $func,
+ $options['prefix'] . $name,
+ $value,
+ $expire,
+ $options['path'],
+ $options['domain'],
+ $options['secure'],
+ $options['httpOnly'] );
}
- $httpOnlySafe = wfHttpOnlySafe() && $wgCookieHttpOnly;
- wfDebugLog( 'cookie',
- 'setcookie: "' . implode( '", "',
- array(
- $prefix . $name,
- $value,
- $expire,
- $wgCookiePath,
- $domain,
- $wgCookieSecure,
- $httpOnlySafe ) ) . '"' );
- setcookie( $prefix . $name,
- $value,
- $expire,
- $wgCookiePath,
- $domain,
- $wgCookieSecure,
- $httpOnlySafe );
}
}
@@ -89,8 +130,8 @@ class FauxResponse extends WebResponse {
/**
* Stores a HTTP header
- * @param $string String: header to output
- * @param $replace Bool: replace current similar header
+ * @param string $string header to output
+ * @param bool $replace replace current similar header
* @param $http_response_code null|int Forces the HTTP response code to the specified value.
*/
public function header( $string, $replace = true, $http_response_code = null ) {
@@ -100,7 +141,9 @@ class FauxResponse extends WebResponse {
} else {
list( $key, $val ) = array_map( 'trim', explode( ":", $string, 2 ) );
- if( $replace || !isset( $this->headers[$key] ) ) {
+ $key = strtoupper( $key );
+
+ if ( $replace || !isset( $this->headers[$key] ) ) {
$this->headers[$key] = $val;
}
}
@@ -111,10 +154,12 @@ class FauxResponse extends WebResponse {
}
/**
- * @param $key string
+ * @param string $key The name of the header to get (case insensitive).
* @return string
*/
public function getheader( $key ) {
+ $key = strtoupper( $key );
+
if ( isset( $this->headers[$key] ) ) {
return $this->headers[$key];
}
@@ -133,14 +178,12 @@ class FauxResponse extends WebResponse {
/**
* @todo document. It just ignore optional parameters.
*
- * @param $name String: name of cookie
- * @param $value String: value to give cookie
- * @param $expire Int: number of seconds til cookie expires (Default: 0)
- * @param $prefix TODO DOCUMENT (Default: null)
- * @param $domain TODO DOCUMENT (Default: null)
- *
+ * @param string $name name of cookie
+ * @param string $value value to give cookie
+ * @param int $expire number of seconds til cookie expires (Default: 0)
+ * @param array $options ignored
*/
- public function setcookie( $name, $value, $expire = 0, $prefix = null, $domain = null ) {
+ public function setcookie( $name, $value, $expire = 0, $options = null ) {
$this->cookies[$name] = $value;
}
@@ -148,7 +191,7 @@ class FauxResponse extends WebResponse {
* @param $name string
* @return string
*/
- public function getcookie( $name ) {
+ public function getcookie( $name ) {
if ( isset( $this->cookies[$name] ) ) {
return $this->cookies[$name];
}
diff --git a/includes/WebStart.php b/includes/WebStart.php
index 01c5eea8..58c953af 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -27,7 +27,7 @@
# This must be done before any globals are set by the code
if ( ini_get( 'register_globals' ) ) {
if ( isset( $_REQUEST['GLOBALS'] ) || isset( $_FILES['GLOBALS'] ) ) {
- die( '<a href="http://www.hardened-php.net/globals-problem">$GLOBALS overwrite vulnerability</a>');
+ die( '<a href="http://www.hardened-php.net/globals-problem">$GLOBALS overwrite vulnerability</a>' );
}
$verboten = array(
'GLOBALS',
@@ -48,7 +48,7 @@ if ( ini_get( 'register_globals' ) ) {
'HTTP_SESSION_VARS'
);
foreach ( $_REQUEST as $name => $value ) {
- if( in_array( $name, $verboten ) ) {
+ if ( in_array( $name, $verboten ) ) {
header( "HTTP/1.1 500 Internal Server Error" );
echo "register_globals security paranoia: trying to overwrite superglobals, aborting.";
die( -1 );
@@ -57,12 +57,12 @@ if ( ini_get( 'register_globals' ) ) {
}
}
-# bug 15461: Make IE8 turn off content sniffing. Everbody else should ignore this
+# bug 15461: Make IE8 turn off content sniffing. Everybody else should ignore this
# We're adding it here so that it's *always* set, even for alternate entry
# points and when $wgOut gets disabled or overridden.
header( 'X-Content-Type-Options: nosniff' );
-$wgRequestTime = microtime(true);
+$wgRequestTime = microtime( true );
# getrusage() does not exist on the Microsoft Windows platforms, catching this
if ( function_exists ( 'getrusage' ) ) {
$wgRUstart = getrusage();
@@ -80,62 +80,60 @@ define( 'MEDIAWIKI', true );
# Full path to working directory.
# Makes it possible to for example to have effective exclude path in apc.
-# Also doesn't break installations using symlinked includes, like
-# __DIR__ would do.
+# __DIR__ breaks symlinked includes, but realpath() returns false
+# if we don't have permissions on parent directories.
$IP = getenv( 'MW_INSTALL_PATH' );
if ( $IP === false ) {
- $IP = realpath( '.' );
+ if ( realpath( '.' ) ) {
+ $IP = realpath( '.' );
+ } else {
+ $IP = dirname( __DIR__ );
+ }
}
-if ( isset( $_SERVER['MW_COMPILED'] ) ) {
- define( 'MW_COMPILED', 1 );
-} else {
- # Get MWInit class
- require_once( "$IP/includes/Init.php" );
+# Start the autoloader, so that extensions can derive classes from core files
+require_once "$IP/includes/AutoLoader.php";
- # Start the autoloader, so that extensions can derive classes from core files
- require_once( "$IP/includes/AutoLoader.php" );
+# Load the profiler
+require_once "$IP/includes/profiler/Profiler.php";
- # Load the profiler
- require_once( "$IP/includes/profiler/Profiler.php" );
-
- # Load up some global defines.
- require_once( "$IP/includes/Defines.php" );
-}
+# Load up some global defines.
+require_once "$IP/includes/Defines.php";
# Start the profiler
$wgProfiler = array();
if ( file_exists( "$IP/StartProfiler.php" ) ) {
- require( "$IP/StartProfiler.php" );
+ require "$IP/StartProfiler.php";
}
wfProfileIn( 'WebStart.php-conf' );
# Load default settings
-require_once( MWInit::compiledPath( "includes/DefaultSettings.php" ) );
+require_once "$IP/includes/DefaultSettings.php";
+
+# Load composer's autoloader if present
+if ( is_readable( "$IP/vendor/autoload.php" ) ) {
+ require_once "$IP/vendor/autoload.php";
+}
if ( defined( 'MW_CONFIG_CALLBACK' ) ) {
# Use a callback function to configure MediaWiki
- MWFunction::call( MW_CONFIG_CALLBACK );
+ call_user_func( MW_CONFIG_CALLBACK );
} else {
if ( !defined( 'MW_CONFIG_FILE' ) ) {
- define('MW_CONFIG_FILE', MWInit::interpretedPath( 'LocalSettings.php' ) );
+ define( 'MW_CONFIG_FILE', "$IP/LocalSettings.php" );
}
# LocalSettings.php is the per site customization file. If it does not exist
# the wiki installer needs to be launched or the generated file uploaded to
# the root wiki directory
- if( !file_exists( MW_CONFIG_FILE ) ) {
- require_once( "$IP/includes/templates/NoLocalSettings.php" );
+ if ( !file_exists( MW_CONFIG_FILE ) ) {
+ require_once "$IP/includes/templates/NoLocalSettings.php";
die();
}
# Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked)
- require_once( MW_CONFIG_FILE );
-}
-
-if ( $wgEnableSelenium ) {
- require_once( MWInit::compiledPath( "includes/SeleniumWebSettings.php" ) );
+ require_once MW_CONFIG_FILE;
}
wfProfileOut( 'WebStart.php-conf' );
@@ -146,14 +144,11 @@ wfProfileIn( 'WebStart.php-ob_start' );
# that would cause us to potentially mix gzip and non-gzip output, creating a
# big mess.
if ( !defined( 'MW_NO_OUTPUT_BUFFER' ) && ob_get_level() == 0 ) {
- if ( !defined( 'MW_COMPILED' ) ) {
- require_once( "$IP/includes/OutputHandler.php" );
- }
+ require_once "$IP/includes/OutputHandler.php";
ob_start( 'wfOutputHandler' );
}
wfProfileOut( 'WebStart.php-ob_start' );
if ( !defined( 'MW_NO_SETUP' ) ) {
- require_once( MWInit::compiledPath( "includes/Setup.php" ) );
+ require_once "$IP/includes/Setup.php";
}
-
diff --git a/includes/Wiki.php b/includes/Wiki.php
index a4a89032..ae75bf33 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -85,8 +85,6 @@ class MediaWiki {
} elseif ( $curid ) {
// URLs like this are generated by RC, because rc_title isn't always accurate
$ret = Title::newFromID( $curid );
- } elseif ( $title == '' && $action != 'delete' ) {
- $ret = Title::newMainPage();
} else {
$ret = Title::newFromURL( $title );
// Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
@@ -102,8 +100,12 @@ class MediaWiki {
$wgContLang->findVariantLink( $title, $ret );
}
}
- // For non-special titles, check for implicit titles
- if ( is_null( $ret ) || !$ret->isSpecialPage() ) {
+
+ // If title is not provided, always allow oldid and diff to set the title.
+ // If title is provided, allow oldid and diff to override the title, unless
+ // we are talking about a special page which might use these parameters for
+ // other purposes.
+ if ( $ret === null || !$ret->isSpecialPage() ) {
// We can have urls with just ?diff=,?oldid= or even just ?diff=
$oldid = $request->getInt( 'oldid' );
$oldid = $oldid ? $oldid : $request->getInt( 'diff' );
@@ -114,6 +116,11 @@ class MediaWiki {
}
}
+ // Use the main page as default title if nothing else has been provided
+ if ( $ret === null && strval( $title ) === '' && $action !== 'delete' ) {
+ $ret = Title::newMainPage();
+ }
+
if ( $ret === null || ( $ret->getDBkey() == '' && $ret->getInterwiki() == '' ) ) {
$ret = SpecialPage::getTitleFor( 'Badtitle' );
}
@@ -126,7 +133,7 @@ class MediaWiki {
* @return Title
*/
public function getTitle() {
- if( $this->context->getTitle() === null ){
+ if ( $this->context->getTitle() === null ) {
$this->context->setTitle( $this->parseTitle() );
}
return $this->context->getTitle();
@@ -169,6 +176,7 @@ class MediaWiki {
* - special pages
* - normal pages
*
+ * @throws MWException|PermissionsError|BadTitleError|HttpError
* @return void
*/
private function performRequest() {
@@ -177,7 +185,7 @@ class MediaWiki {
wfProfileIn( __METHOD__ );
$request = $this->context->getRequest();
- $title = $this->context->getTitle();
+ $requestTitle = $title = $this->context->getTitle();
$output = $this->context->getOutput();
$user = $this->context->getUser();
@@ -226,7 +234,7 @@ class MediaWiki {
if ( $title->getInterwiki() != '' ) {
$rdfrom = $request->getVal( 'rdfrom' );
if ( $rdfrom ) {
- $url = $title->getFullURL( 'rdfrom=' . urlencode( $rdfrom ) );
+ $url = $title->getFullURL( array( 'rdfrom' => $rdfrom ) );
} else {
$query = $request->getValues();
unset( $query['title'] );
@@ -246,7 +254,7 @@ class MediaWiki {
// Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
} elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted()
&& ( $request->getVal( 'title' ) === null ||
- $title->getPrefixedDBKey() != $request->getVal( 'title' ) )
+ $title->getPrefixedDBkey() != $request->getVal( 'title' ) )
&& !count( $request->getValueNames( array( 'action', 'title' ) ) )
&& wfRunHooks( 'TestCanonicalRedirect', array( $request, $title, $output ) ) )
{
@@ -301,12 +309,13 @@ class MediaWiki {
global $wgArticle;
$wgArticle = new DeprecatedGlobal( 'wgArticle', $article, '1.18' );
- $this->performAction( $article );
+ $this->performAction( $article, $requestTitle );
} elseif ( is_string( $article ) ) {
$output->redirect( $article );
} else {
wfProfileOut( __METHOD__ );
- throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle() returned neither an object nor a URL" );
+ throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
+ . " returned neither an object nor a URL" );
}
}
@@ -330,8 +339,18 @@ class MediaWiki {
wfProfileIn( __METHOD__ );
$title = $this->context->getTitle();
- $article = Article::newFromTitle( $title, $this->context );
- $this->context->setWikiPage( $article->getPage() );
+ if ( $this->context->canUseWikiPage() ) {
+ // Try to use request context wiki page, as there
+ // is already data from db saved in per process
+ // cache there from this->getAction() call.
+ $page = $this->context->getWikiPage();
+ $article = Article::newFromWikiPage( $page, $this->context );
+ } else {
+ // This case should not happen, but just in case.
+ $article = Article::newFromTitle( $title, $this->context );
+ $this->context->setWikiPage( $article->getPage() );
+ }
+
// NS_MEDIAWIKI has no redirects.
// It is also used for CSS/JS, so performance matters here...
if ( $title->getNamespace() == NS_MEDIAWIKI ) {
@@ -345,10 +364,10 @@ class MediaWiki {
// Check for redirects ...
$action = $request->getVal( 'action', 'view' );
$file = ( $title->getNamespace() == NS_FILE ) ? $article->getFile() : null;
- if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
- && !$request->getVal( 'oldid' ) && // ... and are not old revisions
- !$request->getVal( 'diff' ) && // ... and not when showing diff
- $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to
+ if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
+ && !$request->getVal( 'oldid' ) && // ... and are not old revisions
+ !$request->getVal( 'diff' ) && // ... and not when showing diff
+ $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to
// ... and the article is not a non-redirect image page with associated file
!( is_object( $file ) && $file->exists() && !$file->getRedirected() ) )
{
@@ -395,8 +414,9 @@ class MediaWiki {
* Perform one of the "standard" actions
*
* @param $page Page
+ * @param $requestTitle The original title, before any redirects were applied
*/
- private function performAction( Page $page ) {
+ private function performAction( Page $page, Title $requestTitle ) {
global $wgUseSquid, $wgSquidMaxage;
wfProfileIn( __METHOD__ );
@@ -415,11 +435,12 @@ class MediaWiki {
$act = $this->getAction();
- $action = Action::factory( $act, $page );
+ $action = Action::factory( $act, $page, $this->context );
+
if ( $action instanceof Action ) {
# Let Squid cache things if we can purge them.
if ( $wgUseSquid &&
- in_array( $request->getFullRequestURL(), $title->getSquidURLs() )
+ in_array( $request->getFullRequestURL(), $requestTitle->getSquidURLs() )
) {
$output->setSquidMaxage( $wgSquidMaxage );
}
@@ -468,7 +489,7 @@ class MediaWiki {
$resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
$resp->header( 'X-Database-Lag: ' . intval( $lag ) );
$resp->header( 'Content-Type: text/plain' );
- if( $wgShowHostnames ) {
+ if ( $wgShowHostnames ) {
echo "Waiting for $host: $lag seconds lagged\n";
} else {
echo "Waiting for a database server: $lag seconds lagged\n";
@@ -510,6 +531,51 @@ class MediaWiki {
$action = $this->getAction();
$wgTitle = $title;
+ // If the user has forceHTTPS set to true, or if the user
+ // is in a group requiring HTTPS, or if they have the HTTPS
+ // preference set, redirect them to HTTPS.
+ // Note: Do this after $wgTitle is setup, otherwise the hooks run from
+ // isLoggedIn() will do all sorts of weird stuff.
+ if (
+ (
+ $request->getCookie( 'forceHTTPS', '' ) ||
+ // check for prefixed version for currently logged in users
+ $request->getCookie( 'forceHTTPS' ) ||
+ // Avoid checking the user and groups unless it's enabled.
+ (
+ $this->context->getUser()->isLoggedIn()
+ && $this->context->getUser()->requiresHTTPS()
+ )
+ ) &&
+ $request->detectProtocol() == 'http'
+ ) {
+ $oldUrl = $request->getFullRequestURL();
+ $redirUrl = str_replace( 'http://', 'https://', $oldUrl );
+
+ if ( $request->wasPosted() ) {
+ // This is weird and we'd hope it almost never happens. This
+ // means that a POST came in via HTTP and policy requires us
+ // redirecting to HTTPS. It's likely such a request is going
+ // to fail due to post data being lost, but let's try anyway
+ // and just log the instance.
+ //
+ // @todo @fixme See if we could issue a 307 or 308 here, need
+ // to see how clients (automated & browser) behave when we do
+ wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
+ }
+
+ // Setup dummy Title, otherwise OutputPage::redirect will fail
+ $title = Title::newFromText( NS_MAIN, 'REDIR' );
+ $this->context->setTitle( $title );
+ $output = $this->context->getOutput();
+ // Since we only do this redir to change proto, always send a vary header
+ $output->addVaryHeader( 'X-Forwarded-Proto' );
+ $output->redirect( $redirUrl );
+ $output->output();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
if ( $wgUseFileCache && $title->getNamespace() >= 0 ) {
wfProfileIn( 'main-try-filecache' );
if ( HTMLFileCache::useFileCache( $this->context ) ) {
@@ -555,9 +621,6 @@ class MediaWiki {
// Execute a job from the queue
$this->doJobs();
- // Log message usage, if $wgAdaptiveMessageCache is set to true
- MessageCache::logMessages();
-
// Log profiling data, e.g. in the database or UDP
wfLogProfilingData();
@@ -573,33 +636,59 @@ class MediaWiki {
* Do a job from the job queue
*/
private function doJobs() {
- global $wgJobRunRate;
+ global $wgJobRunRate, $wgPhpCli, $IP;
if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
return;
}
+
if ( $wgJobRunRate < 1 ) {
$max = mt_getrandmax();
if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
- return;
+ return; // the higher $wgJobRunRate, the less likely we return here
}
$n = 1;
} else {
$n = intval( $wgJobRunRate );
}
- while ( $n-- && false != ( $job = Job::pop() ) ) {
- $output = $job->toString() . "\n";
- $t = - microtime( true );
- $success = $job->run();
- $t += microtime( true );
- $t = round( $t * 1000 );
- if ( !$success ) {
- $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
- } else {
- $output .= "Success, Time: $t ms\n";
+ if ( !wfShellExecDisabled() && is_executable( $wgPhpCli ) ) {
+ // Start a background process to run some of the jobs.
+ // This will be asynchronous on *nix though not on Windows.
+ wfProfileIn( __METHOD__ . '-exec' );
+ $retVal = 1;
+ $cmd = wfShellWikiCmd( "$IP/maintenance/runJobs.php", array( '--maxjobs', $n ) );
+ wfShellExec( "$cmd &", $retVal );
+ wfProfileOut( __METHOD__ . '-exec' );
+ } else {
+ try {
+ // Fallback to running the jobs here while the user waits
+ $group = JobQueueGroup::singleton();
+ do {
+ $job = $group->pop( JobQueueGroup::USE_CACHE ); // job from any queue
+ if ( $job ) {
+ $output = $job->toString() . "\n";
+ $t = - microtime( true );
+ wfProfileIn( __METHOD__ . '-' . get_class( $job ) );
+ $success = $job->run();
+ wfProfileOut( __METHOD__ . '-' . get_class( $job ) );
+ $group->ack( $job ); // done
+ $t += microtime( true );
+ $t = round( $t * 1000 );
+ if ( $success === false ) {
+ $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
+ } else {
+ $output .= "Success, Time: $t ms\n";
+ }
+ wfDebugLog( 'jobqueue', $output );
+ }
+ } while ( --$n && $job );
+ } catch ( MWException $e ) {
+ // We don't want exceptions thrown during job execution to
+ // be reported to the user since the output is already sent.
+ // Instead we just log them.
+ MWExceptionHandler::logException( $e );
}
- wfDebugLog( 'jobqueue', $output );
}
}
}
diff --git a/includes/WikiError.php b/includes/WikiError.php
index 45ee20c9..08eb8004 100644
--- a/includes/WikiError.php
+++ b/includes/WikiError.php
@@ -82,7 +82,7 @@ class WikiError {
*/
class WikiErrorMsg extends WikiError {
/**
- * @param $message String: wiki message name
+ * @param string $message wiki message name
* @param ... parameters to pass to wfMsg()
*
* @deprecated since 1.17
@@ -143,7 +143,7 @@ class WikiXmlError extends WikiError {
}
function _extractContext( $context, $offset ) {
- if( is_null( $context ) ) {
+ if ( is_null( $context ) ) {
return null;
} else {
// Hopefully integer overflow will be handled transparently here
diff --git a/includes/WikiFilePage.php b/includes/WikiFilePage.php
index 9fb1522d..fe1ff88a 100644
--- a/includes/WikiFilePage.php
+++ b/includes/WikiFilePage.php
@@ -41,7 +41,9 @@ class WikiFilePage extends WikiPage {
}
public function getActionOverrides() {
- return array( 'revert' => 'RevertFileAction' );
+ $overrides = parent::getActionOverrides();
+ $overrides['revert'] = 'RevertFileAction';
+ return $overrides;
}
/**
@@ -103,13 +105,12 @@ class WikiFilePage extends WikiPage {
}
/**
- * @param bool $text
* @return bool
*/
- public function isRedirect( $text = false ) {
+ public function isRedirect() {
$this->loadFile();
if ( $this->mFile->isLocal() ) {
- return parent::isRedirect( $text );
+ return parent::isRedirect();
}
return (bool)$this->mFile->getRedirected();
@@ -182,6 +183,10 @@ class WikiFilePage extends WikiPage {
// to be updated (in case the cached information is wrong)
$this->mFile->purgeCache( array( 'forThumbRefresh' => true ) );
}
+ if ( $this->mRepo ) {
+ // Purge redirect cache
+ $this->mRepo->invalidateImageRedirect( $this->mTitle );
+ }
return parent::doPurge();
}
}
diff --git a/includes/WikiMap.php b/includes/WikiMap.php
index 4a5e2bcf..da4416de 100644
--- a/includes/WikiMap.php
+++ b/includes/WikiMap.php
@@ -28,7 +28,7 @@ class WikiMap {
/**
* Get a WikiReference object for $wikiID
*
- * @param $wikiID String: wiki'd id (generally database name)
+ * @param string $wikiID wiki'd id (generally database name)
* @return WikiReference object or null if the wiki was not found
*/
public static function getWiki( $wikiID ) {
@@ -37,13 +37,18 @@ class WikiMap {
$wgConf->loadFullData();
list( $major, $minor ) = $wgConf->siteFromDB( $wikiID );
- if( $major === null ) {
+ if ( $major === null ) {
return null;
}
- $canonicalServer = $wgConf->get( 'wgCanonicalServer', $wikiID, $major,
- array( 'lang' => $minor, 'site' => $major ) );
$server = $wgConf->get( 'wgServer', $wikiID, $major,
array( 'lang' => $minor, 'site' => $major ) );
+
+ $canonicalServer = $wgConf->get( 'wgCanonicalServer', $wikiID, $major,
+ array( 'lang' => $minor, 'site' => $major ) );
+ if ( $canonicalServer === false || $canonicalServer === null ) {
+ $canonicalServer = $server;
+ }
+
$path = $wgConf->get( 'wgArticlePath', $wikiID, $major,
array( 'lang' => $minor, 'site' => $major ) );
return new WikiReference( $major, $minor, $canonicalServer, $path, $server );
@@ -53,7 +58,7 @@ class WikiMap {
* 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)
+ * @param string $wikiID wiki'd id (generally database name)
* @return string|int Wiki's name or $wiki_id if the wiki was not found
*/
public static function getWikiName( $wikiID ) {
@@ -68,24 +73,24 @@ class WikiMap {
/**
* 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"
+ * @param string $wikiID wiki'd id (generally database name)
+ * @param string $user user name (must be normalised before calling this function!)
+ * @param string $text 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 ) {
+ 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
+ * @param string $wikiID wiki'd id (generally database name)
+ * @param string $page page name (must be normalised before calling this function!)
+ * @param string $text 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 ) {
+ public static function makeForeignLink( $wikiID, $page, $text = null ) {
if ( !$text ) {
$text = $page;
}
@@ -101,8 +106,8 @@ class WikiMap {
/**
* 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!)
+ * @param string $wikiID wiki'd id (generally database name)
+ * @param string $page 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 ) {
@@ -176,7 +181,7 @@ class WikiReference {
* Helper function for getUrl()
*
* @todo FIXME: This may be generalized...
- * @param $page String: page name (must be normalised before calling this function!)
+ * @param string $page page name (must be normalised before calling this function!)
* @return String: Url fragment
*/
private function getLocalUrl( $page ) {
@@ -186,7 +191,7 @@ class WikiReference {
/**
* Get a canonical (i.e. based on $wgCanonicalServer) URL to a page on this foreign wiki
*
- * @param $page String: page name (must be normalised before calling this function!)
+ * @param string $page page name (must be normalised before calling this function!)
* @return String: Url
*/
public function getCanonicalUrl( $page ) {
@@ -211,15 +216,14 @@ class WikiReference {
}
/**
- * Get a URL based on $wgServer, like Title::getFullUrl() would produce
+ * Get a URL based on $wgServer, like Title::getFullURL() would produce
* when called locally on the wiki.
*
- * @param $page String: page name (must be normalized before calling this function!)
+ * @param string $page page name (must be normalized before calling this function!)
* @return String: URL
*/
public function getFullUrl( $page ) {
- return
- $this->mServer .
+ return $this->mServer .
$this->getLocalUrl( $page );
}
}
diff --git a/includes/WikiPage.php b/includes/WikiPage.php
index bc8766de..61a05a12 100644
--- a/includes/WikiPage.php
+++ b/includes/WikiPage.php
@@ -23,7 +23,8 @@
/**
* Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
*/
-abstract class Page {}
+interface Page {
+}
/**
* Class representing a MediaWiki article and history.
@@ -33,7 +34,7 @@ abstract class Page {}
*
* @internal documentation reviewed 15 Mar 2010
*/
-class WikiPage extends Page implements IDBAccessObject {
+class WikiPage implements Page, IDBAccessObject {
// Constants for $mDataLoadedFrom and related
/**
@@ -47,9 +48,16 @@ class WikiPage extends Page implements IDBAccessObject {
public $mDataLoaded = false; // !< Boolean
public $mIsRedirect = false; // !< Boolean
public $mLatest = false; // !< Integer (false means "not loaded")
- public $mPreparedEdit = false; // !< Array
/**@}}*/
+ /** @var stdclass Map of cache fields (text, parser output, ect) for a proposed/new edit */
+ protected $mPreparedEdit = false;
+
+ /**
+ * @var int
+ */
+ protected $mId = null;
+
/**
* @var int; one of the READ_* constants
*/
@@ -121,8 +129,8 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* Constructor from a page id
*
- * @param $id Int article ID to load
- * @param $from string|int one of the following values:
+ * @param int $id article ID to load
+ * @param string|int $from one of the following values:
* - "fromdb" or WikiPage::READ_NORMAL to select from a slave database
* - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
*
@@ -144,7 +152,7 @@ class WikiPage extends Page implements IDBAccessObject {
* @since 1.20
* @param $row object: database row containing at least fields returned
* by selectFields().
- * @param $from string|int: source of $data:
+ * @param string|int $from source of $data:
* - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
* - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
* - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
@@ -182,12 +190,26 @@ class WikiPage extends Page implements IDBAccessObject {
* (and only when) $wgActions[$action] === true. This allows subclasses
* to override the default behavior.
*
- * @todo: move this UI stuff somewhere else
+ * @todo Move this UI stuff somewhere else
*
* @return Array
*/
public function getActionOverrides() {
- return array();
+ $content_handler = $this->getContentHandler();
+ return $content_handler->getActionOverrides();
+ }
+
+ /**
+ * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
+ *
+ * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
+ *
+ * @return ContentHandler
+ *
+ * @since 1.21
+ */
+ public function getContentHandler() {
+ return ContentHandler::getForModelID( $this->getContentModel() );
}
/**
@@ -214,13 +236,25 @@ class WikiPage extends Page implements IDBAccessObject {
* @return void
*/
protected function clearCacheFields() {
+ $this->mId = null;
$this->mCounter = null;
- $this->mRedirectTarget = null; # Title object if set
- $this->mLastRevision = null; # Latest revision
+ $this->mRedirectTarget = null; // Title object if set
+ $this->mLastRevision = null; // Latest revision
$this->mTouched = '19700101000000';
$this->mTimestamp = '';
$this->mIsRedirect = false;
$this->mLatest = false;
+ // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
+ // the requested rev ID and immutable content against the cached one.
+ // Clearing it can cause extra parses on edit for no reason.
+ }
+
+ /**
+ * Clear the mPreparedEdit cache field, as may be needed by mutable content types
+ * @return void
+ * @since 1.23
+ */
+ public function clearPreparedEdit() {
$this->mPreparedEdit = false;
}
@@ -231,7 +265,9 @@ class WikiPage extends Page implements IDBAccessObject {
* @return array
*/
public static function selectFields() {
- return array(
+ global $wgContentHandlerUseDB;
+
+ $fields = array(
'page_id',
'page_namespace',
'page_title',
@@ -244,6 +280,12 @@ class WikiPage extends Page implements IDBAccessObject {
'page_latest',
'page_len',
);
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'page_content_model';
+ }
+
+ return $fields;
}
/**
@@ -277,7 +319,7 @@ class WikiPage extends Page implements IDBAccessObject {
public function pageDataFromTitle( $dbr, $title, $options = array() ) {
return $this->pageData( $dbr, array(
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() ), $options );
+ 'page_title' => $title->getDBkey() ), $options );
}
/**
@@ -317,8 +359,8 @@ class WikiPage extends Page implements IDBAccessObject {
$data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
} elseif ( $from === self::READ_NORMAL ) {
$data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
- # Use a "last rev inserted" timestamp key to dimish the issue of slave lag.
- # Note that DB also stores the master position in the session and checks it.
+ // Use a "last rev inserted" timestamp key to diminish the issue of slave lag.
+ // Note that DB also stores the master position in the session and checks it.
$touched = $this->getCachedLastEditTime();
if ( $touched ) { // key set
if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
@@ -341,7 +383,7 @@ class WikiPage extends Page implements IDBAccessObject {
* @since 1.20
* @param $data object: database row containing at least fields returned
* by selectFields()
- * @param $from string|int One of the following:
+ * @param string|int $from One of the following:
* - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
* - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
* - "forupdate" or WikiPage::READ_LOCKING if the data comes from from
@@ -349,19 +391,21 @@ class WikiPage extends Page implements IDBAccessObject {
*/
public function loadFromRow( $data, $from ) {
$lc = LinkCache::singleton();
+ $lc->clearLink( $this->mTitle );
if ( $data ) {
$lc->addGoodLinkObjFromRow( $this->mTitle, $data );
$this->mTitle->loadFromRow( $data );
- # Old-fashioned restrictions
+ // Old-fashioned restrictions
$this->mTitle->loadRestrictions( $data->page_restrictions );
- $this->mCounter = intval( $data->page_counter );
- $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
- $this->mIsRedirect = intval( $data->page_is_redirect );
- $this->mLatest = intval( $data->page_latest );
+ $this->mId = intval( $data->page_id );
+ $this->mCounter = intval( $data->page_counter );
+ $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
+ $this->mIsRedirect = intval( $data->page_is_redirect );
+ $this->mLatest = intval( $data->page_latest );
// Bug 37225: $latest may no longer match the cached latest Revision object.
// Double-check the ID of any cached latest Revision object for consistency.
if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
@@ -374,6 +418,8 @@ class WikiPage extends Page implements IDBAccessObject {
$this->mTitle->loadFromRow( false );
$this->clearCacheFields();
+
+ $this->mId = 0;
}
$this->mDataLoaded = true;
@@ -384,14 +430,20 @@ class WikiPage extends Page implements IDBAccessObject {
* @return int Page ID
*/
public function getId() {
- return $this->mTitle->getArticleID();
+ if ( !$this->mDataLoaded ) {
+ $this->loadPageData();
+ }
+ return $this->mId;
}
/**
* @return bool Whether or not the page exists in the database
*/
public function exists() {
- return $this->mTitle->exists();
+ if ( !$this->mDataLoaded ) {
+ $this->loadPageData();
+ }
+ return $this->mId > 0;
}
/**
@@ -403,7 +455,7 @@ class WikiPage extends Page implements IDBAccessObject {
* @return bool
*/
public function hasViewableContent() {
- return $this->mTitle->exists() || $this->mTitle->isAlwaysKnown();
+ return $this->exists() || $this->mTitle->isAlwaysKnown();
}
/**
@@ -418,21 +470,44 @@ class WikiPage extends Page implements IDBAccessObject {
}
/**
- * Tests if the article text represents a redirect
+ * Tests if the article content represents a redirect
*
- * @param $text mixed string containing article contents, or boolean
* @return bool
*/
- public function isRedirect( $text = false ) {
- if ( $text === false ) {
- if ( !$this->mDataLoaded ) {
- $this->loadPageData();
- }
+ public function isRedirect() {
+ $content = $this->getContent();
+ if ( !$content ) {
+ return false;
+ }
- return (bool)$this->mIsRedirect;
- } else {
- return Title::newFromRedirect( $text ) !== null;
+ return $content->isRedirect();
+ }
+
+ /**
+ * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
+ *
+ * Will use the revisions actual content model if the page exists,
+ * and the page's default if the page doesn't exist yet.
+ *
+ * @return String
+ *
+ * @since 1.21
+ */
+ public function getContentModel() {
+ if ( $this->exists() ) {
+ // look at the revision's actual content model
+ $rev = $this->getRevision();
+
+ if ( $rev !== null ) {
+ return $rev->getContentModel();
+ } else {
+ $title = $this->mTitle->getPrefixedDBkey();
+ wfWarn( "Page $title exists but has no (visible) revisions!" );
+ }
}
+
+ // use the default model for this page
+ return $this->mTitle->getContentModel();
}
/**
@@ -480,6 +555,7 @@ class WikiPage extends Page implements IDBAccessObject {
$db = wfGetDB( DB_SLAVE );
$revSelectFields = Revision::selectFields();
+ $row = null;
while ( $continue ) {
$row = $db->selectRow(
array( 'page', 'revision' ),
@@ -555,36 +631,61 @@ class WikiPage extends Page implements IDBAccessObject {
}
/**
- * Get the text of the current revision. No side-effects...
+ * Get the content of the current revision. No side-effects...
*
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
- * @return String|bool The text of the current revision. False on failure
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return Content|null The content of the current revision
+ *
+ * @since 1.21
*/
- public function getText( $audience = Revision::FOR_PUBLIC ) {
+ public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
$this->loadLastEdit();
if ( $this->mLastRevision ) {
- return $this->mLastRevision->getText( $audience );
+ return $this->mLastRevision->getContent( $audience, $user );
}
- return false;
+ return null;
}
/**
* Get the text of the current revision. No side-effects...
*
- * @return String|bool The text of the current revision. False on failure
+ * @param $audience Integer: one of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to the given user
+ * Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return String|false The text of the current revision
+ * @deprecated as of 1.21, getContent() should be used instead.
*/
- public function getRawText() {
+ public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { // @todo deprecated, replace usage!
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
$this->loadLastEdit();
if ( $this->mLastRevision ) {
- return $this->mLastRevision->getRawText();
+ return $this->mLastRevision->getText( $audience, $user );
}
return false;
}
/**
+ * Get the text of the current revision. No side-effects...
+ *
+ * @return String|bool The text of the current revision. False on failure
+ * @deprecated as of 1.21, getContent() should be used instead.
+ */
+ public function getRawText() {
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ return $this->getText( Revision::RAW );
+ }
+
+ /**
* @return string MW timestamp of last article revision
*/
public function getTimestamp() {
@@ -598,7 +699,7 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* Set the page timestamp (use only to avoid DB queries)
- * @param $ts string MW timestamp of last article revision
+ * @param string $ts MW timestamp of last article revision
* @return void
*/
public function setTimestamp( $ts ) {
@@ -608,14 +709,16 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
* @return int user ID for the user that made the last article revision
*/
- public function getUser( $audience = Revision::FOR_PUBLIC ) {
+ public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
$this->loadLastEdit();
if ( $this->mLastRevision ) {
- return $this->mLastRevision->getUser( $audience );
+ return $this->mLastRevision->getUser( $audience, $user );
} else {
return -1;
}
@@ -625,14 +728,16 @@ class WikiPage extends Page implements IDBAccessObject {
* Get the User object of the user who created the page
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
* @return User|null
*/
- public function getCreator( $audience = Revision::FOR_PUBLIC ) {
+ public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
$revision = $this->getOldestRevision();
if ( $revision ) {
- $userName = $revision->getUserText( $audience );
+ $userName = $revision->getUserText( $audience, $user );
return User::newFromName( $userName, false );
} else {
return null;
@@ -642,14 +747,16 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
* @return string username of the user that made the last article revision
*/
- public function getUserText( $audience = Revision::FOR_PUBLIC ) {
+ public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
$this->loadLastEdit();
if ( $this->mLastRevision ) {
- return $this->mLastRevision->getUserText( $audience );
+ return $this->mLastRevision->getUserText( $audience, $user );
} else {
return '';
}
@@ -658,14 +765,16 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
* @return string Comment stored for the last article revision
*/
- public function getComment( $audience = Revision::FOR_PUBLIC ) {
+ public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
$this->loadLastEdit();
if ( $this->mLastRevision ) {
- return $this->mLastRevision->getComment( $audience );
+ return $this->mLastRevision->getComment( $audience, $user );
} else {
return '';
}
@@ -705,7 +814,7 @@ class WikiPage extends Page implements IDBAccessObject {
public function setCachedLastEditTime( $timestamp ) {
global $wgMemc;
$key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
- $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 );
+ $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 );
}
/**
@@ -723,32 +832,34 @@ class WikiPage extends Page implements IDBAccessObject {
return false;
}
- $text = $editInfo ? $editInfo->pst : false;
+ if ( $editInfo ) {
+ $content = $editInfo->pstContent;
+ } else {
+ $content = $this->getContent();
+ }
- if ( $this->isRedirect( $text ) ) {
+ if ( !$content || $content->isRedirect() ) {
return false;
}
- switch ( $wgArticleCountMethod ) {
- case 'any':
- return true;
- case 'comma':
- if ( $text === false ) {
- $text = $this->getRawText();
- }
- return strpos( $text, ',' ) !== false;
- case 'link':
+ $hasLinks = null;
+
+ if ( $wgArticleCountMethod === 'link' ) {
+ // nasty special case to avoid re-parsing to detect links
+
if ( $editInfo ) {
// ParserOutput::getLinks() is a 2D array of page links, so
// to be really correct we would need to recurse in the array
// but the main array should only have items in it if there are
// links.
- return (bool)count( $editInfo->output->getLinks() );
+ $hasLinks = (bool)count( $editInfo->output->getLinks() );
} else {
- return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+ $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
array( 'pl_from' => $this->getId() ), __METHOD__ );
}
}
+
+ return $content->isCountable( $hasLinks );
}
/**
@@ -767,7 +878,7 @@ class WikiPage extends Page implements IDBAccessObject {
return $this->mRedirectTarget;
}
- # Query the redirect table
+ // Query the redirect table
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'redirect',
array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
@@ -782,7 +893,7 @@ class WikiPage extends Page implements IDBAccessObject {
$row->rd_fragment, $row->rd_interwiki );
}
- # This page doesn't have an entry in the redirect table
+ // This page doesn't have an entry in the redirect table
return $this->mRedirectTarget = $this->insertRedirect();
}
@@ -794,7 +905,8 @@ class WikiPage extends Page implements IDBAccessObject {
*/
public function insertRedirect() {
// recurse through to only get the final target
- $retval = Title::newFromRedirectRecurse( $this->getRawText() );
+ $content = $this->getContent();
+ $retval = $content ? $content->getUltimateRedirectTarget() : null;
if ( !$retval ) {
return null;
}
@@ -811,10 +923,10 @@ class WikiPage extends Page implements IDBAccessObject {
$dbw = wfGetDB( DB_MASTER );
$dbw->replace( 'redirect', array( 'rd_from' ),
array(
- 'rd_from' => $this->getId(),
+ 'rd_from' => $this->getId(),
'rd_namespace' => $rt->getNamespace(),
- 'rd_title' => $rt->getDBkey(),
- 'rd_fragment' => $rt->getFragment(),
+ 'rd_title' => $rt->getDBkey(),
+ 'rd_fragment' => $rt->getFragment(),
'rd_interwiki' => $rt->getInterwiki(),
),
__METHOD__
@@ -849,7 +961,7 @@ class WikiPage extends Page implements IDBAccessObject {
// This can be hard to reverse and may produce loops,
// so they may be disabled in the site configuration.
$source = $this->mTitle->getFullURL( 'redirect=no' );
- return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) );
+ return $rt->getFullURL( array( 'rdfrom' => $source ) );
} else {
// External pages pages without "local" bit set are not valid
// redirect targets
@@ -879,7 +991,7 @@ class WikiPage extends Page implements IDBAccessObject {
* @return UserArrayFromResult
*/
public function getContributors() {
- # @todo FIXME: This is expensive; cache this info somewhere.
+ // @todo FIXME: This is expensive; cache this info somewhere.
$dbr = wfGetDB( DB_SLAVE );
@@ -926,8 +1038,8 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* Get the last N authors
- * @param $num Integer: number of revisions to get
- * @param $revLatest String: the latest rev_id, selected from the master (optional)
+ * @param int $num Number of revisions to get
+ * @param int|string $revLatest the latest rev_id, selected from the master (optional)
* @return array Array of authors, duplicates not removed
*/
public function getLastNAuthors( $num, $revLatest = 0 ) {
@@ -988,9 +1100,9 @@ class WikiPage extends Page implements IDBAccessObject {
return $wgEnableParserCache
&& $parserOptions->getStubThreshold() == 0
- && $this->mTitle->exists()
+ && $this->exists()
&& ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
- && $this->mTitle->isWikitextPage();
+ && $this->getContentHandler()->isParserCacheSupported();
}
/**
@@ -998,9 +1110,10 @@ class WikiPage extends Page implements IDBAccessObject {
* The parser cache will be used if possible.
*
* @since 1.19
- * @param $parserOptions ParserOptions to use for the parse operation
- * @param $oldid Revision ID to get the text from, passing null or 0 will
+ * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
+ * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
* get the current revision (default value)
+ *
* @return ParserOutput or false if the revision was not found
*/
public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
@@ -1042,13 +1155,13 @@ class WikiPage extends Page implements IDBAccessObject {
return;
}
- # Don't update page view counters on views from bot users (bug 14044)
- if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->mTitle->exists() ) {
+ // Don't update page view counters on views from bot users (bug 14044)
+ if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) {
DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) );
DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) );
}
- # Update newtalk / watchlist notification status
+ // Update newtalk / watchlist notification status
$user->clearNotification( $this->mTitle );
}
@@ -1059,13 +1172,12 @@ class WikiPage extends Page implements IDBAccessObject {
public function doPurge() {
global $wgUseSquid;
- if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ){
+ if ( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
return false;
}
// Invalidate the cache
$this->mTitle->invalidateCache();
- $this->clear();
if ( $wgUseSquid ) {
// Commit the transaction before the purge is sent
@@ -1078,8 +1190,18 @@ class WikiPage extends Page implements IDBAccessObject {
}
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- if ( $this->mTitle->exists() ) {
- $text = $this->getRawText();
+ // @todo move this logic to MessageCache
+
+ if ( $this->exists() ) {
+ // NOTE: use transclusion text for messages.
+ // This is consistent with MessageCache::getMsgFromNamespace()
+
+ $content = $this->getContent();
+ $text = $content === null ? null : $content->getWikitextForTransclusion();
+
+ if ( $text === null ) {
+ $text = false;
+ }
} else {
$text = false;
}
@@ -1109,18 +1231,19 @@ class WikiPage extends Page implements IDBAccessObject {
'page_title' => $this->mTitle->getDBkey(),
'page_counter' => 0,
'page_restrictions' => '',
- 'page_is_redirect' => 0, # Will set this shortly...
+ 'page_is_redirect' => 0, // Will set this shortly...
'page_is_new' => 1,
'page_random' => wfRandom(),
'page_touched' => $dbw->timestamp(),
- 'page_latest' => 0, # Fill this in shortly...
- 'page_len' => 0, # Fill this in shortly...
+ 'page_latest' => 0, // Fill this in shortly...
+ 'page_len' => 0, // Fill this in shortly...
), __METHOD__, 'IGNORE' );
$affected = $dbw->affectedRows();
if ( $affected ) {
$newid = $dbw->insertId();
+ $this->mId = $newid;
$this->mTitle->resetArticleID( $newid );
}
wfProfileOut( __METHOD__ );
@@ -1144,28 +1267,36 @@ class WikiPage extends Page implements IDBAccessObject {
* @private
*/
public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
+ global $wgContentHandlerUseDB;
+
wfProfileIn( __METHOD__ );
- $text = $revision->getText();
- $len = strlen( $text );
- $rt = Title::newFromRedirectRecurse( $text );
+ $content = $revision->getContent();
+ $len = $content ? $content->getSize() : 0;
+ $rt = $content ? $content->getUltimateRedirectTarget() : null;
$conditions = array( 'page_id' => $this->getId() );
if ( !is_null( $lastRevision ) ) {
- # An extra check against threads stepping on each other
+ // An extra check against threads stepping on each other
$conditions['page_latest'] = $lastRevision;
}
$now = wfTimestampNow();
+ $row = array( /* SET */
+ 'page_latest' => $revision->getId(),
+ 'page_touched' => $dbw->timestamp( $now ),
+ 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
+ 'page_is_redirect' => $rt !== null ? 1 : 0,
+ 'page_len' => $len,
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $row['page_content_model'] = $revision->getContentModel();
+ }
+
$dbw->update( 'page',
- array( /* SET */
- 'page_latest' => $revision->getId(),
- 'page_touched' => $dbw->timestamp( $now ),
- 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
- 'page_is_redirect' => $rt !== null ? 1 : 0,
- 'page_len' => $len,
- ),
+ $row,
$conditions,
__METHOD__ );
@@ -1176,8 +1307,9 @@ class WikiPage extends Page implements IDBAccessObject {
$this->setCachedLastEditTime( $now );
$this->mLatest = $revision->getId();
$this->mIsRedirect = (bool)$rt;
- # Update the LinkCache.
- LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest );
+ // Update the LinkCache.
+ LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect,
+ $this->mLatest, $revision->getContentModel() );
}
wfProfileOut( __METHOD__ );
@@ -1249,7 +1381,7 @@ class WikiPage extends Page implements IDBAccessObject {
$prev = $row->rev_id;
$lastRevIsRedirect = (bool)$row->page_is_redirect;
} else {
- # No or missing previous revision; mark the page as new
+ // No or missing previous revision; mark the page as new
$prev = 0;
$lastRevIsRedirect = null;
}
@@ -1261,56 +1393,120 @@ class WikiPage extends Page implements IDBAccessObject {
}
/**
+ * Get the content 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 $undoafter Revision Must be an earlier revision than $undo
+ * @return mixed string on success, false on failure
+ * @since 1.21
+ * Before we had the Content object, this was done in getUndoText
+ */
+ public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
+ $handler = $undo->getContentHandler();
+ return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
+ }
+
+ /**
* 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 $undoafter Revision Must be an earlier revision than $undo
* @return mixed string on success, false on failure
+ * @deprecated since 1.21: use ContentHandler::getUndoContent() instead.
*/
public function getUndoText( Revision $undo, Revision $undoafter = null ) {
- $cur_text = $this->getRawText();
- if ( $cur_text === false ) {
- return false; // no page
- }
- $undo_text = $undo->getText();
- $undoafter_text = $undoafter->getText();
+ ContentHandler::deprecated( __METHOD__, '1.21' );
- if ( $cur_text == $undo_text ) {
- # No use doing a merge if it's just a straight revert.
- return $undoafter_text;
- }
+ $this->loadLastEdit();
- $undone_text = '';
+ if ( $this->mLastRevision ) {
+ if ( is_null( $undoafter ) ) {
+ $undoafter = $undo->getPrevious();
+ }
- if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
- return false;
+ $handler = $this->getContentHandler();
+ $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
+
+ if ( !$undone ) {
+ return false;
+ } else {
+ return ContentHandler::getContentText( $undone );
+ }
}
- return $undone_text;
+ return false;
}
/**
* @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
- * @param $text String: new text of the section
- * @param $sectionTitle String: new section's subject, only if $section is 'new'
- * @param $edittime String: revision timestamp or null to use the current revision
- * @return string Complete article text, or null if error
+ * @param string $text new text of the section
+ * @param string $sectionTitle new section's subject, only if $section is 'new'
+ * @param string $edittime revision timestamp or null to use the current revision
+ * @throws MWException
+ * @return String new complete article text, or null if error
+ *
+ * @deprecated since 1.21, use replaceSectionContent() instead
*/
public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent!
+ // Whole-page edit; let the whole text through
+ return $text;
+ }
+
+ if ( !$this->supportsSections() ) {
+ throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
+ }
+
+ // could even make section title, but that's not required.
+ $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
+
+ $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
+
+ return ContentHandler::getContentText( $newContent );
+ }
+
+ /**
+ * Returns true if this page's content model supports sections.
+ *
+ * @return boolean whether sections are supported.
+ *
+ * @todo The skin should check this and not offer section functionality if sections are not supported.
+ * @todo The EditPage should check this and not offer section functionality if sections are not supported.
+ */
+ public function supportsSections() {
+ return $this->getContentHandler()->supportsSections();
+ }
+
+ /**
+ * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
+ * @param $sectionContent Content: new content of the section
+ * @param string $sectionTitle new section's subject, only if $section is 'new'
+ * @param string $edittime revision timestamp or null to use the current revision
+ *
+ * @throws MWException
+ * @return Content new complete article content, or null if error
+ *
+ * @since 1.21
+ */
+ public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
wfProfileIn( __METHOD__ );
if ( strval( $section ) == '' ) {
// Whole-page edit; let the whole text through
+ $newContent = $sectionContent;
} else {
+ if ( !$this->supportsSections() ) {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
+ }
+
// Bug 30711: always use current version when adding a new section
if ( is_null( $edittime ) || $section == 'new' ) {
- $oldtext = $this->getRawText();
- if ( $oldtext === false ) {
- wfDebug( __METHOD__ . ": no page text\n" );
- wfProfileOut( __METHOD__ );
- return null;
- }
+ $oldContent = $this->getContent();
} else {
$dbw = wfGetDB( DB_MASTER );
$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
@@ -1322,28 +1518,21 @@ class WikiPage extends Page implements IDBAccessObject {
return null;
}
- $oldtext = $rev->getText();
+ $oldContent = $rev->getContent();
}
- if ( $section == 'new' ) {
- # Inserting a new section
- $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
- ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
- if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
- $text = strlen( trim( $oldtext ) ) > 0
- ? "{$oldtext}\n\n{$subject}{$text}"
- : "{$subject}{$text}";
- }
- } else {
- # Replacing an existing section; roll out the big guns
- global $wgParser;
-
- $text = $wgParser->replaceSection( $oldtext, $section, $text );
+ if ( ! $oldContent ) {
+ wfDebug( __METHOD__ . ": no page text\n" );
+ wfProfileOut( __METHOD__ );
+ return null;
}
+
+ // FIXME: $oldContent might be null?
+ $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
}
wfProfileOut( __METHOD__ );
- return $text;
+ return $newContent;
}
/**
@@ -1353,7 +1542,7 @@ class WikiPage extends Page implements IDBAccessObject {
*/
function checkFlags( $flags ) {
if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
- if ( $this->mTitle->getArticleID() ) {
+ if ( $this->exists() ) {
$flags |= EDIT_UPDATE;
} else {
$flags |= EDIT_NEW;
@@ -1367,8 +1556,8 @@ class WikiPage extends Page implements IDBAccessObject {
* Change an existing article or create a new article. Updates RC and all necessary caches,
* optionally via the deferred update array.
*
- * @param $text String: new text
- * @param $summary String: edit summary
+ * @param string $text new text
+ * @param string $summary edit summary
* @param $flags Integer bitfield:
* EDIT_NEW
* Article is known or assumed to be non-existent, create a new one
@@ -1409,17 +1598,83 @@ class WikiPage extends Page implements IDBAccessObject {
* revision: The revision object for the inserted revision, or null
*
* Compatibility note: this function previously returned a boolean value indicating success/failure
+ *
+ * @deprecated since 1.21: use doEditContent() instead.
*/
public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+
+ $content = ContentHandler::makeContent( $text, $this->getTitle() );
+
+ return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
+ }
+
+ /**
+ * Change an existing article or create a new article. Updates RC and all necessary caches,
+ * optionally via the deferred update array.
+ *
+ * @param $content Content: new content
+ * @param string $summary edit summary
+ * @param $flags Integer bitfield:
+ * EDIT_NEW
+ * Article is known or assumed to be non-existent, create a new one
+ * EDIT_UPDATE
+ * Article is known or assumed to be pre-existing, update it
+ * EDIT_MINOR
+ * Mark this edit minor, if the user is allowed to do so
+ * EDIT_SUPPRESS_RC
+ * Do not log the change in recentchanges
+ * EDIT_FORCE_BOT
+ * Mark the edit a "bot" edit regardless of user rights
+ * EDIT_DEFER_UPDATES
+ * Defer some of the updates until the end of index.php
+ * EDIT_AUTOSUMMARY
+ * Fill in blank summaries with generated text where possible
+ *
+ * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
+ * If EDIT_UPDATE is specified and the article doesn't exist, the function will return 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 bool|int $baseRevId the revision ID this edit was based off, if any
+ * @param $user User the user doing the edit
+ * @param $serialisation_format String: format for storing the content in the database
+ *
+ * @throws MWException
+ * @return Status object. Possible errors:
+ * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
+ * edit-gone-missing: In update mode, but the article didn't exist
+ * edit-conflict: In update mode, the article changed unexpectedly
+ * edit-no-change: Warning that the text was the same as before
+ * edit-already-exists: In creation mode, but the article already exists
+ *
+ * Extensions may define additional errors.
+ *
+ * $return->value will contain an associative array with members as follows:
+ * new: Boolean indicating if the function attempted to create a new article
+ * revision: The revision object for the inserted revision, or null
+ *
+ * @since 1.21
+ */
+ public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
+ User $user = null, $serialisation_format = null ) {
global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
- # Low-level sanity check
+ // Low-level sanity check
if ( $this->mTitle->getText() === '' ) {
throw new MWException( 'Something is trying to edit an article with an empty title' );
}
wfProfileIn( __METHOD__ );
+ if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) {
+ wfProfileOut( __METHOD__ );
+ return Status::newFatal( 'content-not-allowed-here',
+ ContentHandler::getLocalizedName( $content->getModel() ),
+ $this->getTitle()->getPrefixedText() );
+ }
+
$user = is_null( $user ) ? $wgUser : $user;
$status = Status::newGood( array() );
@@ -1430,10 +1685,14 @@ class WikiPage extends Page implements IDBAccessObject {
$flags = $this->checkFlags( $flags );
- if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
- $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
- {
- wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
+ // handle hook
+ $hook_args = array( &$this, &$user, &$content, &$summary,
+ $flags & EDIT_MINOR, null, null, &$flags, &$status );
+
+ if ( !wfRunHooks( 'PageContentSave', $hook_args )
+ || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
+
+ wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
if ( $status->isOK() ) {
$status->fatal( 'edit-hook-aborted' );
@@ -1443,77 +1702,105 @@ class WikiPage extends Page implements IDBAccessObject {
return $status;
}
- # Silently ignore EDIT_MINOR if not allowed
+ // Silently ignore EDIT_MINOR if not allowed
$isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
$bot = $flags & EDIT_FORCE_BOT;
- $oldtext = $this->getRawText(); // current revision
- $oldsize = strlen( $oldtext );
+ $old_content = $this->getContent( Revision::RAW ); // current revision's content
+
+ $oldsize = $old_content ? $old_content->getSize() : 0;
$oldid = $this->getLatest();
$oldIsRedirect = $this->isRedirect();
$oldcountable = $this->isCountable();
- # Provide autosummaries if one is not provided and autosummaries are enabled.
+ $handler = $content->getContentHandler();
+
+ // Provide autosummaries if one is not provided and autosummaries are enabled.
if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
- $summary = self::getAutosummary( $oldtext, $text, $flags );
+ if ( !$old_content ) {
+ $old_content = null;
+ }
+ $summary = $handler->getAutosummary( $old_content, $content, $flags );
}
- $editInfo = $this->prepareTextForEdit( $text, null, $user );
- $text = $editInfo->pst;
- $newsize = strlen( $text );
+ $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
+ $serialized = $editInfo->pst;
+
+ /**
+ * @var Content $content
+ */
+ $content = $editInfo->pstContent;
+ $newsize = $content->getSize();
$dbw = wfGetDB( DB_MASTER );
$now = wfTimestampNow();
$this->mTimestamp = $now;
if ( $flags & EDIT_UPDATE ) {
- # Update article, but only if changed.
+ // Update article, but only if changed.
$status->value['new'] = false;
if ( !$oldid ) {
- # Article gone missing
+ // Article gone missing
wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
$status->fatal( 'edit-gone-missing' );
wfProfileOut( __METHOD__ );
return $status;
- } elseif ( $oldtext === false ) {
- # Sanity check for bug 37225
+ } elseif ( !$old_content ) {
+ // Sanity check for bug 37225
wfProfileOut( __METHOD__ );
throw new MWException( "Could not find text for current revision {$oldid}." );
}
$revision = new Revision( array(
'page' => $this->getId(),
+ 'title' => $this->getTitle(), // for determining the default content model
'comment' => $summary,
'minor_edit' => $isminor,
- 'text' => $text,
+ 'text' => $serialized,
+ 'len' => $newsize,
'parent_id' => $oldid,
'user' => $user->getId(),
'user_text' => $user->getName(),
- 'timestamp' => $now
- ) );
- # Bug 37225: use accessor to get the text as Revision may trim it.
- # After trimming, the text may be a duplicate of the current text.
- $text = $revision->getText(); // sanity; EditPage should trim already
+ 'timestamp' => $now,
+ 'content_model' => $content->getModel(),
+ 'content_format' => $serialisation_format,
+ ) ); // XXX: pass content object?!
- $changed = ( strcmp( $text, $oldtext ) != 0 );
+ $changed = !$content->equals( $old_content );
if ( $changed ) {
+ if ( !$content->isValid() ) {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "New content failed validity check!" );
+ }
+
$dbw->begin( __METHOD__ );
+
+ $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
+ $status->merge( $prepStatus );
+
+ if ( !$status->isOK() ) {
+ $dbw->rollback( __METHOD__ );
+
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
$revisionId = $revision->insertOn( $dbw );
- # 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()
- # 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.
+ // 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()
+ // 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, $oldid, $oldIsRedirect );
if ( !$ok ) {
- # Belated edit conflict! Run away!!
+ // Belated edit conflict! Run away!!
$status->fatal( 'edit-conflict' );
$dbw->rollback( __METHOD__ );
@@ -1523,18 +1810,18 @@ class WikiPage extends Page implements IDBAccessObject {
}
wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
- # Update recentchanges
+ // Update recentchanges
if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
- # Mark as patrolled if the user can do so
+ // Mark as patrolled if the user can do so
$patrolled = $wgUseRCPatrol && !count(
$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
- # Add RC row to the DB
+ // Add RC row to the DB
$rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
$oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
$revisionId, $patrolled
);
- # Log auto-patrolled edits
+ // Log auto-patrolled edits
if ( $patrolled ) {
PatrolLog::record( $rc, true, $user );
}
@@ -1547,9 +1834,15 @@ class WikiPage extends Page implements IDBAccessObject {
$revision->setId( $this->getLatest() );
}
- # Update links tables, site stats, etc.
- $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
- 'oldcountable' => $oldcountable ) );
+ // Update links tables, site stats, etc.
+ $this->doEditUpdates(
+ $revision,
+ $user,
+ array(
+ 'changed' => $changed,
+ 'oldcountable' => $oldcountable
+ )
+ );
if ( !$changed ) {
$status->warning( 'edit-no-change' );
@@ -1559,13 +1852,25 @@ class WikiPage extends Page implements IDBAccessObject {
$this->mTitle->invalidateCache();
}
} else {
- # Create new article
+ // Create new article
$status->value['new'] = true;
$dbw->begin( __METHOD__ );
- # Add the page record; stake our claim on this title!
- # This will return false if the article already exists
+ $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
+ $status->merge( $prepStatus );
+
+ if ( !$status->isOK() ) {
+ $dbw->rollback( __METHOD__ );
+
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ $status->merge( $prepStatus );
+
+ // Add the page record; stake our claim on this title!
+ // This will return false if the article already exists
$newid = $this->insertOn( $dbw );
if ( $newid === false ) {
@@ -1576,36 +1881,44 @@ class WikiPage extends Page implements IDBAccessObject {
return $status;
}
- # Save the revision text...
+ // Save the revision text...
$revision = new Revision( array(
'page' => $newid,
+ 'title' => $this->getTitle(), // for determining the default content model
'comment' => $summary,
'minor_edit' => $isminor,
- 'text' => $text,
+ 'text' => $serialized,
+ 'len' => $newsize,
'user' => $user->getId(),
'user_text' => $user->getName(),
- 'timestamp' => $now
+ 'timestamp' => $now,
+ 'content_model' => $content->getModel(),
+ 'content_format' => $serialisation_format,
) );
$revisionId = $revision->insertOn( $dbw );
- # Bug 37225: use accessor to get the text as Revision may trim it
- $text = $revision->getText(); // sanity; EditPage should trim already
+ // Bug 37225: use accessor to get the text as Revision may trim it
+ $content = $revision->getContent(); // sanity; get normalized version
+
+ if ( $content ) {
+ $newsize = $content->getSize();
+ }
- # Update the page record with revision data
+ // Update the page record with revision data
$this->updateRevisionOn( $dbw, $revision, 0 );
wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
- # Update recentchanges
+ // Update recentchanges
if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
- # Mark as patrolled if the user can do so
+ // Mark as patrolled if the user can do so
$patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
- # Add RC row to the DB
+ // Add RC row to the DB
$rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
- '', strlen( $text ), $revisionId, $patrolled );
+ '', $newsize, $revisionId, $patrolled );
- # Log auto-patrolled edits
+ // Log auto-patrolled edits
if ( $patrolled ) {
PatrolLog::record( $rc, true, $user );
}
@@ -1613,14 +1926,17 @@ class WikiPage extends Page implements IDBAccessObject {
$user->incEditCount();
$dbw->commit( __METHOD__ );
- # Update links, etc.
+ // Update links, etc.
$this->doEditUpdates( $revision, $user, array( 'created' => true ) );
- wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+ $hook_args = array( &$this, &$user, $content, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision );
+
+ ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
+ wfRunHooks( 'PageContentInsertComplete', $hook_args );
}
- # Do updates right now unless deferral was requested
+ // Do updates right now unless deferral was requested
if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
DeferredUpdates::doUpdates();
}
@@ -1628,10 +1944,13 @@ class WikiPage extends Page implements IDBAccessObject {
// Return the new revision (or null) to the caller
$status->value['revision'] = $revision;
- wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+ $hook_args = array( &$this, &$user, $content, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
+
+ ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
+ wfRunHooks( 'PageContentSaveComplete', $hook_args );
- # Promote user to any groups they meet the criteria for
+ // Promote user to any groups they meet the criteria for
$user->addAutopromoteOnceGroups( 'onEdit' );
wfProfileOut( __METHOD__ );
@@ -1641,6 +1960,8 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* Get parser options suitable for rendering the primary article wikitext
*
+ * @see ContentHandler::makeParserOptions
+ *
* @param IContextSource|User|string $context One of the following:
* - IContextSource: Use the User and the Language of the provided
* context
@@ -1651,38 +1972,54 @@ class WikiPage extends Page implements IDBAccessObject {
* @return ParserOptions
*/
public function makeParserOptions( $context ) {
- global $wgContLang;
-
- if ( $context instanceof IContextSource ) {
- $options = ParserOptions::newFromContext( $context );
- } elseif ( $context instanceof User ) { // settings per user (even anons)
- $options = ParserOptions::newFromUser( $context );
- } else { // canonical settings
- $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
- }
+ $options = $this->getContentHandler()->makeParserOptions( $context );
if ( $this->getTitle()->isConversionTable() ) {
+ // @todo ConversionTable should become a separate content model, so we don't need special cases like this one.
$options->disableContentConversion();
}
- $options->enableLimitReport(); // show inclusion/loop reports
- $options->setTidy( true ); // fix bad HTML
-
return $options;
}
/**
* Prepare text which is about to be saved.
* Returns a stdclass with source, pst and output members
- * @return bool|object
+ *
+ * @deprecated in 1.21: use prepareContentForEdit instead.
*/
public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
- global $wgParser, $wgContLang, $wgUser;
+ ContentHandler::deprecated( __METHOD__, '1.21' );
+ $content = ContentHandler::makeContent( $text, $this->getTitle() );
+ return $this->prepareContentForEdit( $content, $revid, $user );
+ }
+
+ /**
+ * Prepare content which is about to be saved.
+ * Returns a stdclass with source, pst and output members
+ *
+ * @param Content $content
+ * @param int|null $revid
+ * @param User|null $user
+ * @param string|null $serialization_format
+ *
+ * @return bool|object
+ *
+ * @since 1.21
+ */
+ public function prepareContentForEdit( Content $content, $revid = null, User $user = null,
+ $serialization_format = null
+ ) {
+ global $wgContLang, $wgUser;
$user = is_null( $user ) ? $wgUser : $user;
- // @TODO fixme: check $user->getId() here???
+ //XXX: check $user->getId() here???
+
if ( $this->mPreparedEdit
- && $this->mPreparedEdit->newText == $text
+ && $this->mPreparedEdit->newContent
+ && $this->mPreparedEdit->newContent->equals( $content )
&& $this->mPreparedEdit->revid == $revid
+ && $this->mPreparedEdit->format == $serialization_format
+ // XXX: also check $user here?
) {
// Already prepared
return $this->mPreparedEdit;
@@ -1693,14 +2030,22 @@ class WikiPage extends Page implements IDBAccessObject {
$edit = (object)array();
$edit->revid = $revid;
- $edit->newText = $text;
- $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
+
+ $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null;
+
+ $edit->format = $serialization_format;
$edit->popts = $this->makeParserOptions( 'canonical' );
- $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
- $edit->oldText = $this->getRawText();
+ $edit->output = $edit->pstContent ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) : null;
- $this->mPreparedEdit = $edit;
+ $edit->newContent = $content;
+ $edit->oldContent = $this->getContent( Revision::RAW );
+ // NOTE: B/C for hooks! don't use these fields!
+ $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : '';
+ $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
+ $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : '';
+
+ $this->mPreparedEdit = $edit;
return $edit;
}
@@ -1710,10 +2055,9 @@ class WikiPage extends Page implements IDBAccessObject {
* Purges pages that include this page if the text was changed here.
* Every 100th edit, prune the recent changes table.
*
- * @private
* @param $revision Revision object
* @param $user User object that did the revision
- * @param $options Array of options, following indexes are used:
+ * @param array $options of options, following indexes are used:
* - changed: boolean, whether the revision changed the content (default true)
* - created: boolean, whether the revision created the page (default false)
* - oldcountable: boolean or null (default null):
@@ -1727,27 +2071,32 @@ class WikiPage extends Page implements IDBAccessObject {
wfProfileIn( __METHOD__ );
$options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
- $text = $revision->getText();
+ $content = $revision->getContent();
- # Parse the text
- # Be careful not to double-PST: $text is usually already PST-ed once
+ // Parse the text
+ // Be careful not to do pre-save transform twice: $text is usually
+ // already pre-save transformed once.
if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
- $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
+ $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
} else {
wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
$editInfo = $this->mPreparedEdit;
}
- # Save it to the parser cache
+ // Save it to the parser cache
if ( $wgEnableParserCache ) {
$parserCache = ParserCache::singleton();
$parserCache->save( $editInfo->output, $this, $editInfo->popts );
}
- # Update the links tables and other secondary data
- $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
- DataUpdate::runUpdates( $updates );
+ // Update the links tables and other secondary data
+ if ( $content ) {
+ $recursive = $options['changed']; // bug 50785
+ $updates = $content->getSecondaryDataUpdates(
+ $this->getTitle(), null, $recursive, $editInfo->output );
+ DataUpdate::runUpdates( $updates );
+ }
wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
@@ -1755,19 +2104,11 @@ class WikiPage extends Page implements IDBAccessObject {
if ( 0 == mt_rand( 0, 99 ) ) {
// Flush old entries from the `recentchanges` table; we do this on
// random requests so as to avoid an increase in writes for no good reason
- global $wgRCMaxAge;
-
- $dbw = wfGetDB( DB_MASTER );
- $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
- $dbw->delete(
- 'recentchanges',
- array( "rc_timestamp < '$cutoff'" ),
- __METHOD__
- );
+ RecentChange::purgeExpiredChanges();
}
}
- if ( !$this->mTitle->exists() ) {
+ if ( !$this->exists() ) {
wfProfileOut( __METHOD__ );
return;
}
@@ -1791,36 +2132,45 @@ class WikiPage extends Page implements IDBAccessObject {
}
DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
- DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
+ DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
- # If this is another user's talk page, update newtalk.
- # Don't do this if $options['changed'] = false (null-edits) nor if
- # it's a minor edit and the user doesn't want notifications for those.
+ // If this is another user's talk page, update newtalk.
+ // Don't do this if $options['changed'] = false (null-edits) nor if
+ // it's a minor edit and the user doesn't want notifications for those.
if ( $options['changed']
&& $this->mTitle->getNamespace() == NS_USER_TALK
&& $shortTitle != $user->getTitleKey()
&& !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
) {
- if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
- $other = User::newFromName( $shortTitle, false );
- if ( !$other ) {
- wfDebug( __METHOD__ . ": invalid username\n" );
- } elseif ( User::isIP( $shortTitle ) ) {
- // An anonymous user
- $other->setNewtalk( true, $revision );
- } elseif ( $other->isLoggedIn() ) {
- $other->setNewtalk( true, $revision );
- } else {
- wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
+ $recipient = User::newFromName( $shortTitle, false );
+ if ( !$recipient ) {
+ wfDebug( __METHOD__ . ": invalid username\n" );
+ } else {
+ // Allow extensions to prevent user notification when a new message is added to their talk page
+ if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) {
+ if ( User::isIP( $shortTitle ) ) {
+ // An anonymous user
+ $recipient->setNewtalk( true, $revision );
+ } elseif ( $recipient->isLoggedIn() ) {
+ $recipient->setNewtalk( true, $revision );
+ } else {
+ wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
+ }
}
}
}
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- MessageCache::singleton()->replace( $shortTitle, $text );
+ // XXX: could skip pseudo-messages like js/css here, based on content model.
+ $msgtext = $content ? $content->getWikitextForTransclusion() : null;
+ if ( $msgtext === false || $msgtext === null ) {
+ $msgtext = '';
+ }
+
+ MessageCache::singleton()->replace( $shortTitle, $msgtext );
}
- if( $options['created'] ) {
+ if ( $options['created'] ) {
self::onArticleCreate( $this->mTitle );
} else {
self::onArticleEdit( $this->mTitle );
@@ -1834,21 +2184,47 @@ class WikiPage extends Page implements IDBAccessObject {
* The article must already exist; link tables etc
* are not updated, caches are not flushed.
*
- * @param $text String: text submitted
+ * @param string $text text submitted
* @param $user User The relevant user
- * @param $comment String: comment submitted
+ * @param string $comment comment submitted
* @param $minor Boolean: whereas it's a minor modification
+ *
+ * @deprecated since 1.21, use doEditContent() instead.
*/
public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+
+ $content = ContentHandler::makeContent( $text, $this->getTitle() );
+ $this->doQuickEditContent( $content, $user, $comment, $minor );
+ }
+
+ /**
+ * Edit an article without doing all that other stuff
+ * The article must already exist; link tables etc
+ * are not updated, caches are not flushed.
+ *
+ * @param Content $content Content submitted
+ * @param User $user The relevant user
+ * @param string $comment comment submitted
+ * @param string $serialisation_format Format for storing the content in the database
+ * @param bool $minor Whereas it's a minor modification
+ */
+ public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false,
+ $serialisation_format = null
+ ) {
wfProfileIn( __METHOD__ );
+ $serialized = $content->serialize( $serialisation_format );
+
$dbw = wfGetDB( DB_MASTER );
$revision = new Revision( array(
+ 'title' => $this->getTitle(), // for determining the default content model
'page' => $this->getId(),
- 'text' => $text,
+ 'text' => $serialized,
+ 'length' => $content->getSize(),
'comment' => $comment,
'minor_edit' => $minor ? 1 : 0,
- ) );
+ ) ); // XXX: set the content object?
$revision->insertOn( $dbw );
$this->updateRevisionOn( $dbw, $revision );
@@ -1861,15 +2237,15 @@ class WikiPage extends Page implements IDBAccessObject {
* Update the article's restriction field, and leave a log entry.
* This works for protection both existing and non-existing pages.
*
- * @param $limit Array: set of restriction keys
- * @param $reason String
- * @param &$cascade Integer. Set to false if cascading protection isn't allowed.
- * @param $expiry Array: per restriction type expiration
- * @param $user User The user updating the restrictions
+ * @param array $limit set of restriction keys
+ * @param array $expiry per restriction type expiration
+ * @param int &$cascade Set to false if cascading protection isn't allowed.
+ * @param string $reason
+ * @param User $user The user updating the restrictions
* @return Status
*/
public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) {
- global $wgContLang;
+ global $wgCascadingRestrictionLevels;
if ( wfReadOnly() ) {
return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
@@ -1877,7 +2253,7 @@ class WikiPage extends Page implements IDBAccessObject {
$restrictionTypes = $this->mTitle->getRestrictionTypes();
- $id = $this->mTitle->getArticleID();
+ $id = $this->getId();
if ( !$cascade ) {
$cascade = false;
@@ -1886,8 +2262,8 @@ class WikiPage extends Page implements IDBAccessObject {
// Take this opportunity to purge out expired restrictions
Title::purgeExpiredRestrictions();
- # @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
- # we expect a single selection, but the schema allows otherwise.
+ // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
+ // we expect a single selection, but the schema allows otherwise.
$isProtected = false;
$protect = false;
$changed = false;
@@ -1904,7 +2280,7 @@ class WikiPage extends Page implements IDBAccessObject {
$protect = true;
}
- # Get current restrictions on $action
+ // Get current restrictions on $action
$current = implode( '', $this->mTitle->getRestrictions( $action ) );
if ( $current != '' ) {
$isProtected = true;
@@ -1913,9 +2289,9 @@ class WikiPage extends Page implements IDBAccessObject {
if ( $limit[$action] != $current ) {
$changed = true;
} elseif ( $limit[$action] != '' ) {
- # Only check expiry change if the action is actually being
- # protected, since expiry does nothing on an not-protected
- # action.
+ // Only check expiry change if the action is actually being
+ // protected, since expiry does nothing on an not-protected
+ // action.
if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
$changed = true;
}
@@ -1926,12 +2302,12 @@ class WikiPage extends Page implements IDBAccessObject {
$changed = true;
}
- # If nothing's changed, do nothing
+ // If nothing has changed, do nothing
if ( !$changed ) {
return Status::newGood();
}
- if ( !$protect ) { # No protection at all means unprotection
+ if ( !$protect ) { // No protection at all means unprotection
$revCommentMsg = 'unprotectedarticle';
$logAction = 'unprotect';
} elseif ( $isProtected ) {
@@ -1942,44 +2318,41 @@ class WikiPage extends Page implements IDBAccessObject {
$logAction = 'protect';
}
- $encodedExpiry = array();
- $protectDescription = '';
- foreach ( $limit as $action => $restrictions ) {
- $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] );
- if ( $restrictions != '' ) {
- $protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] (";
- if ( $encodedExpiry[$action] != 'infinity' ) {
- $protectDescription .= wfMessage(
- 'protect-expiring',
- $wgContLang->timeanddate( $expiry[$action], false, false ) ,
- $wgContLang->date( $expiry[$action], false, false ) ,
- $wgContLang->time( $expiry[$action], false, false )
- )->inContentLanguage()->text();
- } else {
- $protectDescription .= wfMessage( 'protect-expiry-indefinite' )
- ->inContentLanguage()->text();
- }
-
- $protectDescription .= ') ';
- }
- }
- $protectDescription = trim( $protectDescription );
-
- if ( $id ) { # Protection of existing page
+ if ( $id ) { // Protection of existing page
if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
return Status::newGood();
}
- # Only restrictions with the 'protect' right can cascade...
- # Otherwise, people who cannot normally protect can "protect" pages via transclusion
+ // Only certain restrictions can cascade...
$editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
+ foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
+ $editrestriction[$key] = 'editprotected'; // backwards compatibility
+ }
+ foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
+ $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
+ }
+
+ $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
+ foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
+ $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
+ }
+ foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
+ $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
+ }
- # The schema allows multiple restrictions
- if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) {
+ // The schema allows multiple restrictions
+ if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
$cascade = false;
}
- # Update restrictions table
+ // insert null revision to identify the page protection change as edit summary
+ $latest = $this->getLatest();
+ $nullRevision = $this->insertProtectNullRevision( $revCommentMsg, $limit, $expiry, $cascade, $reason );
+ if ( $nullRevision === null ) {
+ return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
+ }
+
+ // Update restrictions table
foreach ( $limit as $action => $restrictions ) {
if ( $restrictions != '' ) {
$dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ),
@@ -1987,7 +2360,7 @@ class WikiPage extends Page implements IDBAccessObject {
'pr_type' => $action,
'pr_level' => $restrictions,
'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
- 'pr_expiry' => $encodedExpiry[$action]
+ 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
),
__METHOD__
);
@@ -1997,45 +2370,18 @@ class WikiPage extends Page implements IDBAccessObject {
}
}
- # Prepare a null revision to be added to the history
- $editComment = $wgContLang->ucfirst(
- wfMessage(
- $revCommentMsg,
- $this->mTitle->getPrefixedText()
- )->inContentLanguage()->text()
- );
- if ( $reason ) {
- $editComment .= ": $reason";
- }
- if ( $protectDescription ) {
- $editComment .= " ($protectDescription)";
- }
- if ( $cascade ) {
- // FIXME: Should use 'brackets' message.
- $editComment .= ' [' . wfMessage( 'protect-summary-cascade' )
- ->inContentLanguage()->text() . ']';
- }
-
- # Insert a null revision
- $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true );
- $nullRevId = $nullRevision->insertOn( $dbw );
-
- $latest = $this->getLatest();
- # Update page record
- $dbw->update( 'page',
- array( /* SET */
- 'page_touched' => $dbw->timestamp(),
- 'page_restrictions' => '',
- 'page_latest' => $nullRevId
- ), array( /* WHERE */
- 'page_id' => $id
- ), __METHOD__
+ // Clear out legacy restriction fields
+ $dbw->update(
+ 'page',
+ array( 'page_restrictions' => '' ),
+ array( 'page_id' => $id ),
+ __METHOD__
);
wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
- } else { # Protection of non-existing page (also known as "title protection")
- # Cascade protection is meaningless in this case
+ } else { // Protection of non-existing page (also known as "title protection")
+ // Cascade protection is meaningless in this case
$cascade = false;
if ( $limit['create'] != '' ) {
@@ -2046,7 +2392,7 @@ class WikiPage extends Page implements IDBAccessObject {
'pt_title' => $this->mTitle->getDBkey(),
'pt_create_perm' => $limit['create'],
'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
- 'pt_expiry' => $encodedExpiry['create'],
+ 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
'pt_user' => $user->getId(),
'pt_reason' => $reason,
), __METHOD__
@@ -2062,21 +2408,154 @@ class WikiPage extends Page implements IDBAccessObject {
}
$this->mTitle->flushRestrictions();
+ InfoAction::invalidateCache( $this->mTitle );
if ( $logAction == 'unprotect' ) {
- $logParams = array();
+ $params = array();
} else {
- $logParams = array( $protectDescription, $cascade ? 'cascade' : '' );
+ $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
+ $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' );
}
- # Update the protection log
+ // Update the protection log
$log = new LogPage( 'protect' );
- $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user );
+ $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $params, $user );
return Status::newGood();
}
/**
+ * Insert a new null revision for this page.
+ *
+ * @param string $revCommentMsg comment message key for the revision
+ * @param array $limit set of restriction keys
+ * @param array $expiry per restriction type expiration
+ * @param int $cascade Set to false if cascading protection isn't allowed.
+ * @param string $reason
+ * @return Revision|null on error
+ */
+ public function insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason ) {
+ global $wgContLang;
+ $dbw = wfGetDB( DB_MASTER );
+
+ // Prepare a null revision to be added to the history
+ $editComment = $wgContLang->ucfirst(
+ wfMessage(
+ $revCommentMsg,
+ $this->mTitle->getPrefixedText()
+ )->inContentLanguage()->text()
+ );
+ if ( $reason ) {
+ $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
+ }
+ $protectDescription = $this->protectDescription( $limit, $expiry );
+ if ( $protectDescription ) {
+ $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
+ $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text();
+ }
+ if ( $cascade ) {
+ $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
+ $editComment .= wfMessage( 'brackets' )->params(
+ wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
+ )->inContentLanguage()->text();
+ }
+
+ $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true );
+ if ( $nullRev ) {
+ $nullRev->insertOn( $dbw );
+
+ // Update page record and touch page
+ $oldLatest = $nullRev->getParentId();
+ $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
+ }
+
+ return $nullRev;
+ }
+
+ /**
+ * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid
+ * @return string
+ */
+ protected function formatExpiry( $expiry ) {
+ global $wgContLang;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $encodedExpiry = $dbr->encodeExpiry( $expiry );
+ if ( $encodedExpiry != 'infinity' ) {
+ return wfMessage(
+ 'protect-expiring',
+ $wgContLang->timeanddate( $expiry, false, false ),
+ $wgContLang->date( $expiry, false, false ),
+ $wgContLang->time( $expiry, false, false )
+ )->inContentLanguage()->text();
+ } else {
+ return wfMessage( 'protect-expiry-indefinite' )
+ ->inContentLanguage()->text();
+ }
+ }
+
+ /**
+ * Builds the description to serve as comment for the edit.
+ *
+ * @param array $limit set of restriction keys
+ * @param array $expiry per restriction type expiration
+ * @return string
+ */
+ public function protectDescription( array $limit, array $expiry ) {
+ $protectDescription = '';
+
+ foreach ( array_filter( $limit ) as $action => $restrictions ) {
+ # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
+ # All possible message keys are listed here for easier grepping:
+ # * restriction-create
+ # * restriction-edit
+ # * restriction-move
+ # * restriction-upload
+ $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
+ # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
+ # with '' filtered out. All possible message keys are listed below:
+ # * protect-level-autoconfirmed
+ # * protect-level-sysop
+ $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text();
+
+ $expiryText = $this->formatExpiry( $expiry[$action] );
+
+ if ( $protectDescription !== '' ) {
+ $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
+ }
+ $protectDescription .= wfMessage( 'protect-summary-desc' )
+ ->params( $actionText, $restrictionsText, $expiryText )
+ ->inContentLanguage()->text();
+ }
+
+ return $protectDescription;
+ }
+
+ /**
+ * Builds the description to serve as comment for the log entry.
+ *
+ * Some bots may parse IRC lines, which are generated from log entries which contain plain
+ * protect description text. Keep them in old format to avoid breaking compatibility.
+ * TODO: Fix protection log to store structured description and format it on-the-fly.
+ *
+ * @param array $limit set of restriction keys
+ * @param array $expiry per restriction type expiration
+ * @return string
+ */
+ public function protectDescriptionLog( array $limit, array $expiry ) {
+ global $wgContLang;
+
+ $protectDescriptionLog = '';
+
+ foreach ( array_filter( $limit ) as $action => $restrictions ) {
+ $expiryText = $this->formatExpiry( $expiry[$action] );
+ $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)";
+ }
+
+ return trim( $protectDescriptionLog );
+ }
+
+ /**
* Take an array of page restrictions and flatten it to a string
* suitable for insertion into the page_restrictions field.
* @param $limit Array
@@ -2091,10 +2570,8 @@ class WikiPage extends Page implements IDBAccessObject {
$bits = array();
ksort( $limit );
- foreach ( $limit as $action => $restrictions ) {
- if ( $restrictions != '' ) {
- $bits[] = "$action=$restrictions";
- }
+ foreach ( array_filter( $limit ) as $action => $restrictions ) {
+ $bits[] = "$action=$restrictions";
}
return implode( ':', $bits );
@@ -2107,10 +2584,10 @@ class WikiPage extends Page implements IDBAccessObject {
*
* Deletes the article with database consistency, writes logs, purges caches
*
- * @param $reason string delete reason for deletion log
+ * @param string $reason delete reason for deletion log
* @param $suppress boolean suppress all revisions and log the deletion in
* the suppression log instead of the deletion log
- * @param $id int article ID
+ * @param int $id article ID
* @param $commit boolean defaults to true, triggers transaction end
* @param &$error Array of errors to append to
* @param $user User The deleting user
@@ -2129,9 +2606,10 @@ class WikiPage extends Page implements IDBAccessObject {
*
* @since 1.19
*
- * @param $reason string delete reason for deletion log
+ * @param string $reason delete reason for deletion log
* @param $suppress boolean suppress all revisions and log the deletion in
* the suppression log instead of the deletion log
+ * @param int $id article ID
* @param $commit boolean defaults to true, triggers transaction end
* @param &$error Array of errors to append to
* @param $user User The deleting user
@@ -2142,7 +2620,7 @@ class WikiPage extends Page implements IDBAccessObject {
public function doDeleteArticleReal(
$reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
) {
- global $wgUser;
+ global $wgUser, $wgContentHandlerUseDB;
wfDebug( __METHOD__ . "\n" );
@@ -2183,6 +2661,9 @@ class WikiPage extends Page implements IDBAccessObject {
$bitfield = 'rev_deleted';
}
+ // we need to remember the old content so we can use it to generate all deletion updates.
+ $content = $this->getContent( Revision::RAW );
+
$dbw = wfGetDB( DB_MASTER );
$dbw->begin( __METHOD__ );
// For now, shunt the revision data into the archive table.
@@ -2195,33 +2676,42 @@ class WikiPage extends Page implements IDBAccessObject {
//
// In the future, we may keep revisions and mark them with
// the rev_deleted field, which is reserved for this purpose.
+
+ $row = array(
+ 'ar_namespace' => 'page_namespace',
+ 'ar_title' => 'page_title',
+ 'ar_comment' => 'rev_comment',
+ 'ar_user' => 'rev_user',
+ 'ar_user_text' => 'rev_user_text',
+ 'ar_timestamp' => 'rev_timestamp',
+ 'ar_minor_edit' => 'rev_minor_edit',
+ 'ar_rev_id' => 'rev_id',
+ 'ar_parent_id' => 'rev_parent_id',
+ 'ar_text_id' => 'rev_text_id',
+ 'ar_text' => '\'\'', // Be explicit to appease
+ 'ar_flags' => '\'\'', // MySQL's "strict mode"...
+ 'ar_len' => 'rev_len',
+ 'ar_page_id' => 'page_id',
+ 'ar_deleted' => $bitfield,
+ 'ar_sha1' => 'rev_sha1',
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $row['ar_content_model'] = 'rev_content_model';
+ $row['ar_content_format'] = 'rev_content_format';
+ }
+
$dbw->insertSelect( 'archive', array( 'page', 'revision' ),
+ $row,
array(
- 'ar_namespace' => 'page_namespace',
- 'ar_title' => 'page_title',
- 'ar_comment' => 'rev_comment',
- 'ar_user' => 'rev_user',
- 'ar_user_text' => 'rev_user_text',
- 'ar_timestamp' => 'rev_timestamp',
- 'ar_minor_edit' => 'rev_minor_edit',
- 'ar_rev_id' => 'rev_id',
- 'ar_parent_id' => 'rev_parent_id',
- 'ar_text_id' => 'rev_text_id',
- 'ar_text' => '\'\'', // Be explicit to appease
- 'ar_flags' => '\'\'', // MySQL's "strict mode"...
- 'ar_len' => 'rev_len',
- 'ar_page_id' => 'page_id',
- 'ar_deleted' => $bitfield,
- 'ar_sha1' => 'rev_sha1'
- ), array(
'page_id' => $id,
'page_id = rev_page'
), __METHOD__
);
- # Now that it's safely backed up, delete it
+ // Now that it's safely backed up, delete it
$dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
- $ok = ( $dbw->affectedRows() > 0 ); // getArticleID() uses slave, could be laggy
+ $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy
if ( !$ok ) {
$dbw->rollback( __METHOD__ );
@@ -2229,9 +2719,13 @@ class WikiPage extends Page implements IDBAccessObject {
return $status;
}
- $this->doDeleteUpdates( $id );
+ if ( !$dbw->cascadingDeletes() ) {
+ $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
+ }
+
+ $this->doDeleteUpdates( $id, $content );
- # Log the deletion, if the page was suppressed, log it at Oversight instead
+ // Log the deletion, if the page was suppressed, log it at Oversight instead
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry = new ManualLogEntry( $logtype, 'delete' );
@@ -2245,7 +2739,7 @@ class WikiPage extends Page implements IDBAccessObject {
$dbw->commit( __METHOD__ );
}
- wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) );
+ wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) );
$status->value = $logid;
return $status;
}
@@ -2253,34 +2747,26 @@ class WikiPage extends Page implements IDBAccessObject {
/**
* Do some database updates after deletion
*
- * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
+ * @param int $id page_id value of the page being deleted
+ * @param $content Content: optional page content to be used when determining the required updates.
+ * This may be needed because $this->getContent() may already return null when the page proper was deleted.
*/
- public function doDeleteUpdates( $id ) {
- # update site status
+ public function doDeleteUpdates( $id, Content $content = null ) {
+ // update site status
DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
- # remove secondary indexes, etc
- $updates = $this->getDeletionUpdates( );
+ // remove secondary indexes, etc
+ $updates = $this->getDeletionUpdates( $content );
DataUpdate::runUpdates( $updates );
- # Clear caches
+ // Clear caches
WikiPage::onArticleDelete( $this->mTitle );
- # Reset this object
- $this->clear();
+ // Reset this object and the Title object
+ $this->loadFromRow( false, self::READ_LATEST );
- # Clear the cached article id so the interface doesn't act like we exist
- $this->mTitle->resetArticleID( 0 );
- }
-
- public function getDeletionUpdates() {
- $updates = array(
- new LinksDeletionUpdate( $this ),
- );
-
- //@todo: make a hook to add update objects
- //NOTE: deletion updates will be determined by the ContentHandler in the future
- return $updates;
+ // Search engine
+ DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
}
/**
@@ -2290,14 +2776,14 @@ class WikiPage extends Page implements IDBAccessObject {
* performs permissions checks on $user, then calls commitRollback()
* to do the dirty work
*
- * @todo: seperate the business/permission stuff out from backend code
+ * @todo Separate the business/permission stuff out from backend code
*
- * @param $fromP String: Name of the user whose edits to rollback.
- * @param $summary String: Custom summary. Set to default summary if empty.
- * @param $token String: Rollback token.
+ * @param string $fromP Name of the user whose edits to rollback.
+ * @param string $summary Custom summary. Set to default summary if empty.
+ * @param string $token Rollback token.
* @param $bot Boolean: If true, mark all reverted edits as bot.
*
- * @param $resultDetails Array: contains result-specific array of additional values
+ * @param array $resultDetails contains result-specific array of additional values
* 'alreadyrolled' : 'current' (rev)
* success : 'summary' (str), 'current' (rev), 'target' (rev)
*
@@ -2312,7 +2798,7 @@ class WikiPage extends Page implements IDBAccessObject {
) {
$resultDetails = null;
- # Check permissions
+ // Check permissions
$editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
$rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
@@ -2325,7 +2811,7 @@ class WikiPage extends Page implements IDBAccessObject {
$errors[] = array( 'actionthrottledtext' );
}
- # If there were errors, bail out now
+ // If there were errors, bail out now
if ( !empty( $errors ) ) {
return $errors;
}
@@ -2341,11 +2827,11 @@ class WikiPage extends Page implements IDBAccessObject {
* rollback to the DB. Therefore, you should only call this function direct-
* ly if you want to use custom permissions checks. If you don't, use
* doRollback() instead.
- * @param $fromP String: Name of the user whose edits to rollback.
- * @param $summary String: Custom summary. Set to default summary if empty.
+ * @param string $fromP Name of the user whose edits to rollback.
+ * @param string $summary Custom summary. Set to default summary if empty.
* @param $bot Boolean: If true, mark all reverted edits as bot.
*
- * @param $resultDetails Array: contains result-specific array of additional values
+ * @param array $resultDetails contains result-specific array of additional values
* @param $guser User The user performing the rollback
* @return array
*/
@@ -2358,16 +2844,16 @@ class WikiPage extends Page implements IDBAccessObject {
return array( array( 'readonlytext' ) );
}
- # Get the last editor
+ // Get the last editor
$current = $this->getRevision();
if ( is_null( $current ) ) {
- # Something wrong... no page?
+ // Something wrong... no page?
return array( array( 'notanarticle' ) );
}
$from = str_replace( '_', ' ', $fromP );
- # User name given should match up with the top revision.
- # If the user was deleted then $from should be empty.
+ // 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',
@@ -2377,8 +2863,8 @@ class WikiPage extends Page implements IDBAccessObject {
) );
}
- # Get the last edit not by this guy...
- # Note: these may not be public values
+ // 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',
@@ -2390,21 +2876,21 @@ class WikiPage extends Page implements IDBAccessObject {
'ORDER BY' => 'rev_timestamp DESC' )
);
if ( $s === false ) {
- # No one else ever edited this page
+ // No one else ever edited this page
return array( array( 'cantrollback' ) );
} elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) {
- # Only admins can see this text
+ // Only admins can see this text
return array( array( 'notvisiblerev' ) );
}
$set = array();
if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
- # Mark all reverted edits as bot
+ // Mark all reverted edits as bot
$set['rc_bot'] = 1;
}
if ( $wgUseRCPatrol ) {
- # Mark all reverted edits as patrolled
+ // Mark all reverted edits as patrolled
$set['rc_patrolled'] = 1;
}
@@ -2418,7 +2904,7 @@ class WikiPage extends Page implements IDBAccessObject {
);
}
- # Generate the edit summary if necessary
+ // Generate the edit summary if necessary
$target = Revision::newFromId( $s->rev_id );
if ( empty( $summary ) ) {
if ( $from == '' ) { // no public user name
@@ -2428,22 +2914,25 @@ class WikiPage extends Page implements IDBAccessObject {
}
}
- # Allow the custom summary to use the same args as the default message
+ // Allow the custom summary to use the same args as the default message
$args = array(
$target->getUserText(), $from, $s->rev_id,
$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
$current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
);
- if( $summary instanceof Message ) {
+ if ( $summary instanceof Message ) {
$summary = $summary->params( $args )->inContentLanguage()->text();
} else {
$summary = wfMsgReplaceArgs( $summary, $args );
}
- # Truncate for whole multibyte characters.
+ // Trim spaces on user supplied text
+ $summary = trim( $summary );
+
+ // Truncate for whole multibyte characters.
$summary = $wgContLang->truncate( $summary, 255 );
- # Save
+ // Save
$flags = EDIT_UPDATE;
if ( $guser->isAllowed( 'minoredit' ) ) {
@@ -2454,8 +2943,13 @@ class WikiPage extends Page implements IDBAccessObject {
$flags |= EDIT_FORCE_BOT;
}
- # Actually store the edit
- $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
+ // Actually store the edit
+ $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
+
+ if ( !$status->isOK() ) {
+ return $status->getErrorsArray();
+ }
+
if ( !empty( $status->value['revision'] ) ) {
$revId = $status->value['revision']->getId();
} else {
@@ -2467,8 +2961,8 @@ class WikiPage extends Page implements IDBAccessObject {
$resultDetails = array(
'summary' => $summary,
'current' => $current,
- 'target' => $target,
- 'newid' => $revId
+ 'target' => $target,
+ 'newid' => $revId
);
return array();
@@ -2486,7 +2980,7 @@ class WikiPage extends Page implements IDBAccessObject {
* @param $title Title object
*/
public static function onArticleCreate( $title ) {
- # Update existence markers on article/talk tabs...
+ // Update existence markers on article/talk tabs...
if ( $title->isTalkPage() ) {
$other = $title->getSubjectPage();
} else {
@@ -2507,7 +3001,7 @@ class WikiPage extends Page implements IDBAccessObject {
* @param $title Title
*/
public static function onArticleDelete( $title ) {
- # Update existence markers on article/talk tabs...
+ // Update existence markers on article/talk tabs...
if ( $title->isTalkPage() ) {
$other = $title->getSubjectPage();
} else {
@@ -2520,21 +3014,22 @@ class WikiPage extends Page implements IDBAccessObject {
$title->touchLinks();
$title->purgeSquid();
- # File cache
+ // File cache
HTMLFileCache::clearFileCache( $title );
+ InfoAction::invalidateCache( $title );
- # Messages
+ // Messages
if ( $title->getNamespace() == NS_MEDIAWIKI ) {
MessageCache::singleton()->replace( $title->getDBkey(), false );
}
- # Images
+ // Images
if ( $title->getNamespace() == NS_FILE ) {
$update = new HTMLCacheUpdate( $title, 'imagelinks' );
$update->doUpdate();
}
- # User talk pages
+ // User talk pages
if ( $title->getNamespace() == NS_USER_TALK ) {
$user = User::newFromName( $title->getText(), false );
if ( $user ) {
@@ -2542,7 +3037,7 @@ class WikiPage extends Page implements IDBAccessObject {
}
}
- # Image redirects
+ // Image redirects
RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
}
@@ -2550,26 +3045,49 @@ class WikiPage extends Page implements IDBAccessObject {
* Purge caches on page update etc
*
* @param $title Title object
- * @todo: verify that $title is always a Title object (and never false or null), add Title hint to parameter $title
+ * @todo Verify that $title is always a Title object (and never false or null), add Title hint to parameter $title
*/
public static function onArticleEdit( $title ) {
// Invalidate caches of articles which include this page
DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' );
-
// Invalidate the caches of all pages which redirect here
DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' );
- # Purge squid for this page only
+ // Purge squid for this page only
$title->purgeSquid();
- # Clear file cache for this page only
+ // Clear file cache for this page only
HTMLFileCache::clearFileCache( $title );
+ InfoAction::invalidateCache( $title );
}
/**#@-*/
/**
+ * Returns a list of categories this page is a member of.
+ * Results will include hidden categories
+ *
+ * @return TitleArray
+ */
+ public function getCategories() {
+ $id = $this->getId();
+ if ( $id == 0 ) {
+ return TitleArray::newFromResult( new FakeResultWrapper( array() ) );
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'categorylinks',
+ array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ),
+ // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
+ // as not being aliases, and NS_CATEGORY is numeric
+ array( 'cl_from' => $id ),
+ __METHOD__ );
+
+ return TitleArray::newFromResult( $res );
+ }
+
+ /**
* Returns a list of hidden categories this page is a member of.
* Uses the page_props and categorylinks tables.
*
@@ -2577,7 +3095,7 @@ class WikiPage extends Page implements IDBAccessObject {
*/
public function getHiddenCategories() {
$result = array();
- $id = $this->mTitle->getArticleID();
+ $id = $this->getId();
if ( $id == 0 ) {
return array();
@@ -2600,61 +3118,24 @@ class WikiPage extends Page implements IDBAccessObject {
}
/**
- * Return an applicable autosummary if one exists for the given edit.
- * @param $oldtext String: the previous text of the page.
- * @param $newtext String: The submitted text of the page.
- * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
- * @return string An appropriate autosummary, or an empty string.
- */
+ * Return an applicable autosummary if one exists for the given edit.
+ * @param string|null $oldtext the previous text of the page.
+ * @param string|null $newtext The submitted text of the page.
+ * @param int $flags bitmask: a bitmask of flags submitted for the edit.
+ * @return string An appropriate autosummary, or an empty string.
+ *
+ * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
+ */
public static function getAutosummary( $oldtext, $newtext, $flags ) {
- global $wgContLang;
-
- # Decide what kind of autosummary is needed.
-
- # Redirect autosummaries
- $ot = Title::newFromRedirect( $oldtext );
- $rt = Title::newFromRedirect( $newtext );
-
- if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
- $truncatedtext = $wgContLang->truncate(
- str_replace( "\n", ' ', $newtext ),
- max( 0, 255
- - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
- - strlen( $rt->getFullText() )
- ) );
- return wfMessage( 'autoredircomment', $rt->getFullText() )
- ->rawParams( $truncatedtext )->inContentLanguage()->text();
- }
+ // NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
- # New page autosummaries
- if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
- # If they're making a new article, give its text, truncated, in the summary.
+ ContentHandler::deprecated( __METHOD__, '1.21' );
- $truncatedtext = $wgContLang->truncate(
- str_replace( "\n", ' ', $newtext ),
- max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) );
+ $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
+ $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
+ $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
- return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
- ->inContentLanguage()->text();
- }
-
- # Blanking autosummaries
- if ( $oldtext != '' && $newtext == '' ) {
- return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
- } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
- # Removing more than 90% of the article
-
- $truncatedtext = $wgContLang->truncate(
- $newtext,
- max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) );
-
- return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
- ->inContentLanguage()->text();
- }
-
- # If we reach this point, there's no applicable autosummary for our case, so our
- # autosummary is empty.
- return '';
+ return $handler->getAutosummary( $oldContent, $newContent, $flags );
}
/**
@@ -2665,158 +3146,75 @@ class WikiPage extends Page implements IDBAccessObject {
* if no revision occurred
*/
public function getAutoDeleteReason( &$hasHistory ) {
- global $wgContLang;
-
- // Get the last revision
- $rev = $this->getRevision();
-
- if ( is_null( $rev ) ) {
- return false;
- }
-
- // Get the article's contents
- $contents = $rev->getText();
- $blank = false;
-
- // If the page is blank, use the text from the previous revision,
- // which can only be blank if there's a move/import/protect dummy revision involved
- if ( $contents == '' ) {
- $prev = $rev->getPrevious();
-
- if ( $prev ) {
- $contents = $prev->getText();
- $blank = true;
- }
- }
-
- $dbw = wfGetDB( DB_MASTER );
-
- // Find out if there was only one contributor
- // Only scan the last 20 revisions
- $res = $dbw->select( 'revision', 'rev_user_text',
- array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
- __METHOD__,
- array( 'LIMIT' => 20 )
- );
-
- if ( $res === false ) {
- // This page has no revisions, which is very weird
- return false;
- }
-
- $hasHistory = ( $res->numRows() > 1 );
- $row = $dbw->fetchObject( $res );
-
- if ( $row ) { // $row is false if the only contributor is hidden
- $onlyAuthor = $row->rev_user_text;
- // Try to find a second contributor
- foreach ( $res as $row ) {
- if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
- $onlyAuthor = false;
- break;
- }
- }
- } else {
- $onlyAuthor = false;
- }
-
- // Generate the summary with a '$1' placeholder
- if ( $blank ) {
- // The current revision is blank and the one before is also
- // blank. It's just not our lucky day
- $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
- } else {
- if ( $onlyAuthor ) {
- $reason = wfMessage(
- 'excontentauthor',
- '$1',
- $onlyAuthor
- )->inContentLanguage()->text();
- } else {
- $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
- }
- }
-
- if ( $reason == '-' ) {
- // Allow these UI messages to be blanked out cleanly
- return '';
- }
-
- // Replace newlines with spaces to prevent uglyness
- $contents = preg_replace( "/[\n\r]/", ' ', $contents );
- // 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 );
- $contents = $wgContLang->truncate( $contents, $maxLength );
- // Remove possible unfinished links
- $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
- // Now replace the '$1' placeholder
- $reason = str_replace( '$1', $contents, $reason );
-
- return $reason;
+ return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
}
/**
* Update all the appropriate counts in the category table, given that
* we've added the categories $added and deleted the categories $deleted.
*
- * @param $added array The names of categories that were added
- * @param $deleted array The names of categories that were deleted
+ * @param array $added The names of categories that were added
+ * @param array $deleted The names of categories that were deleted
*/
- public function updateCategoryCounts( $added, $deleted ) {
- $ns = $this->mTitle->getNamespace();
+ public function updateCategoryCounts( array $added, array $deleted ) {
+ $that = $this;
+ $method = __METHOD__;
$dbw = wfGetDB( DB_MASTER );
- # First make sure the rows exist. If one of the "deleted" ones didn't
- # exist, we might legitimately not create it, but it's simpler to just
- # create it and then give it a negative value, since the value is bogus
- # anyway.
- #
- # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE.
- $insertCats = array_merge( $added, $deleted );
- if ( !$insertCats ) {
- # Okay, nothing to do
- return;
- }
-
- $insertRows = array();
-
- foreach ( $insertCats as $cat ) {
- $insertRows[] = array(
- 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ),
- 'cat_title' => $cat
- );
- }
- $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' );
+ // Do this at the end of the commit to reduce lock wait timeouts
+ $dbw->onTransactionPreCommitOrIdle(
+ function() use ( $dbw, $that, $method, $added, $deleted ) {
+ $ns = $that->getTitle()->getNamespace();
+
+ $addFields = array( 'cat_pages = cat_pages + 1' );
+ $removeFields = array( 'cat_pages = cat_pages - 1' );
+ if ( $ns == NS_CATEGORY ) {
+ $addFields[] = 'cat_subcats = cat_subcats + 1';
+ $removeFields[] = 'cat_subcats = cat_subcats - 1';
+ } elseif ( $ns == NS_FILE ) {
+ $addFields[] = 'cat_files = cat_files + 1';
+ $removeFields[] = 'cat_files = cat_files - 1';
+ }
- $addFields = array( 'cat_pages = cat_pages + 1' );
- $removeFields = array( 'cat_pages = cat_pages - 1' );
+ if ( count( $added ) ) {
+ $insertRows = array();
+ foreach ( $added as $cat ) {
+ $insertRows[] = array(
+ 'cat_title' => $cat,
+ 'cat_pages' => 1,
+ 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
+ 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
+ );
+ }
+ $dbw->upsert(
+ 'category',
+ $insertRows,
+ array( 'cat_title' ),
+ $addFields,
+ $method
+ );
+ }
- if ( $ns == NS_CATEGORY ) {
- $addFields[] = 'cat_subcats = cat_subcats + 1';
- $removeFields[] = 'cat_subcats = cat_subcats - 1';
- } elseif ( $ns == NS_FILE ) {
- $addFields[] = 'cat_files = cat_files + 1';
- $removeFields[] = 'cat_files = cat_files - 1';
- }
+ if ( count( $deleted ) ) {
+ $dbw->update(
+ 'category',
+ $removeFields,
+ array( 'cat_title' => $deleted ),
+ $method
+ );
+ }
- if ( $added ) {
- $dbw->update(
- 'category',
- $addFields,
- array( 'cat_title' => $added ),
- __METHOD__
- );
- }
+ foreach ( $added as $catName ) {
+ $cat = Category::newFromName( $catName );
+ wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $that ) );
+ }
- if ( $deleted ) {
- $dbw->update(
- 'category',
- $removeFields,
- array( 'cat_title' => $deleted ),
- __METHOD__
- );
- }
+ foreach ( $deleted as $catName ) {
+ $cat = Category::newFromName( $catName );
+ wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $that ) );
+ }
+ }
+ );
}
/**
@@ -2836,8 +3234,8 @@ class WikiPage extends Page implements IDBAccessObject {
// that cascaded protections apply as soon as the changes
// are visible.
- # Get templates from templatelinks
- $id = $this->mTitle->getArticleID();
+ // Get templates from templatelinks
+ $id = $this->getId();
$tlTemplates = array();
@@ -2852,7 +3250,7 @@ class WikiPage extends Page implements IDBAccessObject {
$tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true;
}
- # Get templates from parser output.
+ // Get templates from parser output.
$poTemplates = array();
foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
foreach ( $templates as $dbk => $id ) {
@@ -2860,12 +3258,12 @@ class WikiPage extends Page implements IDBAccessObject {
}
}
- # Get the diff
+ // Get the diff
$templates_diff = array_diff_key( $poTemplates, $tlTemplates );
if ( count( $templates_diff ) > 0 ) {
- # Whee, link updates time.
- # Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output.
+ // Whee, link updates time.
+ // Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output.
$u = new LinksUpdate( $this->mTitle, $parserOutput, false );
$u->doUpdate();
}
@@ -2903,7 +3301,7 @@ class WikiPage extends Page implements IDBAccessObject {
* so we can do things like signatures and links-in-context.
*
* @deprecated in 1.19; use Parser::preSaveTransform() instead
- * @param $text String article contents
+ * @param string $text article contents
* @param $user User object: user doing the edit
* @param $popts ParserOptions object: parser options, default options for
* the user loaded if null given
@@ -2950,10 +3348,10 @@ class WikiPage extends Page implements IDBAccessObject {
* Update the article's restriction field, and leave a log entry.
*
* @deprecated since 1.19
- * @param $limit Array: set of restriction keys
+ * @param array $limit set of restriction keys
* @param $reason String
* @param &$cascade Integer. Set to false if cascading protection isn't allowed.
- * @param $expiry Array: per restriction type expiration
+ * @param array $expiry per restriction type expiration
* @param $user User The user updating the restrictions
* @return bool true on success
*/
@@ -2982,7 +3380,7 @@ class WikiPage extends Page implements IDBAccessObject {
public function viewUpdates() {
wfDeprecated( __METHOD__, '1.18' );
global $wgUser;
- return $this->doViewUpdates( $wgUser );
+ $this->doViewUpdates( $wgUser );
}
/**
@@ -2995,6 +3393,31 @@ class WikiPage extends Page implements IDBAccessObject {
global $wgUser;
return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
}
+
+ /**
+ * Returns a list of updates to be performed when this page is deleted. The updates should remove any information
+ * about this page from secondary data stores such as links tables.
+ *
+ * @param Content|null $content optional Content object for determining the necessary updates
+ * @return Array an array of DataUpdates objects
+ */
+ public function getDeletionUpdates( Content $content = null ) {
+ if ( !$content ) {
+ // load content object, which may be used to determine the necessary updates
+ // XXX: the content may not be needed to determine the updates, then this would be overhead.
+ $content = $this->getContent( Revision::RAW );
+ }
+
+ if ( !$content ) {
+ $updates = array();
+ } else {
+ $updates = $content->getDeletionUpdates( $this );
+ }
+
+ wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
+ return $updates;
+ }
+
}
class PoolWorkArticleView extends PoolCounterWork {
@@ -3020,9 +3443,9 @@ class PoolWorkArticleView extends PoolCounterWork {
private $parserOptions;
/**
- * @var string|null
+ * @var Content|null
*/
- private $text;
+ private $content = null;
/**
* @var ParserOutput|bool
@@ -3042,18 +3465,24 @@ class PoolWorkArticleView extends PoolCounterWork {
/**
* Constructor
*
- * @param $page Page
+ * @param $page Page|WikiPage
* @param $revid Integer: ID of the revision being parsed
* @param $useParserCache Boolean: whether to use the parser cache
* @param $parserOptions parserOptions to use for the parse operation
- * @param $text String: text to parse or null to load it
+ * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
*/
- function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
+ function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
+ if ( is_string( $content ) ) { // BC: old style call
+ $modelId = $page->getRevision()->getContentModel();
+ $format = $page->getRevision()->getContentFormat();
+ $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
+ }
+
$this->page = $page;
$this->revid = $revid;
$this->cacheable = $useParserCache;
$this->parserOptions = $parserOptions;
- $this->text = $text;
+ $this->content = $content;
$this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
}
@@ -3089,35 +3518,49 @@ class PoolWorkArticleView extends PoolCounterWork {
* @return bool
*/
function doWork() {
- global $wgParser, $wgUseFileCache;
+ global $wgUseFileCache;
+
+ // @todo several of the methods called on $this->page are not declared in Page, but present
+ // in WikiPage and delegated by Article.
$isCurrent = $this->revid === $this->page->getLatest();
- if ( $this->text !== null ) {
- $text = $this->text;
+ if ( $this->content !== null ) {
+ $content = $this->content;
} elseif ( $isCurrent ) {
- $text = $this->page->getRawText();
+ // XXX: why use RAW audience here, and PUBLIC (default) below?
+ $content = $this->page->getContent( Revision::RAW );
} else {
$rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
+
if ( $rev === null ) {
- return false;
+ $content = null;
+ } else {
+ // XXX: why use PUBLIC audience here (default), and RAW above?
+ $content = $rev->getContent();
}
- $text = $rev->getText();
}
+ if ( $content === null ) {
+ return false;
+ }
+
+ // Reduce effects of race conditions for slow parses (bug 46014)
+ $cacheTime = wfTimestampNow();
+
$time = - microtime( true );
- $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
- $this->parserOptions, true, true, $this->revid );
+ $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
$time += microtime( true );
- # Timing hack
+ // Timing hack
if ( $time > 3 ) {
wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
$this->page->getTitle()->getPrefixedDBkey() ) );
}
if ( $this->cacheable && $this->parserOutput->isCacheable() ) {
- ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions );
+ ParserCache::singleton()->save(
+ $this->parserOutput, $this->page, $this->parserOptions, $cacheTime );
}
// Make sure file cache is not used on uncacheable content.
diff --git a/includes/Xml.php b/includes/Xml.php
index 120312dd..ac0539d1 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -30,21 +30,21 @@ class Xml {
* Strings are assumed to not contain XML-illegal characters; special
* characters (<, >, &) are escaped but illegals are not touched.
*
- * @param $element String: element name
- * @param $attribs Array: Name=>value pairs. Values will be escaped.
- * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
- * @param $allowShortTag Bool: whether '' in $contents will result in a contentless closed tag
+ * @param string $element element name
+ * @param array $attribs Name=>value pairs. Values will be escaped.
+ * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @param bool $allowShortTag whether '' in $contents will result in a contentless closed tag
* @return string
*/
public static function element( $element, $attribs = null, $contents = '', $allowShortTag = true ) {
$out = '<' . $element;
- if( !is_null( $attribs ) ) {
- $out .= self::expandAttributes( $attribs );
+ if ( !is_null( $attribs ) ) {
+ $out .= self::expandAttributes( $attribs );
}
- if( is_null( $contents ) ) {
+ if ( is_null( $contents ) ) {
$out .= '>';
} else {
- if( $allowShortTag && $contents === '' ) {
+ if ( $allowShortTag && $contents === '' ) {
$out .= ' />';
} else {
$out .= '>' . htmlspecialchars( $contents ) . "</$element>";
@@ -58,15 +58,16 @@ class Xml {
* to set the XML attributes : attributename="value".
* The values are passed to Sanitizer::encodeAttribute.
* Return null if no attributes given.
- * @param $attribs Array of attributes for an XML element
+ * @param array $attribs of attributes for an XML element
+ * @throws MWException
* @return null|string
*/
public static function expandAttributes( $attribs ) {
$out = '';
- if( is_null( $attribs ) ) {
+ if ( is_null( $attribs ) ) {
return null;
- } elseif( is_array( $attribs ) ) {
- foreach( $attribs as $name => $val ) {
+ } elseif ( is_array( $attribs ) ) {
+ foreach ( $attribs as $name => $val ) {
$out .= " {$name}=\"" . Sanitizer::encodeAttribute( $val ) . '"';
}
return $out;
@@ -81,16 +82,16 @@ class Xml {
* is passed.
*
* @param $element String:
- * @param $attribs Array: Name=>value pairs. Values will be escaped.
- * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @param array $attribs Name=>value pairs. Values will be escaped.
+ * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default)
* @return string
*/
- public static function elementClean( $element, $attribs = array(), $contents = '') {
+ public static function elementClean( $element, $attribs = array(), $contents = '' ) {
global $wgContLang;
- if( $attribs ) {
+ if ( $attribs ) {
$attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
}
- if( $contents ) {
+ if ( $contents ) {
wfProfileIn( __METHOD__ . '-norm' );
$contents = $wgContLang->normalize( $contents );
wfProfileOut( __METHOD__ . '-norm' );
@@ -101,8 +102,8 @@ class Xml {
/**
* This opens an XML element
*
- * @param $element String name of the element
- * @param $attribs array of attributes, see Xml::expandAttributes()
+ * @param string $element name of the element
+ * @param array $attribs of attributes, see Xml::expandAttributes()
* @return string
*/
public static function openElement( $element, $attribs = null ) {
@@ -111,18 +112,20 @@ class Xml {
/**
* Shortcut to close an XML element
- * @param $element String element name
+ * @param string $element element name
* @return string
*/
- public static function closeElement( $element ) { return "</$element>"; }
+ public static function closeElement( $element ) {
+ return "</$element>";
+ }
/**
* Same as Xml::element(), but does not escape contents. Handy when the
* content you have is already valid xml.
*
- * @param $element String element name
- * @param $attribs array of attributes
- * @param $contents String content of the element
+ * @param string $element element name
+ * @param array $attribs of attributes
+ * @param string $contents content of the element
* @return string
*/
public static function tags( $element, $attribs = null, $contents ) {
@@ -135,7 +138,7 @@ class Xml {
* @param $selected Mixed: Namespace which should be pre-selected
* @param $all Mixed: Value of an item denoting all namespaces, or null to omit
* @param $element_name String: value of the "name" attribute of the select tag
- * @param $label String: optional label to add to the field
+ * @param string $label optional label to add to the field
* @return string
* @deprecated since 1.19
*/
@@ -143,11 +146,11 @@ class Xml {
wfDeprecated( __METHOD__, '1.19' );
return Html::namespaceSelector( array(
'selected' => $selected,
- 'all' => $all,
- 'label' => $label,
+ 'all' => $all,
+ 'label' => $label,
), array(
- 'name' => $element_name,
- 'id' => 'namespace',
+ 'name' => $element_name,
+ 'id' => 'namespace',
'class' => 'namespaceselector',
) );
}
@@ -156,19 +159,22 @@ class Xml {
* Create a date selector
*
* @param $selected Mixed: the month which should be selected, default ''
- * @param $allmonths String: value of a special item denoting all month. Null to not include (default)
- * @param $id String: Element identifier
+ * @param string $allmonths value of a special item denoting all month. Null to not include (default)
+ * @param string $id Element identifier
* @return String: Html string containing the month selector
*/
public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
global $wgLang;
$options = array();
- if( is_null( $selected ) )
+ if ( is_null( $selected ) ) {
$selected = '';
- if( !is_null( $allmonths ) )
+ }
+ if ( !is_null( $allmonths ) ) {
$options[] = self::option( wfMessage( 'monthsall' )->text(), $allmonths, $selected === $allmonths );
- for( $i = 1; $i < 13; $i++ )
+ }
+ for ( $i = 1; $i < 13; $i++ ) {
$options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
+ }
return self::openElement( 'select', array( 'id' => $id, 'name' => 'month', 'class' => 'mw-month-selector' ) )
. implode( "\n", $options )
. self::closeElement( 'select' );
@@ -181,32 +187,34 @@ class Xml {
*/
public static function dateMenu( $year, $month ) {
# Offset overrides year/month selection
- if( $month && $month !== -1 ) {
+ if ( $month && $month !== -1 ) {
$encMonth = intval( $month );
} else {
$encMonth = '';
}
- if( $year ) {
+ if ( $year ) {
$encYear = intval( $year );
- } elseif( $encMonth ) {
- $thisMonth = intval( gmdate( 'n' ) );
- $thisYear = intval( gmdate( 'Y' ) );
- if( intval($encMonth) > $thisMonth ) {
+ } elseif ( $encMonth ) {
+ $timestamp = MWTimestamp::getInstance();
+ $thisMonth = intval( $timestamp->format( 'n' ) );
+ $thisYear = intval( $timestamp->format( 'Y' ) );
+ if ( intval( $encMonth ) > $thisMonth ) {
$thisYear--;
}
$encYear = $thisYear;
} else {
$encYear = '';
}
- return Xml::label( wfMessage( 'year' )->text(), 'year' ) . ' '.
- Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . ' '.
- Xml::label( wfMessage( 'month' )->text(), 'month' ) . ' '.
- Xml::monthSelector( $encMonth, -1 );
+ $inputAttribs = array( 'id' => 'year', 'maxlength' => 4, 'size' => 7 );
+ return self::label( wfMessage( 'year' )->text(), 'year' ) . ' ' .
+ Html::input( 'year', $encYear, 'number', $inputAttribs ) . ' ' .
+ self::label( wfMessage( 'month' )->text(), 'month' ) . ' ' .
+ self::monthSelector( $encMonth, -1 );
}
/**
* Construct a language selector appropriate for use in a form or preferences
- *
+ *
* @param string $selected The language code of the selected language
* @param boolean $customisedOnly If true only languages which have some content are listed
* @param string $inLanguage The ISO code of the language to display the select list in (optional)
@@ -222,7 +230,7 @@ class Xml {
// Make sure the site language is in the list;
// a custom language code might not have a defined name...
- if( !array_key_exists( $wgLanguageCode, $languages ) ) {
+ if ( !array_key_exists( $wgLanguageCode, $languages ) ) {
$languages[$wgLanguageCode] = $wgLanguageCode;
}
@@ -235,14 +243,14 @@ class Xml {
*/
$selected = isset( $languages[$selected] ) ? $selected : $wgLanguageCode;
$options = "\n";
- foreach( $languages as $code => $name ) {
- $options .= Xml::option( "$code - $name", $code, ($code == $selected) ) . "\n";
+ foreach ( $languages as $code => $name ) {
+ $options .= Xml::option( "$code - $name", $code, $code == $selected ) . "\n";
}
$attrs = array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' );
$attrs = array_merge( $attrs, $overrideAttrs );
- if( $msg === null ) {
+ if ( $msg === null ) {
$msg = wfMessage( 'yourlanguage' );
}
return array(
@@ -254,9 +262,9 @@ class Xml {
/**
* Shortcut to make a span element
- * @param $text String content of the element, will be escaped
- * @param $class String class name of the span element
- * @param $attribs array other attributes
+ * @param string $text content of the element, will be escaped
+ * @param string $class class name of the span element
+ * @param array $attribs other attributes
* @return string
*/
public static function span( $text, $class, $attribs = array() ) {
@@ -265,10 +273,10 @@ class Xml {
/**
* Shortcut to make a specific element with a class attribute
- * @param $text string content of the element, will be escaped
- * @param $class string class name of the span element
- * @param $tag string element name
- * @param $attribs array other attributes
+ * @param string $text content of the element, will be escaped
+ * @param string $class class name of the span element
+ * @param string $tag element name
+ * @param array $attribs other attributes
* @return string
*/
public static function wrapClass( $text, $class, $tag = 'span', $attribs = array() ) {
@@ -277,20 +285,20 @@ class Xml {
/**
* Convenience function to build an HTML text input field
- * @param $name String value of the name attribute
- * @param $size int value of the size attribute
+ * @param string $name value of the name attribute
+ * @param int $size value of the size attribute
* @param $value mixed value of the value attribute
- * @param $attribs array other attributes
+ * @param array $attribs other attributes
* @return string HTML
*/
public static function input( $name, $size = false, $value = false, $attribs = array() ) {
$attributes = array( 'name' => $name );
- if( $size ) {
+ if ( $size ) {
$attributes['size'] = $size;
}
- if( $value !== false ) { // maybe 0
+ if ( $value !== false ) { // maybe 0
$attributes['value'] = $value;
}
@@ -299,10 +307,10 @@ class Xml {
/**
* Convenience function to build an HTML password input field
- * @param $name string value of the name attribute
- * @param $size int value of the size attribute
+ * @param string $name value of the name attribute
+ * @param int $size value of the size attribute
* @param $value mixed value of the value attribute
- * @param $attribs array other attributes
+ * @param array $attribs other attributes
* @return string HTML
*/
public static function password( $name, $size = false, $value = false, $attribs = array() ) {
@@ -323,12 +331,12 @@ class Xml {
/**
* Convenience function to build an HTML checkbox
- * @param $name String value of the name attribute
- * @param $checked Bool Whether the checkbox is checked or not
- * @param $attribs Array other attributes
+ * @param string $name value of the name attribute
+ * @param bool $checked Whether the checkbox is checked or not
+ * @param array $attribs other attributes
* @return string HTML
*/
- public static function check( $name, $checked = false, $attribs=array() ) {
+ public static function check( $name, $checked = false, $attribs = array() ) {
return self::element( 'input', array_merge(
array(
'name' => $name,
@@ -340,10 +348,10 @@ class Xml {
/**
* Convenience function to build an HTML radio button
- * @param $name String value of the name attribute
- * @param $value String value of the value attribute
- * @param $checked Bool Whether the checkbox is checked or not
- * @param $attribs Array other attributes
+ * @param string $name value of the name attribute
+ * @param string $value value of the value attribute
+ * @param bool $checked Whether the checkbox is checked or not
+ * @param array $attribs other attributes
* @return string HTML
*/
public static function radio( $name, $value, $checked = false, $attribs = array() ) {
@@ -355,9 +363,9 @@ class Xml {
/**
* Convenience function to build an HTML form label
- * @param $label String text of the label
+ * @param string $label text of the label
* @param $id
- * @param $attribs Array an attribute array. This will usuall be
+ * @param array $attribs an attribute array. This will usually be
* the same array as is passed to the corresponding input element,
* so this function will cherry-pick appropriate attributes to
* apply to the label as well; only class and title are applied.
@@ -367,10 +375,10 @@ class Xml {
$a = array( 'for' => $id );
# FIXME avoid copy pasting below:
- if( isset( $attribs['class'] ) ){
+ if ( isset( $attribs['class'] ) ) {
$a['class'] = $attribs['class'];
}
- if( isset( $attribs['title'] ) ){
+ if ( isset( $attribs['title'] ) ) {
$a['title'] = $attribs['title'];
}
@@ -379,15 +387,15 @@ class Xml {
/**
* Convenience function to build an HTML text input field with a label
- * @param $label String text of the label
- * @param $name String value of the name attribute
- * @param $id String id of the input
- * @param $size Int|Bool value of the size attribute
- * @param $value String|Bool value of the value attribute
- * @param $attribs array other attributes
+ * @param string $label text of the label
+ * @param string $name value of the name attribute
+ * @param string $id id of the input
+ * @param int|Bool $size value of the size attribute
+ * @param string|Bool $value value of the value attribute
+ * @param array $attribs other attributes
* @return string HTML
*/
- public static function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs = array() ) {
+ public static function inputLabel( $label, $name, $id, $size = false, $value = false, $attribs = array() ) {
list( $label, $input ) = self::inputLabelSep( $label, $name, $id, $size, $value, $attribs );
return $label . '&#160;' . $input;
}
@@ -448,8 +456,8 @@ class Xml {
/**
* Convenience function to build an HTML submit button
- * @param $value String: label text for the button
- * @param $attribs Array: optional custom attributes
+ * @param string $value label text for the button
+ * @param array $attribs optional custom attributes
* @return string HTML
*/
public static function submitButton( $value, $attribs = array() ) {
@@ -458,18 +466,18 @@ class Xml {
/**
* Convenience function to build an HTML drop-down list item.
- * @param $text String: text for this item
- * @param $value String: form submission value; if empty, use text
+ * @param string $text text for this item. Will be HTML escaped
+ * @param string $value form submission value; if empty, use text
* @param $selected boolean: if true, will be the default selected item
- * @param $attribs array: optional additional HTML attributes
+ * @param array $attribs optional additional HTML attributes
* @return string HTML
*/
- public static function option( $text, $value=null, $selected = false,
+ public static function option( $text, $value = null, $selected = false,
$attribs = array() ) {
- if( !is_null( $value ) ) {
+ if ( !is_null( $value ) ) {
$attribs['value'] = $value;
}
- if( $selected ) {
+ if ( $selected ) {
$attribs['selected'] = 'selected';
}
return Html::element( 'option', $attribs, $text );
@@ -486,47 +494,53 @@ class Xml {
* @param $tabindex Mixed: Value of the tabindex attribute
* @return string
*/
- public static function listDropDown( $name= '', $list = '', $other = '', $selected = '', $class = '', $tabindex = null ) {
+ public static function listDropDown( $name = '', $list = '', $other = '', $selected = '', $class = '', $tabindex = null ) {
$optgroup = false;
$options = self::option( $other, 'other', $selected === 'other' );
- foreach ( explode( "\n", $list ) as $option) {
+ foreach ( explode( "\n", $list ) as $option ) {
$value = trim( $option );
if ( $value == '' ) {
continue;
- } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
+ } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
// A new group is starting ...
$value = trim( substr( $value, 1 ) );
- if( $optgroup ) $options .= self::closeElement('optgroup');
+ if ( $optgroup ) {
+ $options .= self::closeElement( 'optgroup' );
+ }
$options .= self::openElement( 'optgroup', array( 'label' => $value ) );
$optgroup = true;
- } elseif ( substr( $value, 0, 2) == '**' ) {
+ } elseif ( substr( $value, 0, 2 ) == '**' ) {
// groupmember
$value = trim( substr( $value, 2 ) );
$options .= self::option( $value, $value, $selected === $value );
} else {
// groupless reason list
- if( $optgroup ) $options .= self::closeElement('optgroup');
+ if ( $optgroup ) {
+ $options .= self::closeElement( 'optgroup' );
+ }
$options .= self::option( $value, $value, $selected === $value );
$optgroup = false;
}
}
- if( $optgroup ) $options .= self::closeElement('optgroup');
+ if ( $optgroup ) {
+ $options .= self::closeElement( 'optgroup' );
+ }
$attribs = array();
- if( $name ) {
+ if ( $name ) {
$attribs['id'] = $name;
$attribs['name'] = $name;
}
- if( $class ) {
+ if ( $class ) {
$attribs['class'] = $class;
}
- if( $tabindex ) {
+ if ( $tabindex ) {
$attribs['tabindex'] = $tabindex;
}
@@ -540,9 +554,9 @@ class Xml {
/**
* Shortcut for creating fieldsets.
*
- * @param $legend string|bool Legend of the fieldset. If evaluates to false, legend is not added.
- * @param $content string Pre-escaped content for the fieldset. If false, only open fieldset is returned.
- * @param $attribs array Any attributes to fieldset-element.
+ * @param string|bool $legend Legend of the fieldset. If evaluates to false, legend is not added.
+ * @param string $content Pre-escaped content for the fieldset. If false, only open fieldset is returned.
+ * @param array $attribs Any attributes to fieldset-element.
*
* @return string
*/
@@ -564,17 +578,18 @@ class Xml {
/**
* Shortcut for creating textareas.
*
- * @param $name string The 'name' for the textarea
- * @param $content string Content for the textarea
- * @param $cols int The number of columns for the textarea
- * @param $rows int The number of rows for the textarea
- * @param $attribs array Any other attributes for the textarea
+ * @param string $name The 'name' for the textarea
+ * @param string $content Content for the textarea
+ * @param int $cols The number of columns for the textarea
+ * @param int $rows The number of rows for the textarea
+ * @param array $attribs Any other attributes for the textarea
*
* @return string
*/
public static function textarea( $name, $content, $cols = 40, $rows = 5, $attribs = array() ) {
return self::element( 'textarea',
- array( 'name' => $name,
+ array(
+ 'name' => $name,
'id' => $name,
'cols' => $cols,
'rows' => $rows
@@ -587,7 +602,8 @@ class Xml {
* for JavaScript source code.
* Illegal control characters are assumed not to be present.
*
- * @param $string String to escape
+ * @deprecated since 1.21; use Xml::encodeJsVar() or Xml::encodeJsCall() instead
+ * @param string $string to escape
* @return String
*/
public static function escapeJsString( $string ) {
@@ -618,90 +634,53 @@ class Xml {
}
/**
- * Encode a variable of unknown type to JavaScript.
- * Arrays are converted to JS arrays, objects are converted to JS associative
- * arrays (objects). So cast your PHP associative arrays to objects before
- * passing them to here.
+ * Encode a variable of arbitrary type to JavaScript.
+ * If the value is an XmlJsCode object, pass through the object's value verbatim.
*
- * @param $value
+ * @note Only use this function for generating JavaScript code. If generating output
+ * for a proper JSON parser, just call FormatJson::encode() directly.
*
- * @return string
+ * @param mixed $value The value being encoded. Can be any type except a resource.
+ * @param bool $pretty If true, add non-significant whitespace to improve readability.
+ * @return string|bool: String if successful; false upon failure
*/
- public static function encodeJsVar( $value ) {
- if ( is_bool( $value ) ) {
- $s = $value ? 'true' : 'false';
- } elseif ( is_null( $value ) ) {
- $s = 'null';
- } elseif ( is_int( $value ) || is_float( $value ) ) {
- $s = strval($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 != '[' ) {
- $s .= ',';
- }
- $s .= self::encodeJsVar( $elt );
- }
- $s .= ']';
- } elseif ( $value instanceof XmlJsCode ) {
- $s = $value->value;
- } elseif ( is_object( $value ) || is_array( $value ) ) {
- // Objects and associative arrays
- $s = '{';
- foreach ( (array)$value as $name => $elt ) {
- if ( $s != '{' ) {
- $s .= ',';
- }
-
- $s .= '"' . self::escapeJsString( $name ) . '":' .
- self::encodeJsVar( $elt );
- }
- $s .= '}';
- } else {
- $s = '"' . self::escapeJsString( $value ) . '"';
+ public static function encodeJsVar( $value, $pretty = false ) {
+ if ( $value instanceof XmlJsCode ) {
+ return $value->value;
}
- return $s;
+ return FormatJson::encode( $value, $pretty, FormatJson::UTF8_OK );
}
/**
* Create a call to a JavaScript function. The supplied arguments will be
* encoded using Xml::encodeJsVar().
*
- * @param $name String The name of the function to call, or a JavaScript expression
- * which evaluates to a function object which is called.
- * @param $args Array of arguments to pass to the function.
- *
* @since 1.17
- *
- * @return string
- */
- public static function encodeJsCall( $name, $args ) {
- $s = "$name(";
- $first = true;
-
- foreach ( $args as $arg ) {
- if ( $first ) {
- $first = false;
- } else {
- $s .= ', ';
+ * @param string $name The name of the function to call, or a JavaScript expression
+ * which evaluates to a function object which is called.
+ * @param array $args The arguments to pass to the function.
+ * @param bool $pretty If true, add non-significant whitespace to improve readability.
+ * @return string|bool: String if successful; false upon failure
+ */
+ public static function encodeJsCall( $name, $args, $pretty = false ) {
+ foreach ( $args as &$arg ) {
+ $arg = Xml::encodeJsVar( $arg, $pretty );
+ if ( $arg === false ) {
+ return false;
}
-
- $s .= Xml::encodeJsVar( $arg );
}
- $s .= ");\n";
-
- return $s;
+ return "$name(" . ( $pretty
+ ? ( ' ' . implode( ', ', $args ) . ' ' )
+ : implode( ',', $args )
+ ) . ");";
}
/**
* Check if a string is well-formed XML.
* Must include the surrounding tag.
*
- * @param $text String: string to test.
+ * @param string $text string to test.
* @return bool
*
* @todo Error position reporting return
@@ -712,7 +691,7 @@ class Xml {
# case folding violates XML standard, turn it off
xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
- if( !xml_parse( $parser, $text, true ) ) {
+ if ( !xml_parse( $parser, $text, true ) ) {
//$err = xml_error_string( xml_get_error_code( $parser ) );
//$position = xml_get_current_byte_index( $parser );
//$fragment = $this->extractFragment( $html, $position );
@@ -748,7 +727,7 @@ class Xml {
* Replace " > and < with their respective HTML entities ( &quot;,
* &gt;, &lt;)
*
- * @param $in String: text that might contain HTML tags.
+ * @param string $in text that might contain HTML tags.
* @return string Escaped string
*/
public static function escapeTagsOnly( $in ) {
@@ -759,28 +738,34 @@ class Xml {
}
/**
- * Generate a form (without the opening form element).
- * Output optionally includes a submit button.
- * @param $fields Array Associative array, key is message corresponding to a description for the field (colon is in the message), value is appropriate input.
- * @param $submitLabel String A message containing a label for the submit button.
- * @return string HTML form.
- */
- public static function buildForm( $fields, $submitLabel = null ) {
+ * Generate a form (without the opening form element).
+ * Output optionally includes a submit button.
+ * @param array $fields Associative array, key is the name of a message that contains a description for the field, value is an HTML string containing the appropriate input.
+ * @param string $submitLabel The name of a message containing a label for the submit button.
+ * @param array $submitAttribs The attributes to add to the submit button
+ * @return string HTML form.
+ */
+ public static function buildForm( $fields, $submitLabel = null, $submitAttribs = array() ) {
$form = '';
$form .= "<table><tbody>";
- foreach( $fields as $labelmsg => $input ) {
+ foreach ( $fields as $labelmsg => $input ) {
$id = "mw-$labelmsg";
$form .= Xml::openElement( 'tr', array( 'id' => $id ) );
- $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMessage( $labelmsg )->parse() );
+
+ // TODO use a <label> here for accessibility purposes - will need
+ // to either not use a table to build the form, or find the ID of
+ // the input somehow.
+
+ $form .= Xml::tags( 'td', array( 'class' => 'mw-label' ), wfMessage( $labelmsg )->parse() );
$form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' );
$form .= Xml::closeElement( 'tr' );
}
- if( $submitLabel ) {
+ if ( $submitLabel ) {
$form .= Xml::openElement( 'tr' );
$form .= Xml::tags( 'td', array(), '' );
- $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMessage( $submitLabel )->text() ) . Xml::closeElement( 'td' );
+ $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMessage( $submitLabel )->text(), $submitAttribs ) . Xml::closeElement( 'td' );
$form .= Xml::closeElement( 'tr' );
}
@@ -791,9 +776,9 @@ class Xml {
/**
* Build a table of data
- * @param $rows array An array of arrays of strings, each to be a row in a table
- * @param $attribs array An array of attributes to apply to the table tag [optional]
- * @param $headers array An array of strings to use as table headers [optional]
+ * @param array $rows An array of arrays of strings, each to be a row in a table
+ * @param array $attribs An array of attributes to apply to the table tag [optional]
+ * @param array $headers An array of strings to use as table headers [optional]
* @return string
*/
public static function buildTable( $rows, $attribs = array(), $headers = null ) {
@@ -802,7 +787,7 @@ class Xml {
if ( is_array( $headers ) ) {
$s .= Xml::openElement( 'thead', $attribs );
- foreach( $headers as $id => $header ) {
+ foreach ( $headers as $id => $header ) {
$attribs = array();
if ( is_string( $id ) ) {
@@ -814,7 +799,7 @@ class Xml {
$s .= Xml::closeElement( 'thead' );
}
- foreach( $rows as $id => $row ) {
+ foreach ( $rows as $id => $row ) {
$attribs = array();
if ( is_string( $id ) ) {
@@ -831,15 +816,14 @@ class Xml {
/**
* Build a row for a table
- * @param $attribs array An array of attributes to apply to the tr tag
- * @param $cells array An array of strings to put in <td>
+ * @param array $attribs An array of attributes to apply to the tr tag
+ * @param array $cells An array of strings to put in <td>
* @return string
*/
public static function buildTableRow( $attribs, $cells ) {
$s = Xml::openElement( 'tr', $attribs );
- foreach( $cells as $id => $cell ) {
-
+ foreach ( $cells as $id => $cell ) {
$attribs = array();
if ( is_string( $id ) ) {
@@ -907,7 +891,7 @@ class XmlSelect {
*/
public function addOption( $name, $value = false ) {
// Stab stab stab
- $value = ($value !== false) ? $value : $name;
+ $value = $value !== false ? $value : $name;
$this->options[] = array( $name => $value );
}
@@ -935,7 +919,7 @@ class XmlSelect {
static function formatOptions( $options, $default = false ) {
$data = '';
- foreach( $options as $label => $value ) {
+ foreach ( $options as $label => $value ) {
if ( is_array( $value ) ) {
$contents = self::formatOptions( $value, $default );
$data .= Html::rawElement( 'optgroup', array( 'label' => $label ), $contents ) . "\n";
@@ -971,6 +955,11 @@ class XmlSelect {
* Xml::encodeJsVar( new XmlJsCode( 'a + b' ) );
*
* Returns "a + b".
+ *
+ * @note As of 1.21, XmlJsCode objects cannot be nested inside objects or arrays. The sole
+ * exception is the $args argument to Xml::encodeJsCall() because Xml::encodeJsVar() is
+ * called for each individual element in that array.
+ *
* @since 1.17
*/
class XmlJsCode {
diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php
index b95dd6a5..eb98a4ad 100644
--- a/includes/XmlTypeCheck.php
+++ b/includes/XmlTypeCheck.php
@@ -40,16 +40,63 @@ class XmlTypeCheck {
public $rootElement = '';
/**
- * @param $file string filename
- * @param $filterCallback callable (optional)
+ * Additional parsing options
+ */
+ private $parserOptions = array(
+ 'processing_instruction_handler' => '',
+ );
+
+ /**
+ * @param string $input a filename or string containing the XML element
+ * @param callable $filterCallback (optional)
* Function to call to do additional custom validity checks from the
* SAX element handler event. This gives you access to the element
* namespace, name, and attributes, but not to text contents.
* Filter should return 'true' to toggle on $this->filterMatch
+ * @param boolean $isFile (optional) indicates if the first parameter is a
+ * filename (default, true) or if it is a string (false)
+ * @param array $options list of additional parsing options:
+ * processing_instruction_handler: Callback for xml_set_processing_instruction_handler
*/
- function __construct( $file, $filterCallback=null ) {
+ function __construct( $input, $filterCallback = null, $isFile = true, $options = array() ) {
$this->filterCallback = $filterCallback;
- $this->run( $file );
+ $this->parserOptions = array_merge( $this->parserOptions, $options );
+
+ if ( $isFile ) {
+ $this->validateFromFile( $input );
+ } else {
+ $this->validateFromString( $input );
+ }
+ }
+
+ /**
+ * Alternative constructor: from filename
+ *
+ * @param string $fname the filename of an XML document
+ * @param callable $filterCallback (optional)
+ * Function to call to do additional custom validity checks from the
+ * SAX element handler event. This gives you access to the element
+ * namespace, name, and attributes, but not to text contents.
+ * Filter should return 'true' to toggle on $this->filterMatch
+ * @return XmlTypeCheck
+ */
+ public static function newFromFilename( $fname, $filterCallback = null ) {
+ return new self( $fname, $filterCallback, true );
+ }
+
+ /**
+ * Alternative constructor: from string
+ *
+ * @param string $string a string containing an XML element
+ * @param callable $filterCallback (optional)
+ * Function to call to do additional custom validity checks from the
+ * SAX element handler event. This gives you access to the element
+ * namespace, name, and attributes, but not to text contents.
+ * Filter should return 'true' to toggle on $this->filterMatch
+ * @return XmlTypeCheck
+ */
+ public static function newFromString( $string, $filterCallback = null ) {
+ return new self( $string, $filterCallback, false );
}
/**
@@ -62,15 +109,29 @@ class XmlTypeCheck {
}
/**
- * @param $fname
+ * Get an XML parser with the root element handler.
+ * @see XmlTypeCheck::rootElementOpen()
+ * @return resource a resource handle for the XML parser
*/
- private function run( $fname ) {
+ private function getParser() {
$parser = xml_parser_create_ns( 'UTF-8' );
-
// case folding violates XML standard, turn it off
xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
-
xml_set_element_handler( $parser, array( $this, 'rootElementOpen' ), false );
+ if ( $this->parserOptions['processing_instruction_handler'] ) {
+ xml_set_processing_instruction_handler(
+ $parser,
+ array( $this, 'processingInstructionHandler' )
+ );
+ }
+ return $parser;
+ }
+
+ /**
+ * @param string $fname the filename
+ */
+ private function validateFromFile( $fname ) {
+ $parser = $this->getParser();
if ( file_exists( $fname ) ) {
$file = fopen( $fname, "rb" );
@@ -78,24 +139,38 @@ class XmlTypeCheck {
do {
$chunk = fread( $file, 32768 );
$ret = xml_parse( $parser, $chunk, feof( $file ) );
- if( $ret == 0 ) {
- // XML isn't well-formed!
+ if ( $ret == 0 ) {
+ $this->wellFormed = false;
fclose( $file );
xml_parser_free( $parser );
return;
}
- } while( !feof( $file ) );
+ } while ( !feof( $file ) );
fclose( $file );
}
}
-
$this->wellFormed = true;
xml_parser_free( $parser );
}
/**
+ *
+ * @param string $string the XML-input-string to be checked.
+ */
+ private function validateFromString( $string ) {
+ $parser = $this->getParser();
+ $ret = xml_parse( $parser, $string, true );
+ xml_parser_free( $parser );
+ if ( $ret == 0 ) {
+ $this->wellFormed = false;
+ return;
+ }
+ $this->wellFormed = true;
+ }
+
+ /**
* @param $parser
* @param $name
* @param $attribs
@@ -103,7 +178,7 @@ class XmlTypeCheck {
private function rootElementOpen( $parser, $name, $attribs ) {
$this->rootElement = $name;
- if( is_callable( $this->filterCallback ) ) {
+ if ( is_callable( $this->filterCallback ) ) {
xml_set_element_handler( $parser, array( $this, 'elementOpen' ), false );
$this->elementOpen( $parser, $name, $attribs );
} else {
@@ -118,7 +193,19 @@ class XmlTypeCheck {
* @param $attribs
*/
private function elementOpen( $parser, $name, $attribs ) {
- if( call_user_func( $this->filterCallback, $name, $attribs ) ) {
+ if ( call_user_func( $this->filterCallback, $name, $attribs ) ) {
+ // Filter hit!
+ $this->filterMatch = true;
+ }
+ }
+
+ /**
+ * @param $parser
+ * @param $target
+ * @param $data
+ */
+ private function processingInstructionHandler( $parser, $target, $data ) {
+ if ( call_user_func( $this->parserOptions['processing_instruction_handler'], $target, $data ) ) {
// Filter hit!
$this->filterMatch = true;
}
diff --git a/includes/ZhClient.php b/includes/ZhClient.php
index 4299841b..c5955aec 100644
--- a/includes/ZhClient.php
+++ b/includes/ZhClient.php
@@ -50,7 +50,7 @@ class ZhClient {
}
/**
- * Establish conncetion
+ * Establish connection
*
* @access private
*
@@ -81,14 +81,14 @@ class ZhClient {
$result = fgets( $this->mFP, 1024 );
list( $status, $len ) = explode( ' ', $result );
- if( $status == 'ERROR' ) {
+ if ( $status == 'ERROR' ) {
// $len is actually the error code...
print "zhdaemon error $len<br />\n";
return false;
}
$bytesread = 0;
$data = '';
- while( !feof( $this->mFP ) && $bytesread < $len ) {
+ while ( !feof( $this->mFP ) && $bytesread < $len ) {
$str = fread( $this->mFP, $len - $bytesread );
$bytesread += strlen( $str );
$data .= $str;
@@ -100,8 +100,8 @@ class ZhClient {
/**
* Convert the input to a different language variant
*
- * @param $text String: input text
- * @param $tolang String: language variant
+ * @param string $text input text
+ * @param string $tolang language variant
* @return string the converted text
*/
function convert( $text, $tolang ) {
@@ -117,7 +117,7 @@ class ZhClient {
/**
* Convert the input to all possible variants
*
- * @param $text String: input text
+ * @param string $text input text
* @return array langcode => converted_string
*/
function convertToAllVariants( $text ) {
@@ -131,7 +131,7 @@ class ZhClient {
$info = explode( ';', $infoline );
$ret = array();
$i = 0;
- foreach( $info as $variant ) {
+ foreach ( $info as $variant ) {
list( $code, $len ) = explode( ' ', $variant );
$ret[strtolower( $code )] = substr( $data, $i, $len );
$i += $len;
@@ -142,7 +142,7 @@ class ZhClient {
/**
* Perform word segmentation
*
- * @param $text String: input text
+ * @param string $text input text
* @return string segmented text
*/
function segment( $text ) {
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index 247b1939..df98836f 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -9,36 +9,66 @@
*/
$zh2Hant = array(
+'㐽' => '偑',
+'㑇' => '㑳',
+'㑈' => '倲',
+'㑔' => '㑯',
'㑩' => '儸',
'㓥' => '劏',
'㔉' => '劚',
'㖊' => '噚',
'㖞' => '喎',
+'㘎' => '㘚',
+'㚯' => '㜄',
+'㛀' => '媰',
'㛟' => '𡞵',
'㛠' => '𡢃',
+'㛣' => '㜏',
+'㛤' => '孋',
'㛿' => '𡠹',
'㟆' => '㠏',
+'㟜' => '𡾱',
+'㤘' => '㥮',
+'㧏' => '掆',
+'㧐' => '㩳',
'㧑' => '撝',
'㧟' => '擓',
+'㧰' => '擽',
'㨫' => '㩜',
+'㭎' => '棡',
+'㭏' => '椲',
+'㭣' => '𣙎',
+'㭤' => '樢',
+'㭴' => '樫',
'㱩' => '殰',
'㱮' => '殨',
'㲿' => '瀇',
+'㳔' => '濧',
+'㳕' => '灡',
'㳠' => '澾',
+'㳡' => '濄',
+'㳢' => '𣾷',
+'㳽' => '瀰',
'㶉' => '鸂',
'㶶' => '燶',
'㶽' => '煱',
'㺍' => '獱',
+'㻅' => '璯',
'㻏' => '𤫩',
'㻘' => '𤪺',
+'䀥' => '䁻',
'䁖' => '瞜',
+'䂵' => '碽',
'䅉' => '稏',
+'䅪' => '𥢢',
'䇲' => '筴',
+'䉤' => '籔',
'䌶' => '䊷',
'䌷' => '紬',
'䌸' => '縳',
'䌹' => '絅',
'䌺' => '䋙',
+'䌻' => '䋚',
'䌼' => '綐',
'䌽' => '綵',
'䌾' => '䋻',
@@ -48,24 +78,34 @@ $zh2Hant = array(
'䓕' => '薳',
'䗖' => '螮',
'䘛' => '𧝞',
+'䘞' => '𧜗',
'䙊' => '𧜵',
+'䙌' => '䙡',
'䙓' => '襬',
'䜣' => '訢',
'䜥' => '𧩙',
-'䜧' => '譅',
+'䜧' => '䜀',
+'䜩' => '讌',
'䝙' => '貙',
'䞌' => '𧵳',
'䞍' => '䝼',
+'䞎' => '𧶧',
'䞐' => '賰',
+'䟢' => '躎',
+'䢀' => '𨊰',
+'䢁' => '𨊸',
'䢂' => '𨋢',
'䥺' => '釾',
'䥽' => '鏺',
+'䥾' => '䥱',
'䥿' => '𨯅',
'䦀' => '𨦫',
'䦁' => '𨧜',
+'䦂' => '䥇',
'䦃' => '鐯',
'䦅' => '鐥',
-'䩄' => '靦',
+'䦶' => '䦛',
+'䦷' => '䦟',
'䭪' => '𩞯',
'䯃' => '𩣑',
'䯄' => '騧',
@@ -136,6 +176,7 @@ $zh2Hant = array(
'伞' => '傘',
'伟' => '偉',
'传' => '傳',
+'伡' => '俥',
'伣' => '俔',
'伤' => '傷',
'伥' => '倀',
@@ -217,6 +258,7 @@ $zh2Hant = array(
'刭' => '剄',
'刹' => '剎',
'刽' => '劊',
+'刾' => '㓨',
'刿' => '劌',
'剀' => '剴',
'剂' => '劑',
@@ -321,6 +363,8 @@ $zh2Hant = array(
'啬' => '嗇',
'啭' => '囀',
'啮' => '嚙',
+'啯' => '嘓',
+'啰' => '囉',
'啴' => '嘽',
'啸' => '嘯',
'喷' => '噴',
@@ -410,6 +454,7 @@ $zh2Hant = array(
'婵' => '嬋',
'婶' => '嬸',
'媪' => '媼',
+'媭' => '嬃',
'嫒' => '嬡',
'嫔' => '嬪',
'嫱' => '嬙',
@@ -513,7 +558,9 @@ $zh2Hant = array(
'归' => '歸',
'当' => '當',
'录' => '錄',
+'彟' => '彠',
'彦' => '彥',
+'彨' => '彲',
'彻' => '徹',
'径' => '徑',
'徕' => '徠',
@@ -648,6 +695,7 @@ $zh2Hant = array(
'攒' => '攢',
'敌' => '敵',
'敛' => '斂',
+'敩' => '斆',
'数' => '數',
'斋' => '齋',
'斓' => '斕',
@@ -719,6 +767,7 @@ $zh2Hant = array(
'桧' => '檜',
'桨' => '槳',
'桩' => '樁',
+'桪' => '樳',
'梦' => '夢',
'梼' => '檮',
'梾' => '棶',
@@ -727,9 +776,12 @@ $zh2Hant = array(
'棁' => '梲',
'棂' => '欞',
'椁' => '槨',
+'椝' => '槼',
'椟' => '櫝',
'椠' => '槧',
+'椢' => '槶',
'椤' => '欏',
+'椫' => '樿',
'椭' => '橢',
'楼' => '樓',
'榄' => '欖',
@@ -783,6 +835,7 @@ $zh2Hant = array(
'沥' => '瀝',
'沦' => '淪',
'沧' => '滄',
+'沨' => '渢',
'沩' => '溈',
'沪' => '滬',
'泞' => '濘',
@@ -813,6 +866,7 @@ $zh2Hant = array(
'浒' => '滸',
'浓' => '濃',
'浔' => '潯',
+'浕' => '濜',
'涂' => '塗',
'涛' => '濤',
'涝' => '澇',
@@ -820,6 +874,7 @@ $zh2Hant = array(
'涟' => '漣',
'涠' => '潿',
'涡' => '渦',
+'涢' => '溳',
'涣' => '渙',
'涤' => '滌',
'润' => '潤',
@@ -833,7 +888,6 @@ $zh2Hant = array(
'渐' => '漸',
'渑' => '澠',
'渔' => '漁',
-'渖' => '瀋',
'渗' => '滲',
'温' => '溫',
'湾' => '灣',
@@ -841,6 +895,7 @@ $zh2Hant = array(
'溃' => '潰',
'溅' => '濺',
'溆' => '漵',
+'溇' => '漊',
'滗' => '潷',
'滚' => '滾',
'滞' => '滯',
@@ -861,6 +916,7 @@ $zh2Hant = array(
'潍' => '濰',
'潜' => '潛',
'潴' => '瀦',
+'澛' => '瀂',
'澜' => '瀾',
'濑' => '瀨',
'濒' => '瀕',
@@ -872,7 +928,6 @@ $zh2Hant = array(
'灿' => '燦',
'炀' => '煬',
'炉' => '爐',
-'炖' => '燉',
'炜' => '煒',
'炝' => '熗',
'点' => '點',
@@ -1014,12 +1069,13 @@ $zh2Hant = array(
'硖' => '硤',
'硗' => '磽',
'硙' => '磑',
+'硚' => '礄',
'确' => '確',
+'硵' => '磠',
'硷' => '礆',
'碍' => '礙',
'碛' => '磧',
'碜' => '磣',
-'碱' => '鹼',
'礼' => '禮',
'祃' => '禡',
'祎' => '禕',
@@ -1081,6 +1137,7 @@ $zh2Hant = array(
'篑' => '簣',
'篓' => '簍',
'篮' => '籃',
+'篯' => '籛',
'篱' => '籬',
'簖' => '籪',
'籁' => '籟',
@@ -1256,6 +1313,8 @@ $zh2Hant = array(
'羟' => '羥',
'羡' => '羨',
'翘' => '翹',
+'翙' => '翽',
+'翚' => '翬',
'耢' => '耮',
'耧' => '耬',
'耸' => '聳',
@@ -1295,6 +1354,7 @@ $zh2Hant = array(
'脶' => '腡',
'脸' => '臉',
'腊' => '臘',
+'腘' => '膕',
'腭' => '齶',
'腻' => '膩',
'腼' => '靦',
@@ -1337,6 +1397,7 @@ $zh2Hant = array(
'荚' => '莢',
'荛' => '蕘',
'荜' => '蓽',
+'荝' => '萴',
'荞' => '蕎',
'荟' => '薈',
'荠' => '薺',
@@ -1405,6 +1466,7 @@ $zh2Hant = array(
'蚀' => '蝕',
'蚁' => '蟻',
'蚂' => '螞',
+'蚃' => '蠁',
'蚕' => '蠶',
'蚬' => '蜆',
'蛊' => '蠱',
@@ -1445,9 +1507,10 @@ $zh2Hant = array(
'裢' => '褳',
'裣' => '襝',
'裤' => '褲',
-'裥' => '襇',
+'裥' => '襉',
'褛' => '褸',
'褴' => '襤',
+'襕' => '襴',
'见' => '見',
'观' => '觀',
'觃' => '覎',
@@ -1469,6 +1532,7 @@ $zh2Hant = array(
'触' => '觸',
'觯' => '觶',
'訚' => '誾',
+'詟' => '讋',
'誉' => '譽',
'誊' => '謄',
'讠' => '訁',
@@ -2209,6 +2273,7 @@ $zh2Hant = array(
'颠' => '顛',
'颡' => '顙',
'颢' => '顥',
+'颣' => '纇',
'颤' => '顫',
'颥' => '顬',
'颦' => '顰',
@@ -2344,6 +2409,7 @@ $zh2Hant = array(
'髋' => '髖',
'髌' => '髕',
'鬓' => '鬢',
+'鬶' => '鬹',
'魇' => '魘',
'魉' => '魎',
'鱼' => '魚',
@@ -2404,7 +2470,6 @@ $zh2Hant = array(
'鲳' => '鯧',
'鲴' => '鯝',
'鲵' => '鯢',
-'鲶' => '鯰',
'鲷' => '鯛',
'鲸' => '鯨',
'鲹' => '鰺',
@@ -2570,9 +2635,144 @@ $zh2Hant = array(
'龚' => '龔',
'龛' => '龕',
'龟' => '龜',
+'𠆲' => '儣',
+'𠆿' => '𠌥',
+'𠉂' => '㒓',
+'𠉗' => '𠏢',
+'𠚳' => '𠠎',
+'𠛅' => '剾',
+'𠛆' => '𠞆',
'𠮶' => '嗰',
+'𠯟' => '哯',
+'𠯠' => '噅',
+'𠲥' => '𡅏',
+'𠴢' => '𡄔',
+'𠵸' => '𡄣',
+'𠵾' => '㗲',
+'𡋀' => '𡓾',
+'𡋗' => '𡑭',
'𡒄' => '壈',
+'𡝠' => '㜷',
+'𡞱' => '㜢',
+'𡭜' => '𡮉',
+'𡭬' => '𡮣',
+'𡶴' => '嵼',
+'𢋈' => '㢝',
+'𢘝' => '𢣚',
+'𢘞' => '𢣭',
+'𢙓' => '懀',
+'𢛯' => '㦎',
+'𢫊' => '𢷮',
+'𢫞' => '𢶫',
+'𢫬' => '摋',
+'𢬦' => '𢹿',
+'𢭏' => '擣',
+'𢽾' => '斅',
+'𣆐' => '曥',
+'𣍨' => '𦢈',
+'𣍯' => '腪',
+'𣍰' => '脥',
+'𣎑' => '臗',
+'𣐤' => '欍',
+'𣑶' => '𣠲',
+'𣗋' => '欓',
+'𣘓' => '𣞻',
+'𣘴' => '檭',
+'𣘷' => '𣝕',
+'𣭤' => '𣯴',
+'𣶩' => '澅',
+'𣶫' => '𣿉',
+'𣸣' => '濆',
+'𣺼' => '灙',
+'𣺽' => '𤁣',
+'𣽷' => '瀃',
+'𤆡' => '熓',
+'𤇃' => '爄',
+'𤇄' => '熌',
+'𤈶' => '熉',
+'𤈷' => '㷿',
+'𤊀' => '𤒎',
+'𤋏' => '熡',
+'𤞤' => '玁',
+'𤠋' => '㺏',
+'𤦀' => '瓕',
+'𤳄' => '𤳸',
+'𤶧' => '𤸫',
+'𤽯' => '㿧',
+'𤾀' => '皟',
+'𥅘' => '𥌃',
+'𥅴' => '䀹',
+'𥆧' => '瞤',
+'𥇢' => '䁪',
+'𥐟' => '礒',
+'𥐯' => '𥖅',
+'𥐰' => '𥕥',
+'𥐻' => '碙',
+'𥧂' => '𥨐',
+'𥬀' => '䉙',
+'𥬞' => '籋',
+'𥬠' => '篘',
+'𥭉' => '𥵊',
+'𥮋' => '𥸠',
+'𥮜' => '䉲',
+'𥱔' => '𥵃',
+'𥹥' => '𥼽',
+'𥺅' => '䊭',
+'𥺇' => '𥽖',
+'𦈈' => '𥿊',
+'𦈉' => '緷',
+'𦈋' => '綇',
+'𦈌' => '綀',
+'𦈎' => '繟',
+'𦈏' => '緍',
+'𦈐' => '縺',
+'𦈑' => '緸',
+'𦈒' => '𦂅',
+'𦈓' => '䋿',
+'𦈔' => '縎',
+'𦈕' => '緰',
'𦈖' => '䌈',
+'𦈗' => '𦃄',
+'𦈘' => '䌋',
+'𦈙' => '䌰',
+'𦈚' => '縬',
+'𦈛' => '繓',
+'𦈜' => '䌖',
+'𦈝' => '繏',
+'𦈞' => '䌟',
+'𦈟' => '䌝',
+'𦈠' => '䌥',
+'𦈡' => '繻',
+'𦛨' => '朥',
+'𦝼' => '膢',
+'𦟗' => '𦣎',
+'𦨩' => '𦪽',
+'𦰴' => '䕳',
+'𧉞' => '䗿',
+'𧒭' => '𧔥',
+'𧮪' => '詀',
+'𧳕' => '𧳟',
+'𧹑' => '䞈',
+'𧹓' => '𧶔',
+'𧹕' => '䝻',
+'𧹖' => '賟',
+'𧹗' => '贃',
+'𧿈' => '𨇁',
+'𨀱' => '𨄣',
+'𨁴' => '𨅍',
+'𨂺' => '𨈊',
+'𨄄' => '𨈌',
+'𨅫' => '𨇞',
+'𨅬' => '躝',
+'𨉗' => '軉',
+'𨐅' => '軗',
+'𨐆' => '𨊻',
+'𨐇' => '𨏠',
+'𨐈' => '輄',
+'𨐉' => '𨎮',
+'𨐊' => '𨏥',
+'𨑹' => '䢨',
+'𨤰' => '𨤻',
'𨰾' => '鎷',
'𨰿' => '釳',
'𨱀' => '𨥛',
@@ -2581,6 +2781,7 @@ $zh2Hant = array(
'𨱃' => '鈲',
'𨱄' => '鈯',
'𨱅' => '鉁',
+'𨱆' => '龯',
'𨱇' => '銶',
'𨱈' => '鋉',
'𨱉' => '鍄',
@@ -2591,12 +2792,28 @@ $zh2Hant = array(
'𨱎' => '鍮',
'𨱏' => '鎝',
'𨱐' => '𨫒',
+'𨱑' => '鐄',
'𨱒' => '鏉',
'𨱓' => '鐎',
'𨱔' => '鐏',
'𨱕' => '𨮂',
+'𨱖' => '䥩',
+'𨷿' => '䦳',
+'𨸀' => '𨳕',
+'𨸁' => '𨳑',
'𨸂' => '閍',
'𨸃' => '閐',
+'𨸄' => '䦘',
+'𨸅' => '𨴗',
+'𨸆' => '𨵩',
+'𨸇' => '𨵸',
+'𨸉' => '𨶀',
+'𨸊' => '𨶏',
+'𨸋' => '𨶲',
+'𨸌' => '𨶮',
+'𨸎' => '𨷲',
+'𨸘' => '𨽏',
+'𨸟' => '䧢',
'𩏼' => '䪏',
'𩏽' => '𩏪',
'𩏾' => '𩎢',
@@ -2617,12 +2834,22 @@ $zh2Hant = array(
'𩙮' => '䬘',
'𩙯' => '䬝',
'𩙰' => '𩙈',
+'𩟿' => '𩚛',
+'𩠀' => '𩚥',
+'𩠁' => '𩚵',
+'𩠂' => '𩛆',
+'𩠃' => '𩛩',
'𩠅' => '𩟐',
'𩠆' => '𩜦',
'𩠇' => '䭀',
'𩠈' => '䭃',
+'𩠉' => '𩜇',
+'𩠊' => '𩜵',
'𩠋' => '𩝔',
'𩠌' => '餸',
+'𩠎' => '𩞄',
+'𩠏' => '𩞦',
+'𩠠' => '𩠴',
'𩧦' => '𩡺',
'𩧨' => '駎',
'𩧩' => '𩤊',
@@ -2649,14 +2876,20 @@ $zh2Hant = array(
'𩨄' => '騪',
'𩨅' => '𩤸',
'𩨆' => '𩤙',
+'𩨇' => '䮫',
'𩨈' => '騟',
'𩨉' => '𩤲',
'𩨊' => '騚',
'𩨋' => '𩥄',
'𩨌' => '𩥑',
'𩨍' => '𩥇',
+'𩨎' => '龭',
'𩨏' => '䮳',
'𩨐' => '𩧆',
+'𩬣' => '𩭙',
+'𩬤' => '𩰀',
+'𩯒' => '𩯳',
+'𩲒' => '𩳤',
'𩽹' => '魥',
'𩽺' => '𩵩',
'𩽻' => '𩵹',
@@ -2666,6 +2899,7 @@ $zh2Hant = array(
'𩽿' => '𩶰',
'𩾀' => '鮕',
'𩾁' => '鯄',
+'𩾂' => '䲖',
'𩾃' => '鮸',
'𩾄' => '𩷰',
'𩾅' => '𩸃',
@@ -2699,6 +2933,8 @@ $zh2Hant = array(
'𪎊' => '麨',
'𪎋' => '䴴',
'𪎌' => '麳',
+'𪎍' => '𪋿',
+'𪔭' => '𪔵',
'𪚏' => '𪘀',
'𪚐' => '𪘯',
'𪞝' => '凙',
@@ -2715,7 +2951,7 @@ $zh2Hant = array(
'𫌀' => '襀',
'𫌨' => '覼',
'𫍙' => '訑',
-'𫍟' => '𧦧',
+'𫍟' => '詑',
'𫍢' => '譊',
'𫍰' => '諰',
'𫍲' => '謏',
@@ -2728,6 +2964,7 @@ $zh2Hant = array(
'𫓧' => '鈇',
'𫓩' => '鏦',
'𫔎' => '鐍',
+'𫖸' => '願',
'𫗠' => '餦',
'𫗦' => '餔',
'𫗧' => '餗',
@@ -3993,7 +4230,6 @@ $zh2Hant = array(
'优游' => '優遊',
'兀术' => '兀朮',
'元凶' => '元兇',
-'充饥' => '充饑',
'兆个' => '兆個',
'兆余' => '兆餘',
'凶刀' => '兇刀',
@@ -4144,7 +4380,6 @@ $zh2Hant = array(
'出于' => '出於',
'出游' => '出遊',
'出丑' => '出醜',
-'出锤' => '出鎚',
'分占' => '分佔',
'分别致' => '分别致',
'分半钟' => '分半鐘',
@@ -4393,6 +4628,7 @@ $zh2Hant = array(
'古书云' => '古書云',
'古書云' => '古書云',
'古柯咸' => '古柯鹹',
+'古柯碱' => '古柯鹼',
'古朴' => '古樸',
'古语云' => '古語云',
'古語云' => '古語云',
@@ -4617,8 +4853,8 @@ $zh2Hant = array(
'严于' => '嚴於',
'严丝合缝' => '嚴絲合縫',
'嚼谷' => '嚼穀',
-'囉囉苏苏' => '囉囉囌囌',
-'囉苏' => '囉囌',
+'啰啰苏苏' => '囉囉囌囌',
+'啰苏' => '囉囌',
'嘱托' => '囑託',
'四个' => '四個',
'四出刊' => '四出刊',
@@ -4815,7 +5051,6 @@ $zh2Hant = array(
'大赞' => '大讚',
'大周折' => '大週摺',
'大金发苔' => '大金髮苔',
-'大锤' => '大鎚',
'大钟' => '大鐘',
'大只' => '大隻',
'大风后' => '大風後',
@@ -4836,6 +5071,7 @@ $zh2Hant = array(
'天文钟' => '天文鐘',
'天历' => '天曆',
'天历史' => '天歷史',
+'天然碱' => '天然鹼',
'天翻地覆' => '天翻地覆',
'天覆地载' => '天覆地載',
'太仆' => '太僕',
@@ -4885,7 +5121,6 @@ $zh2Hant = array(
'好丑' => '好醜',
'好斗' => '好鬥',
'如果干' => '如果幹',
-'如饥似渴' => '如饑似渴',
'妖后' => '妖后',
'妙药' => '妙藥',
'始于' => '始於',
@@ -5279,6 +5514,7 @@ $zh2Hant = array(
'弘历史' => '弘歷史',
'弱于' => '弱於',
'弱水三千只取一瓢' => '弱水三千只取一瓢',
+'弱碱' => '弱鹼',
'张三丰' => '張三丰',
'張三丰' => '張三丰',
'张勋' => '張勳',
@@ -5288,6 +5524,7 @@ $zh2Hant = array(
'强奸' => '強姦',
'强干' => '強幹',
'强于' => '強於',
+'强碱' => '強鹼',
'别口气' => '彆口氣',
'别强' => '彆強',
'别扭' => '彆扭',
@@ -5686,6 +5923,7 @@ $zh2Hant = array(
'抗癌药' => '抗癌藥',
'抗御' => '抗禦',
'抗药' => '抗藥',
+'抗碱' => '抗鹼',
'折向往' => '折向往',
'折子戏' => '折子戲',
'折戟沈河' => '折戟沈河',
@@ -5824,7 +6062,7 @@ $zh2Hant = array(
'挂名' => '掛名',
'挂帘' => '掛帘',
'挂历' => '掛曆',
-'挂钩' => '掛鈎',
+'挂鈎' => '掛鈎',
'挂钟' => '掛鐘',
'采下' => '採下',
'采伐' => '採伐',
@@ -6528,12 +6766,14 @@ $zh2Hant = array(
'水里浊水溪' => '水里濁水溪',
'水里鄉' => '水里鄉',
'水里乡' => '水里鄉',
+'水碱' => '水鹼',
'永历' => '永曆',
'永历史' => '永歷史',
'永志不忘' => '永誌不忘',
'求知欲' => '求知慾',
'求签' => '求籤',
'求道于盲' => '求道於盲',
+'汗碱' => '汗鹼',
'池里' => '池裡',
'污蔑' => '污衊',
'汲于' => '汲於',
@@ -6716,6 +6956,7 @@ $zh2Hant = array(
'准话' => '準話',
'准谱' => '準譜',
'准货币' => '準貨幣',
+'准军事' => '準軍事',
'准头' => '準頭',
'准点' => '準點',
'溟蒙' => '溟濛',
@@ -6725,16 +6966,8 @@ $zh2Hant = array(
'滃郁' => '滃鬱',
'滑借' => '滑藉',
'汇丰' => '滙豐',
-'卤味' => '滷味',
-'卤水' => '滷水',
-'卤汁' => '滷汁',
-'卤湖' => '滷湖',
-'卤肉' => '滷肉',
-'卤菜' => '滷菜',
-'卤蛋' => '滷蛋',
-'卤制' => '滷製',
-'卤鸡' => '滷雞',
-'卤面' => '滷麵',
+'滷制' => '滷製',
+'滷面' => '滷麵',
'满拼自尽' => '滿拚自盡',
'满满当当' => '滿滿當當',
'满头洋发' => '滿頭洋髮',
@@ -6830,6 +7063,7 @@ $zh2Hant = array(
'煎面' => '煎麵',
'烟卷' => '煙捲',
'烟斗丝' => '煙斗絲',
+'烟碱' => '煙鹼',
'照占' => '照佔',
'照入签' => '照入籤',
'照准' => '照準',
@@ -6839,9 +7073,10 @@ $zh2Hant = array(
'熊杰' => '熊杰',
'荧郁' => '熒鬱',
'熬药' => '熬藥',
-'炖药' => '燉藥',
+'燉药' => '燉藥',
'燎发' => '燎髮',
'烧干' => '燒乾',
+'烧碱' => '燒鹼',
'燕几' => '燕几',
'燕巢于幕' => '燕巢於幕',
'燕燕于飞' => '燕燕于飛',
@@ -7014,7 +7249,7 @@ $zh2Hant = array(
'发松' => '發鬆',
'发面' => '發麵',
'白干' => '白乾',
-'白兔擣药' => '白兔擣藥',
+'白兔𢭏药' => '白兔擣藥',
'白干儿' => '白干兒',
'白术' => '白朮',
'白朴' => '白樸',
@@ -7148,6 +7383,7 @@ $zh2Hant = array(
'石英钟表' => '石英鐘錶',
'石莼' => '石蓴',
'石钟乳' => '石鐘乳',
+'石碱' => '石鹼',
'矽谷' => '矽谷',
'研制' => '研製',
'砰当' => '砰噹',
@@ -7158,6 +7394,7 @@ $zh2Hant = array(
'朱红色' => '硃紅色',
'朱色' => '硃色',
'朱谕' => '硃諭',
+'硫化碱' => '硫化鹼',
'硬干' => '硬幹',
'确瘠' => '确瘠',
'碑志' => '碑誌',
@@ -7266,7 +7503,6 @@ $zh2Hant = array(
'积极参加' => '積极參加',
'积淀' => '積澱',
'积谷' => '積穀',
-'积谷防饥' => '積穀防饑',
'积郁' => '積鬱',
'稳占' => '穩佔',
'稳扎' => '穩紮',
@@ -7403,6 +7639,7 @@ $zh2Hant = array(
'纡郁' => '紆鬱',
'纳征' => '納徵',
'纯朴' => '純樸',
+'纯碱' => '純鹼',
'纸扎' => '紙紮',
'素朴' => '素樸',
'素发' => '素髮',
@@ -7598,6 +7835,7 @@ $zh2Hant = array(
'考试' => '考試',
'而克制' => '而剋制',
'耍斗' => '耍鬥',
+'耐碱' => '耐鹼',
'耕佣' => '耕傭',
'耕获' => '耕穫',
'耳余' => '耳餘',
@@ -7731,6 +7969,7 @@ $zh2Hant = array(
'花马吊嘴' => '花馬弔嘴',
'花哄' => '花鬨',
'苑里' => '苑裡',
+'苛性碱' => '苛性鹼',
'若干' => '若干',
'苦干' => '苦幹',
'苦药' => '苦藥',
@@ -7761,7 +8000,7 @@ $zh2Hant = array(
'草药' => '草藥',
'荐居' => '荐居',
'荐臻' => '荐臻',
-'荐饥' => '荐饑',
+'荐饑' => '荐饑',
'荷花淀' => '荷花澱',
'庄上' => '莊上',
'庄主' => '莊主',
@@ -7795,6 +8034,7 @@ $zh2Hant = array(
'菠萝干' => '菠蘿乾',
'华严钟' => '華嚴鐘',
'华发' => '華髮',
+'菸碱' => '菸鹼',
'萬一只' => '萬一只',
'万一只' => '萬一只',
'万个' => '萬個',
@@ -7874,7 +8114,7 @@ $zh2Hant = array(
'姜饼' => '薑餅',
'姜黄' => '薑黃',
'薙发' => '薙髮',
-'薝卜' => '薝蔔',
+'薝蔔' => '薝蔔',
'苧悴' => '薴悴',
'薴烯' => '薴烯',
'苧烯' => '薴烯',
@@ -7961,8 +8201,7 @@ $zh2Hant = array(
'蕴含着' => '蘊含著',
'蕴涵着' => '蘊涵著',
'苹果干' => '蘋果乾',
-'萝卜' => '蘿蔔',
-'萝卜干' => '蘿蔔乾',
+'萝蔔干' => '蘿蔔乾',
'虎须' => '虎鬚',
'虎斗' => '虎鬥',
'号志' => '號誌',
@@ -7982,7 +8221,7 @@ $zh2Hant = array(
'蛏干' => '蟶乾',
'蚁后' => '蟻后',
'蟻后' => '蟻后',
-'蠁干' => '蠁幹',
+'蚃干' => '蠁幹',
'蛮干' => '蠻幹',
'血拼' => '血拚',
'血余' => '血餘',
@@ -8378,7 +8617,6 @@ $zh2Hant = array(
'资金占用' => '資金占用',
'贾后' => '賈后',
'賈后' => '賈后',
-'赈饥' => '賑饑',
'赏赞' => '賞讚',
'贤后' => '賢后',
'賢后' => '賢后',
@@ -8654,6 +8892,7 @@ $zh2Hant = array(
'酒醴曲蘖' => '酒醴麴櫱',
'酒曲' => '酒麴',
'酥松' => '酥鬆',
+'酸碱' => '酸鹼',
'醇朴' => '醇樸',
'醉于' => '醉於',
'醋坛' => '醋罈',
@@ -8728,7 +8967,6 @@ $zh2Hant = array(
'重复' => '重複',
'重托' => '重託',
'重游' => '重遊',
-'重锤' => '重鎚',
'野姜' => '野薑',
'野游' => '野遊',
'厘出' => '釐出',
@@ -8755,10 +8993,10 @@ $zh2Hant = array(
'金装玉里' => '金裝玉裡',
'金表' => '金錶',
'金钟' => '金鐘',
+'金鸡纳碱' => '金雞納鹼',
'金马仑道' => '金馬崙道',
'金发' => '金髮',
-'钉锤' => '釘鎚',
-'钩心斗角' => '鈎心鬥角',
+'鈎心斗角' => '鈎心鬥角',
'银朱' => '銀硃',
'银发' => '銀髮',
'铜范' => '銅範',
@@ -8776,7 +9014,7 @@ $zh2Hant = array(
'钱谷' => '錢穀',
'钱范' => '錢範',
'钱庄' => '錢莊',
-'锦绣花园' => '錦綉花園',
+'锦綉花园' => '錦綉花園',
'锦绣' => '錦繡',
'表停' => '錶停',
'表冠' => '錶冠',
@@ -8813,9 +9051,6 @@ $zh2Hant = array(
'锻炼出' => '鍛鍊出',
'锲而不舍' => '鍥而不捨',
'镰仓' => '鎌倉',
-'锤儿' => '鎚兒',
-'锤子' => '鎚子',
-'锤头' => '鎚頭',
'锈病' => '鏽病',
'锈菌' => '鏽菌',
'锈蚀' => '鏽蝕',
@@ -8895,7 +9130,6 @@ $zh2Hant = array(
'钟鼓' => '鐘鼓',
'铁杆' => '鐵杆',
'铁栏杆' => '鐵欄杆',
-'铁锤' => '鐵鎚',
'铁锈' => '鐵鏽',
'铁钟' => '鐵鐘',
'铸钟' => '鑄鐘',
@@ -9297,13 +9531,8 @@ $zh2Hant = array(
'喂鱼' => '餵魚',
'喂鸭' => '餵鴨',
'喂鹅' => '餵鵝',
-'饥寒' => '饑寒',
-'饥民' => '饑民',
-'饥渴' => '饑渴',
-'饥溺' => '饑溺',
-'饥荒' => '饑荒',
-'饥饱' => '饑飽',
-'饥馑' => '饑饉',
+'饑荒' => '饑荒',
+'饑馑' => '饑饉',
'首当其冲' => '首當其衝',
'首发' => '首發',
'首只' => '首隻',
@@ -9580,7 +9809,7 @@ $zh2Hant = array(
'鱼干' => '魚乾',
'鱼松' => '魚鬆',
'鲸须' => '鯨鬚',
-'鲇鱼' => '鯰魚',
+'鯰鱼' => '鯰魚',
'鸠占鹊巢' => '鳩佔鵲巢',
'凤凰于飞' => '鳳凰于飛',
'凤梨干' => '鳳梨乾',
@@ -9623,9 +9852,22 @@ $zh2Hant = array(
'咸鸭蛋' => '鹹鴨蛋',
'咸卤' => '鹹鹵',
'咸咸' => '鹹鹹',
+'碱化' => '鹼化',
+'碱土金属' => '鹼土金屬',
+'碱地' => '鹼地',
+'碱度' => '鹼度',
+'碱性' => '鹼性',
+'碱水' => '鹼水',
+'碱液' => '鹼液',
+'碱熔' => '鹼熔',
+'碱石灰' => '鹼石灰',
+'碱纤维素' => '鹼纖維素',
+'碱金属' => '鹼金屬',
+'碱类' => '鹼類',
'盐打怎么咸' => '鹽打怎麼鹹',
-'盐卤' => '鹽滷',
'盐余' => '鹽餘',
+'盐碱土' => '鹽鹼土',
+'盐碱滩' => '鹽鹼灘',
'丽于' => '麗於',
'曲尘' => '麴塵',
'曲蘖' => '麴櫱',
@@ -9778,18 +10020,90 @@ $zh2Hant = array(
);
$zh2Hans = array(
+'㑯' => '㑔',
+'㑳' => '㑇',
+'㒓' => '𠉂',
+'㓨' => '刾',
+'㗲' => '𠵾',
+'㘚' => '㘎',
+'㜄' => '㚯',
+'㜏' => '㛣',
+'㜢' => '𡞱',
+'㜷' => '𡝠',
'㞞' => '𪨊',
'㠏' => '㟆',
+'㢝' => '𢋈',
+'㥮' => '㤘',
+'㦎' => '𢛯',
'㩜' => '㨫',
+'㩳' => '㧐',
+'㷿' => '𤈷',
+'㺏' => '𤠋',
+'㿧' => '𤽯',
+'䀹' => '𥅴',
+'䁪' => '𥇢',
+'䁻' => '䀥',
+'䉙' => '𥬀',
'䉬' => '𫂈',
+'䉲' => '𥮜',
+'䊭' => '𥺅',
'䊷' => '䌶',
'䋙' => '䌺',
+'䋚' => '䌻',
'䋻' => '䌾',
+'䋿' => '𦈓',
+'䌈' => '𦈖',
+'䌋' => '𦈘',
+'䌖' => '𦈜',
+'䌝' => '𦈟',
+'䌟' => '𦈞',
+'䌥' => '𦈠',
+'䌰' => '𦈙',
+'䕳' => '𦰴',
+'䗿' => '𧉞',
+'䙡' => '䙌',
+'䜀' => '䜧',
+'䝻' => '𧹕',
'䝼' => '䞍',
+'䞈' => '𧹑',
+'䢨' => '𨑹',
+'䥇' => '䦂',
+'䥩' => '𨱖',
+'䥱' => '䥾',
+'䦘' => '𨸄',
+'䦛' => '䦶',
+'䦟' => '䦷',
+'䦳' => '𨷿',
+'䧢' => '𨸟',
+'䪏' => '𩏼',
+'䪗' => '𩐀',
+'䪘' => '𩏿',
+'䫴' => '𩖗',
+'䬘' => '𩙮',
+'䬝' => '𩙯',
+'䬞' => '𩙧',
+'䭀' => '𩠇',
+'䭃' => '𩠈',
+'䭿' => '𩧭',
+'䮝' => '𩧰',
+'䮞' => '𩨁',
+'䮠' => '𩧿',
+'䮫' => '𩨇',
+'䮳' => '𩨏',
+'䮾' => '𩧪',
'䯀' => '䯅',
'䰾' => '鲃',
+'䱙' => '𩾈',
+'䱬' => '𩾊',
+'䱰' => '𩾋',
+'䱷' => '䲣',
'䱽' => '䲝',
'䲁' => '鳚',
+'䲖' => '𩾂',
+'䲰' => '𪉂',
+'䴉' => '鹮',
+'䴬' => '𪎈',
+'䴴' => '𪎋',
'丟' => '丢',
'並' => '并',
'乾' => '干',
@@ -9807,6 +10121,7 @@ $zh2Hans = array(
'係' => '系',
'俔' => '伣',
'俠' => '侠',
+'俥' => '伡',
'倀' => '伥',
'倆' => '俩',
'倈' => '俫',
@@ -9815,7 +10130,9 @@ $zh2Hans = array(
'們' => '们',
'倖' => '幸',
'倫' => '伦',
+'倲' => '㑈',
'偉' => '伟',
+'偑' => '㐽',
'側' => '侧',
'偵' => '侦',
'偽' => '伪',
@@ -9851,6 +10168,7 @@ $zh2Hans = array(
'儕' => '侪',
'儘' => '尽',
'償' => '偿',
+'儣' => '𠆲',
'優' => '优',
'儲' => '储',
'儷' => '俪',
@@ -9884,6 +10202,7 @@ $zh2Hans = array(
'剴' => '剀',
'創' => '创',
'剷' => '铲',
+'剾' => '𠛅',
'劃' => '划',
'劇' => '剧',
'劉' => '刘',
@@ -9914,7 +10233,6 @@ $zh2Hans = array(
'卻' => '却',
'卽' => '即',
'厙' => '厍',
-'厠' => '厕',
'厤' => '历',
'厭' => '厌',
'厲' => '厉',
@@ -9928,10 +10246,10 @@ $zh2Hans = array(
'呂' => '吕',
'咼' => '呙',
'員' => '员',
+'哯' => '𠯟',
'唄' => '呗',
'唚' => '吣',
'問' => '问',
-'啓' => '启',
'啞' => '哑',
'啟' => '启',
'啢' => '唡',
@@ -9952,6 +10270,7 @@ $zh2Hans = array(
'嗹' => '𪡏',
'嘆' => '叹',
'嘍' => '喽',
+'嘓' => '啯',
'嘔' => '呕',
'嘖' => '啧',
'嘗' => '尝',
@@ -9964,6 +10283,7 @@ $zh2Hans = array(
'嘸' => '呒',
'嘽' => '啴',
'噁' => '恶',
+'噅' => '𠯠',
'噓' => '嘘',
'噚' => '㖊',
'噝' => '咝',
@@ -9994,6 +10314,7 @@ $zh2Hans = array(
'囂' => '嚣',
'囅' => '冁',
'囈' => '呓',
+'囉' => '啰',
'囌' => '苏',
'囑' => '嘱',
'囪' => '囱',
@@ -10031,7 +10352,6 @@ $zh2Hans = array(
'墮' => '堕',
'墰' => '坛',
'墳' => '坟',
-'墻' => '墙',
'墾' => '垦',
'壇' => '坛',
'壈' => '𡒄',
@@ -10058,7 +10378,6 @@ $zh2Hans = array(
'奧' => '奥',
'奩' => '奁',
'奪' => '夺',
-'奬' => '奖',
'奮' => '奋',
'奼' => '姹',
'妝' => '妆',
@@ -10070,13 +10389,14 @@ $zh2Hans = array(
'婭' => '娅',
'媧' => '娲',
'媯' => '妫',
+'媰' => '㛀',
'媼' => '媪',
'媽' => '妈',
'嫗' => '妪',
'嫵' => '妩',
'嫻' => '娴',
'嫿' => '婳',
-'嬀' => '妫',
+'嬃' => '媭',
'嬈' => '娆',
'嬋' => '婵',
'嬌' => '娇',
@@ -10086,6 +10406,7 @@ $zh2Hans = array(
'嬪' => '嫔',
'嬰' => '婴',
'嬸' => '婶',
+'孋' => '㛤',
'孌' => '娈',
'孫' => '孙',
'學' => '学',
@@ -10127,6 +10448,7 @@ $zh2Hans = array(
'崬' => '岽',
'嵐' => '岚',
'嵗' => '岁',
+'嵼' => '𡶴',
'嶁' => '嵝',
'嶄' => '崭',
'嶇' => '岖',
@@ -10188,7 +10510,9 @@ $zh2Hans = array(
'彎' => '弯',
'彙' => '汇',
'彞' => '彝',
+'彠' => '彟',
'彥' => '彦',
+'彲' => '彨',
'後' => '后',
'徑' => '径',
'從' => '从',
@@ -10219,7 +10543,6 @@ $zh2Hans = array(
'慚' => '惭',
'慟' => '恸',
'慣' => '惯',
-'慤' => '悫',
'慪' => '怄',
'慫' => '怂',
'慮' => '虑',
@@ -10238,6 +10561,7 @@ $zh2Hans = array(
'憮' => '怃',
'憲' => '宪',
'憶' => '忆',
+'懀' => '𢙓',
'懇' => '恳',
'應' => '应',
'懌' => '怿',
@@ -10273,6 +10597,7 @@ $zh2Hans = array(
'捲' => '卷',
'掃' => '扫',
'掄' => '抡',
+'掆' => '㧏',
'掗' => '挜',
'掙' => '挣',
'掛' => '挂',
@@ -10286,6 +10611,7 @@ $zh2Hans = array(
'搗' => '捣',
'搵' => '揾',
'搶' => '抢',
+'摋' => '𢫬',
'摑' => '掴',
'摜' => '掼',
'摟' => '搂',
@@ -10317,6 +10643,7 @@ $zh2Hans = array(
'擔' => '担',
'據' => '据',
'擠' => '挤',
+'擣' => '𢭏',
'擬' => '拟',
'擯' => '摈',
'擰' => '拧',
@@ -10327,6 +10654,7 @@ $zh2Hans = array(
'擺' => '摆',
'擻' => '擞',
'擼' => '撸',
+'擽' => '㧰',
'擾' => '扰',
'攄' => '摅',
'攆' => '撵',
@@ -10348,6 +10676,8 @@ $zh2Hans = array(
'數' => '数',
'斂' => '敛',
'斃' => '毙',
+'斅' => '𢽾',
+'斆' => '敩',
'斕' => '斓',
'斬' => '斩',
'斷' => '断',
@@ -10370,10 +10700,12 @@ $zh2Hans = array(
'曏' => '向',
'曖' => '暧',
'曠' => '旷',
+'曥' => '𣆐',
'曨' => '昽',
'曬' => '晒',
'書' => '书',
'會' => '会',
+'朥' => '𦛨',
'朧' => '胧',
'朮' => '术',
'東' => '东',
@@ -10390,10 +10722,12 @@ $zh2Hans = array(
'棖' => '枨',
'棗' => '枣',
'棟' => '栋',
+'棡' => '㭎',
'棧' => '栈',
'棲' => '栖',
'棶' => '梾',
'椏' => '桠',
+'椲' => '㭏',
'楊' => '杨',
'楓' => '枫',
'楨' => '桢',
@@ -10411,6 +10745,8 @@ $zh2Hans = array(
'槧' => '椠',
'槨' => '椁',
'槳' => '桨',
+'槶' => '椢',
+'槼' => '椝',
'樁' => '桩',
'樂' => '乐',
'樅' => '枞',
@@ -10418,10 +10754,14 @@ $zh2Hans = array(
'樓' => '楼',
'標' => '标',
'樞' => '枢',
+'樢' => '㭤',
'樣' => '样',
+'樫' => '㭴',
+'樳' => '桪',
'樸' => '朴',
'樹' => '树',
'樺' => '桦',
+'樿' => '椫',
'橈' => '桡',
'橋' => '桥',
'機' => '机',
@@ -10434,6 +10774,7 @@ $zh2Hans = array(
'檟' => '槚',
'檢' => '检',
'檣' => '樯',
+'檭' => '𣘴',
'檮' => '梼',
'檯' => '台',
'檳' => '槟',
@@ -10459,8 +10800,10 @@ $zh2Hans = array(
'欄' => '栏',
'欅' => '榉',
'權' => '权',
+'欍' => '𣐤',
'欏' => '椤',
'欒' => '栾',
+'欓' => '𣗋',
'欖' => '榄',
'欞' => '棂',
'欽' => '钦',
@@ -10483,7 +10826,6 @@ $zh2Hans = array(
'殰' => '㱩',
'殲' => '歼',
'殺' => '杀',
-'殻' => '壳',
'殼' => '壳',
'毀' => '毁',
'毆' => '殴',
@@ -10517,6 +10859,7 @@ $zh2Hans = array(
'淺' => '浅',
'渙' => '涣',
'減' => '减',
+'渢' => '沨',
'渦' => '涡',
'測' => '测',
'渾' => '浑',
@@ -10528,6 +10871,7 @@ $zh2Hans = array(
'準' => '准',
'溝' => '沟',
'溫' => '温',
+'溳' => '涢',
'滄' => '沧',
'滅' => '灭',
'滌' => '涤',
@@ -10536,12 +10880,12 @@ $zh2Hans = array(
'滬' => '沪',
'滯' => '滞',
'滲' => '渗',
-'滷' => '卤',
'滸' => '浒',
'滻' => '浐',
'滾' => '滚',
'滿' => '满',
'漁' => '渔',
+'漊' => '溇',
'漚' => '沤',
'漢' => '汉',
'漣' => '涟',
@@ -10553,7 +10897,6 @@ $zh2Hans = array(
'潁' => '颍',
'潑' => '泼',
'潔' => '洁',
-'潙' => '沩',
'潛' => '潜',
'潤' => '润',
'潯' => '浔',
@@ -10561,6 +10904,7 @@ $zh2Hans = array(
'潷' => '滗',
'潿' => '涠',
'澀' => '涩',
+'澅' => '𣶩',
'澆' => '浇',
'澇' => '涝',
'澐' => '沄',
@@ -10574,16 +10918,22 @@ $zh2Hans = array(
'澾' => '㳠',
'濁' => '浊',
'濃' => '浓',
+'濄' => '㳡',
+'濆' => '𣸣',
'濕' => '湿',
'濘' => '泞',
+'濜' => '浕',
'濟' => '济',
'濤' => '涛',
+'濧' => '㳔',
'濫' => '滥',
'濰' => '潍',
'濱' => '滨',
'濺' => '溅',
'濼' => '泺',
'濾' => '滤',
+'瀂' => '澛',
+'瀃' => '𣽷',
'瀅' => '滢',
'瀆' => '渎',
'瀇' => '㲿',
@@ -10606,8 +10956,10 @@ $zh2Hans = array(
'灑' => '洒',
'灕' => '漓',
'灘' => '滩',
+'灙' => '𣺼',
'灝' => '灏',
'灠' => '漤',
+'灡' => '㳕',
'灣' => '湾',
'灤' => '滦',
'灧' => '滟',
@@ -10625,14 +10977,17 @@ $zh2Hans = array(
'煬' => '炀',
'煱' => '㶽',
'熅' => '煴',
+'熉' => '𤈶',
+'熌' => '𤇄',
'熒' => '荧',
+'熓' => '𤆡',
'熗' => '炝',
+'熡' => '𤋏',
'熱' => '热',
'熲' => '颎',
'熾' => '炽',
'燁' => '烨',
'燈' => '灯',
-'燉' => '炖',
'燒' => '烧',
'燙' => '烫',
'燜' => '焖',
@@ -10644,6 +10999,7 @@ $zh2Hans = array(
'燶' => '㶶',
'燼' => '烬',
'燾' => '焘',
+'爄' => '𤇃',
'爍' => '烁',
'爐' => '炉',
'爛' => '烂',
@@ -10682,6 +11038,7 @@ $zh2Hans = array(
'獻' => '献',
'獼' => '猕',
'玀' => '猡',
+'玁' => '𤞤',
'現' => '现',
'琺' => '珐',
'琿' => '珲',
@@ -10697,16 +11054,17 @@ $zh2Hans = array(
'璣' => '玑',
'璦' => '瑷',
'璫' => '珰',
+'璯' => '㻅',
'環' => '环',
'璽' => '玺',
'瓊' => '琼',
'瓏' => '珑',
'瓔' => '璎',
+'瓕' => '𤦀',
'瓚' => '瓒',
'甌' => '瓯',
'甕' => '瓮',
'產' => '产',
-'産' => '产',
'甦' => '苏',
'甯' => '宁',
'畝' => '亩',
@@ -10730,7 +11088,6 @@ $zh2Hans = array(
'瘮' => '瘆',
'瘲' => '疭',
'瘺' => '瘘',
-'瘻' => '瘘',
'療' => '疗',
'癆' => '痨',
'癇' => '痫',
@@ -10752,6 +11109,7 @@ $zh2Hans = array(
'癲' => '癫',
'發' => '发',
'皚' => '皑',
+'皟' => '𤾀',
'皰' => '疱',
'皸' => '皲',
'皺' => '皱',
@@ -10773,6 +11131,7 @@ $zh2Hans = array(
'瞘' => '眍',
'瞜' => '䁖',
'瞞' => '瞒',
+'瞤' => '𥆧',
'瞭' => '了',
'瞶' => '瞆',
'瞼' => '睑',
@@ -10786,19 +11145,24 @@ $zh2Hans = array(
'硨' => '砗',
'硯' => '砚',
'碕' => '埼',
+'碙' => '𥐻',
'碩' => '硕',
'碭' => '砀',
'碸' => '砜',
'確' => '确',
'碼' => '码',
+'碽' => '䂵',
'磑' => '硙',
'磚' => '砖',
+'磠' => '硵',
'磣' => '碜',
'磧' => '碛',
'磯' => '矶',
'磽' => '硗',
+'礄' => '硚',
'礆' => '硷',
'礎' => '础',
+'礒' => '𥐟',
'礙' => '碍',
'礦' => '矿',
'礪' => '砺',
@@ -10845,9 +11209,7 @@ $zh2Hans = array(
'竄' => '窜',
'竅' => '窍',
'竇' => '窦',
-'竈' => '灶',
'竊' => '窃',
-'竪' => '竖',
'競' => '竞',
'筆' => '笔',
'筍' => '笋',
@@ -10861,6 +11223,7 @@ $zh2Hans = array(
'築' => '筑',
'篋' => '箧',
'篔' => '筼',
+'篘' => '𥬠',
'篤' => '笃',
'篩' => '筛',
'篳' => '筚',
@@ -10875,8 +11238,11 @@ $zh2Hans = array(
'簽' => '签',
'簾' => '帘',
'籃' => '篮',
+'籋' => '𥬞',
'籌' => '筹',
+'籔' => '䉤',
'籙' => '箓',
+'籛' => '篯',
'籜' => '箨',
'籟' => '籁',
'籠' => '笼',
@@ -10949,14 +11315,14 @@ $zh2Hans = array(
'統' => '统',
'絲' => '丝',
'絳' => '绛',
-'絶' => '绝',
'絹' => '绢',
'絺' => '𫄨',
+'綀' => '𦈌',
'綁' => '绑',
'綃' => '绡',
'綆' => '绠',
+'綇' => '𦈋',
'綈' => '绨',
-'綉' => '绣',
'綌' => '绤',
'綏' => '绥',
'綐' => '䌼',
@@ -10973,7 +11339,6 @@ $zh2Hans = array(
'綰' => '绾',
'綱' => '纲',
'網' => '网',
-'綳' => '绷',
'綴' => '缀',
'綵' => '彩',
'綸' => '纶',
@@ -10987,10 +11352,9 @@ $zh2Hans = array(
'緇' => '缁',
'緊' => '紧',
'緋' => '绯',
-'緑' => '绿',
+'緍' => '𦈏',
'緒' => '绪',
'緓' => '绬',
-'緔' => '绱',
'緗' => '缃',
'緘' => '缄',
'緙' => '缂',
@@ -11005,16 +11369,20 @@ $zh2Hans = array(
'緩' => '缓',
'緬' => '缅',
'緯' => '纬',
+'緰' => '𦈕',
'緱' => '缑',
'緲' => '缈',
'練' => '练',
'緶' => '缏',
+'緷' => '𦈉',
+'緸' => '𦈑',
'緹' => '缇',
'緻' => '致',
'縈' => '萦',
'縉' => '缙',
'縊' => '缢',
'縋' => '缒',
+'縎' => '𦈔',
'縐' => '绉',
'縑' => '缣',
'縕' => '缊',
@@ -11024,8 +11392,8 @@ $zh2Hans = array(
'縞' => '缟',
'縟' => '缛',
'縣' => '县',
-'縧' => '绦',
'縫' => '缝',
+'縬' => '𦈚',
'縭' => '缡',
'縮' => '缩',
'縱' => '纵',
@@ -11036,34 +11404,39 @@ $zh2Hans = array(
'縶' => '絷',
'縷' => '缕',
'縹' => '缥',
+'縺' => '𦈐',
'總' => '总',
'績' => '绩',
'繃' => '绷',
'繅' => '缫',
'繆' => '缪',
+'繏' => '𦈝',
'繐' => '穗',
'繒' => '缯',
+'繓' => '𦈛',
'織' => '织',
'繕' => '缮',
'繚' => '缭',
'繞' => '绕',
+'繟' => '𦈎',
'繡' => '绣',
'繢' => '缋',
'繩' => '绳',
'繪' => '绘',
'繫' => '系',
'繭' => '茧',
-'繮' => '缰',
'繯' => '缳',
'繰' => '缲',
'繳' => '缴',
'繸' => '䍁',
'繹' => '绎',
+'繻' => '𦈡',
'繼' => '继',
'繽' => '缤',
'繾' => '缱',
'繿' => '䍀',
'纁' => '𫄸',
+'纇' => '颣',
'纈' => '缬',
'纊' => '纩',
'續' => '续',
@@ -11089,7 +11462,9 @@ $zh2Hans = array(
'羨' => '羡',
'義' => '义',
'習' => '习',
+'翬' => '翚',
'翹' => '翘',
+'翽' => '翙',
'耬' => '耧',
'耮' => '耢',
'聖' => '圣',
@@ -11109,18 +11484,22 @@ $zh2Hans = array(
'脈' => '脉',
'脛' => '胫',
'脣' => '唇',
+'脥' => '𣍰',
'脫' => '脱',
'脹' => '胀',
'腎' => '肾',
'腖' => '胨',
'腡' => '脶',
'腦' => '脑',
+'腪' => '𣍯',
'腫' => '肿',
'腳' => '脚',
'腸' => '肠',
'膃' => '腽',
+'膕' => '腘',
'膚' => '肤',
'膠' => '胶',
+'膢' => '𦝼',
'膩' => '腻',
'膽' => '胆',
'膾' => '脍',
@@ -11128,6 +11507,7 @@ $zh2Hans = array(
'臉' => '脸',
'臍' => '脐',
'臏' => '膑',
+'臗' => '𣎑',
'臘' => '腊',
'臚' => '胪',
'臟' => '脏',
@@ -11160,14 +11540,13 @@ $zh2Hans = array(
'萇' => '苌',
'萊' => '莱',
'萬' => '万',
+'萴' => '荝',
'萵' => '莴',
'葉' => '叶',
'葒' => '荭',
'葤' => '荮',
'葦' => '苇',
-'葯' => '药',
'葷' => '荤',
-'蒓' => '莼',
'蒔' => '莳',
'蒞' => '莅',
'蒼' => '苍',
@@ -11177,7 +11556,6 @@ $zh2Hans = array(
'蓯' => '苁',
'蓴' => '莼',
'蓽' => '荜',
-'蔔' => '卜',
'蔘' => '参',
'蔞' => '蒌',
'蔣' => '蒋',
@@ -11214,7 +11592,6 @@ $zh2Hans = array(
'藝' => '艺',
'藥' => '药',
'藪' => '薮',
-'藴' => '蕴',
'藶' => '苈',
'藹' => '蔼',
'藺' => '蔺',
@@ -11258,6 +11635,7 @@ $zh2Hans = array(
'蟲' => '虫',
'蟶' => '蛏',
'蟻' => '蚁',
+'蠁' => '蚃',
'蠅' => '蝇',
'蠆' => '虿',
'蠍' => '蝎',
@@ -11269,14 +11647,12 @@ $zh2Hans = array(
'蠱' => '蛊',
'蠶' => '蚕',
'蠻' => '蛮',
-'衆' => '众',
'衊' => '蔑',
'術' => '术',
'衕' => '同',
'衚' => '胡',
'衛' => '卫',
'衝' => '冲',
-'衹' => '只',
'袞' => '衮',
'裊' => '袅',
'裏' => '里',
@@ -11293,7 +11669,7 @@ $zh2Hans = array(
'褻' => '亵',
'襀' => '𫌀',
'襆' => '幞',
-'襇' => '裥',
+'襉' => '裥',
'襏' => '袯',
'襖' => '袄',
'襝' => '裣',
@@ -11303,6 +11679,7 @@ $zh2Hans = array(
'襬' => '䙓',
'襯' => '衬',
'襲' => '袭',
+'襴' => '襕',
'見' => '见',
'覎' => '觃',
'規' => '规',
@@ -11354,10 +11731,12 @@ $zh2Hans = array(
'訶' => '诃',
'診' => '诊',
'註' => '注',
+'詀' => '𧮪',
'詁' => '诂',
'詆' => '诋',
'詎' => '讵',
'詐' => '诈',
+'詑' => '𫍟',
'詒' => '诒',
'詔' => '诏',
'評' => '评',
@@ -11403,7 +11782,6 @@ $zh2Hans = array(
'誦' => '诵',
'誨' => '诲',
'說' => '说',
-'説' => '说',
'誰' => '谁',
'課' => '课',
'誶' => '谇',
@@ -11459,15 +11837,12 @@ $zh2Hans = array(
'講' => '讲',
'謝' => '谢',
'謠' => '谣',
-'謡' => '谣',
'謨' => '谟',
'謫' => '谪',
'謬' => '谬',
-'謭' => '谫',
'謳' => '讴',
'謹' => '谨',
'謾' => '谩',
-'譅' => '䜧',
'證' => '证',
'譊' => '𫍢',
'譎' => '谲',
@@ -11488,6 +11863,8 @@ $zh2Hans = array(
'譾' => '谫',
'讀' => '读',
'變' => '变',
+'讋' => '詟',
+'讌' => '䜩',
'讎' => '仇',
'讒' => '谗',
'讓' => '让',
@@ -11546,6 +11923,7 @@ $zh2Hans = array(
'賚' => '赉',
'賜' => '赐',
'賞' => '赏',
+'賟' => '𧹖',
'賠' => '赔',
'賡' => '赓',
'賢' => '贤',
@@ -11554,7 +11932,6 @@ $zh2Hans = array(
'賦' => '赋',
'賧' => '赕',
'質' => '质',
-'賫' => '赍',
'賬' => '账',
'賭' => '赌',
'賰' => '䞐',
@@ -11565,12 +11942,12 @@ $zh2Hans = array(
'購' => '购',
'賽' => '赛',
'賾' => '赜',
+'贃' => '𧹗',
'贄' => '贽',
'贅' => '赘',
'贇' => '赟',
'贈' => '赠',
'贊' => '赞',
-'贋' => '赝',
'贍' => '赡',
'贏' => '赢',
'贐' => '赆',
@@ -11579,7 +11956,6 @@ $zh2Hans = array(
'贖' => '赎',
'贗' => '赝',
'贛' => '赣',
-'贜' => '赃',
'赬' => '赪',
'趕' => '赶',
'趙' => '赵',
@@ -11599,16 +11975,19 @@ $zh2Hans = array(
'躊' => '踌',
'躋' => '跻',
'躍' => '跃',
+'躎' => '䟢',
'躑' => '踯',
'躒' => '跞',
'躓' => '踬',
'躕' => '蹰',
'躚' => '跹',
+'躝' => '𨅬',
'躡' => '蹑',
'躥' => '蹿',
'躦' => '躜',
'躪' => '躏',
'軀' => '躯',
+'軉' => '𨉗',
'車' => '车',
'軋' => '轧',
'軌' => '轨',
@@ -11617,6 +11996,7 @@ $zh2Hans = array(
'軑' => '轪',
'軒' => '轩',
'軔' => '轫',
+'軗' => '𨐅',
'軛' => '轭',
'軟' => '软',
'軤' => '轷',
@@ -11630,6 +12010,7 @@ $zh2Hans = array(
'軼' => '轶',
'軾' => '轼',
'較' => '较',
+'輄' => '𨐈',
'輅' => '辂',
'輇' => '辁',
'輈' => '辀',
@@ -11720,7 +12101,6 @@ $zh2Hans = array(
'鄺' => '邝',
'酇' => '酂',
'酈' => '郦',
-'醖' => '酝',
'醜' => '丑',
'醞' => '酝',
'醣' => '糖',
@@ -11746,10 +12126,12 @@ $zh2Hans = array(
'釤' => '钐',
'釧' => '钏',
'釩' => '钒',
+'釳' => '𨰿',
'釵' => '钗',
'釷' => '钍',
'釹' => '钕',
'釺' => '钎',
+'釾' => '䥺',
'鈀' => '钯',
'鈁' => '钫',
'鈃' => '钘',
@@ -11757,20 +12139,23 @@ $zh2Hans = array(
'鈇' => '𫓧',
'鈈' => '钚',
'鈉' => '钠',
+'鈋' => '𨱂',
'鈍' => '钝',
-'鈎' => '钩',
'鈐' => '钤',
'鈑' => '钣',
'鈒' => '钑',
'鈔' => '钞',
'鈕' => '钮',
'鈞' => '钧',
+'鈠' => '𨱁',
'鈣' => '钙',
'鈥' => '钬',
'鈦' => '钛',
'鈧' => '钪',
'鈮' => '铌',
+'鈯' => '𨱄',
'鈰' => '铈',
+'鈲' => '𨱃',
'鈳' => '钶',
'鈴' => '铃',
'鈷' => '钴',
@@ -11781,6 +12166,7 @@ $zh2Hans = array(
'鈾' => '铀',
'鈿' => '钿',
'鉀' => '钾',
+'鉁' => '𨱅',
'鉅' => '钜',
'鉈' => '铊',
'鉉' => '铉',
@@ -11792,7 +12178,6 @@ $zh2Hans = array(
'鉚' => '铆',
'鉛' => '铅',
'鉞' => '钺',
-'鉢' => '钵',
'鉤' => '钩',
'鉦' => '钲',
'鉬' => '钼',
@@ -11824,14 +12209,15 @@ $zh2Hans = array(
'銬' => '铐',
'銱' => '铞',
'銳' => '锐',
+'銶' => '𨱇',
'銷' => '销',
-'銹' => '锈',
'銻' => '锑',
'銼' => '锉',
'鋁' => '铝',
'鋃' => '锒',
'鋅' => '锌',
'鋇' => '钡',
+'鋉' => '𨱈',
'鋌' => '铤',
'鋏' => '铗',
'鋒' => '锋',
@@ -11845,7 +12231,6 @@ $zh2Hans = array(
'鋨' => '锇',
'鋩' => '铓',
'鋪' => '铺',
-'鋭' => '锐',
'鋮' => '铖',
'鋯' => '锆',
'鋰' => '锂',
@@ -11854,6 +12239,7 @@ $zh2Hans = array(
'鋸' => '锯',
'鋼' => '钢',
'錁' => '锞',
+'錂' => '𨱋',
'錄' => '录',
'錆' => '锖',
'錇' => '锫',
@@ -11876,13 +12262,12 @@ $zh2Hans = array(
'錫' => '锡',
'錮' => '锢',
'錯' => '错',
-'録' => '录',
'錳' => '锰',
'錶' => '表',
'錸' => '铼',
'鍀' => '锝',
-'鍁' => '锨',
'鍃' => '锪',
+'鍄' => '𨱉',
'鍆' => '钔',
'鍇' => '锴',
'鍈' => '锳',
@@ -11898,6 +12283,7 @@ $zh2Hans = array(
'鍥' => '锲',
'鍩' => '锘',
'鍬' => '锹',
+'鍮' => '𨱎',
'鍰' => '锾',
'鍵' => '键',
'鍶' => '锶',
@@ -11911,7 +12297,6 @@ $zh2Hans = array(
'鎔' => '镕',
'鎖' => '锁',
'鎘' => '镉',
-'鎚' => '锤',
'鎛' => '镈',
'鎝' => '𨱏',
'鎡' => '镃',
@@ -11924,15 +12309,18 @@ $zh2Hans = array(
'鎬' => '镐',
'鎭' => '鎮',
'鎮' => '镇',
+'鎯' => '𨱍',
'鎰' => '镒',
'鎲' => '镋',
'鎳' => '镍',
'鎵' => '镓',
-'鎸' => '镌',
+'鎷' => '𨰾',
'鎿' => '镎',
'鏃' => '镞',
+'鏆' => '𨱌',
'鏇' => '镟',
'鏈' => '链',
+'鏉' => '𨱒',
'鏌' => '镆',
'鏍' => '镙',
'鏐' => '镠',
@@ -11953,23 +12341,28 @@ $zh2Hans = array(
'鏵' => '铧',
'鏷' => '镤',
'鏹' => '镪',
+'鏺' => '䥽',
'鏽' => '锈',
'鐃' => '铙',
+'鐄' => '𨱑',
'鐋' => '铴',
'鐍' => '𫔎',
+'鐎' => '𨱓',
+'鐏' => '𨱔',
'鐐' => '镣',
'鐒' => '铹',
'鐓' => '镦',
'鐔' => '镡',
'鐘' => '钟',
'鐙' => '镫',
-'鐝' => '镢',
'鐠' => '镨',
+'鐥' => '䦅',
'鐦' => '锎',
'鐧' => '锏',
'鐨' => '镄',
'鐫' => '镌',
'鐮' => '镰',
+'鐯' => '䦃',
'鐲' => '镯',
'鐳' => '镭',
'鐵' => '铁',
@@ -12009,8 +12402,10 @@ $zh2Hans = array(
'閉' => '闭',
'開' => '开',
'閌' => '闶',
+'閍' => '𨸂',
'閎' => '闳',
'閏' => '闰',
+'閐' => '𨸃',
'閑' => '闲',
'閒' => '闲',
'間' => '间',
@@ -12026,7 +12421,6 @@ $zh2Hans = array(
'閬' => '阆',
'閭' => '闾',
'閱' => '阅',
-'閲' => '阅',
'閶' => '阊',
'閹' => '阉',
'閻' => '阎',
@@ -12142,7 +12536,7 @@ $zh2Hans = array(
'頸' => '颈',
'頹' => '颓',
'頻' => '频',
-'頽' => '颓',
+'顃' => '𩖖',
'顆' => '颗',
'題' => '题',
'額' => '额',
@@ -12150,7 +12544,6 @@ $zh2Hans = array(
'顏' => '颜',
'顒' => '颙',
'顓' => '颛',
-'顔' => '颜',
'願' => '愿',
'顙' => '颡',
'顛' => '颠',
@@ -12169,13 +12562,16 @@ $zh2Hans = array(
'颭' => '飐',
'颮' => '飑',
'颯' => '飒',
+'颰' => '𩙥',
'颱' => '台',
'颳' => '刮',
'颶' => '飓',
+'颷' => '𩙪',
'颸' => '飔',
'颺' => '飏',
'颻' => '飖',
'颼' => '飕',
+'颾' => '𩙫',
'飀' => '飗',
'飄' => '飘',
'飆' => '飙',
@@ -12226,6 +12622,7 @@ $zh2Hans = array(
'餵' => '喂',
'餶' => '馉',
'餷' => '馇',
+'餸' => '𩠌',
'餺' => '馎',
'餼' => '饩',
'餾' => '馏',
@@ -12238,7 +12635,6 @@ $zh2Hans = array(
'饊' => '馓',
'饋' => '馈',
'饌' => '馔',
-'饑' => '饥',
'饒' => '饶',
'饗' => '飨',
'饘' => '𫗴',
@@ -12254,6 +12650,7 @@ $zh2Hans = array(
'馹' => '驲',
'駁' => '驳',
'駃' => '𫘝',
+'駎' => '𩧨',
'駐' => '驻',
'駑' => '驽',
'駒' => '驹',
@@ -12261,14 +12658,17 @@ $zh2Hans = array(
'駕' => '驾',
'駘' => '骀',
'駙' => '驸',
+'駚' => '𩧫',
'駛' => '驶',
'駝' => '驼',
'駟' => '驷',
-'駡' => '骂',
'駢' => '骈',
+'駧' => '𩧲',
+'駩' => '𩧴',
'駭' => '骇',
'駰' => '骃',
'駱' => '骆',
+'駶' => '𩧺',
'駸' => '骎',
'駻' => '𫘣',
'駿' => '骏',
@@ -12280,11 +12680,16 @@ $zh2Hans = array(
'騍' => '骒',
'騎' => '骑',
'騏' => '骐',
+'騔' => '𩨀',
'騖' => '骛',
'騙' => '骗',
+'騚' => '𩨊',
+'騝' => '𩨃',
+'騟' => '𩨈',
'騠' => '𫘨',
'騤' => '骙',
'騧' => '䯄',
+'騪' => '𩨄',
'騫' => '骞',
'騭' => '骘',
'騮' => '骝',
@@ -12300,6 +12705,7 @@ $zh2Hans = array(
'驄' => '骢',
'驅' => '驱',
'驊' => '骅',
+'驋' => '𩧯',
'驌' => '骕',
'驍' => '骁',
'驏' => '骣',
@@ -12331,12 +12737,14 @@ $zh2Hans = array(
'鬩' => '阋',
'鬮' => '阄',
'鬱' => '郁',
+'鬹' => '鬶',
'魎' => '魉',
'魘' => '魇',
'魚' => '鱼',
'魛' => '鱽',
'魟' => '𫚉',
'魢' => '鱾',
+'魥' => '𩽹',
'魨' => '鲀',
'魯' => '鲁',
'魴' => '鲂',
@@ -12348,15 +12756,15 @@ $zh2Hans = array(
'鮊' => '鲌',
'鮋' => '鲉',
'鮍' => '鲏',
-'鮎' => '鲇',
'鮐' => '鲐',
'鮑' => '鲍',
'鮒' => '鲋',
'鮓' => '鲊',
'鮚' => '鲒',
'鮜' => '鲘',
-'鮝' => '鲞',
'鮞' => '鲕',
+'鮟' => '𩽾',
+'鮣' => '䲟',
'鮦' => '鲖',
'鮪' => '鲔',
'鮫' => '鲛',
@@ -12365,9 +12773,11 @@ $zh2Hans = array(
'鮰' => '𫚔',
'鮳' => '鲓',
'鮶' => '鲪',
+'鮸' => '𩾃',
'鮺' => '鲝',
'鯀' => '鲧',
'鯁' => '鲠',
+'鯄' => '𩾁',
'鯆' => '𫚙',
'鯇' => '鲩',
'鯉' => '鲤',
@@ -12386,19 +12796,21 @@ $zh2Hans = array(
'鯨' => '鲸',
'鯪' => '鲮',
'鯫' => '鲰',
-'鯰' => '鲇',
+'鯱' => '𩾇',
'鯴' => '鲺',
+'鯶' => '𩽼',
'鯷' => '鳀',
'鯽' => '鲫',
'鯿' => '鳊',
'鰁' => '鳈',
'鰂' => '鲗',
'鰃' => '鳂',
+'鰆' => '䲠',
'鰈' => '鲽',
'鰉' => '鳇',
+'鰌' => '䲡',
'鰍' => '鳅',
'鰏' => '鲾',
-'鰐' => '鳄',
'鰒' => '鳆',
'鰓' => '鳃',
'鰜' => '鳒',
@@ -12407,6 +12819,7 @@ $zh2Hans = array(
'鰣' => '鲥',
'鰤' => '𫚕',
'鰥' => '鳏',
+'鰧' => '䲢',
'鰨' => '鳎',
'鰩' => '鳐',
'鰭' => '鳍',
@@ -12423,6 +12836,7 @@ $zh2Hans = array(
'鰾' => '鳔',
'鱂' => '鳉',
'鱅' => '鳙',
+'鱇' => '𩾌',
'鱈' => '鳕',
'鱉' => '鳖',
'鱒' => '鳟',
@@ -12446,12 +12860,12 @@ $zh2Hans = array(
'鳥' => '鸟',
'鳧' => '凫',
'鳩' => '鸠',
-'鳬' => '凫',
'鳲' => '鸤',
'鳳' => '凤',
'鳴' => '鸣',
'鳶' => '鸢',
'鳷' => '𫛛',
+'鳼' => '𪉃',
'鳾' => '䴓',
'鴃' => '𫛞',
'鴆' => '鸩',
@@ -12461,6 +12875,7 @@ $zh2Hans = array(
'鴕' => '鸵',
'鴗' => '𫁡',
'鴛' => '鸳',
+'鴜' => '𪉈',
'鴝' => '鸲',
'鴞' => '鸮',
'鴟' => '鸱',
@@ -12469,6 +12884,7 @@ $zh2Hans = array(
'鴨' => '鸭',
'鴯' => '鸸',
'鴰' => '鸹',
+'鴲' => '𪉆',
'鴴' => '鸻',
'鴷' => '䴕',
'鴻' => '鸿',
@@ -12480,6 +12896,7 @@ $zh2Hans = array(
'鵑' => '鹃',
'鵒' => '鹆',
'鵓' => '鹁',
+'鵚' => '𪉍',
'鵜' => '鹈',
'鵝' => '鹅',
'鵠' => '鹄',
@@ -12515,19 +12932,19 @@ $zh2Hans = array(
'鶻' => '鹘',
'鶼' => '鹣',
'鶿' => '鹚',
-'鷀' => '鹚',
'鷁' => '鹢',
'鷂' => '鹞',
-'鷄' => '鸡',
'鷈' => '䴘',
'鷊' => '鹝',
'鷓' => '鹧',
+'鷔' => '𪉑',
'鷖' => '鹥',
'鷗' => '鸥',
'鷙' => '鸷',
'鷚' => '鹨',
'鷥' => '鸶',
'鷦' => '鹪',
+'鷨' => '𪉊',
'鷫' => '鹔',
'鷯' => '鹩',
'鷲' => '鹫',
@@ -12555,10 +12972,12 @@ $zh2Hans = array(
'鹽' => '盐',
'麗' => '丽',
'麥' => '麦',
+'麨' => '𪎊',
'麩' => '麸',
'麪' => '面',
'麫' => '面',
'麯' => '曲',
+'麲' => '𪎉',
'麴' => '曲',
'麵' => '面',
'麼' => '么',
@@ -12604,25 +13023,167 @@ $zh2Hans = array(
'龔' => '龚',
'龕' => '龛',
'龜' => '龟',
+'龭' => '𩨎',
+'龯' => '𨱆',
+'𠌥' => '𠆿',
+'𠏢' => '𠉗',
+'𠞆' => '𠛆',
+'𠠎' => '𠚳',
+'𡄔' => '𠴢',
+'𡄣' => '𠵸',
+'𡅏' => '𠲥',
+'𡑭' => '𡋗',
+'𡓾' => '𡋀',
'𡞵' => '㛟',
'𡠹' => '㛿',
'𡢃' => '㛠',
+'𡮉' => '𡭜',
+'𡮣' => '𡭬',
'𡻕' => '岁',
+'𡾱' => '㟜',
+'𢣚' => '𢘝',
+'𢣭' => '𢘞',
+'𢶫' => '𢫞',
+'𢷮' => '𢫊',
+'𢹿' => '𢬦',
+'𣙎' => '㭣',
+'𣝕' => '𣘷',
+'𣞻' => '𣘓',
+'𣠲' => '𣑶',
+'𣯴' => '𣭤',
+'𣾷' => '㳢',
+'𣿉' => '𣶫',
+'𤁣' => '𣺽',
+'𤒎' => '𤊀',
'𤪺' => '㻘',
'𤫩' => '㻏',
+'𤳸' => '𤳄',
+'𤸫' => '𤶧',
+'𥌃' => '𥅘',
+'𥕥' => '𥐰',
+'𥖅' => '𥐯',
+'𥢢' => '䅪',
+'𥨐' => '𥧂',
+'𥵃' => '𥱔',
+'𥵊' => '𥭉',
+'𥸠' => '𥮋',
+'𥼽' => '𥹥',
+'𥽖' => '𥺇',
+'𥿊' => '𦈈',
+'𦂅' => '𦈒',
+'𦃄' => '𦈗',
+'𦢈' => '𣍨',
+'𦣎' => '𦟗',
+'𦪽' => '𦨩',
+'𧔥' => '𧒭',
+'𧜗' => '䘞',
'𧜵' => '䙊',
'𧝞' => '䘛',
-'𧦧' => '𫍟',
'𧩙' => '䜥',
+'𧳟' => '𧳕',
'𧵳' => '䞌',
+'𧶔' => '𧹓',
+'𧶧' => '䞎',
+'𨄣' => '𨀱',
+'𨅍' => '𨁴',
+'𨇁' => '𧿈',
+'𨇞' => '𨅫',
+'𨈊' => '𨂺',
+'𨈌' => '𨄄',
+'𨊰' => '䢀',
+'𨊸' => '䢁',
+'𨊻' => '𨐆',
'𨋢' => '䢂',
+'𨎮' => '𨐉',
+'𨏠' => '𨐇',
+'𨏥' => '𨐊',
+'𨤻' => '𨤰',
+'𨥛' => '𨱀',
'𨦫' => '䦀',
'𨧜' => '䦁',
+'𨧱' => '𨱊',
+'𨫒' => '𨱐',
+'𨮂' => '𨱕',
'𨯅' => '䥿',
+'𨳑' => '𨸁',
+'𨳕' => '𨸀',
+'𨴗' => '𨸅',
+'𨵩' => '𨸆',
+'𨵸' => '𨸇',
+'𨶀' => '𨸉',
+'𨶏' => '𨸊',
+'𨶮' => '𨸌',
+'𨶲' => '𨸋',
+'𨷲' => '𨸎',
+'𨽏' => '𨸘',
+'𩎢' => '𩏾',
+'𩏪' => '𩏽',
+'𩓣' => '𩖕',
+'𩗀' => '𩙦',
+'𩘀' => '𩙩',
+'𩘝' => '𩙭',
+'𩘹' => '𩙨',
+'𩘺' => '𩙬',
+'𩙈' => '𩙰',
+'𩚛' => '𩟿',
+'𩚥' => '𩠀',
+'𩚵' => '𩠁',
+'𩛆' => '𩠂',
+'𩛩' => '𩠃',
+'𩜇' => '𩠉',
+'𩜦' => '𩠆',
+'𩜵' => '𩠊',
+'𩝔' => '𩠋',
+'𩞄' => '𩠎',
+'𩞦' => '𩠏',
+'𩞯' => '䭪',
+'𩟐' => '𩠅',
+'𩠴' => '𩠠',
+'𩡺' => '𩧦',
+'𩢡' => '𩧬',
+'𩢴' => '𩧵',
+'𩢸' => '𩧳',
+'𩢾' => '𩧮',
+'𩣏' => '𩧶',
'𩣑' => '䯃',
'𩣵' => '𩧻',
+'𩣺' => '𩧼',
+'𩤊' => '𩧩',
+'𩤙' => '𩨆',
+'𩤲' => '𩨉',
+'𩤸' => '𩨅',
+'𩥄' => '𩨋',
+'𩥇' => '𩨍',
+'𩥉' => '𩧱',
+'𩥑' => '𩨌',
+'𩧆' => '𩨐',
+'𩭙' => '𩬣',
+'𩯳' => '𩯒',
+'𩰀' => '𩬤',
+'𩳤' => '𩲒',
+'𩵩' => '𩽺',
+'𩵹' => '𩽻',
'𩶘' => '䲞',
-'𫚒' => '軿',
+'𩶰' => '𩽿',
+'𩶱' => '𩽽',
+'𩷰' => '𩾄',
+'𩸃' => '𩾅',
+'𩸦' => '𩾆',
+'𩿪' => '𪉄',
+'𪀦' => '𪉅',
+'𪀾' => '𪉋',
+'𪁈' => '𪉉',
+'𪁖' => '𪉌',
+'𪂆' => '𪉎',
+'𪃍' => '𪉐',
+'𪃏' => '𪉏',
+'𪄆' => '𪉔',
+'𪄕' => '𪉒',
+'𪇳' => '𪉕',
+'𪋿' => '𪎍',
+'𪔵' => '𪔭',
+'𪘀' => '𪚏',
+'𪘯' => '𪚐',
'《易乾' => '《易乾',
'不著痕跡' => '不着痕迹',
'不著邊際' => '不着边际',
@@ -12761,8 +13322,8 @@ $zh2Hans = array(
'乾曜' => '乾曜',
'乾构' => '乾构',
'乾構' => '乾构',
-'乾枢' => '乾枢',
'乾樞' => '乾枢',
+'乾枢' => '乾枢',
'乾栋' => '乾栋',
'乾棟' => '乾栋',
'乾步' => '乾步',
@@ -13472,6 +14033,7 @@ $zh2Hans = array(
'尋著稱' => '寻著称',
'尋著者' => '寻著者',
'尋著述' => '寻著述',
+'將軍抽俥' => '将军抽俥',
'將軍抽車' => '将军抽車',
'尼乾陀' => '尼乾陀',
'展著' => '展着',
@@ -14789,6 +15351,8 @@ $zh2Hans = array(
'繞著稱' => '绕著称',
'繞著者' => '绕著者',
'繞著述' => '绕著述',
+'綳著勁' => '绷着劲',
+'綳著臉' => '绷着脸',
'編著' => '编著',
'纏著' => '缠着',
'纏著書' => '缠著书',
@@ -14917,7 +15481,9 @@ $zh2Hans = array(
'著者' => '著者',
'著身' => '著身',
'著述' => '著述',
+'蒙汗葯' => '蒙汗药',
'蒙著' => '蒙着',
+'蒙葯' => '蒙药',
'蒙著書' => '蒙著书',
'蒙著书' => '蒙著书',
'蒙著作' => '蒙著作',
@@ -15203,6 +15769,7 @@ $zh2Hans = array(
'達著述' => '达著述',
'近角聪信' => '近角聪信',
'近角聰信' => '近角聪信',
+'这么' => '这么',
'遠著' => '远着',
'遠著書' => '远著书',
'遠著作' => '远著作',
@@ -15511,7 +16078,6 @@ $zh2Hans = array(
'鬱氏' => '鬱氏',
'魏徵' => '魏徵',
'魚乾乾' => '鱼干干',
-'鯰魚' => '鲶鱼',
'麯崇裕' => '麯崇裕',
'麴義' => '麴义',
'麴义' => '麴义',
@@ -18515,4 +19081,4 @@ $zh2SG = array(
'笨豬跳' => '绑紧跳',
'蹦极跳' => '绑紧跳',
'笑星' => '谐星',
-); \ No newline at end of file
+);
diff --git a/includes/ZipDirectoryReader.php b/includes/ZipDirectoryReader.php
index 0e84583f..307efcea 100644
--- a/includes/ZipDirectoryReader.php
+++ b/includes/ZipDirectoryReader.php
@@ -34,10 +34,10 @@ class ZipDirectoryReader {
*
* Because this class is aimed at verification, an error is raised on
* suspicious or ambiguous input, instead of emulating some standard
- * behaviour.
+ * behavior.
*
- * @param $fileName string The archive file name
- * @param $callback Array The callback function. It will be called for each file
+ * @param string $fileName The archive file name
+ * @param array $callback The callback function. It will be called for each file
* with a single associative array each time, with members:
*
* - name: The file name. Directories conventionally have a trailing
@@ -47,7 +47,7 @@ class ZipDirectoryReader {
*
* - size: The uncompressed file size
*
- * @param $options Array An associative array of read options, with the option
+ * @param array $options An associative array of read options, with the option
* name in the key. This may currently contain:
*
* - zip64: If this is set to true, then we will emulate a
@@ -162,7 +162,7 @@ class ZipDirectoryReader {
|| $this->eocdr['CD entries total'] == 0xffff )
{
$this->error( 'zip-unsupported', 'Central directory header indicates ZIP64, ' .
- 'but we are in legacy mode. Rejecting this upload is necessary to avoid '.
+ 'but we are in legacy mode. Rejecting this upload is necessary to avoid ' .
'opening vulnerabilities on clients using OpenJDK 7 or later.' );
}
@@ -181,7 +181,7 @@ class ZipDirectoryReader {
* Throw an error, and log a debug message
*/
function error( $code, $debugMessage ) {
- wfDebug( __CLASS__.": Fatal error: $debugMessage\n" );
+ wfDebug( __CLASS__ . ": Fatal error: $debugMessage\n" );
throw new ZipDirectoryReaderError( $code );
}
@@ -220,7 +220,7 @@ class ZipDirectoryReader {
if ( $structSize + $this->eocdr['file comment length'] != strlen( $block ) - $sigPos ) {
$this->error( 'zip-bad', 'trailing bytes after the end of the file comment' );
}
- if ( $this->eocdr['disk'] !== 0
+ if ( $this->eocdr['disk'] !== 0
|| $this->eocdr['CD start disk'] !== 0 )
{
$this->error( 'zip-unsupported', 'more than one disk (in EOCDR)' );
@@ -262,7 +262,7 @@ class ZipDirectoryReader {
* may replace the regular "end of central directory record" in ZIP64 files.
*/
function readZip64EndOfCentralDirectoryRecord() {
- if ( $this->eocdr64Locator['eocdr64 start disk'] != 0
+ if ( $this->eocdr64Locator['eocdr64 start disk'] != 0
|| $this->eocdr64Locator['number of disks'] != 0 )
{
$this->error( 'zip-unsupported', 'more than one disk (in EOCDR64 locator)' );
@@ -286,7 +286,7 @@ class ZipDirectoryReader {
if ( $data['signature'] !== "PK\x06\x06" ) {
$this->error( 'zip-bad', 'wrong signature on Zip64 end of central directory record' );
}
- if ( $data['disk'] !== 0
+ if ( $data['disk'] !== 0
|| $data['CD start disk'] !== 0 )
{
$this->error( 'zip-unsupported', 'more than one disk (in EOCDR64)' );
@@ -327,7 +327,7 @@ class ZipDirectoryReader {
$offset = $this->eocdr['CD offset'];
$numEntries = $this->eocdr['CD entries total'];
$endPos = $this->eocdr['position'];
- if ( $size == 0xffffffff
+ if ( $size == 0xffffffff
|| $offset == 0xffffffff
|| $numEntries == 0xffff )
{
@@ -395,7 +395,7 @@ class ZipDirectoryReader {
$data += $this->unpack( $block, $variableInfo, $pos );
$pos += $this->getStructSize( $variableInfo );
- if ( $this->zip64 && (
+ if ( $this->zip64 && (
$data['compressed size'] == 0xffffffff
|| $data['uncompressed size'] == 0xffffffff
|| $data['local header offset'] == 0xffffffff ) )
@@ -494,8 +494,8 @@ class ZipDirectoryReader {
* Get the file contents from a given offset. If there are not enough bytes
* in the file to satisfy the request, an exception will be thrown.
*
- * @param $start int The byte offset of the start of the block.
- * @param $length int The number of bytes to return. If omitted, the remainder
+ * @param int $start The byte offset of the start of the block.
+ * @param int $length The number of bytes to return. If omitted, the remainder
* of the file will be returned.
*
* @return string
@@ -538,7 +538,7 @@ class ZipDirectoryReader {
* of length self::SEGSIZE. The result is cached. This is a helper function
* for getBlock().
*
- * If there are not enough bytes in the file to satsify the request, the
+ * If there are not enough bytes in the file to satisfy the request, the
* return value will be truncated. If a request is made for a segment beyond
* the end of the file, an empty string will be returned.
* @return string
@@ -570,7 +570,7 @@ class ZipDirectoryReader {
$size = 0;
foreach ( $struct as $type ) {
if ( is_array( $type ) ) {
- list( $typeName, $fieldSize ) = $type;
+ list( , $fieldSize ) = $type;
$size += $fieldSize;
} else {
$size += $type;
@@ -583,9 +583,9 @@ class ZipDirectoryReader {
* Unpack a binary structure. This is like the built-in unpack() function
* except nicer.
*
- * @param $string string The binary data input
+ * @param string $string The binary data input
*
- * @param $struct array An associative array giving structure members and their
+ * @param array $struct An associative array giving structure members and their
* types. In the key is the field name. The value may be either an
* integer, in which case the field is a little-endian unsigned integer
* encoded in the given number of bytes, or an array, in which case the
@@ -594,8 +594,9 @@ class ZipDirectoryReader {
* - "string": The second array element gives the length of string.
* Not null terminated.
*
- * @param $offset int The offset into the string at which to start unpacking.
+ * @param int $offset The offset into the string at which to start unpacking.
*
+ * @throws MWException
* @return array Unpacked associative array. Note that large integers in the input
* may be represented as floating point numbers in the return value, so
* the use of weak comparison is advised.
@@ -617,12 +618,11 @@ class ZipDirectoryReader {
$pos += $fieldSize;
break;
default:
- throw new MWException( __METHOD__.": invalid type \"$typeName\"" );
+ throw new MWException( __METHOD__ . ": invalid type \"$typeName\"" );
}
} else {
// Unsigned little-endian integer
$length = intval( $type );
- $bytes = substr( $string, $pos, $length );
// Calculate the value. Use an algorithm which automatically
// upgrades the value to floating point if necessary.
@@ -651,7 +651,7 @@ class ZipDirectoryReader {
* boolean.
*
* @param $value integer
- * @param $bitIndex int The index of the bit, where 0 is the LSB.
+ * @param int $bitIndex The index of the bit, where 0 is the LSB.
* @return bool
*/
function testBit( $value, $bitIndex ) {
diff --git a/includes/actions/CachedAction.php b/includes/actions/CachedAction.php
index d21f9aeb..bfdda7b9 100644
--- a/includes/actions/CachedAction.php
+++ b/includes/actions/CachedAction.php
@@ -1,22 +1,8 @@
<?php
-
/**
* Abstract action class with scaffolding for caching HTML and other values
* in a single blob.
*
- * Before using any of the caching functionality, call startCache.
- * After the last call to either getCachedValue or addCachedHTML, call saveCache.
- *
- * To get a cached value or compute it, use getCachedValue like this:
- * $this->getCachedValue( $callback );
- *
- * To add HTML that should be cached, use addCachedHTML like this:
- * $this->addCachedHTML( $callback );
- *
- * The callback function is only called when needed, so do all your expensive
- * computations here. This function should returns the HTML to be cached.
- * It should not add anything to the PageOutput object!
- *
* 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
@@ -33,10 +19,30 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Action
+ * @ingroup Actions
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
* @since 1.20
*/
+
+/**
+ * Abstract action class with scaffolding for caching HTML and other values
+ * in a single blob.
+ *
+ * Before using any of the caching functionality, call startCache.
+ * After the last call to either getCachedValue or addCachedHTML, call saveCache.
+ *
+ * To get a cached value or compute it, use getCachedValue like this:
+ * $this->getCachedValue( $callback );
+ *
+ * To add HTML that should be cached, use addCachedHTML like this:
+ * $this->addCachedHTML( $callback );
+ *
+ * The callback function is only called when needed, so do all your expensive
+ * computations here. This function should returns the HTML to be cached.
+ * It should not add anything to the PageOutput object!
+ *
+ * @ingroup Actions
+ */
abstract class CachedAction extends FormlessAction implements ICacheHelper {
/**
diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php
index f7152297..0a2bf306 100644
--- a/includes/actions/CreditsAction.php
+++ b/includes/actions/CreditsAction.php
@@ -23,6 +23,9 @@
* @author <evan@wikitravel.org>
*/
+/**
+ * @ingroup Actions
+ */
class CreditsAction extends FormlessAction {
public function getName() {
@@ -55,8 +58,8 @@ class CreditsAction extends FormlessAction {
/**
* Get a list of contributors
*
- * @param $cnt Int: maximum list of contributors to show
- * @param $showIfMax Bool: whether to contributors if there more than $cnt
+ * @param int $cnt maximum list of contributors to show
+ * @param bool $showIfMax whether to contributors if there more than $cnt
* @return String: html
*/
public function getCredits( $cnt, $showIfMax = true ) {
@@ -76,17 +79,17 @@ class CreditsAction extends FormlessAction {
/**
* Get the last author with the last modification time
- * @param $article Article object
+ * @param Page $page
* @return String HTML
*/
- protected function getAuthor( Page $article ) {
- $user = User::newFromName( $article->getUserText(), false );
+ protected function getAuthor( Page $page ) {
+ $user = User::newFromName( $page->getUserText(), false );
- $timestamp = $article->getTimestamp();
+ $timestamp = $page->getTimestamp();
if ( $timestamp ) {
$lang = $this->getLanguage();
- $d = $lang->date( $article->getTimestamp(), true );
- $t = $lang->time( $article->getTimestamp(), true );
+ $d = $lang->date( $page->getTimestamp(), true );
+ $t = $lang->time( $page->getTimestamp(), true );
} else {
$d = '';
$t = '';
@@ -97,8 +100,8 @@ class CreditsAction extends FormlessAction {
/**
* Get a list of contributors of $article
- * @param $cnt Int: maximum list of contributors to show
- * @param $showIfMax Bool: whether to contributors if there more than $cnt
+ * @param int $cnt maximum list of contributors to show
+ * @param bool $showIfMax whether to contributors if there more than $cnt
* @return String: html
*/
protected function getContributors( $cnt, $showIfMax ) {
@@ -111,9 +114,10 @@ class CreditsAction extends FormlessAction {
# Hmm... too many to fit!
if ( $cnt > 0 && $contributors->count() > $cnt ) {
$others_link = $this->othersLink();
- if ( !$showIfMax )
+ if ( !$showIfMax ) {
return $this->msg( 'othercontribs' )->rawParams(
$others_link )->params( $contributors->count() )->escaped();
+ }
}
$real_names = array();
@@ -122,7 +126,7 @@ class CreditsAction extends FormlessAction {
# Sift for real versus user names
foreach ( $contributors as $user ) {
- $cnt--;
+ $cnt--;
if ( $user->isLoggedIn() ) {
$link = $this->link( $user );
if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) {
diff --git a/includes/actions/DeleteAction.php b/includes/actions/DeleteAction.php
index 5a5a382b..db7123db 100644
--- a/includes/actions/DeleteAction.php
+++ b/includes/actions/DeleteAction.php
@@ -23,17 +23,24 @@
* @author Timo Tijhof
*/
+/**
+ * Handle page deletion
+ *
+ * This is a wrapper that will call Article::delete().
+ *
+ * @ingroup Actions
+ */
class DeleteAction extends FormlessAction {
public function getName() {
return 'delete';
}
- public function onView(){
+ public function onView() {
return null;
}
- public function show(){
+ public function show() {
$this->page->delete();
diff --git a/includes/actions/EditAction.php b/includes/actions/EditAction.php
index 08a33f4c..3dd4c483 100644
--- a/includes/actions/EditAction.php
+++ b/includes/actions/EditAction.php
@@ -23,46 +23,51 @@
* @author Timo Tijhof
*/
+/**
+ * Page edition handler
+ *
+ * This is a wrapper that will call the EditPage class, or ExternalEdit
+ * if $wgUseExternalEditor is set to true and requested by the user.
+ *
+ * @ingroup Actions
+ */
class EditAction extends FormlessAction {
public function getName() {
return 'edit';
}
- public function onView(){
+ public function onView() {
return null;
}
- public function show(){
+ public function show() {
$page = $this->page;
- $request = $this->getRequest();
$user = $this->getUser();
- $context = $this->getContext();
if ( wfRunHooks( 'CustomEditor', array( $page, $user ) ) ) {
- if ( ExternalEdit::useExternalEngine( $context, 'edit' )
- && $this->getName() == 'edit' && !$request->getVal( 'section' )
- && !$request->getVal( 'oldid' ) )
- {
- $extedit = new ExternalEdit( $context );
- $extedit->execute();
- } else {
- $editor = new EditPage( $page );
- $editor->edit();
- }
+ $editor = new EditPage( $page );
+ $editor->edit();
}
}
}
+/**
+ * Edit submission handler
+ *
+ * This is the same as EditAction; except that it sets the session cookie.
+ *
+ * @ingroup Actions
+ */
class SubmitAction extends EditAction {
public function getName() {
return 'submit';
}
- public function show(){
+ public function show() {
if ( session_id() == '' ) {
// Send a cookie so anons get talk message notifications
wfSetupSession();
diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php
index dcd6fe55..e58791ea 100644
--- a/includes/actions/HistoryAction.php
+++ b/includes/actions/HistoryAction.php
@@ -20,16 +20,18 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
+ * @ingroup Actions
*/
/**
- * This class handles printing the history page for an article. In order to
+ * This class handles printing the history page for an article. In order to
* be efficient, it uses timestamps rather than offsets for paging, to avoid
* costly LIMIT,offset queries.
*
* Construct it by passing in an Article, and call $h->history() to print the
* history.
*
+ * @ingroup Actions
*/
class HistoryAction extends FormlessAction {
const DIR_PREV = 0;
@@ -113,7 +115,7 @@ class HistoryAction extends FormlessAction {
// Setup page variables.
$out->setFeedAppendQuery( 'action=history' );
- $out->addModules( array( 'mediawiki.legacy.history', 'mediawiki.action.history' ) );
+ $out->addModules( 'mediawiki.action.history' );
// Handle atom/RSS feeds.
$feedType = $request->getVal( 'feed' );
@@ -132,7 +134,7 @@ class HistoryAction extends FormlessAction {
array( 'delete', 'move' ),
$this->getTitle(),
'',
- array( 'lim' => 10,
+ array( 'lim' => 10,
'conds' => array( "log_action != 'revision'" ),
'showIfEmpty' => false,
'msgKey' => array( 'moveddeleted-notice' )
@@ -145,21 +147,25 @@ class HistoryAction extends FormlessAction {
/**
* Add date selector to quickly get to a certain time
*/
- $year = $request->getInt( 'year' );
- $month = $request->getInt( 'month' );
- $tagFilter = $request->getVal( 'tagfilter' );
+ $year = $request->getInt( 'year' );
+ $month = $request->getInt( 'month' );
+ $tagFilter = $request->getVal( 'tagfilter' );
$tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
/**
* Option to show only revisions that have been (partially) hidden via RevisionDelete
*/
if ( $request->getBool( 'deleted' ) ) {
- $conds = array( "rev_deleted != '0'" );
+ $conds = array( 'rev_deleted != 0' );
} else {
$conds = array();
}
- $checkDeleted = Xml::checkLabel( $this->msg( 'history-show-deleted' )->text(),
+ if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
+ $checkDeleted = Xml::checkLabel( $this->msg( 'history-show-deleted' )->text(),
'deleted', 'mw-show-deleted-only', $request->getBool( 'deleted' ) ) . "\n";
+ } else {
+ $checkDeleted = '';
+ }
// Add the general form
$action = htmlspecialchars( $wgScript );
@@ -170,16 +176,16 @@ class HistoryAction extends FormlessAction {
false,
array( 'id' => 'mw-history-search' )
) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedDBKey() ) . "\n" .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" .
Html::hidden( 'action', 'history' ) . "\n" .
- Xml::dateMenu( $year, $month ) . '&#160;' .
+ Xml::dateMenu( ( $year == null ? MWTimestamp::getLocalInstance()->format( 'Y' ) : $year ), $month ) . '&#160;' .
( $tagSelector ? ( implode( '&#160;', $tagSelector ) . '&#160;' ) : '' ) .
$checkDeleted .
Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n" .
'</fieldset></form>'
);
- wfRunHooks( 'PageHistoryBeforeList', array( &$this->page ) );
+ wfRunHooks( 'PageHistoryBeforeList', array( &$this->page, $this->getContext() ) );
// Create and output the list.
$pager = new HistoryPager( $this, $year, $month, $tagFilter, $conds );
@@ -205,7 +211,7 @@ class HistoryAction extends FormlessAction {
*/
function fetchRevisions( $limit, $offset, $direction ) {
// Fail if article doesn't exist.
- if( !$this->getTitle()->exists() ) {
+ if ( !$this->getTitle()->exists() ) {
return new FakeResultWrapper( array() );
}
@@ -218,7 +224,7 @@ class HistoryAction extends FormlessAction {
}
if ( $offset ) {
- $offsets = array( "rev_timestamp $oper '$offset'" );
+ $offsets = array( "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) );
} else {
$offsets = array();
}
@@ -227,7 +233,7 @@ class HistoryAction extends FormlessAction {
return $dbr->select( 'revision',
Revision::selectFields(),
- array_merge( array( "rev_page=$page_id" ), $offsets ),
+ array_merge( array( 'rev_page' => $page_id ), $offsets ),
__METHOD__,
array( 'ORDER BY' => "rev_timestamp $dirs",
'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit )
@@ -237,7 +243,7 @@ class HistoryAction extends FormlessAction {
/**
* Output a subscription feed listing recent edits to this page.
*
- * @param $type String: feed type
+ * @param string $type feed type
*/
function feed( $type ) {
global $wgFeedClasses, $wgFeedLimit;
@@ -250,15 +256,14 @@ class HistoryAction extends FormlessAction {
$this->getTitle()->getPrefixedText() . ' - ' .
$this->msg( 'history-feed-title' )->inContentLanguage()->text(),
$this->msg( 'history-feed-description' )->inContentLanguage()->text(),
- $this->getTitle()->getFullUrl( 'action=history' )
+ $this->getTitle()->getFullURL( 'action=history' )
);
// Get a limit on number of feed entries. Provide a sane default
// of 10 if none is defined (but limit to $wgFeedLimit max)
$limit = $request->getInt( 'limit', 10 );
- if ( $limit > $wgFeedLimit || $limit < 1 ) {
- $limit = 10;
- }
+ $limit = min( max( $limit, 1 ), $wgFeedLimit );
+
$items = $this->fetchRevisions( $limit, 0, HistoryPage::DIR_NEXT );
// Generate feed elements enclosed between header and footer.
@@ -277,10 +282,10 @@ class HistoryAction extends FormlessAction {
return new FeedItem(
$this->msg( 'nohistory' )->inContentLanguage()->text(),
$this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
- $this->getTitle()->getFullUrl(),
+ $this->getTitle()->getFullURL(),
wfTimestamp( TS_MW ),
'',
- $this->getTitle()->getTalkPage()->getFullUrl()
+ $this->getTitle()->getTalkPage()->getFullURL()
);
}
@@ -317,16 +322,17 @@ class HistoryAction extends FormlessAction {
return new FeedItem(
$title,
$text,
- $this->getTitle()->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ),
+ $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
$rev->getTimestamp(),
$rev->getUserText(),
- $this->getTitle()->getTalkPage()->getFullUrl()
+ $this->getTitle()->getTalkPage()->getFullURL()
);
}
}
/**
* @ingroup Pager
+ * @ingroup Actions
*/
class HistoryPager extends ReverseChronologicalPager {
public $lastRow = false, $counter, $historyPage, $buttons, $conds;
@@ -360,15 +366,13 @@ class HistoryPager extends ReverseChronologicalPager {
function getQueryInfo() {
$queryInfo = array(
- 'tables' => array( 'revision', 'user' ),
- 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
- 'conds' => array_merge(
+ 'tables' => array( 'revision', 'user' ),
+ 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
+ 'conds' => array_merge(
array( 'rev_page' => $this->getWikiPage()->getId() ),
$this->conds ),
'options' => array( 'USE INDEX' => array( 'revision' => 'page_timestamp' ) ),
- 'join_conds' => array(
- 'user' => Revision::userJoinCond(),
- 'tag_summary' => array( 'LEFT JOIN', 'ts_rev_id=rev_id' ) ),
+ 'join_conds' => array( 'user' => Revision::userJoinCond() ),
);
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
@@ -406,10 +410,10 @@ class HistoryPager extends ReverseChronologicalPager {
$batch = new LinkBatch();
$revIds = array();
foreach ( $this->mResult as $row ) {
- if( $row->rev_parent_id ) {
+ if ( $row->rev_parent_id ) {
$revIds[] = $row->rev_parent_id;
}
- if( !is_null( $row->user_name ) ) {
+ if ( !is_null( $row->user_name ) ) {
$batch->add( NS_USER, $row->user_name );
$batch->add( NS_USER_TALK, $row->user_name );
} else { # for anons or usernames of imported revisions
@@ -436,7 +440,7 @@ class HistoryPager extends ReverseChronologicalPager {
$this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
$s = Html::openElement( 'form', array( 'action' => $wgScript,
'id' => 'mw-history-compare' ) ) . "\n";
- $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) . "\n";
+ $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
$s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
// Button container stored in $this->buttons for re-use in getEndBody()
@@ -504,14 +508,14 @@ class HistoryPager extends ReverseChronologicalPager {
/**
* Creates a submit button
*
- * @param $message String: text of the submit button, will be escaped
- * @param $attributes Array: attributes
+ * @param string $message text of the submit button, will be escaped
+ * @param array $attributes attributes
* @return String: HTML output for the submit button
*/
function submitButton( $message, $attributes = array() ) {
# Disable submit button if history has 1 revision only
if ( $this->getNumRows() > 1 ) {
- return Xml::submitButton( $message , $attributes );
+ return Xml::submitButton( $message, $attributes );
} else {
return '';
}
@@ -572,11 +576,11 @@ class HistoryPager extends ReverseChronologicalPager {
} elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) {
// If revision was hidden from sysops, disable the link
if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
- $cdel = Linker::revDeleteLinkDisabled( false );
+ $del = Linker::revDeleteLinkDisabled( false );
// Otherwise, show the link...
} else {
$query = array( 'type' => 'revision',
- 'target' => $this->getTitle()->getPrefixedDbkey(), 'ids' => $rev->getId() );
+ 'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId() );
$del .= Linker::revDeleteLink( $query,
$rev->isDeleted( Revision::DELETED_RESTRICTED ), false );
}
@@ -598,19 +602,22 @@ class HistoryPager extends ReverseChronologicalPager {
$s .= ' ' . ChangesList::flag( 'minor' );
}
- # Size is always public data
- $prevSize = isset( $this->parentLens[$row->rev_parent_id] )
- ? $this->parentLens[$row->rev_parent_id]
- : 0;
- $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() );
- $fSize = Linker::formatRevisionSize($rev->getSize());
- $s .= ' <span class="mw-changeslist-separator">. .</span> ' . "$fSize $sDiff";
+ # Sometimes rev_len isn't populated
+ if ( $rev->getSize() !== null ) {
+ # Size is always public data
+ $prevSize = isset( $this->parentLens[$row->rev_parent_id] )
+ ? $this->parentLens[$row->rev_parent_id]
+ : 0;
+ $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() );
+ $fSize = Linker::formatRevisionSize( $rev->getSize() );
+ $s .= ' <span class="mw-changeslist-separator">. .</span> ' . "$fSize $sDiff";
+ }
# Text following the character difference is added just before running hooks
$s2 = Linker::revComment( $rev, false, true );
if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
- $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
+ $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
$classes[] = 'mw-history-line-updated';
}
@@ -619,9 +626,12 @@ class HistoryPager extends ReverseChronologicalPager {
# Rollback and undo links
if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) {
if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) {
- $this->preventClickjacking();
- $tools[] = '<span class="mw-rollback-link">' .
- Linker::buildRollbackLink( $rev, $this->getContext() ) . '</span>';
+ // Get a rollback link without the brackets
+ $rollbackLink = Linker::generateRollback( $rev, $this->getContext(), array( 'verify', 'noBrackets' ) );
+ if ( $rollbackLink ) {
+ $this->preventClickjacking();
+ $tools[] = $rollbackLink;
+ }
}
if ( !$rev->isDeleted( Revision::DELETED_TEXT )
@@ -636,17 +646,19 @@ class HistoryPager extends ReverseChronologicalPager {
$this->msg( 'editundo' )->escaped(),
$undoTooltip,
array(
- 'action' => 'edit',
+ 'action' => 'edit',
'undoafter' => $prevRev->getId(),
- 'undo' => $rev->getId()
+ 'undo' => $rev->getId()
)
);
$tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
}
}
+ // Allow extension to add their own links here
+ wfRunHooks( 'HistoryRevisionTools', array( $rev, &$tools ) );
if ( $tools ) {
- $s2 .= ' '. $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped();
+ $s2 .= ' ' . $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped();
}
# Tags
@@ -661,7 +673,7 @@ class HistoryPager extends ReverseChronologicalPager {
$s .= ' <span class="mw-changeslist-separator">. .</span> ' . $s2;
}
- wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s, &$classes ) );
+ wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row, &$s, &$classes ) );
$attribs = array();
if ( $classes ) {
@@ -773,13 +785,13 @@ class HistoryPager extends ReverseChronologicalPager {
function diffButtons( $rev, $firstInList ) {
if ( $this->getNumRows() > 1 ) {
$id = $rev->getId();
- $radio = array( 'type' => 'radio', 'value' => $id );
- /** @todo: move title texts to javascript */
+ $radio = array( 'type' => 'radio', 'value' => $id );
+ /** @todo Move title texts to javascript */
if ( $firstInList ) {
$first = Xml::element( 'input',
array_merge( $radio, array(
'style' => 'visibility:hidden',
- 'name' => 'oldid',
+ 'name' => 'oldid',
'id' => 'mw-oldid-null' ) )
);
$checkmark = array( 'checked' => 'checked' );
@@ -796,13 +808,13 @@ class HistoryPager extends ReverseChronologicalPager {
}
$first = Xml::element( 'input',
array_merge( $radio, $checkmark, array(
- 'name' => 'oldid',
+ 'name' => 'oldid',
'id' => "mw-oldid-$id" ) ) );
$checkmark = array();
}
$second = Xml::element( 'input',
array_merge( $radio, $checkmark, array(
- 'name' => 'diff',
+ 'name' => 'diff',
'id' => "mw-diff-$id" ) ) );
return $first . $second;
} else {
diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php
index ae550391..7fc90339 100644
--- a/includes/actions/InfoAction.php
+++ b/includes/actions/InfoAction.php
@@ -22,7 +22,14 @@
* @ingroup Actions
*/
+/**
+ * Displays information about a page.
+ *
+ * @ingroup Actions
+ */
class InfoAction extends FormlessAction {
+ const CACHE_VERSION = '2013-03-17';
+
/**
* Returns the name of the action this object responds to.
*
@@ -51,6 +58,22 @@ class InfoAction extends FormlessAction {
}
/**
+ * Clear the info cache for a given Title.
+ *
+ * @since 1.22
+ * @param Title $title Title to clear cache for
+ */
+ public static function invalidateCache( Title $title ) {
+ global $wgMemc;
+ // Clear page info.
+ $revision = WikiPage::factory( $title )->getRevision();
+ if ( $revision !== null ) {
+ $key = wfMemcKey( 'infoaction', sha1( $title->getPrefixedText() ), $revision->getId() );
+ $wgMemc->delete( $key );
+ }
+ }
+
+ /**
* Shows page information on GET request.
*
* @return string Page information that will be added to the output
@@ -81,11 +104,11 @@ class InfoAction extends FormlessAction {
// Hide "This page is a member of # hidden categories" explanation
$content .= Html::element( 'style', array(),
- '.mw-hiddenCategoriesExplanation { display: none; }' );
+ '.mw-hiddenCategoriesExplanation { display: none; }' ) . "\n";
// Hide "Templates used on this page" explanation
$content .= Html::element( 'style', array(),
- '.mw-templatesUsedExplanation { display: none; }' );
+ '.mw-templatesUsedExplanation { display: none; }' ) . "\n";
// Get page information
$pageInfo = $this->pageInfo();
@@ -95,14 +118,18 @@ class InfoAction extends FormlessAction {
// Render page information
foreach ( $pageInfo as $header => $infoTable ) {
- $content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() );
- $table = '';
+ // Messages:
+ // pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
+ // pageinfo-header-properties, pageinfo-category-info
+ $content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() ) . "\n";
+ $table = "\n";
foreach ( $infoTable as $infoRow ) {
$name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
$value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
- $table = $this->addRow( $table, $name, $value );
+ $id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
+ $table = $this->addRow( $table, $name, $value, $id ) . "\n";
}
- $content = $this->addTable( $content, $table );
+ $content = $this->addTable( $content, $table ) . "\n";
}
// Page footer
@@ -121,25 +148,25 @@ class InfoAction extends FormlessAction {
/**
* Creates a header that can be added to the output.
*
- * @param $header The header text.
+ * @param string $header The header text.
* @return string The HTML.
*/
protected function makeHeader( $header ) {
- global $wgParser;
- $spanAttribs = array( 'class' => 'mw-headline', 'id' => $wgParser->guessSectionNameFromWikiText( $header ) );
+ $spanAttribs = array( 'class' => 'mw-headline', 'id' => Sanitizer::escapeId( $header ) );
return Html::rawElement( 'h2', array(), Html::element( 'span', $spanAttribs, $header ) );
}
/**
* Adds a row to a table that will be added to the content.
*
- * @param $table string The table that will be added to the content
- * @param $name string The name of the row
- * @param $value string The value of the row
+ * @param string $table The table that will be added to the content
+ * @param string $name The name of the row
+ * @param string $value The value of the row
+ * @param string $id The ID to use for the 'tr' element
* @return string The table with the row added
*/
- protected function addRow( $table, $name, $value ) {
- return $table . Html::rawElement( 'tr', array(),
+ protected function addRow( $table, $name, $value, $id ) {
+ return $table . Html::rawElement( 'tr', $id === null ? array() : array( 'id' => 'mw-' . $id ),
Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) .
Html::rawElement( 'td', array(), $value )
);
@@ -148,8 +175,8 @@ class InfoAction extends FormlessAction {
/**
* Adds a table to the content that will be added to the output.
*
- * @param $content string The content that will be added to the output
- * @param $table string The table
+ * @param string $content The content that will be added to the output
+ * @param string $table The table
* @return string The content with the table added
*/
protected function addTable( $content, $table ) {
@@ -161,17 +188,29 @@ class InfoAction extends FormlessAction {
* Returns page information in an easily-manipulated format. Array keys are used so extensions
* may add additional information in arbitrary positions. Array values are arrays with one
* element to be rendered as a header, arrays with two elements to be rendered as a table row.
+ *
+ * @return array
*/
protected function pageInfo() {
- global $wgContLang, $wgRCMaxAge;
+ global $wgContLang, $wgRCMaxAge, $wgMemc,
+ $wgUnwatchedPageThreshold, $wgPageInfoTransclusionLimit;
$user = $this->getUser();
$lang = $this->getLanguage();
$title = $this->getTitle();
$id = $title->getArticleID();
- // Get page information that would be too "expensive" to retrieve by normal means
- $pageCounts = self::pageCounts( $title, $user );
+ $memcKey = wfMemcKey( 'infoaction',
+ sha1( $title->getPrefixedText() ), $this->page->getLatest() );
+ $pageCounts = $wgMemc->get( $memcKey );
+ $version = isset( $pageCounts['cacheversion'] ) ? $pageCounts['cacheversion'] : false;
+ if ( $pageCounts === false || $version !== self::CACHE_VERSION ) {
+ // Get page information that would be too "expensive" to retrieve by normal means
+ $pageCounts = self::pageCounts( $title );
+ $pageCounts['cacheversion'] = self::CACHE_VERSION;
+
+ $wgMemc->set( $memcKey, $pageCounts );
+ }
// Get page properties
$dbr = wfGetDB( DB_SLAVE );
@@ -201,8 +240,23 @@ class InfoAction extends FormlessAction {
$this->msg( 'pageinfo-display-title' ), $displayTitle
);
+ // Is it a redirect? If so, where to?
+ if ( $title->isRedirect() ) {
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-redirectsto' ),
+ Linker::link( $this->page->getRedirectTarget() ) .
+ $this->msg( 'word-separator' )->text() .
+ $this->msg( 'parentheses', Linker::link(
+ $this->page->getRedirectTarget(),
+ $this->msg( 'pageinfo-redirectsto-info' )->escaped(),
+ array(),
+ array( 'action' => 'info' )
+ ) )->text()
+ );
+ }
+
// Default sort key
- $sortKey = $title->getCategorySortKey();
+ $sortKey = $title->getCategorySortkey();
if ( !empty( $pageProperties['defaultsort'] ) ) {
$sortKey = $pageProperties['defaultsort'];
}
@@ -217,6 +271,12 @@ class InfoAction extends FormlessAction {
// Page ID (number not localised, as it's a database ID)
$pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-article-id' ), $id );
+ // Language in which the page content is (supposed to be) written
+ $pageLang = $title->getPageLanguage()->getCode();
+ $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-language' ),
+ Language::fetchLanguageName( $pageLang, $lang->getCode() )
+ . ' ' . $this->msg( 'parentheses', $pageLang ) );
+
// Search engine status
$pOutput = new ParserOutput();
if ( isset( $pageProperties['noindex'] ) ) {
@@ -226,6 +286,7 @@ class InfoAction extends FormlessAction {
// Use robot policy logic
$policy = $this->page->getRobotPolicy( 'view', $pOutput );
$pageInfo['header-basic'][] = array(
+ // Messages: pageinfo-robot-index, pageinfo-robot-noindex
$this->msg( 'pageinfo-robot-policy' ), $this->msg( "pageinfo-robot-${policy['index']}" )
);
@@ -236,11 +297,20 @@ class InfoAction extends FormlessAction {
);
}
- if ( isset( $pageCounts['watchers'] ) ) {
+ if (
+ $user->isAllowed( 'unwatchedpages' ) ||
+ ( $wgUnwatchedPageThreshold !== false &&
+ $pageCounts['watchers'] >= $wgUnwatchedPageThreshold )
+ ) {
// Number of page watchers
$pageInfo['header-basic'][] = array(
$this->msg( 'pageinfo-watchers' ), $lang->formatNum( $pageCounts['watchers'] )
);
+ } elseif ( $wgUnwatchedPageThreshold !== false ) {
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-watchers' ),
+ $this->msg( 'pageinfo-few-watchers' )->numParams( $wgUnwatchedPageThreshold )
+ );
}
// Redirects to this page
@@ -256,6 +326,14 @@ class InfoAction extends FormlessAction {
->numParams( count( $title->getRedirectsHere() ) )
);
+ // Is it counted as a content page?
+ if ( $this->page->isCountable() ) {
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-contentpage' ),
+ $this->msg( 'pageinfo-contentpage-yes' )
+ );
+ }
+
// Subpages of this page, if subpages are enabled for the current NS
if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
$prefixIndex = SpecialPage::getTitleFor( 'Prefixindex', $title->getPrefixedText() . '/' );
@@ -269,9 +347,51 @@ class InfoAction extends FormlessAction {
);
}
+ if ( $title->inNamespace( NS_CATEGORY ) ) {
+ $category = Category::newFromTitle( $title );
+ $pageInfo['category-info'] = array(
+ array(
+ $this->msg( 'pageinfo-category-pages' ),
+ $lang->formatNum( $category->getPageCount() )
+ ),
+ array(
+ $this->msg( 'pageinfo-category-subcats' ),
+ $lang->formatNum( $category->getSubcatCount() )
+ ),
+ array(
+ $this->msg( 'pageinfo-category-files' ),
+ $lang->formatNum( $category->getFileCount() )
+ )
+ );
+ }
+
// Page protection
$pageInfo['header-restrictions'] = array();
+ // Is this page effected by the cascading protection of something which includes it?
+ if ( $title->isCascadeProtected() ) {
+ $cascadingFrom = '';
+ $sources = $title->getCascadeProtectionSources(); // Array deferencing is in PHP 5.4 :(
+
+ foreach ( $sources[0] as $sourceTitle ) {
+ $cascadingFrom .= Html::rawElement( 'li', array(), Linker::linkKnown( $sourceTitle ) );
+ }
+
+ $cascadingFrom = Html::rawElement( 'ul', array(), $cascadingFrom );
+ $pageInfo['header-restrictions'][] = array(
+ $this->msg( 'pageinfo-protect-cascading-from' ),
+ $cascadingFrom
+ );
+ }
+
+ // Is out protection set to cascade to other pages?
+ if ( $title->areRestrictionsCascading() ) {
+ $pageInfo['header-restrictions'][] = array(
+ $this->msg( 'pageinfo-protect-cascading' ),
+ $this->msg( 'pageinfo-protect-cascading-yes' )
+ );
+ }
+
// Page protection
foreach ( $title->getRestrictionTypes() as $restrictionType ) {
$protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
@@ -281,6 +401,7 @@ class InfoAction extends FormlessAction {
$message = $this->msg( 'protect-default' )->escaped();
} else {
// Administrators only
+ // Messages: protect-level-autoconfirmed, protect-level-sysop
$message = $this->msg( "protect-level-$protectionLevel" );
if ( $message->isDisabled() ) {
// Require "$1" permission
@@ -290,6 +411,8 @@ class InfoAction extends FormlessAction {
}
}
+ // Messages: restriction-edit, restriction-move, restriction-create,
+ // restriction-upload
$pageInfo['header-restrictions'][] = array(
$this->msg( "restriction-$restrictionType" ), $message
);
@@ -303,40 +426,64 @@ class InfoAction extends FormlessAction {
$pageInfo['header-edits'] = array();
$firstRev = $this->page->getOldestRevision();
+ $lastRev = $this->page->getRevision();
+ $batch = new LinkBatch;
+
+ if ( $firstRev ) {
+ $firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER );
+ if ( $firstRevUser !== '' ) {
+ $batch->add( NS_USER, $firstRevUser );
+ $batch->add( NS_USER_TALK, $firstRevUser );
+ }
+ }
- // Page creator
- $pageInfo['header-edits'][] = array(
- $this->msg( 'pageinfo-firstuser' ),
- Linker::revUserTools( $firstRev )
- );
+ if ( $lastRev ) {
+ $lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER );
+ if ( $lastRevUser !== '' ) {
+ $batch->add( NS_USER, $lastRevUser );
+ $batch->add( NS_USER_TALK, $lastRevUser );
+ }
+ }
- // Date of page creation
- $pageInfo['header-edits'][] = array(
- $this->msg( 'pageinfo-firsttime' ),
- Linker::linkKnown(
- $title,
- $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
- array(),
- array( 'oldid' => $firstRev->getId() )
- )
- );
+ $batch->execute();
- // Latest editor
- $pageInfo['header-edits'][] = array(
- $this->msg( 'pageinfo-lastuser' ),
- Linker::revUserTools( $this->page->getRevision() )
- );
+ if ( $firstRev ) {
+ // Page creator
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-firstuser' ),
+ Linker::revUserTools( $firstRev )
+ );
- // Date of latest edit
- $pageInfo['header-edits'][] = array(
- $this->msg( 'pageinfo-lasttime' ),
- Linker::linkKnown(
- $title,
- $lang->userTimeAndDate( $this->page->getTimestamp(), $user ),
- array(),
- array( 'oldid' => $this->page->getLatest() )
- )
- );
+ // Date of page creation
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-firsttime' ),
+ Linker::linkKnown(
+ $title,
+ $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
+ array(),
+ array( 'oldid' => $firstRev->getId() )
+ )
+ );
+ }
+
+ if ( $lastRev ) {
+ // Latest editor
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-lastuser' ),
+ Linker::revUserTools( $lastRev )
+ );
+
+ // Date of latest edit
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-lasttime' ),
+ Linker::linkKnown(
+ $title,
+ $lang->userTimeAndDate( $this->page->getTimestamp(), $user ),
+ array(),
+ array( 'oldid' => $this->page->getLatest() )
+ )
+ );
+ }
// Total number of edits
$pageInfo['header-edits'][] = array(
@@ -377,11 +524,17 @@ class InfoAction extends FormlessAction {
$localizedList = Html::rawElement( 'ul', array(), implode( '', $listItems ) );
$hiddenCategories = $this->page->getHiddenCategories();
- $transcludedTemplates = $title->getTemplateLinksFrom();
- if ( count( $listItems ) > 0
- || count( $hiddenCategories ) > 0
- || count( $transcludedTemplates ) > 0 ) {
+ if (
+ count( $listItems ) > 0 ||
+ count( $hiddenCategories ) > 0 ||
+ $pageCounts['transclusion']['from'] > 0 ||
+ $pageCounts['transclusion']['to'] > 0
+ ) {
+ $options = array( 'LIMIT' => $wgPageInfoTransclusionLimit );
+ $transcludedTemplates = $title->getTemplateLinksFrom( $options );
+ $transcludedTargets = $title->getTemplateLinksTo( $options );
+
// Page properties
$pageInfo['header-properties'] = array();
@@ -403,11 +556,44 @@ class InfoAction extends FormlessAction {
}
// Transcluded templates
- if ( count( $transcludedTemplates ) > 0 ) {
+ if ( $pageCounts['transclusion']['from'] > 0 ) {
+ if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
+ $more = $this->msg( 'morenotlisted' )->escaped();
+ } else {
+ $more = null;
+ }
+
$pageInfo['header-properties'][] = array(
$this->msg( 'pageinfo-templates' )
- ->numParams( count( $transcludedTemplates ) ),
- Linker::formatTemplates( $transcludedTemplates )
+ ->numParams( $pageCounts['transclusion']['from'] ),
+ Linker::formatTemplates(
+ $transcludedTemplates,
+ false,
+ false,
+ $more )
+ );
+ }
+
+ if ( $pageCounts['transclusion']['to'] > 0 ) {
+ if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
+ $more = Linker::link(
+ $whatLinksHere,
+ $this->msg( 'moredotdotdot' )->escaped(),
+ array(),
+ array( 'hidelinks' => 1, 'hideredirs' => 1 )
+ );
+ } else {
+ $more = null;
+ }
+
+ $pageInfo['header-properties'][] = array(
+ $this->msg( 'pageinfo-transclusions' )
+ ->numParams( $pageCounts['transclusion']['to'] ),
+ Linker::formatTemplates(
+ $transcludedTargets,
+ false,
+ false,
+ $more )
);
}
}
@@ -418,11 +604,10 @@ class InfoAction extends FormlessAction {
/**
* Returns page counts that would be too "expensive" to retrieve by normal means.
*
- * @param $title Title object
- * @param $user User object
+ * @param Title $title Title to get counts for
* @return array
*/
- protected static function pageCounts( $title, $user ) {
+ protected static function pageCounts( Title $title ) {
global $wgRCMaxAge, $wgDisableCounters;
wfProfileIn( __METHOD__ );
@@ -433,7 +618,7 @@ class InfoAction extends FormlessAction {
if ( !$wgDisableCounters ) {
// Number of views
- $views = (int) $dbr->selectField(
+ $views = (int)$dbr->selectField(
'page',
'page_counter',
array( 'page_id' => $id ),
@@ -442,22 +627,20 @@ class InfoAction extends FormlessAction {
$result['views'] = $views;
}
- if ( $user->isAllowed( 'unwatchedpages' ) ) {
- // Number of page watchers
- $watchers = (int) $dbr->selectField(
- 'watchlist',
- 'COUNT(*)',
- array(
- 'wl_namespace' => $title->getNamespace(),
- 'wl_title' => $title->getDBkey(),
- ),
- __METHOD__
- );
- $result['watchers'] = $watchers;
- }
+ // Number of page watchers
+ $watchers = (int)$dbr->selectField(
+ 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $title->getNamespace(),
+ 'wl_title' => $title->getDBkey(),
+ ),
+ __METHOD__
+ );
+ $result['watchers'] = $watchers;
// Total number of edits
- $edits = (int) $dbr->selectField(
+ $edits = (int)$dbr->selectField(
'revision',
'COUNT(rev_page)',
array( 'rev_page' => $id ),
@@ -466,7 +649,7 @@ class InfoAction extends FormlessAction {
$result['edits'] = $edits;
// Total number of distinct authors
- $authors = (int) $dbr->selectField(
+ $authors = (int)$dbr->selectField(
'revision',
'COUNT(DISTINCT rev_user_text)',
array( 'rev_page' => $id ),
@@ -478,24 +661,24 @@ class InfoAction extends FormlessAction {
$threshold = $dbr->timestamp( time() - $wgRCMaxAge );
// Recent number of edits
- $edits = (int) $dbr->selectField(
+ $edits = (int)$dbr->selectField(
'revision',
'COUNT(rev_page)',
array(
- 'rev_page' => $id ,
- "rev_timestamp >= $threshold"
+ 'rev_page' => $id,
+ "rev_timestamp >= " . $dbr->addQuotes( $threshold )
),
__METHOD__
);
$result['recent_edits'] = $edits;
// Recent number of distinct authors
- $authors = (int) $dbr->selectField(
+ $authors = (int)$dbr->selectField(
'revision',
'COUNT(DISTINCT rev_user_text)',
array(
'rev_page' => $id,
- "rev_timestamp >= $threshold"
+ "rev_timestamp >= " . $dbr->addQuotes( $threshold )
),
__METHOD__
);
@@ -508,7 +691,7 @@ class InfoAction extends FormlessAction {
// Subpages of this page (redirects)
$conds['page_is_redirect'] = 1;
- $result['subpages']['redirects'] = (int) $dbr->selectField(
+ $result['subpages']['redirects'] = (int)$dbr->selectField(
'page',
'COUNT(page_id)',
$conds,
@@ -516,7 +699,7 @@ class InfoAction extends FormlessAction {
// Subpages of this page (non-redirects)
$conds['page_is_redirect'] = 0;
- $result['subpages']['nonredirects'] = (int) $dbr->selectField(
+ $result['subpages']['nonredirects'] = (int)$dbr->selectField(
'page',
'COUNT(page_id)',
$conds,
@@ -528,12 +711,30 @@ class InfoAction extends FormlessAction {
+ $result['subpages']['nonredirects'];
}
+ // Counts for the number of transclusion links (to/from)
+ $result['transclusion']['to'] = (int)$dbr->selectField(
+ 'templatelinks',
+ 'COUNT(tl_from)',
+ array(
+ 'tl_namespace' => $title->getNamespace(),
+ 'tl_title' => $title->getDBkey()
+ ),
+ __METHOD__
+ );
+
+ $result['transclusion']['from'] = (int)$dbr->selectField(
+ 'templatelinks',
+ 'COUNT(*)',
+ array( 'tl_from' => $title->getArticleID() ),
+ __METHOD__
+ );
+
wfProfileOut( __METHOD__ );
return $result;
}
/**
- * Returns the name that goes in the <h1> page title.
+ * Returns the name that goes in the "<h1>" page title.
*
* @return string
*/
@@ -604,7 +805,7 @@ class InfoAction extends FormlessAction {
}
/**
- * Returns the description that goes below the <h1> tag.
+ * Returns the description that goes below the "<h1>" tag.
*
* @return string
*/
diff --git a/includes/actions/MarkpatrolledAction.php b/includes/actions/MarkpatrolledAction.php
index ae9223f4..ff6cf13a 100644
--- a/includes/actions/MarkpatrolledAction.php
+++ b/includes/actions/MarkpatrolledAction.php
@@ -22,6 +22,11 @@
* @ingroup Actions
*/
+/**
+ * Mark a revision as patrolled on a page
+ *
+ * @ingroup Actions
+ */
class MarkpatrolledAction extends FormlessAction {
public function getName() {
diff --git a/includes/actions/ProtectAction.php b/includes/actions/ProtectAction.php
index f053ede7..ec6648e2 100644
--- a/includes/actions/ProtectAction.php
+++ b/includes/actions/ProtectAction.php
@@ -23,17 +23,24 @@
* @author Timo Tijhof
*/
+/**
+ * Handle page protection
+ *
+ * This is a wrapper that will call Article::protect().
+ *
+ * @ingroup Actions
+ */
class ProtectAction extends FormlessAction {
public function getName() {
return 'protect';
}
- public function onView(){
+ public function onView() {
return null;
}
- public function show(){
+ public function show() {
$this->page->protect();
@@ -41,13 +48,20 @@ class ProtectAction extends FormlessAction {
}
+/**
+ * Handle page unprotection
+ *
+ * This is a wrapper that will call Article::unprotect().
+ *
+ * @ingroup Actions
+ */
class UnprotectAction extends ProtectAction {
public function getName() {
return 'unprotect';
}
- public function show(){
+ public function show() {
$this->page->unprotect();
diff --git a/includes/actions/PurgeAction.php b/includes/actions/PurgeAction.php
index cd58889d..ed0bff7b 100644
--- a/includes/actions/PurgeAction.php
+++ b/includes/actions/PurgeAction.php
@@ -1,8 +1,6 @@
<?php
/**
- * Formats credits for articles
- *
- * Copyright 2004, Evan Prodromou <evan@wikitravel.org>.
+ * User-requested page cache purging.
*
* 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
@@ -20,9 +18,16 @@
*
* @file
* @ingroup Actions
- * @author <evan@wikitravel.org>
*/
+/**
+ * User-requested page cache purging.
+ *
+ * For users with 'purge', this will directly trigger the cache purging and
+ * for users without that right, it will show a confirmation form.
+ *
+ * @ingroup Actions
+ */
class PurgeAction extends FormAction {
private $redirectParams;
@@ -62,11 +67,11 @@ class PurgeAction extends FormAction {
$this->checkCanExecute( $this->getUser() );
if ( $this->getUser()->isAllowed( 'purge' ) ) {
- $this->redirectParams = wfArrayToCGI( array_diff_key(
+ $this->redirectParams = wfArrayToCgi( array_diff_key(
$this->getRequest()->getQueryValues(),
array( 'title' => null, 'action' => null )
) );
- if( $this->onSubmit( array() ) ) {
+ if ( $this->onSubmit( array() ) ) {
$this->onSuccess();
}
} else {
@@ -91,6 +96,6 @@ class PurgeAction extends FormAction {
}
public function onSuccess() {
- $this->getOutput()->redirect( $this->getTitle()->getFullUrl( $this->redirectParams ) );
+ $this->getOutput()->redirect( $this->getTitle()->getFullURL( $this->redirectParams ) );
}
}
diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php
index 174ca3f8..1a2b3cb1 100644
--- a/includes/actions/RawAction.php
+++ b/includes/actions/RawAction.php
@@ -29,6 +29,8 @@
/**
* A simple method to retrieve the plain source of an article,
* using "action=raw" in the GET request string.
+ *
+ * @ingroup Actions
*/
class RawAction extends FormlessAction {
private $mGen;
@@ -46,7 +48,7 @@ class RawAction extends FormlessAction {
}
function onView() {
- global $wgGroupPermissions, $wgSquidMaxage, $wgForcedRawSMaxage, $wgJsMimeType;
+ global $wgSquidMaxage, $wgForcedRawSMaxage;
$this->getOutput()->disable();
$request = $this->getRequest();
@@ -77,7 +79,7 @@ class RawAction extends FormlessAction {
# Force caching for CSS and JS raw content, default: 5 minutes
if ( $smaxage === null ) {
- if ( $contentType == 'text/css' || $contentType == $wgJsMimeType ) {
+ if ( $contentType == 'text/css' || $contentType == 'text/javascript' ) {
$smaxage = intval( $wgForcedRawSMaxage );
} else {
$smaxage = 0;
@@ -91,7 +93,10 @@ class RawAction extends FormlessAction {
$response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
# Output may contain user-specific data;
# vary generated content for open sessions on private wikis
- $privateCache = !$wgGroupPermissions['*']['read'] && ( $smaxage == 0 || session_id() != '' );
+ $privateCache = !User::isEveryoneAllowed( 'read' ) && ( $smaxage == 0 || session_id() != '' );
+ // Bug 53032 - make this private if user is logged in,
+ // so we don't accidentally cache cookies
+ $privateCache = $privateCache ?: $this->getUser()->isLoggedIn();
# allow the client to cache this for 24 hours
$mode = $privateCache ? 'private' : 'public';
$response->header( 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage );
@@ -123,7 +128,7 @@ class RawAction extends FormlessAction {
global $wgParser;
# No longer used
- if( $this->mGen ) {
+ if ( $this->mGen ) {
return '';
}
@@ -148,10 +153,29 @@ class RawAction extends FormlessAction {
$request->response()->header( "Last-modified: $lastmod" );
// Public-only due to cache headers
- $text = $rev->getText();
- $section = $request->getIntOrNull( 'section' );
- if ( $section !== null ) {
- $text = $wgParser->getSection( $text, $section );
+ $content = $rev->getContent();
+
+ if ( $content === null ) {
+ // revision not found (or suppressed)
+ $text = false;
+ } elseif ( !$content instanceof TextContent ) {
+ // non-text content
+ wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `"
+ . $content->getModel() . "` which is not supported via this interface." );
+ die();
+ } else {
+ // want a section?
+ $section = $request->getIntOrNull( 'section' );
+ if ( $section !== null ) {
+ $content = $content->getSection( $section );
+ }
+
+ if ( $content === null || $content === false ) {
+ // section not found (or section not supported, e.g. for JS and CSS)
+ $text = false;
+ } else {
+ $text = $content->getNativeData();
+ }
}
}
}
@@ -173,19 +197,19 @@ class RawAction extends FormlessAction {
switch ( $this->getRequest()->getText( 'direction' ) ) {
case 'next':
# output next revision, or nothing if there isn't one
- if( $oldid ) {
- $oldid = $this->getTitle()->getNextRevisionId( $oldid );
+ if ( $oldid ) {
+ $oldid = $this->getTitle()->getNextRevisionID( $oldid );
}
$oldid = $oldid ? $oldid : -1;
break;
case 'prev':
# output previous revision, or nothing if there isn't one
- if( !$oldid ) {
+ if ( !$oldid ) {
# get the current revision so we can get the penultimate one
$oldid = $this->page->getLatest();
}
- $prev = $this->getTitle()->getPreviousRevisionId( $oldid );
- $oldid = $prev ? $prev : -1 ;
+ $prev = $this->getTitle()->getPreviousRevisionID( $oldid );
+ $oldid = $prev ? $prev : -1;
break;
case 'cur':
$oldid = 0;
@@ -200,20 +224,18 @@ class RawAction extends FormlessAction {
* @return String
*/
public function getContentType() {
- global $wgJsMimeType;
-
$ctype = $this->getRequest()->getVal( 'ctype' );
if ( $ctype == '' ) {
$gen = $this->getRequest()->getVal( 'gen' );
if ( $gen == 'js' ) {
- $ctype = $wgJsMimeType;
+ $ctype = 'text/javascript';
} elseif ( $gen == 'css' ) {
$ctype = 'text/css';
}
}
- $allowedCTypes = array( 'text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit' );
+ $allowedCTypes = array( 'text/x-wiki', 'text/javascript', 'text/css', 'application/x-zope-edit' );
if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
$ctype = 'text/x-wiki';
}
@@ -230,6 +252,10 @@ class RawAction extends FormlessAction {
class RawPage extends RawAction {
public $mOldId;
+ /**
+ * @param Page $page
+ * @param WebRequest|bool $request The WebRequest (default: false).
+ */
function __construct( Page $page, $request = false ) {
wfDeprecated( __CLASS__, '1.19' );
parent::__construct( $page );
diff --git a/includes/actions/RenderAction.php b/includes/actions/RenderAction.php
index 80af79cc..3d244fb3 100644
--- a/includes/actions/RenderAction.php
+++ b/includes/actions/RenderAction.php
@@ -23,17 +23,24 @@
* @author Timo Tijhof
*/
+/**
+ * Handle action=render
+ *
+ * This is a wrapper that will call Article::render().
+ *
+ * @ingroup Actions
+ */
class RenderAction extends FormlessAction {
public function getName() {
return 'render';
}
- public function onView(){
+ public function onView() {
return null;
}
- public function show(){
+ public function show() {
$this->page->render();
diff --git a/includes/actions/RevertAction.php b/includes/actions/RevertAction.php
index 77434384..a5fc4e17 100644
--- a/includes/actions/RevertAction.php
+++ b/includes/actions/RevertAction.php
@@ -115,7 +115,7 @@ class RevertFileAction extends FormAction {
$source = $this->page->getFile()->getArchiveVirtualUrl( $this->getRequest()->getText( 'oldimage' ) );
$comment = $data['comment'];
// TODO: Preserve file properties from database instead of reloading from file
- return $this->page->getFile()->upload( $source, $comment, $comment );
+ return $this->page->getFile()->upload( $source, $comment, $comment, 0, false, false, $this->getUser() );
}
public function onSuccess() {
@@ -124,7 +124,7 @@ class RevertFileAction extends FormAction {
$lang = $this->getLanguage();
$userDate = $lang->userDate( $timestamp, $user );
$userTime = $lang->userTime( $timestamp, $user );
-
+
$this->getOutput()->addWikiMsg( 'filerevert-success', $this->getTitle()->getText(),
$userDate, $userTime,
wfExpandUrl( $this->page->getFile()->getArchiveUrl( $this->getRequest()->getText( 'oldimage' ) ),
@@ -136,7 +136,7 @@ class RevertFileAction extends FormAction {
protected function getPageTitle() {
return $this->msg( 'filerevert', $this->getTitle()->getText() );
}
-
+
protected function getDescription() {
$this->getOutput()->addBacklinkSubtitle( $this->getTitle() );
return '';
diff --git a/includes/actions/RevisiondeleteAction.php b/includes/actions/RevisiondeleteAction.php
index 14da2fcf..2949fa95 100644
--- a/includes/actions/RevisiondeleteAction.php
+++ b/includes/actions/RevisiondeleteAction.php
@@ -23,6 +23,11 @@
* @author Alexandre Emsenhuber
*/
+/**
+ * An action that just pass the request to Special:RevisionDelete
+ *
+ * @ingroup Actions
+ */
class RevisiondeleteAction extends FormlessAction {
public function getName() {
diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php
index 0d9a9027..81bad9da 100644
--- a/includes/actions/RollbackAction.php
+++ b/includes/actions/RollbackAction.php
@@ -71,45 +71,32 @@ class RollbackAction extends FormlessAction {
return;
}
- # Display permissions errors before read-only message -- there's no
- # point in misleading the user into thinking the inability to rollback
- # is only temporary.
- if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
- # array_diff is completely broken for arrays of arrays, sigh.
- # Remove any 'readonlytext' error manually.
- $out = array();
- foreach ( $result as $error ) {
- if ( $error != array( 'readonlytext' ) ) {
- $out [] = $error;
- }
- }
- throw new PermissionsError( 'rollback', $out );
- }
+ #NOTE: Permission errors already handled by Action::checkExecute.
if ( $result == array( array( 'readonlytext' ) ) ) {
throw new ReadOnlyError;
}
+ #XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object.
+ # Right now, we only show the first error
+ foreach ( $result as $error ) {
+ throw new ErrorPageError( 'rollbackfailed', $error[0], array_slice( $error, 1 ) );
+ }
+
$current = $details['current'];
$target = $details['target'];
$newId = $details['newid'];
$this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
$this->getOutput()->setRobotPolicy( 'noindex,nofollow' );
- if ( $current->getUserText() === '' ) {
- $old = $this->msg( 'rev-deleted-user' )->escaped();
- } else {
- $old = Linker::userLink( $current->getUser(), $current->getUserText() )
- . Linker::userToolLinks( $current->getUser(), $current->getUserText() );
- }
-
- $new = Linker::userLink( $target->getUser(), $target->getUserText() )
- . Linker::userToolLinks( $target->getUser(), $target->getUserText() );
+ $old = Linker::revUserTools( $current );
+ $new = Linker::revUserTools( $target );
$this->getOutput()->addHTML( $this->msg( 'rollback-success' )->rawParams( $old, $new )->parseAsBlock() );
$this->getOutput()->returnToMain( false, $this->getTitle() );
if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) {
- $de = new DifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
+ $contentHandler = $current->getContentHandler();
+ $de = $contentHandler->createDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
$de->showDiff( '', '' );
}
}
diff --git a/includes/actions/ViewAction.php b/includes/actions/ViewAction.php
index d57585ee..e227197d 100644
--- a/includes/actions/ViewAction.php
+++ b/includes/actions/ViewAction.php
@@ -23,17 +23,24 @@
* @author Timo Tijhof
*/
+/**
+ * An action that views article content
+ *
+ * This is a wrapper that will call Article::render().
+ *
+ * @ingroup Actions
+ */
class ViewAction extends FormlessAction {
public function getName() {
return 'view';
}
- public function onView(){
+ public function onView() {
return null;
}
- public function show(){
+ public function show() {
$this->page->view();
}
diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php
index e2636452..929c1b5f 100644
--- a/includes/actions/WatchAction.php
+++ b/includes/actions/WatchAction.php
@@ -20,6 +20,11 @@
* @ingroup Actions
*/
+/**
+ * Page addition to a user's watchlist
+ *
+ * @ingroup Actions
+ */
class WatchAction extends FormAction {
public function getName() {
@@ -82,24 +87,72 @@ class WatchAction extends FormAction {
return parent::checkCanExecute( $user );
}
- public static function doWatch( Title $title, User $user ) {
+ /**
+ * Watch or unwatch a page
+ * @since 1.22
+ * @param bool $watch Whether to watch or unwatch the page
+ * @param Title $title Page to watch/unwatch
+ * @param User $user User who is watching/unwatching
+ * @return Status
+ */
+ public static function doWatchOrUnwatch( $watch, Title $title, User $user ) {
+ if ( $user->isLoggedIn() && $user->isWatched( $title, WatchedItem::IGNORE_USER_RIGHTS ) != $watch ) {
+ // If the user doesn't have 'editmywatchlist', we still want to
+ // allow them to add but not remove items via edits and such.
+ if ( $watch ) {
+ return self::doWatch( $title, $user, WatchedItem::IGNORE_USER_RIGHTS );
+ } else {
+ return self::doUnwatch( $title, $user );
+ }
+ }
+ return Status::newGood();
+ }
+
+ /**
+ * Watch a page
+ * @since 1.22 Returns Status, $checkRights parameter added
+ * @param Title $title Page to watch/unwatch
+ * @param User $user User who is watching/unwatching
+ * @param int $checkRights Passed through to $user->addWatch()
+ * @return Status
+ */
+ public static function doWatch( Title $title, User $user, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+ if ( $checkRights !== WatchedItem::IGNORE_USER_RIGHTS && !$user->isAllowed( 'editmywatchlist' ) ) {
+ return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
+ }
+
$page = WikiPage::factory( $title );
- if ( wfRunHooks( 'WatchArticle', array( &$user, &$page ) ) ) {
- $user->addWatch( $title );
+ $status = Status::newFatal( 'hookaborted' );
+ if ( wfRunHooks( 'WatchArticle', array( &$user, &$page, &$status ) ) ) {
+ $status = Status::newGood();
+ $user->addWatch( $title, $checkRights );
wfRunHooks( 'WatchArticleComplete', array( &$user, &$page ) );
}
- return true;
+ return $status;
}
- public static function doUnwatch( Title $title, User $user ) {
+ /**
+ * Unwatch a page
+ * @since 1.22 Returns Status
+ * @param Title $title Page to watch/unwatch
+ * @param User $user User who is watching/unwatching
+ * @return Status
+ */
+ public static function doUnwatch( Title $title, User $user ) {
+ if ( !$user->isAllowed( 'editmywatchlist' ) ) {
+ return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
+ }
+
$page = WikiPage::factory( $title );
- if ( wfRunHooks( 'UnwatchArticle', array( &$user, &$page ) ) ) {
+ $status = Status::newFatal( 'hookaborted' );
+ if ( wfRunHooks( 'UnwatchArticle', array( &$user, &$page, &$status ) ) ) {
+ $status = Status::newGood();
$user->removeWatch( $title );
wfRunHooks( 'UnwatchArticleComplete', array( &$user, &$page ) );
}
- return true;
+ return $status;
}
/**
@@ -148,6 +201,11 @@ class WatchAction extends FormAction {
}
}
+/**
+ * Page removal from a user's watchlist
+ *
+ * @ingroup Actions
+ */
class UnwatchAction extends WatchAction {
public function getName() {
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index 875a3814..ce6ecda6 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -66,14 +66,22 @@ abstract class ApiBase extends ContextSource {
const LIMIT_SML1 = 50; // Slow query, std user limit
const LIMIT_SML2 = 500; // Slow query, bot/sysop limit
+ /**
+ * getAllowedParams() flag: When set, the result could take longer to generate,
+ * but should be more thorough. E.g. get the list of generators for ApiSandBox extension
+ * @since 1.21
+ */
+ const GET_VALUES_FOR_HELP = 1;
+
private $mMainModule, $mModuleName, $mModulePrefix;
+ private $mSlaveDB = null;
private $mParamCache = array();
/**
* Constructor
* @param $mainModule ApiMain object
- * @param $moduleName string Name of this module
- * @param $modulePrefix string Prefix to use for parameter names
+ * @param string $moduleName Name of this module
+ * @param string $modulePrefix Prefix to use for parameter names
*/
public function __construct( $mainModule, $moduleName, $modulePrefix = '' ) {
$this->mMainModule = $mainModule;
@@ -105,15 +113,19 @@ abstract class ApiBase extends ContextSource {
* The result data should be stored in the ApiResult object available
* through getResult().
*/
- public abstract function execute();
+ abstract public function execute();
/**
* Returns a string that identifies the version of the extending class.
* Typically includes the class name, the svn revision, timestamp, and
* last author. Usually done with SVN's Id keyword
* @return string
+ * @deprecated since 1.21, version string is no longer supported
*/
- public abstract function getVersion();
+ public function getVersion() {
+ wfDeprecated( __METHOD__, '1.21' );
+ return '';
+ }
/**
* Get the name of the module being executed by this instance
@@ -124,6 +136,15 @@ abstract class ApiBase extends ContextSource {
}
/**
+ * Get the module manager, or null if this module has no sub-modules
+ * @since 1.21
+ * @return ApiModuleManager
+ */
+ public function getModuleManager() {
+ return null;
+ }
+
+ /**
* Get parameter prefix (usually two letters or an empty string).
* @return string
*/
@@ -168,7 +189,7 @@ abstract class ApiBase extends ContextSource {
* @return ApiResult
*/
public function getResult() {
- // Main module has getResult() method overriden
+ // Main module has getResult() method overridden
// Safety - avoid infinite loop:
if ( $this->isMain() ) {
ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
@@ -203,26 +224,32 @@ abstract class ApiBase extends ContextSource {
* section to notice any changes in API. Multiple calls to this
* function will result in the warning messages being separated by
* newlines
- * @param $warning string Warning message
+ * @param string $warning Warning message
*/
public function setWarning( $warning ) {
$result = $this->getResult();
$data = $result->getData();
- if ( isset( $data['warnings'][$this->getModuleName()] ) ) {
+ $moduleName = $this->getModuleName();
+ if ( isset( $data['warnings'][$moduleName] ) ) {
// Don't add duplicate warnings
- $warn_regex = preg_quote( $warning, '/' );
- if ( preg_match( "/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*'] ) ) {
- return;
+ $oldWarning = $data['warnings'][$moduleName]['*'];
+ $warnPos = strpos( $oldWarning, $warning );
+ // If $warning was found in $oldWarning, check if it starts at 0 or after "\n"
+ if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) {
+ // Check if $warning is followed by "\n" or the end of the $oldWarning
+ $warnPos += strlen( $warning );
+ if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) {
+ return;
+ }
}
- $oldwarning = $data['warnings'][$this->getModuleName()]['*'];
// If there is a warning already, append it to the existing one
- $warning = "$oldwarning\n$warning";
- $result->unsetValue( 'warnings', $this->getModuleName() );
+ $warning = "$oldWarning\n$warning";
}
$msg = array();
ApiResult::setContent( $msg, $warning );
$result->disableSizeCheck();
- $result->addValue( 'warnings', $this->getModuleName(), $msg );
+ $result->addValue( 'warnings', $moduleName,
+ $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
$result->enableSizeCheck();
}
@@ -254,6 +281,8 @@ abstract class ApiBase extends ContextSource {
}
$msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n";
+ $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() );
+
if ( $this->isReadMode() ) {
$msg .= "\nThis module requires read rights";
}
@@ -275,15 +304,14 @@ abstract class ApiBase extends ContextSource {
}
$examples = $this->getExamples();
- if ( $examples !== false && $examples !== '' ) {
+ if ( $examples ) {
if ( !is_array( $examples ) ) {
$examples = array(
$examples
);
}
$msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n";
- foreach( $examples as $k => $v ) {
-
+ foreach ( $examples as $k => $v ) {
if ( is_numeric( $k ) ) {
$msg .= " $v\n";
} else {
@@ -297,25 +325,6 @@ abstract class ApiBase extends ContextSource {
}
}
}
-
- $msg .= $this->makeHelpArrayToString( $lnPrfx, "Help page", $this->getHelpUrls() );
-
- if ( $this->getMain()->getShowVersions() ) {
- $versions = $this->getVersion();
- $pattern = '/(\$.*) ([0-9a-z_]+\.php) (.*\$)/i';
- $callback = array( $this, 'makeHelpMsg_callback' );
-
- 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 );
- }
-
- $msg .= "Version:\n $versions\n";
- }
}
return $msg;
@@ -330,8 +339,8 @@ abstract class ApiBase extends ContextSource {
}
/**
- * @param $prefix string Text to split output items
- * @param $title string What is being output
+ * @param string $prefix Text to split output items
+ * @param string $title What is being output
* @param $input string|array
* @return string
*/
@@ -340,13 +349,15 @@ abstract class ApiBase extends ContextSource {
return '';
}
if ( !is_array( $input ) ) {
- $input = array(
- $input
- );
+ $input = array( $input );
}
if ( count( $input ) > 0 ) {
- $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n ";
+ if ( $title ) {
+ $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n ";
+ } else {
+ $msg = ' ';
+ }
$msg .= implode( $prefix, $input ) . "\n";
return $msg;
}
@@ -359,7 +370,7 @@ abstract class ApiBase extends ContextSource {
* @return string or false
*/
public function makeHelpMsgParameters() {
- $params = $this->getFinalParams();
+ $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
if ( $params ) {
$paramsDescription = $this->getFinalParamDescription();
@@ -416,7 +427,7 @@ abstract class ApiBase extends ContextSource {
if ( $t === '' ) {
$nothingPrompt = 'Can be empty, or ';
} else {
- $choices[] = $t;
+ $choices[] = $t;
}
}
$desc .= $paramPrefix . $nothingPrompt . $prompt;
@@ -433,7 +444,7 @@ abstract class ApiBase extends ContextSource {
$hintPipeSeparated = false;
break;
case 'limit':
- $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]}";
+ $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}";
if ( isset( $paramSettings[self::PARAM_MAX2] ) ) {
$desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)";
}
@@ -455,6 +466,9 @@ abstract class ApiBase extends ContextSource {
$desc .= $paramPrefix . $intRangeStr;
}
break;
+ case 'upload':
+ $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data";
+ break;
}
}
@@ -487,44 +501,6 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Callback for preg_replace_callback() call in makeHelpMsg().
- * Replaces a source file name with a link to ViewVC
- *
- * @param $matches array
- * @return string
- */
- public function makeHelpMsg_callback( $matches ) {
- global $wgAutoloadClasses, $wgAutoloadLocalClasses;
-
- $file = '';
- 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 https://svn.wikimedia.org/" .
- "viewvc/mediawiki/trunk/" . dirname( $path ) .
- "/{$matches[2]}";
- }
- return $matches[0];
- }
-
- /**
* Returns the description string for this module
* @return mixed string or array of strings
*/
@@ -545,15 +521,22 @@ abstract class ApiBase extends ContextSource {
* value) or (parameter name) => (array with PARAM_* constants as keys)
* Don't call this function directly: use getFinalParams() to allow
* hooks to modify parameters as needed.
+ *
+ * Some derived classes may choose to handle an integer $flags parameter
+ * in the overriding methods. Callers of this method can pass zero or
+ * more OR-ed flags like GET_VALUES_FOR_HELP.
+ *
* @return array|bool
*/
- protected function getAllowedParams() {
+ protected function getAllowedParams( /* $flags = 0 */ ) {
+ // int $flags is not declared because it causes "Strict standards"
+ // warning. Most derived classes do not implement it.
return false;
}
/**
* Returns an array of parameter descriptions.
- * Don't call this functon directly: use getFinalParamDescription() to
+ * Don't call this function directly: use getFinalParamDescription() to
* allow hooks to modify descriptions as needed.
* @return array|bool False on no parameter descriptions
*/
@@ -565,11 +548,13 @@ abstract class ApiBase extends ContextSource {
* Get final list of parameters, after hooks have had a chance to
* tweak it as needed.
*
+ * @param int $flags Zero or more flags like GET_VALUES_FOR_HELP
* @return array|Bool False on no parameters
+ * @since 1.21 $flags param added
*/
- public function getFinalParams() {
- $params = $this->getAllowedParams();
- wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params ) );
+ public function getFinalParams( $flags = 0 ) {
+ $params = $this->getAllowedParams( $flags );
+ wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params, $flags ) );
return $params;
}
@@ -596,7 +581,7 @@ abstract class ApiBase extends ContextSource {
* The array can also contain a boolean under the key PROP_LIST,
* indicating whether the result is a list.
*
- * Don't call this functon directly: use getFinalResultProperties() to
+ * Don't call this function directly: use getFinalResultProperties() to
* allow hooks to modify descriptions as needed.
*
* @return array|bool False on no properties
@@ -645,7 +630,7 @@ abstract class ApiBase extends ContextSource {
/**
* This method mangles parameter name based on the prefix supplied to the constructor.
* Override this method to change parameter name during runtime
- * @param $paramName string Parameter name
+ * @param string $paramName Parameter name
* @return string Prefixed parameter name
*/
public function encodeParamName( $paramName ) {
@@ -680,8 +665,8 @@ abstract class ApiBase extends ContextSource {
/**
* Get a value for the given parameter
- * @param $paramName string Parameter name
- * @param $parseLimit bool see extractRequestParams()
+ * @param string $paramName Parameter name
+ * @param bool $parseLimit see extractRequestParams()
* @return mixed Parameter value
*/
protected function getParameter( $paramName, $parseLimit = true ) {
@@ -692,7 +677,7 @@ abstract class ApiBase extends ContextSource {
/**
* Die if none or more than one of a certain set of parameters is set and not false.
- * @param $params array of parameter names
+ * @param array $params of parameter names
*/
public function requireOnlyOneParameter( $params ) {
$required = func_get_args();
@@ -703,9 +688,9 @@ abstract class ApiBase extends ContextSource {
array( $this, "parameterNotEmpty" ) ) ), $required );
if ( count( $intersection ) > 1 ) {
- $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" );
+ $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', 'invalidparammix' );
} elseif ( count( $intersection ) == 0 ) {
- $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" );
+ $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', 'missingparam' );
}
}
@@ -739,7 +724,7 @@ abstract class ApiBase extends ContextSource {
array( $this, "parameterNotEmpty" ) ) ), $required );
if ( count( $intersection ) > 1 ) {
- $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" );
+ $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', 'invalidparammix' );
}
}
@@ -760,7 +745,7 @@ abstract class ApiBase extends ContextSource {
/**
* @param $params array
- * @param $load bool|string Whether load the object's state from the database:
+ * @param bool|string $load Whether load the object's state from the database:
* - false: don't load (if the pageid is given, it will still be loaded)
* - 'fromdb': load from a slave database
* - 'fromdbmaster': load from the master database
@@ -772,9 +757,12 @@ abstract class ApiBase extends ContextSource {
$pageObj = null;
if ( isset( $params['title'] ) ) {
$titleObj = Title::newFromText( $params['title'] );
- if ( !$titleObj ) {
+ if ( !$titleObj || $titleObj->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
+ if ( !$titleObj->canExist() ) {
+ $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' );
+ }
$pageObj = WikiPage::factory( $titleObj );
if ( $load !== false ) {
$pageObj->loadPageData( $load );
@@ -806,7 +794,7 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Callback function used in requireOnlyOneParameter to check whether reequired parameters are set
+ * Callback function used in requireOnlyOneParameter to check whether required parameters are set
*
* @param $x object Parameter to check is not null/false
* @return bool
@@ -827,15 +815,15 @@ abstract class ApiBase extends ContextSource {
/**
* Return true if we're to watch the page, false if not, null if no change.
- * @param $watchlist String Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
+ * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
* @param $titleObj Title the page under consideration
- * @param $userOption String The user option to consider when $watchlist=preferences.
+ * @param string $userOption The user option to consider when $watchlist=preferences.
* If not set will magically default to either watchdefault or watchcreations
* @return bool
*/
- protected function getWatchlistValue ( $watchlist, $titleObj, $userOption = null ) {
+ protected function getWatchlistValue( $watchlist, $titleObj, $userOption = null ) {
- $userWatching = $this->getUser()->isWatched( $titleObj );
+ $userWatching = $this->getUser()->isWatched( $titleObj, WatchedItem::IGNORE_USER_RIGHTS );
switch ( $watchlist ) {
case 'watch':
@@ -849,13 +837,13 @@ abstract class ApiBase extends ContextSource {
if ( $userWatching ) {
return true;
}
- # If no user option was passed, use watchdefault or watchcreation
+ # If no user option was passed, use watchdefault or watchcreations
if ( is_null( $userOption ) ) {
$userOption = $titleObj->exists()
? 'watchdefault' : 'watchcreations';
}
# Watch the article based on the user preference
- return (bool)$this->getUser()->getOption( $userOption );
+ return $this->getUser()->getBoolOption( $userOption );
case 'nochange':
return $userWatching;
@@ -867,9 +855,9 @@ abstract class ApiBase extends ContextSource {
/**
* Set a watch (or unwatch) based the based on a watchlist parameter.
- * @param $watch String Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
+ * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
* @param $titleObj Title the article's title to change
- * @param $userOption String The user option to consider when $watch=preferences
+ * @param string $userOption The user option to consider when $watch=preferences
*/
protected function setWatch( $watch, $titleObj, $userOption = null ) {
$value = $this->getWatchlistValue( $watch, $titleObj, $userOption );
@@ -877,19 +865,14 @@ abstract class ApiBase extends ContextSource {
return;
}
- $user = $this->getUser();
- if ( $value ) {
- WatchAction::doWatch( $titleObj, $user );
- } else {
- WatchAction::doUnwatch( $titleObj, $user );
- }
+ WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() );
}
/**
* Using the settings determine the value for the given parameter
*
- * @param $paramName String: parameter name
- * @param $paramSettings array|mixed default value or an array of settings
+ * @param string $paramName parameter name
+ * @param array|mixed $paramSettings default value or an array of settings
* using PARAM_* constants.
* @param $parseLimit Boolean: parse limit?
* @return mixed Parameter value
@@ -929,9 +912,32 @@ abstract class ApiBase extends ContextSource {
ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'. Boolean parameters must default to false." );
}
- $value = $this->getRequest()->getCheck( $encParamName );
+ $value = $this->getMain()->getCheck( $encParamName );
+ } elseif ( $type == 'upload' ) {
+ if ( isset( $default ) ) {
+ // Having a default value is not allowed
+ ApiBase::dieDebug( __METHOD__, "File upload param $encParamName's default is set to '$default'. File upload parameters may not have a default." );
+ }
+ if ( $multi ) {
+ ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
+ }
+ $value = $this->getMain()->getUpload( $encParamName );
+ if ( !$value->exists() ) {
+ // This will get the value without trying to normalize it
+ // (because trying to normalize a large binary file
+ // accidentally uploaded as a field fails spectacularly)
+ $value = $this->getMain()->getRequest()->unsetVal( $encParamName );
+ if ( $value !== null ) {
+ $this->dieUsage(
+ "File upload param $encParamName is not a file upload; " .
+ "be sure to use multipart/form-data for your POST and include " .
+ "a filename in the Content-Disposition header.",
+ "badupload_{$encParamName}"
+ );
+ }
+ }
} else {
- $value = $this->getRequest()->getVal( $encParamName, $default );
+ $value = $this->getMain()->getVal( $encParamName, $default );
if ( isset( $value ) && $type == 'namespace' ) {
$type = MWNamespace::getValidNamespaces();
@@ -953,12 +959,11 @@ abstract class ApiBase extends ContextSource {
if ( $required && $value === '' ) {
$this->dieUsageMsg( array( 'missingparam', $paramName ) );
}
-
break;
case 'integer': // Force everything using intval() and optionally validate limits
- $min = isset ( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null;
- $max = isset ( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null;
- $enforceLimits = isset ( $paramSettings[self::PARAM_RANGE_ENFORCE] )
+ $min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null;
+ $max = isset( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null;
+ $enforceLimits = isset( $paramSettings[self::PARAM_RANGE_ENFORCE] )
? $paramSettings[self::PARAM_RANGE_ENFORCE] : false;
if ( is_array( $value ) ) {
@@ -1010,29 +1015,23 @@ abstract class ApiBase extends ContextSource {
}
break;
case 'user':
- if ( !is_array( $value ) ) {
- $value = array( $value );
- }
-
- foreach ( $value as $key => $val ) {
- $title = Title::makeTitleSafe( NS_USER, $val );
- if ( is_null( $title ) ) {
- $this->dieUsage( "Invalid value for user parameter $encParamName", "baduser_{$encParamName}" );
+ if ( is_array( $value ) ) {
+ foreach ( $value as $key => $val ) {
+ $value[$key] = $this->validateUser( $val, $encParamName );
}
- $value[$key] = $title->getText();
- }
-
- if ( !$multi ) {
- $value = $value[0];
+ } else {
+ $value = $this->validateUser( $value, $encParamName );
}
break;
+ case 'upload': // nothing to do
+ break;
default:
ApiBase::dieDebug( __METHOD__, "Param $encParamName's type is unknown - $type" );
}
}
// Throw out duplicates if requested
- if ( is_array( $value ) && !$dupes ) {
+ if ( !$dupes && is_array( $value ) ) {
$value = array_unique( $value );
}
@@ -1051,10 +1050,10 @@ abstract class ApiBase extends ContextSource {
* 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
+ * @param string $valueName 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
+ * @param bool $allowMultiple 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.
@@ -1076,7 +1075,7 @@ abstract class ApiBase extends ContextSource {
if ( !$allowMultiple && count( $valuesList ) != 1 ) {
// Bug 33482 - Allow entries with | in them for non-multiple values
- if ( in_array( $value, $allowedValues ) ) {
+ if ( in_array( $value, $allowedValues, true ) ) {
return $value;
}
@@ -1106,11 +1105,11 @@ abstract class ApiBase extends ContextSource {
/**
* Validate the value against the minimum and user/bot maximum limits.
* Prints usage info on failure.
- * @param $paramName string Parameter name
- * @param $value int Parameter value
- * @param $min int|null Minimum value
- * @param $max int|null Maximum value for users
- * @param $botMax int Maximum value for sysops/bots
+ * @param string $paramName Parameter name
+ * @param int $value Parameter value
+ * @param int|null $min Minimum value
+ * @param int|null $max Maximum value for users
+ * @param int $botMax Maximum value for sysops/bots
* @param $enforceLimits Boolean Whether to enforce (die) if value is outside limits
*/
function validateLimit( $paramName, &$value, $min, $max, $botMax = null, $enforceLimits = false ) {
@@ -1144,16 +1143,31 @@ abstract class ApiBase extends ContextSource {
}
/**
- * @param $value string
- * @param $paramName string
- * @return string
+ * Validate and normalize of parameters of type 'timestamp'
+ * @param string $value Parameter value
+ * @param string $encParamName Parameter name
+ * @return string Validated and normalized parameter
+ */
+ function validateTimestamp( $value, $encParamName ) {
+ $unixTimestamp = wfTimestamp( TS_UNIX, $value );
+ if ( $unixTimestamp === false ) {
+ $this->dieUsage( "Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}" );
+ }
+ return wfTimestamp( TS_MW, $unixTimestamp );
+ }
+
+ /**
+ * Validate and normalize of parameters of type 'user'
+ * @param string $value Parameter value
+ * @param string $encParamName Parameter name
+ * @return string Validated and normalized parameter
*/
- function validateTimestamp( $value, $paramName ) {
- $value = wfTimestamp( TS_UNIX, $value );
- if ( $value === 0 ) {
- $this->dieUsage( "Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$paramName}" );
+ private function validateUser( $value, $encParamName ) {
+ $title = Title::makeTitleSafe( NS_USER, $value );
+ if ( $title === null ) {
+ $this->dieUsage( "Invalid value '$value' for user parameter $encParamName", "baduser_{$encParamName}" );
}
- return wfTimestamp( TS_MW, $value );
+ return $title->getText();
}
/**
@@ -1172,8 +1186,8 @@ abstract class ApiBase extends ContextSource {
/**
* Truncate an array to a certain length.
- * @param $arr array Array to truncate
- * @param $limit int Maximum length
+ * @param array $arr Array to truncate
+ * @param int $limit Maximum length
* @return bool True if the array was truncated, false otherwise
*/
public static function truncateArray( &$arr, $limit ) {
@@ -1189,12 +1203,12 @@ abstract class ApiBase extends ContextSource {
* 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
+ * @param string $description 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
+ * @param string $errorCode Brief, arbitrary, stable string to allow easy
* automated identification of the error, e.g., 'unknown_action'
- * @param $httpRespCode int HTTP response code
- * @param $extradata array Data to add to the "<error>" element; array in ApiResult format
+ * @param int $httpRespCode HTTP response code
+ * @param array $extradata Data to add to the "<error>" element; array in ApiResult format
* @throws UsageException
*/
public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
@@ -1203,6 +1217,44 @@ abstract class ApiBase extends ContextSource {
}
/**
+ * Throw a UsageException based on the errors in the Status object.
+ *
+ * @since 1.22
+ * @param Status $status Status object
+ * @throws UsageException
+ */
+ public function dieStatus( $status ) {
+ if ( $status->isGood() ) {
+ throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
+ }
+
+ $errors = $status->getErrorsArray();
+ if ( !$errors ) {
+ // No errors? Assume the warnings should be treated as errors
+ $errors = $status->getWarningsArray();
+ }
+ if ( !$errors ) {
+ // Still no errors? Punt
+ $errors = array( array( 'unknownerror-nocode' ) );
+ }
+
+ // Cannot use dieUsageMsg() because extensions might return custom
+ // error messages.
+ if ( $errors[0] instanceof Message ) {
+ $msg = $errors[0];
+ $code = $msg->getKey();
+ } else {
+ $code = array_shift( $errors[0] );
+ $msg = wfMessage( $code, $errors[0] );
+ }
+ if ( isset( ApiBase::$messageMap[$code] ) ) {
+ // Translate message to code, for backwards compatability
+ $code = ApiBase::$messageMap[$code]['code'];
+ }
+ $this->dieUsage( $msg->inLanguage( 'en' )->useDatabase( false )->plain(), $code );
+ }
+
+ /**
* Array that maps message keys to error messages. $1 and friends are replaced.
*/
public static $messageMap = array(
@@ -1226,7 +1278,7 @@ abstract class ApiBase extends ContextSource {
'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" ),
+ 'confirmedittext' => array( 'code' => 'confirmemail', 'info' => "You must confirm your email 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" ),
@@ -1254,15 +1306,15 @@ abstract class ApiBase extends ContextSource {
'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_blocked_as_range' => array( 'code' => 'blockedasrange', 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, 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" ),
+ 'mailnologin' => array( 'code' => 'cantsend', 'info' => "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email" ),
'ipbblocked' => array( 'code' => 'ipbblocked', 'info' => 'You cannot block or unblock users while you are yourself blocked' ),
'ipbnounblockself' => array( 'code' => 'ipbnounblockself', 'info' => 'You are not allowed to unblock yourself' ),
'usermaildisabled' => array( 'code' => 'usermaildisabled', 'info' => "User email has been disabled" ),
- 'blockedemailuser' => array( 'code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail" ),
+ 'blockedemailuser' => array( 'code' => 'blockedfrommail', 'info' => "You have been blocked from sending email" ),
'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" ),
+ 'noemail' => array( 'code' => 'noemail', 'info' => "The user has not specified a valid email address, or has chosen not to receive email 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" ),
@@ -1291,7 +1343,7 @@ abstract class ApiBase extends ContextSource {
'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" ),
+ 'cantblock-email' => array( 'code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending email 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" ),
@@ -1349,9 +1401,10 @@ abstract class ApiBase extends ContextSource {
// uploadMsgs
'invalid-file-key' => array( 'code' => 'invalid-file-key', 'info' => 'Not a valid file 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' ),
- 'copyuploaddisabled' => array( 'code' => 'copyuploaddisabled', 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' ),
+ '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' ),
+ 'copyuploaddisabled' => array( 'code' => 'copyuploaddisabled', 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' ),
'copyuploadbaddomain' => array( 'code' => 'copyuploadbaddomain', 'info' => 'Uploads by URL are not allowed from this domain.' ),
+ 'copyuploadbadurl' => array( 'code' => 'copyuploadbadurl', 'info' => 'Upload not allowed from this URL.' ),
'filename-tooshort' => array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ),
'filename-toolong' => array( 'code' => 'filename-toolong', 'info' => 'The filename is too long' ),
@@ -1377,7 +1430,7 @@ abstract class ApiBase extends ContextSource {
public function dieUsageMsg( $error ) {
# most of the time we send a 1 element, so we might as well send it as
# a string and make this an array here.
- if( is_string( $error ) ) {
+ if ( is_string( $error ) ) {
$error = array( $error );
}
$parsed = $this->parseMsg( $error );
@@ -1385,8 +1438,41 @@ abstract class ApiBase extends ContextSource {
}
/**
+ * Will only set a warning instead of failing if the global $wgDebugAPI
+ * is set to true. Otherwise behaves exactly as dieUsageMsg().
+ * @param $error (array|string) Element of a getUserPermissionsErrors()-style array
+ * @since 1.21
+ */
+ public function dieUsageMsgOrDebug( $error ) {
+ global $wgDebugAPI;
+ if ( $wgDebugAPI !== true ) {
+ $this->dieUsageMsg( $error );
+ } else {
+ if ( is_string( $error ) ) {
+ $error = array( $error );
+ }
+ $parsed = $this->parseMsg( $error );
+ $this->setWarning( '$wgDebugAPI: ' . $parsed['code']
+ . ' - ' . $parsed['info'] );
+ }
+ }
+
+ /**
+ * Die with the $prefix.'badcontinue' error. This call is common enough to make it into the base method.
+ * @param $condition boolean will only die if this value is true
+ * @since 1.21
+ */
+ protected function dieContinueUsageIf( $condition ) {
+ if ( $condition ) {
+ $this->dieUsage(
+ 'Invalid continue param. You should pass the original value returned by the previous query',
+ 'badcontinue' );
+ }
+ }
+
+ /**
* Return the error message related to a certain array
- * @param $error array Element of a getUserPermissionsErrors()-style array
+ * @param array $error Element of a getUserPermissionsErrors()-style array
* @return array('code' => code, 'info' => info)
*/
public function parseMsg( $error ) {
@@ -1395,7 +1481,7 @@ abstract class ApiBase extends ContextSource {
// Check whether the error array was nested
// array( array( <code>, <params> ), array( <another_code>, <params> ) )
- if( is_array( $key ) ){
+ if ( is_array( $key ) ) {
$error = $key;
$key = array_shift( $error );
}
@@ -1413,11 +1499,11 @@ abstract class ApiBase extends ContextSource {
/**
* Internal code errors should be reported with this method
- * @param $method string Method or function name
- * @param $message string Error message
+ * @param string $method Method or function name
+ * @param string $message Error message
*/
protected static function dieDebug( $method, $message ) {
- wfDebugDieBacktrace( "Internal error in $method: $message" );
+ throw new MWException( "Internal error in $method: $message" );
}
/**
@@ -1482,7 +1568,7 @@ abstract class ApiBase extends ContextSource {
public function getWatchlistUser( $params ) {
if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) {
$user = User::newFromName( $params['owner'], false );
- if ( !($user && $user->getId()) ) {
+ if ( !( $user && $user->getId() ) ) {
$this->dieUsage( 'Specified user does not exist', 'bad_wlowner' );
}
$token = $user->getOption( 'watchlisttoken' );
@@ -1493,6 +1579,9 @@ abstract class ApiBase extends ContextSource {
if ( !$this->getUser()->isLoggedIn() ) {
$this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
}
+ if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
+ $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' );
+ }
$user = $this->getUser();
}
return $user;
@@ -1507,6 +1596,10 @@ abstract class ApiBase extends ContextSource {
/**
* Returns a list of all possible errors returned by the module
+ *
+ * Don't call this function directly: use getFinalPossibleErrors() to allow
+ * hooks to modify parameters as needed.
+ *
* @return array in the format of array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... )
*/
public function getPossibleErrors() {
@@ -1515,10 +1608,16 @@ abstract class ApiBase extends ContextSource {
$params = $this->getFinalParams();
if ( $params ) {
foreach ( $params as $paramName => $paramSettings ) {
- if ( isset( $paramSettings[ApiBase::PARAM_REQUIRED] ) ) {
+ if ( isset( $paramSettings[ApiBase::PARAM_REQUIRED] ) && $paramSettings[ApiBase::PARAM_REQUIRED] ) {
$ret[] = array( 'missingparam', $paramName );
}
}
+ if ( array_key_exists( 'continue', $params ) ) {
+ $ret[] = array(
+ 'code' => 'badcontinue',
+ 'info' => 'Invalid continue param. You should pass the original value returned by the previous query'
+ );
+ }
}
if ( $this->mustBePosted() ) {
@@ -1535,7 +1634,12 @@ abstract class ApiBase extends ContextSource {
}
if ( $this->needsToken() ) {
- $ret[] = array( 'missingparam', 'token' );
+ if ( !isset( $params['token'][ApiBase::PARAM_REQUIRED] )
+ || !$params['token'][ApiBase::PARAM_REQUIRED]
+ ) {
+ // Add token as possible missing parameter, if not already done
+ $ret[] = array( 'missingparam', 'token' );
+ }
$ret[] = array( 'sessionfailure' );
}
@@ -1543,8 +1647,21 @@ abstract class ApiBase extends ContextSource {
}
/**
+ * Get final list of possible errors, after hooks have had a chance to
+ * tweak it as needed.
+ *
+ * @return array
+ * @since 1.22
+ */
+ public function getFinalPossibleErrors() {
+ $possibleErrors = $this->getPossibleErrors();
+ wfRunHooks( 'APIGetPossibleErrors', array( $this, &$possibleErrors ) );
+ return $possibleErrors;
+ }
+
+ /**
* 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' => ... )
+ * @param array $errors 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 ) {
@@ -1666,17 +1783,23 @@ abstract class ApiBase extends ContextSource {
}
/**
+ * Gets a default slave database connection object
* @return DatabaseBase
*/
protected function getDB() {
- return wfGetDB( DB_SLAVE, 'api' );
+ if ( !isset( $this->mSlaveDB ) ) {
+ $this->profileDBIn();
+ $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' );
+ $this->profileDBOut();
+ }
+ return $this->mSlaveDB;
}
/**
* Debugging function that prints a value and an optional backtrace
* @param $value mixed Value to print
- * @param $name string Description of the printed value
- * @param $backtrace bool If true, print a backtrace
+ * @param string $name Description of the printed value
+ * @param bool $backtrace If true, print a backtrace
*/
public static function debugPrint( $value, $name = 'unknown', $backtrace = false ) {
print "\n\n<pre><b>Debugging value '$name':</b>\n\n";
@@ -1686,12 +1809,4 @@ abstract class ApiBase extends ContextSource {
}
print "\n</pre>\n";
}
-
- /**
- * Returns a string that identifies the version of this class.
- * @return string
- */
- public static function getBaseVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index c879b35d..975153ac 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -25,17 +25,13 @@
*/
/**
-* API module that facilitates the blocking of users. Requires API write mode
-* to be enabled.
-*
+ * API module that facilitates the blocking of users. Requires API write mode
+ * to be enabled.
+ *
* @ingroup API
*/
class ApiBlock extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* Blocks the user specified in the parameters for the given expiry, with the
* given reason, and with all other settings provided in the params. If the block
@@ -46,15 +42,10 @@ class ApiBlock extends ApiBase {
$user = $this->getUser();
$params = $this->extractRequestParams();
- if ( $params['gettoken'] ) {
- $res['blocktoken'] = $user->getEditToken();
- $this->getResult()->addValue( null, $this->getModuleName(), $res );
- return;
- }
-
if ( !$user->isAllowed( 'block' ) ) {
$this->dieUsageMsg( 'cantblock' );
}
+
# bug 15810: blocked admins should have limited access here
if ( $user->isBlocked() ) {
$status = SpecialBlock::checkUnblockSelf( $params['user'], $user );
@@ -62,6 +53,13 @@ class ApiBlock extends ApiBase {
$this->dieUsageMsg( array( $status ) );
}
}
+
+ $target = User::newFromName( $params['user'] );
+ // Bug 38633 - if the target is a user (not an IP address), but it doesn't exist or is unusable, error.
+ if ( $target instanceof User && ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) ) ) {
+ $this->dieUsageMsg( array( 'nosuchuser', $params['user'] ) );
+ }
+
if ( $params['hidename'] && !$user->isAllowed( 'hideuser' ) ) {
$this->dieUsageMsg( 'canthide' );
}
@@ -70,6 +68,7 @@ class ApiBlock extends ApiBase {
}
$data = array(
+ 'PreviousTarget' => $params['user'],
'Target' => $params['user'],
'Reason' => array(
$params['reason'],
@@ -83,7 +82,7 @@ class ApiBlock extends ApiBase {
'DisableEmail' => $params['noemail'],
'HideUser' => $params['hidename'],
'DisableUTEdit' => !$params['allowusertalk'],
- 'AlreadyBlocked' => $params['reblock'],
+ 'Reblock' => $params['reblock'],
'Watch' => $params['watchuser'],
'Confirm' => true,
);
@@ -99,7 +98,7 @@ class ApiBlock extends ApiBase {
$res['userID'] = $target instanceof User ? $target->getId() : 0;
$block = Block::newFromTarget( $target );
- if( $block instanceof Block ){
+ if ( $block instanceof Block ) {
$res['expiry'] = $block->mExpiry == $this->getDB()->getInfinity()
? 'infinite'
: wfTimestamp( TS_ISO_8601, $block->mExpiry );
@@ -151,10 +150,6 @@ class ApiBlock extends ApiBase {
ApiBase::PARAM_REQUIRED => true
),
'token' => null,
- 'gettoken' => array(
- ApiBase::PARAM_DFLT => false,
- ApiBase::PARAM_DEPRECATED => true,
- ),
'expiry' => 'never',
'reason' => '',
'anononly' => false,
@@ -172,13 +167,12 @@ class ApiBlock extends ApiBase {
return array(
'user' => 'Username, IP address or IP range you want to block',
'token' => 'A block token previously obtained through prop=info',
- 'gettoken' => 'If set, a block token will be returned, and no other action will be taken',
'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.',
'reason' => 'Reason for block',
'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)',
'nocreate' => 'Prevent account creation',
'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from',
- 'noemail' => 'Prevent user from sending e-mail through the wiki. (Requires the "blockemail" right.)',
+ 'noemail' => 'Prevent user from sending email through the wiki. (Requires the "blockemail" right.)',
'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)',
'allowusertalk' => 'Allow the user to edit their own talk page (depends on $wgBlockAllowsUTEdit)',
'reblock' => 'If the user is already blocked, overwrite the existing block',
@@ -189,10 +183,6 @@ class ApiBlock extends ApiBase {
public function getResultProperties() {
return array(
'' => array(
- 'blocktoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
'user' => array(
ApiBase::PROP_TYPE => 'string',
ApiBase::PROP_NULLABLE => true
@@ -256,8 +246,4 @@ class ApiBlock extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Block';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php
index ed72b29b..1e35c349 100644
--- a/includes/api/ApiComparePages.php
+++ b/includes/api/ApiComparePages.php
@@ -25,17 +25,21 @@
class ApiComparePages extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$params = $this->extractRequestParams();
$rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] );
$rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] );
- $de = new DifferenceEngine( $this->getContext(),
+ $revision = Revision::newFromId( $rev1 );
+
+ if ( !$revision ) {
+ $this->dieUsage( 'The diff cannot be retrieved, ' .
+ 'one revision does not exist or you do not have permission to view it.', 'baddiff' );
+ }
+
+ $contentHandler = $revision->getContentHandler();
+ $de = $contentHandler->createDifferenceEngine( $this->getContext(),
$rev1,
$rev2,
null, // rcid
@@ -77,17 +81,17 @@ class ApiComparePages extends ApiBase {
* @return int
*/
private function revisionOrTitleOrId( $revision, $titleText, $titleId ) {
- if( $revision ){
+ if ( $revision ) {
return $revision;
- } elseif( $titleText ) {
+ } elseif ( $titleText ) {
$title = Title::newFromText( $titleText );
- if( !$title ){
+ if ( !$title || $title->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $titleText ) );
}
return $title->getLatestRevID();
} elseif ( $titleId ) {
$title = Title::newFromID( $titleId );
- if( !$title ) {
+ if ( !$title ) {
$this->dieUsageMsg( array( 'nosuchpageid', $titleId ) );
}
return $title->getLatestRevID();
@@ -164,8 +168,4 @@ class ApiComparePages extends ApiBase {
'api.php?action=compare&fromrev=1&torev=2' => 'Create a diff between revision 1 and 2',
);
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiCreateAccount.php b/includes/api/ApiCreateAccount.php
new file mode 100644
index 00000000..0e752c56
--- /dev/null
+++ b/includes/api/ApiCreateAccount.php
@@ -0,0 +1,296 @@
+<?php
+/**
+ * Created on August 7, 2012
+ *
+ * Copyright © 2012 Tyler Romeo <tylerromeo@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Unit to authenticate account registration attempts to the current wiki.
+ *
+ * @ingroup API
+ */
+class ApiCreateAccount extends ApiBase {
+ public function execute() {
+ // If we're in JSON callback mode, no tokens can be obtained
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) {
+ $this->dieUsage( 'Cannot create account when using a callback', 'aborted' );
+ }
+
+ // $loginForm->addNewaccountInternal will throw exceptions
+ // if wiki is read only (already handled by api), user is blocked or does not have rights.
+ // Use userCan in order to hit GlobalBlock checks (according to Special:userlogin)
+ $loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
+ if ( !$loginTitle->userCan( 'createaccount', $this->getUser() ) ) {
+ $this->dieUsage( 'You do not have the right to create a new account', 'permdenied-createaccount' );
+ }
+ if ( $this->getUser()->isBlockedFromCreateAccount() ) {
+ $this->dieUsage( 'You cannot create a new account because you are blocked', 'blocked' );
+ }
+
+ $params = $this->extractRequestParams();
+
+ // Init session if necessary
+ if ( session_id() == '' ) {
+ wfSetupSession();
+ }
+
+ if ( $params['mailpassword'] && !$params['email'] ) {
+ $this->dieUsageMsg( 'noemail' );
+ }
+
+ if ( $params['language'] && !Language::isSupportedLanguage( $params['language'] ) ) {
+ $this->dieUsage( 'Invalid language parameter', 'langinvalid' );
+ }
+
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setRequest( new DerivativeRequest(
+ $this->getContext()->getRequest(),
+ array(
+ 'type' => 'signup',
+ 'uselang' => $params['language'],
+ 'wpName' => $params['name'],
+ 'wpPassword' => $params['password'],
+ 'wpRetype' => $params['password'],
+ 'wpDomain' => $params['domain'],
+ 'wpEmail' => $params['email'],
+ 'wpRealName' => $params['realname'],
+ 'wpCreateaccountToken' => $params['token'],
+ 'wpCreateaccount' => $params['mailpassword'] ? null : '1',
+ 'wpCreateaccountMail' => $params['mailpassword'] ? '1' : null
+ )
+ ) );
+
+ $loginForm = new LoginForm();
+ $loginForm->setContext( $context );
+ $loginForm->load();
+
+ $status = $loginForm->addNewaccountInternal();
+ $result = array();
+ if ( $status->isGood() ) {
+ // Success!
+ global $wgEmailAuthentication;
+ $user = $status->getValue();
+
+ if ( $params['language'] ) {
+ $user->setOption( 'language', $params['language'] );
+ }
+
+ if ( $params['mailpassword'] ) {
+ // If mailpassword was set, disable the password and send an email.
+ $user->setPassword( null );
+ $status->merge( $loginForm->mailPasswordInternal( $user, false, 'createaccount-title', 'createaccount-text' ) );
+ } elseif ( $wgEmailAuthentication && Sanitizer::validateEmail( $user->getEmail() ) ) {
+ // Send out an email authentication message if needed
+ $status->merge( $user->sendConfirmationMail() );
+ }
+
+ // Save settings (including confirmation token)
+ $user->saveSettings();
+
+ wfRunHooks( 'AddNewAccount', array( $user, $params['mailpassword'] ) );
+
+ if ( $params['mailpassword'] ) {
+ $logAction = 'byemail';
+ } elseif ( $this->getUser()->isLoggedIn() ) {
+ $logAction = 'create2';
+ } else {
+ $logAction = 'create';
+ }
+ $user->addNewUserLogEntry( $logAction, (string)$params['reason'] );
+
+ // Add username, id, and token to result.
+ $result['username'] = $user->getName();
+ $result['userid'] = $user->getId();
+ $result['token'] = $user->getToken();
+ }
+
+ $apiResult = $this->getResult();
+
+ if ( $status->hasMessage( 'sessionfailure' ) || $status->hasMessage( 'nocookiesfornew' ) ) {
+ // Token was incorrect, so add it to result, but don't throw an exception
+ // since not having the correct token is part of the normal
+ // flow of events.
+ $result['token'] = LoginForm::getCreateaccountToken();
+ $result['result'] = 'needtoken';
+ } elseif ( !$status->isOK() ) {
+ // There was an error. Die now.
+ $this->dieStatus( $status );
+ } elseif ( !$status->isGood() ) {
+ // Status is not good, but OK. This means warnings.
+ $result['result'] = 'warning';
+
+ // Add any warnings to the result
+ $warnings = $status->getErrorsByType( 'warning' );
+ if ( $warnings ) {
+ foreach ( $warnings as &$warning ) {
+ $apiResult->setIndexedTagName( $warning['params'], 'param' );
+ }
+ $apiResult->setIndexedTagName( $warnings, 'warning' );
+ $result['warnings'] = $warnings;
+ }
+ } else {
+ // Everything was fine.
+ $result['result'] = 'success';
+ }
+
+ $apiResult->addValue( null, 'createaccount', $result );
+ }
+
+ public function getDescription() {
+ return 'Create a new user account.';
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isReadMode() {
+ return false;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ global $wgEmailConfirmToEdit;
+ return array(
+ 'name' => array(
+ ApiBase::PARAM_TYPE => 'user',
+ ApiBase::PARAM_REQUIRED => true
+ ),
+ 'password' => null,
+ 'domain' => null,
+ 'token' => null,
+ 'email' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => $wgEmailConfirmToEdit
+ ),
+ 'realname' => null,
+ 'mailpassword' => array(
+ ApiBase::PARAM_TYPE => 'boolean',
+ ApiBase::PARAM_DFLT => false
+ ),
+ 'reason' => null,
+ 'language' => null
+ );
+ }
+
+ public function getParamDescription() {
+ $p = $this->getModulePrefix();
+ return array(
+ 'name' => 'Username',
+ 'password' => "Password (ignored if {$p}mailpassword is set)",
+ 'domain' => 'Domain for external authentication (optional)',
+ 'token' => 'Account creation token obtained in first request',
+ 'email' => 'Email address of user (optional)',
+ 'realname' => 'Real name of user (optional)',
+ 'mailpassword' => 'If set to any value, a random password will be emailed to the user',
+ 'reason' => 'Optional reason for creating the account to be put in the logs',
+ 'language' => 'Language code to set as default for the user (optional, defaults to content language)'
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ 'createaccount' => array(
+ 'result' => array(
+ ApiBase::PROP_TYPE => array(
+ 'success',
+ 'warning',
+ 'needtoken'
+ )
+ ),
+ 'username' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'userid' => array(
+ ApiBase::PROP_TYPE => 'int',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'token' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ )
+ );
+ }
+
+ public function getPossibleErrors() {
+ // Note the following errors aren't possible and don't need to be listed:
+ // sessionfailure, nocookiesfornew, badretype
+ $localErrors = array(
+ 'wrongpassword', // Actually caused by wrong domain field. Riddle me that...
+ 'sorbs_create_account_reason',
+ 'noname',
+ 'userexists',
+ 'password-name-match', // from User::getPasswordValidity
+ 'password-login-forbidden', // from User::getPasswordValidity
+ 'noemailtitle',
+ 'invalidemailaddress',
+ 'externaldberror',
+ 'acct_creation_throttle_hit',
+ );
+
+ $errors = parent::getPossibleErrors();
+ // All local errors are from LoginForm, which means they're actually message keys.
+ foreach ( $localErrors as $error ) {
+ $errors[] = array( 'code' => $error, 'info' => wfMessage( $error )->inLanguage( 'en' )->useDatabase( false )->parse() );
+ }
+
+ $errors[] = array(
+ 'code' => 'permdenied-createaccount',
+ 'info' => 'You do not have the right to create a new account'
+ );
+ $errors[] = array(
+ 'code' => 'blocked',
+ 'info' => 'You cannot create a new account because you are blocked'
+ );
+ $errors[] = array(
+ 'code' => 'aborted',
+ 'info' => 'Account creation aborted by hook (info may vary)'
+ );
+ $errors[] = array(
+ 'code' => 'langinvalid',
+ 'info' => 'Invalid language parameter'
+ );
+
+ // 'passwordtooshort' has parameters. :(
+ global $wgMinimalPasswordLength;
+ $errors[] = array(
+ 'code' => 'passwordtooshort',
+ 'info' => wfMessage( 'passwordtooshort', $wgMinimalPasswordLength )->inLanguage( 'en' )->useDatabase( false )->parse()
+ );
+ return $errors;
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=createaccount&name=testuser&password=test123',
+ 'api.php?action=createaccount&name=testmailuser&mailpassword=true&reason=MyReason',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Account_creation';
+ }
+}
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index 2d36f19a..aea10482 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -32,10 +32,6 @@
*/
class ApiDelete extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* Extracts the title, token, and reason from the request parameters and invokes
* the local delete() function with these as arguments. It does not make use of
@@ -61,9 +57,11 @@ class ApiDelete extends ApiBase {
$status = self::delete( $pageObj, $user, $params['token'], $reason );
}
+ if ( is_array( $status ) ) {
+ $this->dieUsageMsg( $status[0] );
+ }
if ( !$status->isGood() ) {
- $errors = $status->getErrorsArray();
- $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them
+ $this->dieStatus( $status );
}
// Deprecated parameters
@@ -98,11 +96,11 @@ class ApiDelete extends ApiBase {
/**
* We have our own delete() function, since Article.php's implementation is split in two phases
*
- * @param $page WikiPage object to work on
+ * @param $page Page|WikiPage object to work on
* @param $user User doing the action
- * @param $token String: delete token (same as edit token)
- * @param $reason String: reason for the deletion. Autogenerated if NULL
- * @return Status
+ * @param string $token delete token (same as edit token)
+ * @param string|null $reason reason for the deletion. Autogenerated if NULL
+ * @return Status|array
*/
public static function delete( Page $page, User $user, $token, &$reason = null ) {
$title = $page->getTitle();
@@ -128,13 +126,13 @@ class ApiDelete extends ApiBase {
}
/**
- * @param $page WikiPage object to work on
+ * @param $page WikiPage|Page object to work on
* @param $user User doing the action
* @param $token
* @param $oldimage
* @param $reason
* @param $suppress bool
- * @return Status
+ * @return Status|array
*/
public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) {
$title = $page->getTitle();
@@ -161,7 +159,7 @@ class ApiDelete extends ApiBase {
if ( is_null( $reason ) ) { // Log and RC don't like null reasons
$reason = '';
}
- return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
+ return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user );
}
public function mustBePosted() {
@@ -264,8 +262,4 @@ class ApiDelete extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Delete';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php
index 13975aec..e5ef3b7e 100644
--- a/includes/api/ApiDisabled.php
+++ b/includes/api/ApiDisabled.php
@@ -36,10 +36,6 @@
*/
class ApiDisabled extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$this->dieUsage( "The \"{$this->getModuleName()}\" module has been disabled.", 'moduledisabled' );
}
@@ -63,8 +59,4 @@ class ApiDisabled extends ApiBase {
public function getExamples() {
return array();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
index 0963fe7c..bd61895b 100644
--- a/includes/api/ApiEditPage.php
+++ b/includes/api/ApiEditPage.php
@@ -33,10 +33,6 @@
*/
class ApiEditPage extends ApiBase {
- public function __construct( $query, $moduleName ) {
- parent::__construct( $query, $moduleName );
- }
-
public function execute() {
$user = $this->getUser();
$params = $this->extractRequestParams();
@@ -50,32 +46,28 @@ class ApiEditPage extends ApiBase {
$pageObj = $this->getTitleOrPageId( $params );
$titleObj = $pageObj->getTitle();
- if ( $titleObj->isExternal() ) {
- $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
- }
-
$apiResult = $this->getResult();
if ( $params['redirect'] ) {
if ( $titleObj->isRedirect() ) {
$oldTitle = $titleObj;
- $titles = Title::newFromRedirectArray(
- Revision::newFromTitle(
- $oldTitle, false, Revision::READ_LATEST
- )->getText( Revision::FOR_THIS_USER )
- );
+ $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
+ ->getContent( Revision::FOR_THIS_USER, $user )
+ ->getRedirectChain();
// array_shift( $titles );
$redirValues = array();
+
+ /** @var $newTitle Title */
foreach ( $titles as $id => $newTitle ) {
- if ( !isset( $titles[ $id - 1 ] ) ) {
- $titles[ $id - 1 ] = $oldTitle;
+ if ( !isset( $titles[$id - 1] ) ) {
+ $titles[$id - 1] = $oldTitle;
}
$redirValues[] = array(
- 'from' => $titles[ $id - 1 ]->getPrefixedText(),
+ 'from' => $titles[$id - 1]->getPrefixedText(),
'to' => $newTitle->getPrefixedText()
);
@@ -84,9 +76,34 @@ class ApiEditPage extends ApiBase {
$apiResult->setIndexedTagName( $redirValues, 'r' );
$apiResult->addValue( null, 'redirects', $redirValues );
+
+ // Since the page changed, update $pageObj
+ $pageObj = WikiPage::factory( $titleObj );
}
}
+ if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
+ $contentHandler = $pageObj->getContentHandler();
+ } else {
+ $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
+ }
+
+ // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such
+
+ if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
+ $params['contentformat'] = $contentHandler->getDefaultFormat();
+ }
+
+ $contentFormat = $params['contentformat'];
+
+ if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
+ $name = $titleObj->getPrefixedDBkey();
+ $model = $contentHandler->getModelID();
+
+ $this->dieUsage( "The requested format $contentFormat is not supported for content model " .
+ " $model used by $name", 'badformat' );
+ }
+
if ( $params['createonly'] && $titleObj->exists() ) {
$this->dieUsageMsg( 'createonly-exists' );
}
@@ -103,31 +120,66 @@ class ApiEditPage extends ApiBase {
$this->dieUsageMsg( $errors[0] );
}
- $articleObj = Article::newFromTitle( $titleObj, $this->getContext() );
-
$toMD5 = $params['text'];
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 ) {
- $content = '';
- } else {
- $content = $articleObj->getContent();
+ $content = $pageObj->getContent();
+
+ if ( !$content ) {
+ if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
+ # If this is a MediaWiki:x message, then load the messages
+ # and return the message value for x.
+ $text = $titleObj->getDefaultMessageText();
+ if ( $text === false ) {
+ $text = '';
+ }
+
+ try {
+ $content = ContentHandler::makeContent( $text, $this->getTitle() );
+ } catch ( MWContentSerializationException $ex ) {
+ $this->dieUsage( $ex->getMessage(), 'parseerror' );
+ return;
+ }
+ } else {
+ # Otherwise, make a new empty content.
+ $content = $contentHandler->makeEmptyContent();
+ }
+ }
+
+ // @todo Add support for appending/prepending to the Content interface
+
+ if ( !( $content instanceof TextContent ) ) {
+ $mode = $contentHandler->getModelID();
+ $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
}
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' );
+ if ( !$contentHandler->supportsSections() ) {
+ $modelName = $contentHandler->getModelID();
+ $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' );
+ }
+
+ if ( $params['section'] == 'new' ) {
+ // DWIM if they're trying to prepend/append to a new section.
+ $content = null;
+ } else {
+ // Process the content for section edits
+ $section = intval( $params['section'] );
+ $content = $content->getSection( $section );
+
+ if ( !$content ) {
+ $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
+ }
}
}
- $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
+
+ if ( !$content ) {
+ $text = '';
+ } else {
+ $text = $content->serialize( $contentFormat );
+ }
+
+ $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
$toMD5 = $params['prependtext'] . $params['appendtext'];
}
@@ -151,18 +203,21 @@ class ApiEditPage extends ApiBase {
$this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) );
}
- if ( $undoRev->getPage() != $articleObj->getID() ) {
+ if ( $undoRev->getPage() != $pageObj->getID() ) {
$this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) );
}
- if ( $undoafterRev->getPage() != $articleObj->getID() ) {
+ if ( $undoafterRev->getPage() != $pageObj->getID() ) {
$this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) );
}
- $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev );
- if ( $newtext === false ) {
+ $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev );
+
+ if ( !$newContent ) {
$this->dieUsageMsg( 'undo-failure' );
}
- $params['text'] = $newtext;
+
+ $params['text'] = $newContent->serialize( $params['contentformat'] );
+
// 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'] ) {
@@ -179,6 +234,8 @@ class ApiEditPage extends ApiBase {
// That interface kind of sucks, but it's workable
$requestArray = array(
'wpTextbox1' => $params['text'],
+ 'format' => $contentFormat,
+ 'model' => $contentHandler->getModelID(),
'wpEditToken' => $params['token'],
'wpIgnoreBlankSummary' => ''
);
@@ -191,21 +248,26 @@ class ApiEditPage extends ApiBase {
$requestArray['wpSectionTitle'] = $params['sectiontitle'];
}
+ // TODO: Pass along information from 'undoafter' as well
+ if ( $params['undo'] > 0 ) {
+ $requestArray['wpUndidRevision'] = $params['undo'];
+ }
+
// Watch out for basetimestamp == ''
// wfTimestamp() treats it as NOW, almost certainly causing an edit conflict
if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) {
$requestArray['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] );
} else {
- $requestArray['wpEdittime'] = $articleObj->getTimestamp();
+ $requestArray['wpEdittime'] = $pageObj->getTimestamp();
}
if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) {
$requestArray['wpStarttime'] = wfTimestamp( TS_MW, $params['starttimestamp'] );
} else {
- $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
+ $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
}
- if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) {
+ if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) {
$requestArray['wpMinoredit'] = '';
}
@@ -218,6 +280,10 @@ class ApiEditPage extends ApiBase {
if ( $section == 0 && $params['section'] != '0' && $params['section'] != 'new' ) {
$this->dieUsage( "The section parameter must be set to an integer or 'new'", "invalidsection" );
}
+ $content = $pageObj->getContent();
+ if ( $section !== 0 && ( !$content || !$content->getSection( $section ) ) ) {
+ $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
+ }
$requestArray['wpSection'] = $params['section'];
} else {
$requestArray['wpSection'] = '';
@@ -236,6 +302,10 @@ class ApiEditPage extends ApiBase {
$requestArray['wpWatchthis'] = '';
}
+ // Pass through anything else we might have been given, to support extensions
+ // This is kind of a hack but it's the best we can do to make extensions work
+ $requestArray += $this->getRequest()->getValues();
+
global $wgTitle, $wgRequest;
$req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
@@ -244,14 +314,52 @@ class ApiEditPage extends ApiBase {
// TODO: Make them not or check if they still do
$wgTitle = $titleObj;
- $ep = new EditPage( $articleObj );
+ $articleContext = new RequestContext;
+ $articleContext->setRequest( $req );
+ $articleContext->setWikiPage( $pageObj );
+ $articleContext->setUser( $this->getUser() );
+
+ /** @var $articleObject Article */
+ $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
+
+ $ep = new EditPage( $articleObject );
+
+ // allow editing of non-textual content.
+ $ep->allowNonTextContent = true;
+
$ep->setContextTitle( $titleObj );
$ep->importFormData( $req );
+ $content = $ep->textbox1;
+
+ // The following is needed to give the hook the full content of the
+ // new revision rather than just the current section. (Bug 52077)
+ if ( !is_null( $params['section'] ) && $contentHandler->supportsSections() && $titleObj->exists() ) {
+
+ $sectionTitle = '';
+ // If sectiontitle is set, use it, otherwise use the summary as the section title (for
+ // backwards compatibility with old forms/bots).
+ if ( $ep->sectiontitle !== '' ) {
+ $sectionTitle = $ep->sectiontitle;
+ } else {
+ $sectionTitle = $ep->summary;
+ }
+
+ $contentObj = $contentHandler->unserializeContent( $content, $contentFormat );
+
+ $fullContentObj = $articleObject->replaceSectionContent( $params['section'], $contentObj, $sectionTitle );
+ if ( $fullContentObj ) {
+ $content = $fullContentObj->serialize( $contentFormat );
+ } else {
+ // This most likely means we have an edit conflict which means that the edit
+ // wont succeed anyway.
+ $this->dieUsageMsg( 'editconflict' );
+ }
+ }
// Run hooks
// Handle APIEditBeforeSave parameters
$r = array();
- if ( !wfRunHooks( 'APIEditBeforeSave', array( $ep, $ep->textbox1, &$r ) ) ) {
+ if ( !wfRunHooks( 'APIEditBeforeSave', array( $ep, $content, &$r ) ) ) {
if ( count( $r ) ) {
$r['result'] = 'Failure';
$apiResult->addValue( null, $this->getModuleName(), $r );
@@ -262,7 +370,7 @@ class ApiEditPage extends ApiBase {
}
// Do the actual save
- $oldRevId = $articleObj->getRevIdFetched();
+ $oldRevId = $articleObject->getRevIdFetched();
$result = null;
// Fake $wgRequest for some hooks inside EditPage
// @todo FIXME: This interface SUCKS
@@ -273,11 +381,14 @@ class ApiEditPage extends ApiBase {
$wgRequest = $oldRequest;
global $wgMaxArticleSize;
- switch( $status->value ) {
+ switch ( $status->value ) {
case EditPage::AS_HOOK_ERROR:
case EditPage::AS_HOOK_ERROR_EXPECTED:
$this->dieUsageMsg( 'hookaborted' );
+ case EditPage::AS_PARSE_ERROR:
+ $this->dieUsage( $status->getMessage(), 'parseerror' );
+
case EditPage::AS_IMAGE_REDIRECT_ANON:
$this->dieUsageMsg( 'noimageredirect-anon' );
@@ -324,19 +435,21 @@ class ApiEditPage extends ApiBase {
case EditPage::AS_SUCCESS_NEW_ARTICLE:
$r['new'] = '';
+ // fall-through
case EditPage::AS_SUCCESS_UPDATE:
$r['result'] = 'Success';
$r['pageid'] = intval( $titleObj->getArticleID() );
$r['title'] = $titleObj->getPrefixedText();
- $newRevId = $articleObj->getLatest();
+ $r['contentmodel'] = $titleObj->getContentModel();
+ $newRevId = $articleObject->getLatest();
if ( $newRevId == $oldRevId ) {
$r['nochange'] = '';
} else {
$r['oldrevid'] = intval( $oldRevId );
$r['newrevid'] = intval( $newRevId );
$r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
- $articleObj->getTimestamp() );
+ $pageObj->getTimestamp() );
}
break;
@@ -380,6 +493,7 @@ class ApiEditPage extends ApiBase {
array( 'undo-failure' ),
array( 'hashcheckfailed' ),
array( 'hookaborted' ),
+ array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
array( 'noimageredirect-anon' ),
array( 'noimageredirect-logged' ),
array( 'spamdetected', 'spam' ),
@@ -397,6 +511,13 @@ class ApiEditPage extends ApiBase {
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\'' ),
+ array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ),
+ array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using '
+ . 'the text based edit API.' ),
+ array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending '
+ . 'or prepending text.' ),
+ array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to '
+ . 'the page\'s content model' ),
array( 'customcssprotected' ),
array( 'customjsprotected' ),
)
@@ -414,7 +535,6 @@ class ApiEditPage extends ApiBase {
'section' => null,
'sectiontitle' => array(
ApiBase::PARAM_TYPE => 'string',
- ApiBase::PARAM_REQUIRED => false,
),
'text' => null,
'token' => array(
@@ -460,6 +580,12 @@ class ApiEditPage extends ApiBase {
ApiBase::PARAM_TYPE => 'boolean',
ApiBase::PARAM_DFLT => false,
),
+ 'contentformat' => array(
+ ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+ ),
+ 'contentmodel' => array(
+ ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+ )
);
}
@@ -490,7 +616,7 @@ class ApiEditPage extends ApiBase {
'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',
- 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.",
+ 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.",
'If set, the edit won\'t be done unless the hash is correct' ),
'prependtext' => "Add this text to the beginning of the page. Overrides {$p}text",
'appendtext' => array( "Add this text to the end of the page. Overrides {$p}text.",
@@ -498,6 +624,8 @@ class ApiEditPage extends ApiBase {
'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext",
'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision',
'redirect' => 'Automatically resolve redirects',
+ 'contentformat' => 'Content serialization format used for the input text',
+ 'contentmodel' => 'Content model of the new content',
);
}
@@ -546,10 +674,8 @@ class ApiEditPage extends ApiBase {
public function getExamples() {
return array(
-
'api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\'
=> 'Edit a page (anonymous user)',
-
'api.php?action=edit&title=Test&summary=NOTOC&minor=&prependtext=__NOTOC__%0A&basetimestamp=20070824123454&token=%2B\\'
=> 'Prepend __NOTOC__ to a page (anonymous user)',
'api.php?action=edit&title=Test&undo=13585&undoafter=13579&basetimestamp=20070824123454&token=%2B\\'
@@ -560,8 +686,4 @@ class ApiEditPage extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Edit';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
index 4fa03434..cd0d0cba 100644
--- a/includes/api/ApiEmailUser.php
+++ b/includes/api/ApiEmailUser.php
@@ -30,10 +30,6 @@
*/
class ApiEmailUser extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$params = $this->extractRequestParams();
@@ -158,10 +154,6 @@ class ApiEmailUser extends ApiBase {
}
public function getHelpUrls() {
- return 'https://www.mediawiki.org/wiki/API:E-mail';
- }
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ return 'https://www.mediawiki.org/wiki/API:Email';
}
}
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index 160f5b91..d5c789c3 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -33,10 +33,6 @@
*/
class ApiExpandTemplates extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
// Cache may vary on $wgUser because ParserOptions gets data from it
$this->getMain()->setCacheMode( 'anon-public-user-private' );
@@ -46,7 +42,7 @@ class ApiExpandTemplates extends ApiBase {
// Create title for parser
$title_obj = Title::newFromText( $params['title'] );
- if ( !$title_obj ) {
+ if ( !$title_obj || $title_obj->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
@@ -69,14 +65,14 @@ class ApiExpandTemplates extends ApiBase {
$xml = $dom->__toString();
}
$xml_result = array();
- $result->setContent( $xml_result, $xml );
+ ApiResult::setContent( $xml_result, $xml );
$result->addValue( null, 'parsetree', $xml_result );
}
$retval = $wgParser->preprocess( $params['text'], $title_obj, $options );
// Return result
$retval_array = array();
- $result->setContent( $retval_array, $retval );
+ ApiResult::setContent( $retval_array, $retval );
$result->addValue( null, $this->getModuleName(), $retval_array );
}
@@ -130,8 +126,4 @@ class ApiExpandTemplates extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#expandtemplates';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php
index 1cf760ae..05691093 100644
--- a/includes/api/ApiFeedContributions.php
+++ b/includes/api/ApiFeedContributions.php
@@ -29,10 +29,6 @@
*/
class ApiFeedContributions extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* This module uses a custom feed wrapper printer.
*
@@ -47,11 +43,11 @@ class ApiFeedContributions extends ApiBase {
global $wgFeed, $wgFeedClasses, $wgSitename, $wgLanguageCode;
- if( !$wgFeed ) {
+ if ( !$wgFeed ) {
$this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' );
}
- if( !isset( $wgFeedClasses[ $params['feedformat'] ] ) ) {
+ if ( !isset( $wgFeedClasses[$params['feedformat']] ) ) {
$this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' );
}
@@ -86,7 +82,7 @@ class ApiFeedContributions extends ApiBase {
) );
$feedItems = array();
- if( $pager->getNumRows() > 0 ) {
+ if ( $pager->getNumRows() > 0 ) {
foreach ( $pager->mResult as $row ) {
$feedItems[] = $this->feedItem( $row );
}
@@ -97,7 +93,7 @@ class ApiFeedContributions extends ApiBase {
protected function feedItem( $row ) {
$title = Title::makeTitle( intval( $row->page_namespace ), $row->page_title );
- if( $title ) {
+ if ( $title && $title->userCan( 'read', $this->getUser() ) ) {
$date = $row->rev_timestamp;
$comments = $title->getTalkPage()->getFullURL();
$revision = Revision::newFromRow( $row );
@@ -110,9 +106,8 @@ class ApiFeedContributions extends ApiBase {
$this->feedItemAuthor( $revision ),
$comments
);
- } else {
- return null;
}
+ return null;
}
/**
@@ -128,12 +123,24 @@ class ApiFeedContributions extends ApiBase {
* @return string
*/
protected function feedItemDesc( $revision ) {
- if( $revision ) {
+ if ( $revision ) {
$msg = wfMessage( 'colon-separator' )->inContentLanguage()->text();
+ $content = $revision->getContent();
+
+ if ( $content instanceof TextContent ) {
+ // only textual content has a "source view".
+ $html = nl2br( htmlspecialchars( $content->getNativeData() ) );
+ } else {
+ //XXX: we could get an HTML representation of the content via getParserOutput, but that may
+ // contain JS magic and generally may not be suitable for inclusion in a feed.
+ // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
+ //Compare also FeedUtils::formatDiffRow.
+ $html = '';
+ }
+
return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg .
htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
- "</p>\n<hr />\n<div>" .
- nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+ "</p>\n<hr />\n<div>" . $html . "</div>";
}
return '';
}
@@ -141,7 +148,7 @@ class ApiFeedContributions extends ApiBase {
public function getAllowedParams() {
global $wgFeedClasses;
$feedFormatNames = array_keys( $wgFeedClasses );
- return array (
+ return array(
'feedformat' => array(
ApiBase::PARAM_DFLT => 'rss',
ApiBase::PARAM_TYPE => $feedFormatNames
@@ -201,8 +208,4 @@ class ApiFeedContributions extends ApiBase {
'api.php?action=feedcontributions&user=Reedy',
);
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index 6ccb02fe..fbb70fbc 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -33,9 +33,9 @@
*/
class ApiFeedWatchlist extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
+ private $watchlistModule = null;
+ private $linkToDiffs = false;
+ private $linkToSections = false;
/**
* This module uses a custom feed wrapper printer.
@@ -46,8 +46,6 @@ class ApiFeedWatchlist extends ApiBase {
return new ApiFormatFeedWrapper( $this->getMain() );
}
- private $linkToDiffs = false;
-
/**
* Make a nested call to the API to request watchlist items in the last $hours.
* Wrap the result as an RSS/Atom feed.
@@ -58,16 +56,13 @@ class ApiFeedWatchlist extends ApiBase {
try {
$params = $this->extractRequestParams();
- if( !$wgFeed ) {
+ if ( !$wgFeed ) {
$this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' );
}
- if( !isset( $wgFeedClasses[ $params['feedformat'] ] ) ) {
+ if ( !isset( $wgFeedClasses[$params['feedformat']] ) ) {
$this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' );
}
- if ( !is_null( $params['wlexcludeuser'] ) ) {
- $fauxReqArr['wlexcludeuser'] = $params['wlexcludeuser'];
- }
// limit to the number of hours going from now back
$endTime = wfTimestamp( TS_MW, time() - intval( $params['hours'] * 60 * 60 ) );
@@ -81,15 +76,24 @@ class ApiFeedWatchlist extends ApiBase {
'wlprop' => 'title|user|comment|timestamp',
'wldir' => 'older', // reverse order - from newest to oldest
'wlend' => $endTime, // stop at this time
- 'wllimit' => ( 50 > $wgFeedLimit ) ? $wgFeedLimit : 50
+ 'wllimit' => min( 50, $wgFeedLimit )
);
- if ( !is_null( $params['wlowner'] ) ) {
+ if ( $params['wlowner'] !== null ) {
$fauxReqArr['wlowner'] = $params['wlowner'];
}
- if ( !is_null( $params['wltoken'] ) ) {
+ if ( $params['wltoken'] !== null ) {
$fauxReqArr['wltoken'] = $params['wltoken'];
}
+ if ( $params['wlexcludeuser'] !== null ) {
+ $fauxReqArr['wlexcludeuser'] = $params['wlexcludeuser'];
+ }
+ if ( $params['wlshow'] !== null ) {
+ $fauxReqArr['wlshow'] = $params['wlshow'];
+ }
+ if ( $params['wltype'] !== null ) {
+ $fauxReqArr['wltype'] = $params['wltype'];
+ }
// Support linking to diffs instead of article
if ( $params['linktodiffs'] ) {
@@ -97,6 +101,12 @@ class ApiFeedWatchlist extends ApiBase {
$fauxReqArr['wlprop'] .= '|ids';
}
+ // Support linking directly to sections when possible
+ // (possible only if section name is present in comment)
+ if ( $params['linktosections'] ) {
+ $this->linkToSections = true;
+ }
+
// Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
if ( $params['allrev'] ) {
$fauxReqArr['wlallrev'] = '';
@@ -164,6 +174,18 @@ class ApiFeedWatchlist extends ApiBase {
$titleUrl = $title->getFullURL();
}
$comment = isset( $info['comment'] ) ? $info['comment'] : null;
+
+ // Create an anchor to section.
+ // The anchor won't work for sections that have dupes on page
+ // as there's no way to strip that info from ApiWatchlist (apparently?).
+ // RegExp in the line below is equal to Linker::formatAutocomments().
+ if ( $this->linkToSections && $comment !== null && preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches ) ) {
+ global $wgParser;
+ $sectionTitle = $wgParser->stripSectionName( $matches[2] );
+ $sectionTitle = Sanitizer::normalizeSectionNameWhitespace( $sectionTitle );
+ $titleUrl .= Title::newFromText( '#' . $sectionTitle )->getFragmentForURL();
+ }
+
$timestamp = $info['timestamp'];
$user = $info['user'];
@@ -172,10 +194,18 @@ class ApiFeedWatchlist extends ApiBase {
return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user );
}
- public function getAllowedParams() {
+ private function getWatchlistModule() {
+ if ( $this->watchlistModule === null ) {
+ $this->watchlistModule = $this->getMain()->getModuleManager()->getModule( 'query' )
+ ->getModuleManager()->getModule( 'watchlist' );
+ }
+ return $this->watchlistModule;
+ }
+
+ public function getAllowedParams( $flags = 0 ) {
global $wgFeedClasses;
$feedFormatNames = array_keys( $wgFeedClasses );
- return array (
+ $ret = array(
'feedformat' => array(
ApiBase::PARAM_DFLT => 'rss',
ApiBase::PARAM_TYPE => $feedFormatNames
@@ -186,29 +216,41 @@ class ApiFeedWatchlist extends ApiBase {
ApiBase::PARAM_MIN => 1,
ApiBase::PARAM_MAX => 72,
),
- 'allrev' => false,
- 'wlowner' => array(
- ApiBase::PARAM_TYPE => 'user'
- ),
- 'wltoken' => array(
- ApiBase::PARAM_TYPE => 'string'
- ),
- 'wlexcludeuser' => array(
- ApiBase::PARAM_TYPE => 'user'
- ),
'linktodiffs' => false,
+ 'linktosections' => false,
);
+ if ( $flags ) {
+ $wlparams = $this->getWatchlistModule()->getAllowedParams( $flags );
+ $ret['allrev'] = $wlparams['allrev'];
+ $ret['wlowner'] = $wlparams['owner'];
+ $ret['wltoken'] = $wlparams['token'];
+ $ret['wlshow'] = $wlparams['show'];
+ $ret['wltype'] = $wlparams['type'];
+ $ret['wlexcludeuser'] = $wlparams['excludeuser'];
+ } else {
+ $ret['allrev'] = null;
+ $ret['wlowner'] = null;
+ $ret['wltoken'] = null;
+ $ret['wlshow'] = null;
+ $ret['wltype'] = null;
+ $ret['wlexcludeuser'] = null;
+ }
+ return $ret;
}
public function getParamDescription() {
+ $wldescr = $this->getWatchlistModule()->getParamDescription();
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',
- 'wlowner' => "The user whose watchlist you want (must be accompanied by {$this->getModulePrefix()}wltoken if it's not you)",
- 'wltoken' => 'Security token that requested user set in their preferences',
- 'wlexcludeuser' => 'A user whose edits should not be shown in the watchlist',
+ 'hours' => 'List pages modified within this many hours from now',
'linktodiffs' => 'Link to change differences instead of article pages',
+ 'linktosections' => 'Link directly to changed sections if possible',
+ 'allrev' => $wldescr['allrev'],
+ 'wlowner' => $wldescr['owner'],
+ 'wltoken' => $wldescr['token'],
+ 'wlshow' => $wldescr['show'],
+ 'wltype' => $wldescr['type'],
+ 'wlexcludeuser' => $wldescr['excludeuser'],
);
}
@@ -233,8 +275,4 @@ class ApiFeedWatchlist extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Watchlist_feed';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php
index 83d078d2..cbb2ba6a 100644
--- a/includes/api/ApiFileRevert.php
+++ b/includes/api/ApiFileRevert.php
@@ -37,10 +37,6 @@ class ApiFileRevert extends ApiBase {
protected $params;
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$this->params = $this->extractRequestParams();
// Extract the file and archiveName from the request parameters
@@ -50,7 +46,7 @@ class ApiFileRevert extends ApiBase {
$this->checkPermissions( $this->getUser() );
$sourceUrl = $this->file->getArchiveVirtualUrl( $this->archiveName );
- $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'] );
+ $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'], 0, false, false, $this->getUser() );
if ( $status->isGood() ) {
$result = array( 'result' => 'Success' );
@@ -73,8 +69,8 @@ class ApiFileRevert extends ApiBase {
protected function checkPermissions( $user ) {
$title = $this->file->getTitle();
$permissionErrors = array_merge(
- $title->getUserPermissionsErrors( 'edit' , $user ),
- $title->getUserPermissionsErrors( 'upload' , $user )
+ $title->getUserPermissionsErrors( 'edit', $user ),
+ $title->getUserPermissionsErrors( 'upload', $user )
);
if ( $permissionErrors ) {
@@ -191,12 +187,8 @@ class ApiFileRevert extends ApiBase {
public function getExamples() {
return array(
- 'api.php?action=filerevert&filename=Wiki.png&comment=Revert&archivename=20110305152740!Wiki.png&token=+\\'
+ 'api.php?action=filerevert&filename=Wiki.png&comment=Revert&archivename=20110305152740!Wiki.png&token=123ABC'
=> 'Revert Wiki.png to the version of 20110305152740',
);
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 8ad9b8ca..b89fb3a7 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -38,7 +38,7 @@ abstract class ApiFormatBase extends ApiBase {
* Constructor
* If $format ends with 'fm', pretty-print the output in HTML.
* @param $main ApiMain
- * @param $format string Format name
+ * @param string $format Format name
*/
public function __construct( $main, $format ) {
parent::__construct( $main, $format );
@@ -58,7 +58,7 @@ abstract class ApiFormatBase extends ApiBase {
* This method is not called if getIsHtml() returns true.
* @return string
*/
- public abstract function getMimeType();
+ abstract public function getMimeType();
/**
* Whether this formatter needs raw data such as _element tags
@@ -83,9 +83,9 @@ abstract class ApiFormatBase extends ApiBase {
* special-case fix that should be removed once the help has been
* reworked to use a fully HTML interface.
*
- * @param $b bool Whether or not ampersands should be escaped.
+ * @param bool $b Whether or not ampersands should be escaped.
*/
- public function setUnescapeAmps ( $b ) {
+ public function setUnescapeAmps( $b ) {
$this->mUnescapeAmps = $b;
}
@@ -123,11 +123,13 @@ abstract class ApiFormatBase extends ApiBase {
/**
* 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
+ * This method must be the first outputting method during execution.
+ * A human-targeted notice about available formats is printed for the HTML-based output,
+ * except for help screens (caused by either an error in the API parameters,
+ * the calling of action=help, or requesting the root script api.php).
+ * @param bool $isHelpScreen Whether a help screen is going to be shown
*/
- function initPrinter( $isError ) {
+ function initPrinter( $isHelpScreen ) {
if ( $this->mDisabled ) {
return;
}
@@ -164,26 +166,29 @@ abstract class ApiFormatBase extends ApiBase {
<?php
- if ( !$isError ) {
+ if ( !$isHelpScreen ) {
?>
<br />
<small>
-You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br />
+You are looking at the HTML representation of the <?php echo $this->mFormat; ?> format.<br />
HTML is good for debugging, but is unsuitable for application use.<br />
Specify the format parameter to change the output format.<br />
-To see the non HTML representation of the <?php echo( $this->mFormat ); ?> format, set format=<?php echo( strtolower( $this->mFormat ) ); ?>.<br />
+To see the non HTML representation of the <?php echo $this->mFormat; ?> format, set format=<?php echo strtolower( $this->mFormat ); ?>.<br />
See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or
-<a href='<?php echo( $script ); ?>'>API help</a> for more information.
+<a href='<?php echo $script; ?>'>API help</a> for more information.
</small>
+<pre style='white-space: pre-wrap;'>
<?php
- }
+ } else { // don't wrap the contents of the <pre> for help screens
+ // because these are actually formatted to rely on
+ // the monospaced font for layout purposes
?>
<pre>
<?php
-
+ }
}
}
@@ -248,7 +253,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>,
}
/**
- * Sets whether the pretty-printer should format *bold* and $italics$
+ * Sets whether the pretty-printer should format *bold*
* @param $help bool
*/
public function setHelp( $help = true ) {
@@ -264,22 +269,19 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>,
protected function formatHTML( $text ) {
// Escape everything first for full coverage
$text = htmlspecialchars( $text );
-
// encode all comments or tags as safe blue strings
$text = str_replace( '&lt;', '<span style="color:blue;">&lt;', $text );
$text = str_replace( '&gt;', '&gt;</span>', $text );
- // identify URLs
- $protos = wfUrlProtocolsWithoutProtRel();
- // This regex hacks around bug 13218 (&quot; included in the URL)
- $text = preg_replace( "#(((?i)$protos).*?)(&quot;)?([ \\'\"<>\n]|&lt;|&gt;|&quot;)#", '<a href="\\1">\\1</a>\\3\\4', $text );
// identify requests to api.php
$text = preg_replace( "#api\\.php\\?[^ <\n\t]+#", '<a href="\\0">\\0</a>', $text );
if ( $this->mHelp ) {
// make strings inside * bold
$text = preg_replace( "#\\*[^<>\n]+\\*#", '<b>\\0</b>', $text );
- // make strings inside $ italic
- $text = preg_replace( "#\\$[^<>\n]+\\$#", '<b><i>\\0</i></b>', $text );
}
+ // identify URLs
+ $protos = wfUrlProtocolsWithoutProtRel();
+ // This regex hacks around bug 13218 (&quot; included in the URL)
+ $text = preg_replace( "#(((?i)$protos).*?)(&quot;)?([ \\'\"<>\n]|&lt;|&gt;|&quot;)#", '<a href="\\1">\\1</a>\\3\\4', $text );
/**
* Temporary fix for bad links in help messages. As a special case,
@@ -308,10 +310,6 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>,
public function getDescription() {
return $this->getIsHtml() ? ' (pretty-print in HTML)' : '';
}
-
- public static function getBaseVersion() {
- return __CLASS__ . ': $Id$';
- }
}
/**
@@ -328,7 +326,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
* Call this method to initialize output data. See execute()
* @param $result ApiResult
* @param $feed object an instance of one of the $wgFeedClasses classes
- * @param $feedItems array of FeedItem objects
+ * @param array $feedItems of FeedItem objects
*/
public static function setResult( $result, $feed, $feedItems ) {
// Store output in the Result data.
@@ -381,8 +379,4 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' );
}
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php
index 3d2a39ca..1b2e02c9 100644
--- a/includes/api/ApiFormatDbg.php
+++ b/includes/api/ApiFormatDbg.php
@@ -30,10 +30,6 @@
*/
class ApiFormatDbg extends ApiFormatBase {
- 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
@@ -48,8 +44,4 @@ class ApiFormatDbg extends ApiFormatBase {
public function getDescription() {
return 'Output data in PHP\'s var_export() format' . parent::getDescription();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatDump.php b/includes/api/ApiFormatDump.php
index 0f055e13..62253e14 100644
--- a/includes/api/ApiFormatDump.php
+++ b/includes/api/ApiFormatDump.php
@@ -30,10 +30,6 @@
*/
class ApiFormatDump extends ApiFormatBase {
- 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
@@ -52,8 +48,4 @@ class ApiFormatDump extends ApiFormatBase {
public function getDescription() {
return 'Output data in PHP\'s var_dump() format' . parent::getDescription();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index acbc7d3b..342a580f 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -56,42 +56,40 @@ class ApiFormatJson extends ApiFormatBase {
}
public function execute() {
- $prefix = $suffix = '';
-
$params = $this->extractRequestParams();
+ $json = FormatJson::encode(
+ $this->getResultData(),
+ $this->getIsHtml(),
+ $params['utf8'] ? FormatJson::ALL_OK : FormatJson::XMLMETA_OK
+ );
$callback = $params['callback'];
- if ( !is_null( $callback ) ) {
- $prefix = preg_replace( "/[^][.\\'\\\"_A-Za-z0-9]/", '', $callback ) . '(';
- $suffix = ')';
+ if ( $callback !== null ) {
+ $callback = preg_replace( "/[^][.\\'\\\"_A-Za-z0-9]/", '', $callback );
+ $this->printText( "$callback($json)" );
+ } else {
+ $this->printText( $json );
}
- $this->printText(
- $prefix .
- FormatJson::encode( $this->getResultData(), $this->getIsHtml() ) .
- $suffix
- );
}
public function getAllowedParams() {
return array(
- 'callback' => null,
+ 'callback' => null,
+ 'utf8' => false,
);
}
public function getParamDescription() {
return array(
'callback' => 'If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.',
+ 'utf8' => 'If specified, encodes most (but not all) non-ASCII characters as UTF-8 instead of replacing them with hexadecimal escape sequences.',
);
}
public function getDescription() {
if ( $this->mIsRaw ) {
- return 'Output data with the debuging elements in JSON format' . parent::getDescription();
+ return 'Output data with the debugging elements in JSON format' . parent::getDescription();
} else {
return 'Output data in JSON format' . parent::getDescription();
}
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/HttpFunctions.old.php b/includes/api/ApiFormatNone.php
index feb9b93c..78023af3 100644
--- a/includes/HttpFunctions.old.php
+++ b/includes/api/ApiFormatNone.php
@@ -1,6 +1,10 @@
<?php
/**
- * Class alias kept for backward compatibility.
+ *
+ *
+ * Created on Oct 22, 2006
+ *
+ * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,16 +22,22 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup HTTP
*/
/**
- * HttpRequest was renamed to MWHttpRequest in order
- * to prevent conflicts with PHP's HTTP extension
- * which also defines an HttpRequest class.
- * http://www.php.net/manual/en/class.httprequest.php
- *
- * This is for backwards compatibility.
- * @since 1.17
+ * API Serialized PHP output formatter
+ * @ingroup API
*/
-class HttpRequest extends MWHttpRequest { }
+class ApiFormatNone extends ApiFormatBase {
+
+ public function getMimeType() {
+ return 'text/plain';
+ }
+
+ public function execute() {
+ }
+
+ public function getDescription() {
+ return 'Output nothing' . parent::getDescription();
+ }
+}
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index fac2ca58..b2d1f044 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -30,10 +30,6 @@
*/
class ApiFormatPhp extends ApiFormatBase {
- public function __construct( $main, $format ) {
- parent::__construct( $main, $format );
- }
-
public function getMimeType() {
return 'application/vnd.php.serialized';
}
@@ -45,8 +41,4 @@ class ApiFormatPhp extends ApiFormatBase {
public function getDescription() {
return 'Output data in serialized PHP format' . parent::getDescription();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php
index 184f0a34..d278efa0 100644
--- a/includes/api/ApiFormatRaw.php
+++ b/includes/api/ApiFormatRaw.php
@@ -66,8 +66,4 @@ class ApiFormatRaw extends ApiFormatBase {
}
$this->printText( $data['text'] );
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php
index 71414593..4130e70c 100644
--- a/includes/api/ApiFormatTxt.php
+++ b/includes/api/ApiFormatTxt.php
@@ -30,10 +30,6 @@
*/
class ApiFormatTxt extends ApiFormatBase {
- 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
@@ -48,8 +44,4 @@ class ApiFormatTxt extends ApiFormatBase {
public function getDescription() {
return 'Output data in PHP\'s print_r() format' . parent::getDescription();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index 65056e44..5685d937 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -30,10 +30,6 @@
*/
class ApiFormatWddx extends ApiFormatBase {
- public function __construct( $main, $format ) {
- parent::__construct( $main, $format );
- }
-
public function getMimeType() {
return 'text/xml';
}
@@ -50,7 +46,7 @@ class ApiFormatWddx extends ApiFormatBase {
} else {
// Don't do newlines and indentation if we weren't asked
// for pretty output
- $nl = ( $this->getIsHtml() ? '' : "\n" );
+ $nl = ( $this->getIsHtml() ? "\n" : '' );
$indstr = ' ';
$this->printText( "<?xml version=\"1.0\"?>$nl" );
$this->printText( "<wddxPacket version=\"1.0\">$nl" );
@@ -68,52 +64,47 @@ class ApiFormatWddx extends ApiFormatBase {
* @param $indent int
*/
function slowWddxPrinter( $elemValue, $indent = 0 ) {
- $indstr = ( $this->getIsHtml() ? '' : str_repeat( ' ', $indent ) );
- $indstr2 = ( $this->getIsHtml() ? '' : str_repeat( ' ', $indent + 2 ) );
- $nl = ( $this->getIsHtml() ? '' : "\n" );
- switch ( gettype( $elemValue ) ) {
- case 'array':
- // Check whether we've got an associative array (<struct>)
- // or a regular array (<array>)
- $cnt = count( $elemValue );
- if ( $cnt == 0 || array_keys( $elemValue ) === range( 0, $cnt - 1 ) ) {
- // Regular array
- $this->printText( $indstr . Xml::element( 'array', array(
- 'length' => $cnt ), null ) . $nl );
- foreach ( $elemValue as $subElemValue ) {
- $this->slowWddxPrinter( $subElemValue, $indent + 2 );
- }
- $this->printText( "$indstr</array>$nl" );
- } else {
- // Associative array (<struct>)
- $this->printText( "$indstr<struct>$nl" );
- foreach ( $elemValue as $subElemName => $subElemValue ) {
- $this->printText( $indstr2 . Xml::element( 'var', array(
- 'name' => $subElemName
- ), null ) . $nl );
- $this->slowWddxPrinter( $subElemValue, $indent + 4 );
- $this->printText( "$indstr2</var>$nl" );
- }
- $this->printText( "$indstr</struct>$nl" );
+ $indstr = ( $this->getIsHtml() ? str_repeat( ' ', $indent ) : '' );
+ $indstr2 = ( $this->getIsHtml() ? str_repeat( ' ', $indent + 2 ) : '' );
+ $nl = ( $this->getIsHtml() ? "\n" : '' );
+ if ( is_array( $elemValue ) ) {
+ // Check whether we've got an associative array (<struct>)
+ // or a regular array (<array>)
+ $cnt = count( $elemValue );
+ if ( $cnt == 0 || array_keys( $elemValue ) === range( 0, $cnt - 1 ) ) {
+ // Regular array
+ $this->printText( $indstr . Xml::element( 'array', array(
+ 'length' => $cnt ), null ) . $nl );
+ foreach ( $elemValue as $subElemValue ) {
+ $this->slowWddxPrinter( $subElemValue, $indent + 2 );
+ }
+ $this->printText( "$indstr</array>$nl" );
+ } else {
+ // Associative array (<struct>)
+ $this->printText( "$indstr<struct>$nl" );
+ foreach ( $elemValue as $subElemName => $subElemValue ) {
+ $this->printText( $indstr2 . Xml::element( 'var', array(
+ 'name' => $subElemName
+ ), null ) . $nl );
+ $this->slowWddxPrinter( $subElemValue, $indent + 4 );
+ $this->printText( "$indstr2</var>$nl" );
}
- break;
- case 'integer':
- case 'double':
- $this->printText( $indstr . Xml::element( 'number', null, $elemValue ) . $nl );
- break;
- case 'string':
- $this->printText( $indstr . Xml::element( 'string', null, $elemValue ) . $nl );
- break;
- default:
- ApiBase::dieDebug( __METHOD__, 'Unknown type ' . gettype( $elemValue ) );
+ $this->printText( "$indstr</struct>$nl" );
+ }
+ } elseif ( is_int( $elemValue ) || is_float( $elemValue ) ) {
+ $this->printText( $indstr . Xml::element( 'number', null, $elemValue ) . $nl );
+ } elseif ( is_string( $elemValue ) ) {
+ $this->printText( $indstr . Xml::element( 'string', null, $elemValue ) . $nl );
+ } elseif ( is_bool( $elemValue ) ) {
+ $this->printText( $indstr . Xml::element( 'boolean',
+ array( 'value' => $elemValue ? 'true' : 'false' ) ) . $nl
+ );
+ } else {
+ ApiBase::dieDebug( __METHOD__, 'Unknown type ' . gettype( $elemValue ) );
}
}
public function getDescription() {
return 'Output data in WDDX format' . parent::getDescription();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index 5ccf1859..4ec149c0 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -32,14 +32,9 @@ class ApiFormatXml extends ApiFormatBase {
private $mRootElemName = 'api';
public static $namespace = 'http://www.mediawiki.org/xml/api/';
- private $mDoubleQuote = false;
private $mIncludeNamespace = false;
private $mXslt = null;
- public function __construct( $main, $format ) {
- parent::__construct( $main, $format );
- }
-
public function getMimeType() {
return 'text/xml';
}
@@ -54,7 +49,6 @@ class ApiFormatXml extends ApiFormatBase {
public function execute() {
$params = $this->extractRequestParams();
- $this->mDoubleQuote = $params['xmldoublequote'];
$this->mIncludeNamespace = $params['includexmlnamespace'];
$this->mXslt = $params['xslt'];
@@ -75,8 +69,7 @@ class ApiFormatXml extends ApiFormatBase {
$this->printText(
self::recXmlPrint( $this->mRootElemName,
$data,
- $this->getIsHtml() ? - 2 : null,
- $this->mDoubleQuote
+ $this->getIsHtml() ? - 2 : null
)
);
}
@@ -92,7 +85,7 @@ class ApiFormatXml extends ApiFormatBase {
*
* @par Example:
* @verbatim
- * name='root', value = array( '_element'=>'page', 'x', 'y', 'z')
+ * name='root', value = array( '_element'=>'page', 'x', 'y', 'z')
* @endverbatim
* creates:
* @verbatim
@@ -105,7 +98,7 @@ class ApiFormatXml extends ApiFormatBase {
*
* @par Example:
* @verbatim
- * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10)
+ * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10)
* @endverbatim
* creates:
* @verbatim
@@ -121,11 +114,10 @@ class ApiFormatXml extends ApiFormatBase {
* @param $elemName
* @param $elemValue
* @param $indent
- * @param $doublequote bool
*
* @return string
*/
- public static function recXmlPrint( $elemName, $elemValue, $indent, $doublequote = false ) {
+ public static function recXmlPrint( $elemName, $elemValue, $indent ) {
$retval = '';
if ( !is_null( $indent ) ) {
$indent += 2;
@@ -135,78 +127,71 @@ class ApiFormatXml extends ApiFormatBase {
}
$elemName = str_replace( ' ', '_', $elemName );
- switch ( gettype( $elemValue ) ) {
- case 'array':
- if ( isset( $elemValue['*'] ) ) {
- $subElemContent = $elemValue['*'];
- if ( $doublequote ) {
- $subElemContent = Sanitizer::encodeAttribute( $subElemContent );
- }
- unset( $elemValue['*'] );
-
- // Add xml:space="preserve" to the
- // element so XML parsers will leave
- // whitespace in the content alone
- $elemValue['xml:space'] = 'preserve';
- } else {
- $subElemContent = null;
+ if ( is_array( $elemValue ) ) {
+ if ( isset( $elemValue['*'] ) ) {
+ $subElemContent = $elemValue['*'];
+ unset( $elemValue['*'] );
+
+ // Add xml:space="preserve" to the
+ // element so XML parsers will leave
+ // whitespace in the content alone
+ $elemValue['xml:space'] = 'preserve';
+ } else {
+ $subElemContent = null;
+ }
+
+ if ( isset( $elemValue['_element'] ) ) {
+ $subElemIndName = $elemValue['_element'];
+ unset( $elemValue['_element'] );
+ } else {
+ $subElemIndName = null;
+ }
+
+ $indElements = array();
+ $subElements = array();
+ foreach ( $elemValue as $subElemId => & $subElemValue ) {
+ if ( is_int( $subElemId ) ) {
+ $indElements[] = $subElemValue;
+ unset( $elemValue[$subElemId] );
+ } elseif ( is_array( $subElemValue ) ) {
+ $subElements[$subElemId] = $subElemValue;
+ unset( $elemValue[$subElemId] );
}
+ }
- if ( isset( $elemValue['_element'] ) ) {
- $subElemIndName = $elemValue['_element'];
- unset( $elemValue['_element'] );
- } else {
- $subElemIndName = null;
- }
+ if ( is_null( $subElemIndName ) && count( $indElements ) ) {
+ ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()." );
+ }
- $indElements = array();
- $subElements = array();
- foreach ( $elemValue as $subElemId => & $subElemValue ) {
- if ( is_string( $subElemValue ) && $doublequote ) {
- $subElemValue = Sanitizer::encodeAttribute( $subElemValue );
- }
-
- if ( gettype( $subElemId ) === 'integer' ) {
- $indElements[] = $subElemValue;
- unset( $elemValue[$subElemId] );
- } elseif ( is_array( $subElemValue ) ) {
- $subElements[$subElemId] = $subElemValue;
- unset ( $elemValue[$subElemId] );
- }
- }
+ if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) ) {
+ ApiBase::dieDebug( __METHOD__, "($elemName, ...) has content and subelements" );
+ }
- if ( is_null( $subElemIndName ) && count( $indElements ) ) {
- ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()." );
- }
+ if ( !is_null( $subElemContent ) ) {
+ $retval .= $indstr . Xml::element( $elemName, $elemValue, $subElemContent );
+ } elseif ( !count( $indElements ) && !count( $subElements ) ) {
+ $retval .= $indstr . Xml::element( $elemName, $elemValue );
+ } else {
+ $retval .= $indstr . Xml::element( $elemName, $elemValue, null );
- if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) ) {
- ApiBase::dieDebug( __METHOD__, "($elemName, ...) has content and subelements" );
+ foreach ( $subElements as $subElemId => & $subElemValue ) {
+ $retval .= self::recXmlPrint( $subElemId, $subElemValue, $indent );
}
- if ( !is_null( $subElemContent ) ) {
- $retval .= $indstr . Xml::element( $elemName, $elemValue, $subElemContent );
- } elseif ( !count( $indElements ) && !count( $subElements ) ) {
- $retval .= $indstr . Xml::element( $elemName, $elemValue );
- } else {
- $retval .= $indstr . Xml::element( $elemName, $elemValue, null );
-
- foreach ( $subElements as $subElemId => & $subElemValue ) {
- $retval .= self::recXmlPrint( $subElemId, $subElemValue, $indent );
- }
-
- foreach ( $indElements as &$subElemValue ) {
- $retval .= self::recXmlPrint( $subElemIndName, $subElemValue, $indent );
- }
-
- $retval .= $indstr . Xml::closeElement( $elemName );
+ foreach ( $indElements as &$subElemValue ) {
+ $retval .= self::recXmlPrint( $subElemIndName, $subElemValue, $indent );
}
- break;
- case 'object':
- // ignore
- break;
- default:
+
+ $retval .= $indstr . Xml::closeElement( $elemName );
+ }
+ } elseif ( !is_object( $elemValue ) ) {
+ // to make sure null value doesn't produce unclosed element,
+ // which is what Xml::element( $elemName, null, null ) returns
+ if ( $elemValue === null ) {
+ $retval .= $indstr . Xml::element( $elemName );
+ } else {
$retval .= $indstr . Xml::element( $elemName, null, $elemValue );
- break;
+ }
}
return $retval;
}
@@ -230,7 +215,6 @@ class ApiFormatXml extends ApiFormatBase {
public function getAllowedParams() {
return array(
- 'xmldoublequote' => false,
'xslt' => null,
'includexmlnamespace' => false,
);
@@ -238,7 +222,6 @@ class ApiFormatXml extends ApiFormatBase {
public function getParamDescription() {
return array(
- 'xmldoublequote' => 'If specified, double quotes all attributes and content',
'xslt' => 'If specified, adds <xslt> as stylesheet. This should be a wiki page '
. 'in the MediaWiki namespace whose page name ends with ".xsl"',
'includexmlnamespace' => 'If specified, adds an XML namespace'
@@ -248,8 +231,4 @@ class ApiFormatXml extends ApiFormatBase {
public function getDescription() {
return 'Output data in XML format' . parent::getDescription();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php
index 730ad8ea..700d4a5e 100644
--- a/includes/api/ApiFormatYaml.php
+++ b/includes/api/ApiFormatYaml.php
@@ -35,10 +35,6 @@ class ApiFormatYaml extends ApiFormatJson {
}
public function getDescription() {
- return 'Output data in YAML format' . parent::getDescription();
- }
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ return 'Output data in YAML format' . ApiFormatBase::getDescription();
}
}
diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php
index 2b5de21a..9cafc5bb 100644
--- a/includes/api/ApiHelp.php
+++ b/includes/api/ApiHelp.php
@@ -31,10 +31,6 @@
*/
class ApiHelp extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* Module for displaying help
*/
@@ -47,43 +43,62 @@ class ApiHelp extends ApiBase {
}
$this->getMain()->setHelp();
-
$result = $this->getResult();
- $queryObj = new ApiQuery( $this->getMain(), 'query' );
- $r = array();
- if ( is_array( $params['modules'] ) ) {
- $modArr = $this->getMain()->getModules();
- foreach ( $params['modules'] as $m ) {
- if ( !isset( $modArr[$m] ) ) {
- $r[] = array( 'name' => $m, 'missing' => '' );
- continue;
- }
- $module = new $modArr[$m]( $this->getMain(), $m );
-
- $r[] = $this->buildModuleHelp( $module, 'action' );
- }
+ if ( is_array( $params['modules'] ) ) {
+ $modules = $params['modules'];
+ } else {
+ $modules = array();
}
if ( is_array( $params['querymodules'] ) ) {
- $qmodArr = $queryObj->getModules();
+ $queryModules = $params['querymodules'];
+ foreach ( $queryModules as $m ) {
+ $modules[] = 'query+' . $m;
+ }
+ } else {
+ $queryModules = array();
+ }
- foreach ( $params['querymodules'] as $qm ) {
- if ( !isset( $qmodArr[$qm] ) ) {
- $r[] = array( 'name' => $qm, 'missing' => '' );
- continue;
+ $r = array();
+ foreach ( $modules as $m ) {
+ // sub-modules could be given in the form of "name[+name[+name...]]"
+ $subNames = explode( '+', $m );
+ if ( count( $subNames ) === 1 ) {
+ // In case the '+' was typed into URL, it resolves as a space
+ $subNames = explode( ' ', $m );
+ }
+ $module = $this->getMain();
+ for ( $i = 0; $i < count( $subNames ); $i++ ) {
+ $subs = $module->getModuleManager();
+ if ( $subs === null ) {
+ $module = null;
+ } else {
+ $module = $subs->getModule( $subNames[$i] );
}
- $module = new $qmodArr[$qm]( $this, $qm );
- $type = $queryObj->getModuleType( $qm );
-
- if ( $type === null ) {
- $r[] = array( 'name' => $qm, 'missing' => '' );
- continue;
+ if ( $module === null ) {
+ if ( count( $subNames ) === 2
+ && $i === 1
+ && $subNames[0] === 'query'
+ && in_array( $subNames[1], $queryModules )
+ ) {
+ // Legacy: This is one of the renamed 'querymodule=...' parameters,
+ // do not use '+' notation in the output, use submodule's name instead.
+ $name = $subNames[1];
+ } else {
+ $name = implode( '+', array_slice( $subNames, 0, $i + 1 ) );
+ }
+ $r[] = array( 'name' => $name, 'missing' => '' );
+ break;
+ } else {
+ $type = $subs->getModuleGroup( $subNames[$i] );
}
-
+ }
+ if ( $module !== null ) {
$r[] = $this->buildModuleHelp( $module, $type );
}
}
+
$result->setIndexedTagName( $r, 'module' );
$result->addValue( null, $this->getModuleName(), $r );
}
@@ -118,15 +133,16 @@ class ApiHelp extends ApiBase {
ApiBase::PARAM_ISMULTI => true
),
'querymodules' => array(
- ApiBase::PARAM_ISMULTI => true
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_DEPRECATED => true
),
);
}
public function getParamDescription() {
return array(
- 'modules' => 'List of module names (value of the action= parameter)',
- 'querymodules' => 'List of query module names (value of prop=, meta= or list= parameter)',
+ 'modules' => 'List of module names (value of the action= parameter). Can specify submodules with a \'+\'',
+ 'querymodules' => 'Use modules=query+value instead. List of query module names (value of prop=, meta= or list= parameter)',
);
}
@@ -138,9 +154,8 @@ class ApiHelp extends ApiBase {
return array(
'api.php?action=help' => 'Whole help page',
'api.php?action=help&modules=protect' => 'Module (action) help page',
- 'api.php?action=help&querymodules=categorymembers' => 'Query (list) modules help page',
- 'api.php?action=help&querymodules=info' => 'Query (prop) modules help page',
- 'api.php?action=help&querymodules=siteinfo' => 'Query (meta) modules help page',
+ 'api.php?action=help&modules=query+categorymembers' => 'Help for the query/categorymembers module',
+ 'api.php?action=help&modules=login|query+info' => 'Help for the login and query/info modules',
);
}
@@ -151,8 +166,4 @@ class ApiHelp extends ApiBase {
'https://www.mediawiki.org/wiki/API:Quick_start_guide',
);
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php
new file mode 100644
index 00000000..7a60e831
--- /dev/null
+++ b/includes/api/ApiImageRotate.php
@@ -0,0 +1,230 @@
+<?php
+/**
+ *
+ * Created on January 3rd, 2013
+ *
+ * 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
+ */
+
+class ApiImageRotate extends ApiBase {
+ private $mPageSet = null;
+
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
+ }
+
+ /**
+ * Add all items from $values into the result
+ * @param array $result output
+ * @param array $values values to add
+ * @param string $flag the name of the boolean flag to mark this element
+ * @param string $name if given, name of the value
+ */
+ private static function addValues( array &$result, $values, $flag = null, $name = null ) {
+ foreach ( $values as $val ) {
+ if ( $val instanceof Title ) {
+ $v = array();
+ ApiQueryBase::addTitleInfo( $v, $val );
+ } elseif ( $name !== null ) {
+ $v = array( $name => $val );
+ } else {
+ $v = $val;
+ }
+ if ( $flag !== null ) {
+ $v[$flag] = '';
+ }
+ $result[] = $v;
+ }
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $rotation = $params['rotation'];
+
+ $pageSet = $this->getPageSet();
+ $pageSet->execute();
+
+ $result = array();
+
+ self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' );
+ self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' );
+ self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' );
+ self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' );
+ self::addValues( $result, $pageSet->getInterwikiTitlesAsResult() );
+
+ foreach ( $pageSet->getTitles() as $title ) {
+ $r = array();
+ $r['id'] = $title->getArticleID();
+ ApiQueryBase::addTitleInfo( $r, $title );
+ if ( !$title->exists() ) {
+ $r['missing'] = '';
+ }
+
+ $file = wfFindFile( $title );
+ if ( !$file ) {
+ $r['result'] = 'Failure';
+ $r['errormessage'] = 'File does not exist';
+ $result[] = $r;
+ continue;
+ }
+ $handler = $file->getHandler();
+ if ( !$handler || !$handler->canRotate() ) {
+ $r['result'] = 'Failure';
+ $r['errormessage'] = 'File type cannot be rotated';
+ $result[] = $r;
+ continue;
+ }
+
+ // Check whether we're allowed to rotate this file
+ $permError = $this->checkPermissions( $this->getUser(), $file->getTitle() );
+ if ( $permError !== null ) {
+ $r['result'] = 'Failure';
+ $r['errormessage'] = $permError;
+ $result[] = $r;
+ continue;
+ }
+
+ $srcPath = $file->getLocalRefPath();
+ if ( $srcPath === false ) {
+ $r['result'] = 'Failure';
+ $r['errormessage'] = 'Cannot get local file path';
+ $result[] = $r;
+ continue;
+ }
+ $ext = strtolower( pathinfo( "$srcPath", PATHINFO_EXTENSION ) );
+ $tmpFile = TempFSFile::factory( 'rotate_', $ext );
+ $dstPath = $tmpFile->getPath();
+ $err = $handler->rotate( $file, array(
+ "srcPath" => $srcPath,
+ "dstPath" => $dstPath,
+ "rotation" => $rotation
+ ) );
+ if ( !$err ) {
+ $comment = wfMessage(
+ 'rotate-comment'
+ )->numParams( $rotation )->inContentLanguage()->text();
+ $status = $file->upload( $dstPath,
+ $comment, $comment, 0, false, false, $this->getUser() );
+ if ( $status->isGood() ) {
+ $r['result'] = 'Success';
+ } else {
+ $r['result'] = 'Failure';
+ $r['errormessage'] = $this->getResult()->convertStatusToArray( $status );
+ }
+ } else {
+ $r['result'] = 'Failure';
+ $r['errormessage'] = $err->toText();
+ }
+ $result[] = $r;
+ }
+ $apiResult = $this->getResult();
+ $apiResult->setIndexedTagName( $result, 'page' );
+ $apiResult->addValue( null, $this->getModuleName(), $result );
+ }
+
+ /**
+ * Get a cached instance of an ApiPageSet object
+ * @return ApiPageSet
+ */
+ private function getPageSet() {
+ if ( $this->mPageSet === null ) {
+ $this->mPageSet = new ApiPageSet( $this, 0, NS_FILE );
+ }
+ return $this->mPageSet;
+ }
+
+ /**
+ * Checks that the user has permissions to perform rotations.
+ * @param User $user The user to check
+ * @param Title $title
+ * @return string|null Permission error message, or null if there is no error
+ */
+ protected function checkPermissions( $user, $title ) {
+ $permissionErrors = array_merge(
+ $title->getUserPermissionsErrors( 'edit', $user ),
+ $title->getUserPermissionsErrors( 'upload', $user )
+ );
+
+ if ( $permissionErrors ) {
+ // Just return the first error
+ $msg = $this->parseMsg( $permissionErrors[0] );
+ return $msg['info'];
+ }
+
+ return null;
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams( $flags = 0 ) {
+ $result = array(
+ 'rotation' => array(
+ ApiBase::PARAM_TYPE => array( '90', '180', '270' ),
+ ApiBase::PARAM_REQUIRED => true
+ ),
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
+ );
+ if ( $flags ) {
+ $result += $this->getPageSet()->getFinalParams( $flags );
+ }
+ return $result;
+ }
+
+ public function getParamDescription() {
+ $pageSet = $this->getPageSet();
+ return $pageSet->getFinalParamDescription() + array(
+ 'rotation' => 'Degrees to rotate image clockwise',
+ 'token' => 'Edit token. You can get one of these through action=tokens',
+ );
+ }
+
+ public function getDescription() {
+ return 'Rotate one or more images';
+ }
+
+ public function needsToken() {
+ return true;
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
+
+ public function getPossibleErrors() {
+ $pageSet = $this->getPageSet();
+ return array_merge(
+ parent::getPossibleErrors(),
+ $pageSet->getFinalPossibleErrors()
+ );
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=imagerotate&titles=Example.jpg&rotation=90&token=123ABC',
+ );
+ }
+}
diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php
index 637c1fff..f48a822e 100644
--- a/includes/api/ApiImport.php
+++ b/includes/api/ApiImport.php
@@ -31,10 +31,6 @@
*/
class ApiImport extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$user = $this->getUser();
$params = $this->extractRequestParams();
@@ -61,7 +57,7 @@ class ApiImport extends ApiBase {
$source = ImportStreamSource::newFromUpload( 'xml' );
}
if ( !$source->isOK() ) {
- $this->dieUsageMsg( $source->getErrorsArray() );
+ $this->dieStatus( $source );
}
$importer = new WikiImporter( $source->value );
@@ -70,8 +66,8 @@ class ApiImport extends ApiBase {
}
if ( isset( $params['rootpage'] ) ) {
$statusRootPage = $importer->setTargetRootPage( $params['rootpage'] );
- if( !$statusRootPage->isGood() ) {
- $this->dieUsageMsg( $statusRootPage->getErrorsArray() );
+ if ( !$statusRootPage->isGood() ) {
+ $this->dieStatus( $statusRootPage );
}
}
$reporter = new ApiImportReporter(
@@ -109,7 +105,9 @@ class ApiImport extends ApiBase {
ApiBase::PARAM_REQUIRED => true
),
'summary' => null,
- 'xml' => null,
+ 'xml' => array(
+ ApiBase::PARAM_TYPE => 'upload',
+ ),
'interwikisource' => array(
ApiBase::PARAM_TYPE => $wgImportSources
),
@@ -150,7 +148,7 @@ class ApiImport extends ApiBase {
public function getDescription() {
return array(
- 'Import a page from another wiki, or an XML file.' ,
+ 'Import a page from another wiki, or an XML file.',
'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when',
'sending a file for the "xml" parameter.'
);
@@ -186,10 +184,6 @@ class ApiImport extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Import';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
/**
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index 1f91fe92..b51d441d 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -38,7 +38,7 @@ class ApiLogin extends ApiBase {
/**
* Executes the log-in attempt using the parameters passed. If
- * the log-in succeeeds, it attaches a cookie to the session
+ * the log-in succeeds, it attaches a cookie to the session
* and outputs the user id, username, and session token. If a
* log-in fails, as the result of a bad password, a nonexistent
* user, or any other reason, the host is cached with an expiry
@@ -46,6 +46,15 @@ class ApiLogin extends ApiBase {
* is reached. The expiry is $this->mLoginThrottle.
*/
public function execute() {
+ // If we're in JSON callback mode, no tokens can be obtained
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) {
+ $this->getResult()->addValue( null, 'login', array(
+ 'result' => 'Aborted',
+ 'reason' => 'Cannot log in when using a callback',
+ ) );
+ return;
+ }
+
$params = $this->extractRequestParams();
$result = array();
@@ -147,7 +156,7 @@ class ApiLogin extends ApiBase {
case LoginForm::ABORTED:
$result['result'] = 'Aborted';
- $result['reason'] = $loginForm->mAbortLoginErrorMsg;
+ $result['reason'] = $loginForm->mAbortLoginErrorMsg;
break;
default:
@@ -278,8 +287,4 @@ class ApiLogin extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Login';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
index b2f634d0..2ba92a63 100644
--- a/includes/api/ApiLogout.php
+++ b/includes/api/ApiLogout.php
@@ -32,10 +32,6 @@
*/
class ApiLogout extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$user = $this->getUser();
$oldName = $user->getName();
@@ -75,8 +71,4 @@ class ApiLogout extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Logout';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 35febd95..c11f16cb 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -51,6 +51,7 @@ class ApiMain extends ApiBase {
private static $Modules = array(
'login' => 'ApiLogin',
'logout' => 'ApiLogout',
+ 'createaccount' => 'ApiCreateAccount',
'query' => 'ApiQuery',
'expandtemplates' => 'ApiExpandTemplates',
'parse' => 'ApiParse',
@@ -82,6 +83,7 @@ class ApiMain extends ApiBase {
'import' => 'ApiImport',
'userrights' => 'ApiUserrights',
'options' => 'ApiOptions',
+ 'imagerotate' => 'ApiImageRotate',
);
/**
@@ -105,6 +107,7 @@ class ApiMain extends ApiBase {
'dbgfm' => 'ApiFormatDbg',
'dump' => 'ApiFormatDump',
'dumpfm' => 'ApiFormatDump',
+ 'none' => 'ApiFormatNone',
);
/**
@@ -118,7 +121,7 @@ class ApiMain extends ApiBase {
'msg' => 'Use of the write API',
'params' => array()
),
- 'apihighlimits' => 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( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 )
)
@@ -129,18 +132,20 @@ class ApiMain extends ApiBase {
*/
private $mPrinter;
- private $mModules, $mModuleNames, $mFormats, $mFormatNames;
- private $mResult, $mAction, $mShowVersions, $mEnableWrite;
+ private $mModuleMgr, $mResult;
+ private $mAction;
+ private $mEnableWrite;
private $mInternalMode, $mSquidMaxage, $mModule;
private $mCacheMode = 'private';
private $mCacheControl = array();
+ private $mParamsUsed = array();
/**
* Constructs an instance of ApiMain that utilizes the module and format specified by $request.
*
* @param $context IContextSource|WebRequest - 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
+ * @param bool $enableWrite should be set to true if the api may modify data
*/
public function __construct( $context = null, $enableWrite = false ) {
if ( $context === null ) {
@@ -168,7 +173,7 @@ class ApiMain extends ApiBase {
// Remove all modules other than login
global $wgUser;
- if ( $this->getRequest()->getVal( 'callback' ) !== null ) {
+ if ( $this->getVal( 'callback' ) !== null ) {
// JSON callback allows cross-site reads.
// For safety, strip user credentials.
wfDebug( "API: stripping user credentials for JSON callback\n" );
@@ -177,15 +182,13 @@ class ApiMain extends ApiBase {
}
}
- global $wgAPIModules; // extension modules
- $this->mModules = $wgAPIModules + self::$Modules;
-
- $this->mModuleNames = array_keys( $this->mModules );
- $this->mFormats = self::$Formats;
- $this->mFormatNames = array_keys( $this->mFormats );
+ global $wgAPIModules;
+ $this->mModuleMgr = new ApiModuleManager( $this );
+ $this->mModuleMgr->addModules( self::$Modules, 'action' );
+ $this->mModuleMgr->addModules( $wgAPIModules, 'action' );
+ $this->mModuleMgr->addModules( self::$Formats, 'format' );
$this->mResult = new ApiResult( $this );
- $this->mShowVersions = false;
$this->mEnableWrite = $enableWrite;
$this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling()
@@ -242,7 +245,7 @@ class ApiMain extends ApiBase {
/**
* Set the type of caching headers which will be sent.
*
- * @param $mode String One of:
+ * @param string $mode One of:
* - 'public': Cache this object in public caches, if the maxage or smaxage
* parameter is set, or if setCacheMaxAge() was called. If a maximum age is
* not provided by any of these means, the object will be private.
@@ -271,7 +274,7 @@ class ApiMain extends ApiBase {
return;
}
- if ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) ) {
+ if ( !User::isEveryoneAllowed( 'read' ) ) {
// Private wiki, only private headers
if ( $mode !== 'private' ) {
wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
@@ -330,10 +333,11 @@ class ApiMain extends ApiBase {
* @return ApiFormatBase
*/
public function createPrinterByName( $format ) {
- if ( !isset( $this->mFormats[$format] ) ) {
+ $printer = $this->mModuleMgr->getModule( $format, 'format' );
+ if ( $printer === null ) {
$this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
}
- return new $this->mFormats[$format] ( $this, $format );
+ return $printer;
}
/**
@@ -361,10 +365,17 @@ class ApiMain extends ApiBase {
return;
}
+ // Exit here if the request method was OPTIONS
+ // (assume there will be a followup GET or POST)
+ if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
+ return;
+ }
+
// In case an error occurs during data output,
// clear the output buffer and print just the error information
ob_start();
+ $t = microtime( true );
try {
$this->executeAction();
} catch ( Exception $e ) {
@@ -373,10 +384,10 @@ class ApiMain extends ApiBase {
// Log it
if ( !( $e instanceof UsageException ) ) {
- wfDebugLog( 'exception', $e->getLogMessage() );
+ MWExceptionHandler::logException( $e );
}
- // Handle any kind of exception by outputing properly formatted error message.
+ // Handle any kind of exception by outputting properly formatted error message.
// If this fails, an unhandled exception should be thrown so that global error
// handler will process and log it.
@@ -401,6 +412,9 @@ class ApiMain extends ApiBase {
$this->printResult( true );
}
+ // Log the request whether or not there was an error
+ $this->logRequest( microtime( true ) - $t );
+
// Send cache headers after any code which might generate an error, to
// avoid sending public cache headers for errors.
$this->sendCacheHeaders();
@@ -461,9 +475,9 @@ class ApiMain extends ApiBase {
/**
* Attempt to match an Origin header against a set of rules and a set of exceptions
- * @param $value string Origin header
- * @param $rules array Set of wildcard rules
- * @param $exceptions array Set of wildcard rules
+ * @param string $value Origin header
+ * @param array $rules Set of wildcard rules
+ * @param array $exceptions Set of wildcard rules
* @return bool True if $value matches a rule in $rules and doesn't match any rules in $exceptions, false otherwise
*/
protected static function matchOrigin( $value, $rules, $exceptions ) {
@@ -486,7 +500,7 @@ class ApiMain extends ApiBase {
* '*' => '.*?'
* '?' => '.'
*
- * @param $wildcard string String with wildcards
+ * @param string $wildcard String with wildcards
* @return string Regular expression
*/
protected static function wildcardToRegex( $wildcard ) {
@@ -590,10 +604,10 @@ class ApiMain extends ApiBase {
$result = $this->getResult();
// Printer may not be initialized if the extractRequestParams() fails for the main module
- if ( !isset ( $this->mPrinter ) ) {
+ 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 ) ) {
+ if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
$value = self::API_DEFAULT_FORMAT;
}
@@ -611,7 +625,6 @@ class ApiMain extends ApiBase {
if ( $this->mPrinter->getWantsHelp() || $this->mAction == 'help' ) {
ApiResult::setContent( $errMessage, $this->makeHelpMsg() );
}
-
} else {
global $wgShowSQLErrors, $wgShowExceptionDetails;
// Something is seriously wrong
@@ -628,6 +641,10 @@ class ApiMain extends ApiBase {
ApiResult::setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' );
}
+ // Remember all the warnings to re-add them later
+ $oldResult = $result->getData();
+ $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
+
$result->reset();
$result->disableSizeCheck();
// Re-add the id
@@ -635,11 +652,13 @@ class ApiMain extends ApiBase {
if ( !is_null( $requestid ) ) {
$result->addValue( null, 'requestid', $requestid );
}
-
if ( $wgShowHostnames ) {
// servedby is especially useful when debugging errors
$result->addValue( null, 'servedby', wfHostName() );
}
+ if ( $warnings !== null ) {
+ $result->addValue( null, 'warnings', $warnings );
+ }
$result->addValue( null, 'error', $errMessage );
@@ -669,7 +688,6 @@ class ApiMain extends ApiBase {
$params = $this->extractRequestParams();
- $this->mShowVersions = $params['version'];
$this->mAction = $params['action'];
if ( !is_string( $this->mAction ) ) {
@@ -685,20 +703,15 @@ class ApiMain extends ApiBase {
*/
protected function setupModule() {
// Instantiate the module requested by the user
- $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)
- if ( isset( $moduleParams['gettoken'] ) ) {
- $gettoken = $moduleParams['gettoken'];
- } else {
- $gettoken = false;
+ $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
+ if ( $module === null ) {
+ $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
}
+ $moduleParams = $module->extractRequestParams();
+ // Die if token required, but not provided
$salt = $module->getTokenSalt();
- if ( $salt !== false && !$gettoken ) {
+ if ( $salt !== false ) {
if ( !isset( $moduleParams['token'] ) ) {
$this->dieUsageMsg( array( 'missingparam', 'token' ) );
} else {
@@ -713,7 +726,7 @@ class ApiMain extends ApiBase {
/**
* Check the max lag if necessary
* @param $module ApiBase object: Api module being used
- * @param $params Array an array containing the request parameters.
+ * @param array $params an array containing the request parameters.
* @return boolean True on success, false should exit immediately
*/
protected function checkMaxLag( $module, $params ) {
@@ -745,7 +758,7 @@ class ApiMain extends ApiBase {
*/
protected function checkExecutePermissions( $module ) {
$user = $this->getUser();
- if ( $module->isReadMode() && !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) &&
+ if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
!$user->isAllowed( 'read' ) )
{
$this->dieUsageMsg( 'readrequired' );
@@ -764,7 +777,7 @@ class ApiMain extends ApiBase {
// Allow extensions to stop execution for arbitrary reasons.
$message = false;
- if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
+ if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
$this->dieUsageMsg( $message );
}
}
@@ -772,12 +785,13 @@ class ApiMain extends ApiBase {
/**
* Check POST for external response and setup result printer
* @param $module ApiBase An Api module
- * @param $params Array an array with the request parameters
+ * @param array $params an array with the request parameters
*/
protected function setupExternalResponse( $module, $params ) {
- // Ignore mustBePosted() for internal calls
- if ( $module->mustBePosted() && !$this->getRequest()->wasPosted() ) {
- $this->dieUsageMsg( array( 'mustbeposted', $this->mAction ) );
+ if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
+ // Module requires POST. GET request might still be allowed
+ // if $wgDebugApi is true, otherwise fail.
+ $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) );
}
// See if custom printer is used
@@ -798,6 +812,7 @@ class ApiMain extends ApiBase {
protected function executeAction() {
$params = $this->setupExecuteAction();
$module = $this->setupModule();
+ $this->mModule = $module;
$this->checkExecutePermissions( $module );
@@ -815,6 +830,8 @@ class ApiMain extends ApiBase {
wfRunHooks( 'APIAfterExecute', array( &$module ) );
$module->profileOut();
+ $this->reportUnusedParams();
+
if ( !$this->mInternalMode ) {
//append Debug information
MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
@@ -825,11 +842,120 @@ class ApiMain extends ApiBase {
}
/**
+ * Log the preceding request
+ * @param $time Time in seconds
+ */
+ protected function logRequest( $time ) {
+ $request = $this->getRequest();
+ $milliseconds = $time === null ? '?' : round( $time * 1000 );
+ $s = 'API' .
+ ' ' . $request->getMethod() .
+ ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
+ ' ' . $request->getIP() .
+ ' T=' . $milliseconds . 'ms';
+ foreach ( $this->getParamsUsed() as $name ) {
+ $value = $request->getVal( $name );
+ if ( $value === null ) {
+ continue;
+ }
+ $s .= ' ' . $name . '=';
+ if ( strlen( $value ) > 256 ) {
+ $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
+ $s .= $encValue . '[...]';
+ } else {
+ $s .= $this->encodeRequestLogValue( $value );
+ }
+ }
+ $s .= "\n";
+ wfDebugLog( 'api', $s, false );
+ }
+
+ /**
+ * Encode a value in a format suitable for a space-separated log line.
+ */
+ protected function encodeRequestLogValue( $s ) {
+ static $table;
+ if ( !$table ) {
+ $chars = ';@$!*(),/:';
+ for ( $i = 0; $i < strlen( $chars ); $i++ ) {
+ $table[rawurlencode( $chars[$i] )] = $chars[$i];
+ }
+ }
+ return strtr( rawurlencode( $s ), $table );
+ }
+
+ /**
+ * Get the request parameters used in the course of the preceding execute() request
+ */
+ protected function getParamsUsed() {
+ return array_keys( $this->mParamsUsed );
+ }
+
+ /**
+ * Get a request value, and register the fact that it was used, for logging.
+ */
+ public function getVal( $name, $default = null ) {
+ $this->mParamsUsed[$name] = true;
+ return $this->getRequest()->getVal( $name, $default );
+ }
+
+ /**
+ * Get a boolean request value, and register the fact that the parameter
+ * was used, for logging.
+ */
+ public function getCheck( $name ) {
+ $this->mParamsUsed[$name] = true;
+ return $this->getRequest()->getCheck( $name );
+ }
+
+ /**
+ * Get a request upload, and register the fact that it was used, for logging.
+ *
+ * @since 1.21
+ * @param string $name Parameter name
+ * @return WebRequestUpload
+ */
+ public function getUpload( $name ) {
+ $this->mParamsUsed[$name] = true;
+ return $this->getRequest()->getUpload( $name );
+ }
+
+ /**
+ * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
+ * for example in case of spelling mistakes or a missing 'g' prefix for generators.
+ */
+ protected function reportUnusedParams() {
+ $paramsUsed = $this->getParamsUsed();
+ $allParams = $this->getRequest()->getValueNames();
+
+ if ( !$this->mInternalMode ) {
+ // Printer has not yet executed; don't warn that its parameters are unused
+ $printerParams = array_map(
+ array( $this->mPrinter, 'encodeParamName' ),
+ array_keys( $this->mPrinter->getFinalParams() ?: array() )
+ );
+ $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
+ } else {
+ $unusedParams = array_diff( $allParams, $paramsUsed );
+ }
+
+ if ( count( $unusedParams ) ) {
+ $s = count( $unusedParams ) > 1 ? 's' : '';
+ $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
+ }
+ }
+
+ /**
* Print results using the current printer
*
* @param $isError bool
*/
protected function printResult( $isError ) {
+ global $wgDebugAPI;
+ if ( $wgDebugAPI !== false ) {
+ $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
+ }
+
$this->getResult()->cleanUpUTF8();
$printer = $this->mPrinter;
$printer->profileIn();
@@ -839,10 +965,10 @@ class ApiMain extends ApiBase {
* tell the printer not to escape ampersands so that our links do
* not break.
*/
- $printer->setUnescapeAmps( ( $this->mAction == 'help' || $isError )
- && $printer->getFormat() == 'XML' && $printer->getIsHtml() );
+ $isHelp = $isError || $this->mAction == 'help';
+ $printer->setUnescapeAmps( $isHelp && $printer->getFormat() == 'XML' && $printer->getIsHtml() );
- $printer->initPrinter( $isError );
+ $printer->initPrinter( $isHelp );
$printer->execute();
$printer->closePrinter();
@@ -865,14 +991,13 @@ class ApiMain extends ApiBase {
return array(
'format' => array(
ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
- ApiBase::PARAM_TYPE => $this->mFormatNames
+ ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' )
),
'action' => array(
ApiBase::PARAM_DFLT => 'help',
- ApiBase::PARAM_TYPE => $this->mModuleNames
+ ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' )
),
- 'version' => false,
- 'maxlag' => array(
+ 'maxlag' => array(
ApiBase::PARAM_TYPE => 'integer'
),
'smaxage' => array(
@@ -884,7 +1009,7 @@ class ApiMain extends ApiBase {
ApiBase::PARAM_DFLT => 0
),
'requestid' => null,
- 'servedby' => false,
+ 'servedby' => false,
'origin' => null,
);
}
@@ -898,12 +1023,11 @@ class ApiMain extends ApiBase {
return array(
'format' => 'The format of the output',
'action' => 'What action you would like to perform. See below for module help',
- 'version' => 'When showing help, include version for each module',
'maxlag' => array(
'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
'To save actions causing any more site replication lag, this parameter can make the client',
'wait until the replication lag is less than the specified value.',
- 'In case of a replag error, a HTTP 503 error is returned, with the message like',
+ 'In case of a replag error, error code "maxlag" is returned, with the message like',
'"Waiting for $host: $lag seconds lagged\n".',
'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
),
@@ -913,6 +1037,7 @@ class ApiMain extends ApiBase {
'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error',
'origin' => array(
'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.',
+ 'This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).',
'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .',
'If this parameter does not match the Origin: header, a 403 response will be returned.',
'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.',
@@ -984,11 +1109,11 @@ class ApiMain extends ApiBase {
protected function getCredits() {
return array(
'API developers:',
- ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-present)',
- ' Victor Vasiliev - vasilvv at gee mail dot com',
+ ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-2009)',
+ ' Victor Vasiliev - vasilvv @ gmail . com',
' Bryan Tong Minh - bryan . tongminh @ gmail . com',
' Sam Reed - sam @ reedyboy . net',
- ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007)',
+ ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007, 2012-present)',
'',
'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org',
'or file a bug report at https://bugzilla.wikimedia.org/'
@@ -1014,8 +1139,7 @@ class ApiMain extends ApiBase {
$this->setHelp();
// Get help text from cache if present
$key = wfMemcKey( 'apihelp', $this->getModuleName(),
- SpecialVersion::getVersion( 'nodb' ) .
- $this->getShowVersions() );
+ str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
if ( $wgAPICacheHelpTimeout > 0 ) {
$cached = $wgMemc->get( $key );
if ( $cached ) {
@@ -1040,9 +1164,11 @@ class ApiMain extends ApiBase {
$astriks = str_repeat( '*** ', 14 );
$msg .= "\n\n$astriks Modules $astriks\n\n";
- foreach ( array_keys( $this->mModules ) as $moduleName ) {
- $module = new $this->mModules[$moduleName] ( $this, $moduleName );
+
+ foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
+ $module = $this->mModuleMgr->getModule( $name );
$msg .= self::makeHelpMsgHeader( $module, 'action' );
+
$msg2 = $module->makeHelpMsg();
if ( $msg2 !== false ) {
$msg .= $msg2;
@@ -1053,14 +1179,13 @@ 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\n";
-
}
$msg .= "\n$astriks Formats $astriks\n\n";
- foreach ( array_keys( $this->mFormats ) as $formatName ) {
- $module = $this->createPrinterByName( $formatName );
+ foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
+ $module = $this->mModuleMgr->getModule( $name );
$msg .= self::makeHelpMsgHeader( $module, 'format' );
$msg2 = $module->makeHelpMsg();
if ( $msg2 !== false ) {
@@ -1076,7 +1201,7 @@ class ApiMain extends ApiBase {
/**
* @param $module ApiBase
- * @param $paramName String What type of request is this? e.g. action, query, list, prop, meta, format
+ * @param string $paramName What type of request is this? e.g. action, query, list, prop, meta, format
* @return string
*/
public static function makeHelpMsgHeader( $module, $paramName ) {
@@ -1105,25 +1230,19 @@ class ApiMain extends ApiBase {
/**
* Check whether the user wants us to show version information in the API help
* @return bool
+ * @deprecated since 1.21, always returns false
*/
public function getShowVersions() {
- return $this->mShowVersions;
+ wfDeprecated( __METHOD__, '1.21' );
+ return false;
}
/**
- * Returns the version information of this file, plus it includes
- * the versions for all files that are not callable proper API modules
- *
- * @return array
+ * Overrides to return this instance's module manager.
+ * @return ApiModuleManager
*/
- public function getVersion() {
- $vers = array();
- $vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n https://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/";
- $vers[] = __CLASS__ . ': $Id$';
- $vers[] = ApiBase::getBaseVersion();
- $vers[] = ApiFormatBase::getBaseVersion();
- $vers[] = ApiQueryBase::getBaseVersion();
- return $vers;
+ public function getModuleManager() {
+ return $this->mModuleMgr;
}
/**
@@ -1131,40 +1250,44 @@ class ApiMain extends ApiBase {
* classes who wish to add their own modules to their lexicon or override the
* behavior of inherent ones.
*
- * @param $mdlName String The identifier for this module.
- * @param $mdlClass String The class where this module is implemented.
+ * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
+ * @param string $name The identifier for this module.
+ * @param $class ApiBase The class where this module is implemented.
*/
- protected function addModule( $mdlName, $mdlClass ) {
- $this->mModules[$mdlName] = $mdlClass;
+ protected function addModule( $name, $class ) {
+ $this->getModuleManager()->addModule( $name, 'action', $class );
}
/**
* Add or overwrite an output format for this ApiMain. Intended for use by extending
* classes who wish to add to or modify current formatters.
*
- * @param $fmtName string The identifier for this format.
- * @param $fmtClass ApiFormatBase The class implementing this format.
+ * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
+ * @param string $name The identifier for this format.
+ * @param $class ApiFormatBase The class implementing this format.
*/
- protected function addFormat( $fmtName, $fmtClass ) {
- $this->mFormats[$fmtName] = $fmtClass;
+ protected function addFormat( $name, $class ) {
+ $this->getModuleManager()->addModule( $name, 'format', $class );
}
/**
* Get the array mapping module names to class names
+ * @deprecated since 1.21, Use getModuleManager()'s methods instead.
* @return array
*/
function getModules() {
- return $this->mModules;
+ return $this->getModuleManager()->getNamesWithClasses( 'action' );
}
/**
* Returns the list of supported formats in form ( 'format' => 'ClassName' )
*
* @since 1.18
+ * @deprecated since 1.21, Use getModuleManager()'s methods instead.
* @return array
*/
public function getFormats() {
- return $this->mFormats;
+ return $this->getModuleManager()->getNamesWithClasses( 'format' );
}
}
diff --git a/includes/api/ApiModuleManager.php b/includes/api/ApiModuleManager.php
new file mode 100644
index 00000000..100392bf
--- /dev/null
+++ b/includes/api/ApiModuleManager.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ *
+ *
+ * Created on Dec 27, 2012
+ *
+ * Copyright © 2012 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ */
+
+/**
+ * This class holds a list of modules and handles instantiation
+ *
+ * @since 1.21
+ * @ingroup API
+ */
+class ApiModuleManager extends ContextSource {
+
+ private $mParent;
+ private $mInstances = array();
+ private $mGroups = array();
+ private $mModules = array();
+
+ /**
+ * Construct new module manager
+ * @param ApiBase $parentModule Parent module instance will be used during instantiation
+ */
+ public function __construct( ApiBase $parentModule ) {
+ $this->mParent = $parentModule;
+ }
+
+ /**
+ * Add a list of modules to the manager
+ * @param array $modules A map of ModuleName => ModuleClass
+ * @param string $group Which group modules belong to (action,format,...)
+ */
+ public function addModules( array $modules, $group ) {
+ foreach ( $modules as $name => $class ) {
+ $this->addModule( $name, $group, $class );
+ }
+ }
+
+ /**
+ * Add or overwrite a module in this ApiMain instance. Intended for use by extending
+ * classes who wish to add their own modules to their lexicon or override the
+ * behavior of inherent ones.
+ *
+ * @param string $group Name of the module group
+ * @param string $name The identifier for this module.
+ * @param string $class The class where this module is implemented.
+ */
+ public function addModule( $name, $group, $class ) {
+ $this->mGroups[$group] = null;
+ $this->mModules[$name] = array( $group, $class );
+ }
+
+ /**
+ * Get module instance by name, or instantiate it if it does not exist
+ * @param string $moduleName module name
+ * @param string $group optionally validate that the module is in a specific group
+ * @param bool $ignoreCache if true, force-creates a new instance and does not cache it
+ * @return mixed the new module instance, or null if failed
+ */
+ public function getModule( $moduleName, $group = null, $ignoreCache = false ) {
+ if ( !isset( $this->mModules[$moduleName] ) ) {
+ return null;
+ }
+ $grpCls = $this->mModules[$moduleName];
+ if ( $group !== null && $grpCls[0] !== $group ) {
+ return null;
+ }
+ if ( !$ignoreCache && isset( $this->mInstances[$moduleName] ) ) {
+ // already exists
+ return $this->mInstances[$moduleName];
+ } else {
+ // new instance
+ $class = $grpCls[1];
+ $instance = new $class ( $this->mParent, $moduleName );
+ if ( !$ignoreCache ) {
+ // cache this instance in case it is needed later
+ $this->mInstances[$moduleName] = $instance;
+ }
+ return $instance;
+ }
+ }
+
+ /**
+ * Get an array of modules in a specific group or all if no group is set.
+ * @param string $group optional group filter
+ * @return array list of module names
+ */
+ public function getNames( $group = null ) {
+ if ( $group === null ) {
+ return array_keys( $this->mModules );
+ }
+ $result = array();
+ foreach ( $this->mModules as $name => $grpCls ) {
+ if ( $grpCls[0] === $group ) {
+ $result[] = $name;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Create an array of (moduleName => moduleClass) for a specific group or for all.
+ * @param string $group name of the group to get or null for all
+ * @return array name=>class map
+ */
+ public function getNamesWithClasses( $group = null ) {
+ $result = array();
+ foreach ( $this->mModules as $name => $grpCls ) {
+ if ( $group === null || $grpCls[0] === $group ) {
+ $result[$name] = $grpCls[1];
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Returns true if the specific module is defined at all or in a specific group.
+ * @param string $moduleName module name
+ * @param string $group group name to check against, or null to check all groups,
+ * @return boolean true if defined
+ */
+ public function isDefined( $moduleName, $group = null ) {
+ if ( isset( $this->mModules[$moduleName] ) ) {
+ return $group === null || $this->mModules[$moduleName][0] === $group;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the group name for the given module
+ * @param string $moduleName
+ * @return string group name or null if missing
+ */
+ public function getModuleGroup( $moduleName ) {
+ if ( isset( $this->mModules[$moduleName] ) ) {
+ return $this->mModules[$moduleName][0];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get a list of groups this manager contains.
+ * @return array
+ */
+ public function getGroups() {
+ return array_keys( $this->mGroups );
+ }
+}
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index 9d73562b..c18036cf 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -30,10 +30,6 @@
*/
class ApiMove extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$user = $this->getUser();
$params = $this->extractRequestParams();
@@ -42,7 +38,7 @@ class ApiMove extends ApiBase {
if ( isset( $params['from'] ) ) {
$fromTitle = Title::newFromText( $params['from'] );
- if ( !$fromTitle ) {
+ if ( !$fromTitle || $fromTitle->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['from'] ) );
}
} elseif ( isset( $params['fromid'] ) ) {
@@ -58,7 +54,7 @@ class ApiMove extends ApiBase {
$fromTalk = $fromTitle->getTalkPage();
$toTitle = Title::newFromText( $params['to'] );
- if ( !$toTitle ) {
+ if ( !$toTitle || $toTitle->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['to'] ) );
}
$toTalk = $toTitle->getTalkPage();
@@ -82,10 +78,17 @@ class ApiMove extends ApiBase {
}
$r = array( 'from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason'] );
- if ( !$params['noredirect'] || !$user->isAllowed( 'suppressredirect' ) ) {
+
+ if ( $fromTitle->exists() ) {
+ //NOTE: we assume that if the old title exists, it's because it was re-created as
+ // a redirect to the new title. This is not safe, but what we did before was
+ // even worse: we just determined whether a redirect should have been created,
+ // and reported that it was created if it should have, without any checks.
+ // Also note that isRedirect() is unreliable because of bug 37209.
$r['redirectcreated'] = '';
}
- if( $toTitleExists ) {
+
+ if ( $toTitleExists ) {
$r['moveoverredirect'] = '';
}
@@ -96,7 +99,7 @@ class ApiMove extends ApiBase {
if ( $retval === true ) {
$r['talkfrom'] = $fromTalk->getPrefixedText();
$r['talkto'] = $toTalk->getPrefixedText();
- if( $toTalkExists ) {
+ if ( $toTalkExists ) {
$r['talkmoveoverredirect'] = '';
}
} else {
@@ -122,7 +125,7 @@ class ApiMove extends ApiBase {
}
}
- $watch = "preferences";
+ $watch = 'preferences';
if ( isset( $params['watchlist'] ) ) {
$watch = $params['watchlist'];
} elseif ( $params['watch'] ) {
@@ -288,15 +291,11 @@ class ApiMove extends ApiBase {
public function getExamples() {
return array(
- 'api.php?action=move&from=Exampel&to=Example&token=123ABC&reason=Misspelled%20title&movetalk=&noredirect='
+ 'api.php?action=move&from=Badtitle&to=Goodtitle&token=123ABC&reason=Misspelled%20title&movetalk=&noredirect='
);
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Move';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index ef562741..315ace37 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -1,7 +1,5 @@
<?php
/**
- *
- *
* Created on Oct 13, 2006
*
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
@@ -29,12 +27,20 @@
*/
class ApiOpenSearch extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
+ /**
+ * Override built-in handling of format parameter.
+ * Only JSON is supported.
+ *
+ * @return ApiFormatBase
+ */
public function getCustomPrinter() {
- return $this->getMain()->createPrinterByName( 'json' );
+ $params = $this->extractRequestParams();
+ $format = $params['format'];
+ $allowed = array( 'json', 'jsonfm' );
+ if ( in_array( $format, $allowed ) ) {
+ return $this->getMain()->createPrinterByName( $format );
+ }
+ return $this->getMain()->createPrinterByName( $allowed[0] );
}
public function execute() {
@@ -98,6 +104,10 @@ class ApiOpenSearch extends ApiBase {
ApiBase::PARAM_ISMULTI => true
),
'suggest' => false,
+ 'format' => array(
+ ApiBase::PARAM_DFLT => 'json',
+ ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ),
+ )
);
}
@@ -107,6 +117,7 @@ class ApiOpenSearch extends ApiBase {
'limit' => 'Maximum amount of results to return',
'namespace' => 'Namespaces to search',
'suggest' => 'Do nothing if $wgEnableOpenSearchSuggest is false',
+ 'format' => 'The format of the output',
);
}
@@ -123,8 +134,4 @@ class ApiOpenSearch extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Opensearch';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php
index 265c2ccb..7256066d 100644
--- a/includes/api/ApiOptions.php
+++ b/includes/api/ApiOptions.php
@@ -25,17 +25,13 @@
*/
/**
-* API module that facilitates the changing of user's preferences.
-* Requires API write mode to be enabled.
-*
+ * API module that facilitates the changing of user's preferences.
+ * Requires API write mode to be enabled.
+ *
* @ingroup API
*/
class ApiOptions extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* Changes preferences of the current user.
*/
@@ -46,6 +42,10 @@ class ApiOptions extends ApiBase {
$this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' );
}
+ if ( !$user->isAllowed( 'editmyoptions' ) ) {
+ $this->dieUsage( 'You don\'t have permission to edit your options', 'permissiondenied' );
+ }
+
$params = $this->extractRequestParams();
$changed = false;
@@ -54,7 +54,7 @@ class ApiOptions extends ApiBase {
}
if ( $params['reset'] ) {
- $user->resetOptions();
+ $user->resetOptions( $params['resetkinds'], $this->getContext() );
$changed = true;
}
@@ -74,13 +74,36 @@ class ApiOptions extends ApiBase {
}
$prefs = Preferences::getPreferences( $user, $this->getContext() );
+ $prefsKinds = $user->getOptionKinds( $this->getContext(), $changes );
+
foreach ( $changes as $key => $value ) {
- if ( !isset( $prefs[$key] ) ) {
- $this->setWarning( "Not a valid preference: $key" );
- continue;
+ switch ( $prefsKinds[$key] ) {
+ case 'registered':
+ // Regular option.
+ $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] );
+ $validation = $field->validate( $value, $user->getOptions() );
+ break;
+ case 'registered-multiselect':
+ case 'registered-checkmatrix':
+ // A key for a multiselect or checkmatrix option.
+ $validation = true;
+ $value = $value !== null ? (bool)$value : null;
+ break;
+ case 'userjs':
+ // Allow non-default preferences prefixed with 'userjs-', to be set by user scripts
+ if ( strlen( $key ) > 255 ) {
+ $validation = "key too long (no more than 255 bytes allowed)";
+ } elseif ( preg_match( "/[^a-zA-Z0-9_-]/", $key ) !== 0 ) {
+ $validation = "invalid key (only a-z, A-Z, 0-9, _, - allowed)";
+ } else {
+ $validation = true;
+ }
+ break;
+ case 'unused':
+ default:
+ $validation = "not a valid preference";
+ break;
}
- $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] );
- $validation = $field->validate( $value, $user->getOptions() );
if ( $validation === true ) {
$user->setOption( $key, $value );
$changed = true;
@@ -106,12 +129,20 @@ class ApiOptions extends ApiBase {
}
public function getAllowedParams() {
+ $optionKinds = User::listOptionKinds();
+ $optionKinds[] = 'all';
+
return array(
'token' => array(
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
'reset' => false,
+ 'resetkinds' => array(
+ ApiBase::PARAM_TYPE => $optionKinds,
+ ApiBase::PARAM_DFLT => 'all',
+ ApiBase::PARAM_ISMULTI => true
+ ),
'change' => array(
ApiBase::PARAM_ISMULTI => true,
),
@@ -139,15 +170,20 @@ class ApiOptions extends ApiBase {
public function getParamDescription() {
return array(
'token' => 'An options token previously obtained through the action=tokens',
- 'reset' => 'Resets all preferences to the site defaults',
- 'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters',
+ 'reset' => 'Resets preferences to the site defaults',
+ 'resetkinds' => 'List of types of options to reset when the "reset" option is set',
+ 'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters. If no value is given (not even an equals sign), e.g., optionname|otheroption|..., the option will be reset to its default value',
'optionname' => 'A name of a option which should have an optionvalue set',
'optionvalue' => 'A value of the option specified by the optionname, can contain pipe characters',
);
}
public function getDescription() {
- return 'Change preferences of the current user';
+ return array(
+ 'Change preferences of the current user',
+ 'Only options which are registered in core or in one of installed extensions,',
+ 'or as options with keys prefixed with \'userjs-\' (intended to be used by user scripts), can be set.'
+ );
}
public function getPossibleErrors() {
@@ -176,8 +212,4 @@ class ApiOptions extends ApiBase {
'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC',
);
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index 0f5be6b2..b05cb2b6 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -4,7 +4,7 @@
*
* Created on Sep 24, 2006
*
- * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ * Copyright © 2006, 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,52 +36,183 @@
* the second instance for all their work.
*
* @ingroup API
+ * @since 1.21 derives from ApiBase instead of ApiQueryBase
*/
-class ApiPageSet extends ApiQueryBase {
+class ApiPageSet extends ApiBase {
- private $mAllPages; // [ns][dbkey] => page_id or negative when missing
- private $mTitles, $mGoodTitles, $mMissingTitles, $mInvalidTitles;
- private $mMissingPageIDs, $mRedirectTitles, $mSpecialTitles;
- private $mNormalizedTitles, $mInterwikiTitles;
- private $mResolveRedirects, $mPendingRedirectIDs;
- private $mConvertTitles, $mConvertedTitles;
- private $mGoodRevIDs, $mMissingRevIDs;
- private $mFakePageId;
-
- private $mRequestedPageFields;
+ /**
+ * Constructor flag: The new instance of ApiPageSet will ignore the 'generator=' parameter
+ * @since 1.21
+ */
+ const DISABLE_GENERATORS = 1;
+
+ private $mDbSource;
+ private $mParams;
+ private $mResolveRedirects;
+ private $mConvertTitles;
+ private $mAllowGenerator;
+
+ private $mAllPages = array(); // [ns][dbkey] => page_id or negative when missing
+ private $mTitles = array();
+ private $mGoodTitles = array();
+ private $mMissingTitles = array();
+ private $mInvalidTitles = array();
+ private $mMissingPageIDs = array();
+ private $mRedirectTitles = array();
+ private $mSpecialTitles = array();
+ private $mNormalizedTitles = array();
+ private $mInterwikiTitles = array();
+ private $mPendingRedirectIDs = array();
+ private $mConvertedTitles = array();
+ private $mGoodRevIDs = array();
+ private $mMissingRevIDs = array();
+ private $mFakePageId = -1;
+ private $mCacheMode = 'public';
+ private $mRequestedPageFields = array();
+ /**
+ * @var int
+ */
+ private $mDefaultNamespace = NS_MAIN;
/**
* Constructor
- * @param $query ApiBase
- * @param $resolveRedirects bool Whether redirects should be resolved
- * @param $convertTitles bool
+ * @param $dbSource ApiBase Module implementing getDB().
+ * Allows PageSet to reuse existing db connection from the shared state like ApiQuery.
+ * @param int $flags Zero or more flags like DISABLE_GENERATORS
+ * @param int $defaultNamespace the namespace to use if none is specified by a prefix.
+ * @since 1.21 accepts $flags instead of two boolean values
*/
- public function __construct( $query, $resolveRedirects = false, $convertTitles = false ) {
- parent::__construct( $query, 'query' );
+ public function __construct( ApiBase $dbSource, $flags = 0, $defaultNamespace = NS_MAIN ) {
+ parent::__construct( $dbSource->getMain(), $dbSource->getModuleName() );
+ $this->mDbSource = $dbSource;
+ $this->mAllowGenerator = ( $flags & ApiPageSet::DISABLE_GENERATORS ) == 0;
+ $this->mDefaultNamespace = $defaultNamespace;
- $this->mAllPages = array();
- $this->mTitles = array();
- $this->mGoodTitles = array();
- $this->mMissingTitles = array();
- $this->mInvalidTitles = array();
- $this->mMissingPageIDs = array();
- $this->mRedirectTitles = array();
- $this->mNormalizedTitles = array();
- $this->mInterwikiTitles = array();
- $this->mGoodRevIDs = array();
- $this->mMissingRevIDs = array();
- $this->mSpecialTitles = array();
+ $this->profileIn();
+ $this->mParams = $this->extractRequestParams();
+ $this->mResolveRedirects = $this->mParams['redirects'];
+ $this->mConvertTitles = $this->mParams['converttitles'];
+ $this->profileOut();
+ }
- $this->mRequestedPageFields = array();
- $this->mResolveRedirects = $resolveRedirects;
- if ( $resolveRedirects ) {
- $this->mPendingRedirectIDs = array();
- }
+ /**
+ * In case execute() is not called, call this method to mark all relevant parameters as used
+ * This prevents unused parameters from being reported as warnings
+ */
+ public function executeDryRun() {
+ $this->executeInternal( true );
+ }
+
+ /**
+ * Populate the PageSet from the request parameters.
+ */
+ public function execute() {
+ $this->executeInternal( false );
+ }
+
+ /**
+ * Populate the PageSet from the request parameters.
+ * @param bool $isDryRun If true, instantiates generator, but only to mark relevant parameters as used
+ */
+ private function executeInternal( $isDryRun ) {
+ $this->profileIn();
+
+ $generatorName = $this->mAllowGenerator ? $this->mParams['generator'] : null;
+ if ( isset( $generatorName ) ) {
+ $dbSource = $this->mDbSource;
+ $isQuery = $dbSource instanceof ApiQuery;
+ if ( !$isQuery ) {
+ // If the parent container of this pageset is not ApiQuery, we must create it to run generator
+ $dbSource = $this->getMain()->getModuleManager()->getModule( 'query' );
+ // Enable profiling for query module because it will be used for db sql profiling
+ $dbSource->profileIn();
+ }
+ $generator = $dbSource->getModuleManager()->getModule( $generatorName, null, true );
+ if ( $generator === null ) {
+ $this->dieUsage( 'Unknown generator=' . $generatorName, 'badgenerator' );
+ }
+ if ( !$generator instanceof ApiQueryGeneratorBase ) {
+ $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' );
+ }
+ // Create a temporary pageset to store generator's output,
+ // add any additional fields generator may need, and execute pageset to populate titles/pageids
+ $tmpPageSet = new ApiPageSet( $dbSource, ApiPageSet::DISABLE_GENERATORS );
+ $generator->setGeneratorMode( $tmpPageSet );
+ $this->mCacheMode = $generator->getCacheMode( $generator->extractRequestParams() );
+
+ if ( !$isDryRun ) {
+ $generator->requestExtraData( $tmpPageSet );
+ }
+ $tmpPageSet->executeInternal( $isDryRun );
+
+ // populate this pageset with the generator output
+ $this->profileOut();
+ $generator->profileIn();
+
+ if ( !$isDryRun ) {
+ $generator->executeGenerator( $this );
+ wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$this ) );
+ } else {
+ // Prevent warnings from being reported on these parameters
+ $main = $this->getMain();
+ foreach ( $generator->extractRequestParams() as $paramName => $param ) {
+ $main->getVal( $generator->encodeParamName( $paramName ) );
+ }
+ }
+ $generator->profileOut();
+ $this->profileIn();
+
+ if ( !$isDryRun ) {
+ $this->resolvePendingRedirects();
+ }
- $this->mConvertTitles = $convertTitles;
- $this->mConvertedTitles = array();
+ if ( !$isQuery ) {
+ // If this pageset is not part of the query, we called profileIn() above
+ $dbSource->profileOut();
+ }
+ } else {
+ // Only one of the titles/pageids/revids is allowed at the same time
+ $dataSource = null;
+ if ( isset( $this->mParams['titles'] ) ) {
+ $dataSource = 'titles';
+ }
+ if ( isset( $this->mParams['pageids'] ) ) {
+ if ( isset( $dataSource ) ) {
+ $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' );
+ }
+ $dataSource = 'pageids';
+ }
+ if ( isset( $this->mParams['revids'] ) ) {
+ if ( isset( $dataSource ) ) {
+ $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' );
+ }
+ $dataSource = 'revids';
+ }
- $this->mFakePageId = - 1;
+ if ( !$isDryRun ) {
+ // Populate page information with the original user input
+ switch ( $dataSource ) {
+ case 'titles':
+ $this->initFromTitles( $this->mParams['titles'] );
+ break;
+ case 'pageids':
+ $this->initFromPageIds( $this->mParams['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.' );
+ }
+ $this->mResolveRedirects = false;
+ $this->initFromRevIDs( $this->mParams['revids'] );
+ break;
+ default:
+ // Do nothing - some queries do not need any of the data sources.
+ break;
+ }
+ }
+ }
+ $this->profileOut();
}
/**
@@ -93,9 +224,33 @@ class ApiPageSet extends ApiQueryBase {
}
/**
- * Request an additional field from the page table. Must be called
- * before execute()
- * @param $fieldName string Field name
+ * Return the parameter name that is the source of data for this PageSet
+ *
+ * If multiple source parameters are specified (e.g. titles and pageids),
+ * one will be named arbitrarily.
+ *
+ * @return string|null
+ */
+ public function getDataSource() {
+ if ( $this->mAllowGenerator && isset( $this->mParams['generator'] ) ) {
+ return 'generator';
+ }
+ if ( isset( $this->mParams['titles'] ) ) {
+ return 'titles';
+ }
+ if ( isset( $this->mParams['pageids'] ) ) {
+ return 'pageids';
+ }
+ if ( isset( $this->mParams['revids'] ) ) {
+ return 'revids';
+ }
+ return null;
+ }
+
+ /**
+ * Request an additional field from the page table.
+ * Must be called before execute()
+ * @param string $fieldName Field name
*/
public function requestField( $fieldName ) {
$this->mRequestedPageFields[$fieldName] = null;
@@ -104,7 +259,7 @@ class ApiPageSet extends ApiQueryBase {
/**
* Get the value of a custom field previously requested through
* requestField()
- * @param $fieldName string Field name
+ * @param string $fieldName Field name
* @return mixed Field value
*/
public function getCustomField( $fieldName ) {
@@ -207,14 +362,39 @@ class ApiPageSet extends ApiQueryBase {
/**
* Get a list of redirect resolutions - maps a title to its redirect
- * target.
- * @return array prefixed_title (string) => Title object
+ * target, as an array of output-ready arrays
+ * @return array
*/
public function getRedirectTitles() {
return $this->mRedirectTitles;
}
/**
+ * Get a list of redirect resolutions - maps a title to its redirect
+ * target.
+ * @param $result ApiResult
+ * @return array of prefixed_title (string) => Title object
+ * @since 1.21
+ */
+ public function getRedirectTitlesAsResult( $result = null ) {
+ $values = array();
+ foreach ( $this->getRedirectTitles() as $titleStrFrom => $titleTo ) {
+ $r = array(
+ 'from' => strval( $titleStrFrom ),
+ 'to' => $titleTo->getPrefixedText(),
+ );
+ if ( $titleTo->getFragment() !== '' ) {
+ $r['tofragment'] = $titleTo->getFragment();
+ }
+ $values[] = $r;
+ }
+ if ( !empty( $values ) && $result ) {
+ $result->setIndexedTagName( $values, 'r' );
+ }
+ return $values;
+ }
+
+ /**
* Get a list of title normalizations - maps a title to its normalized
* version.
* @return array raw_prefixed_title (string) => prefixed_title (string)
@@ -224,6 +404,27 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Get a list of title normalizations - maps a title to its normalized
+ * version in the form of result array.
+ * @param $result ApiResult
+ * @return array of raw_prefixed_title (string) => prefixed_title (string)
+ * @since 1.21
+ */
+ public function getNormalizedTitlesAsResult( $result = null ) {
+ $values = array();
+ foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
+ $values[] = array(
+ 'from' => $rawTitleStr,
+ 'to' => $titleStr
+ );
+ }
+ if ( !empty( $values ) && $result ) {
+ $result->setIndexedTagName( $values, 'n' );
+ }
+ return $values;
+ }
+
+ /**
* Get a list of title conversions - maps a title to its converted
* version.
* @return array raw_prefixed_title (string) => prefixed_title (string)
@@ -233,6 +434,27 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Get a list of title conversions - maps a title to its converted
+ * version as a result array.
+ * @param $result ApiResult
+ * @return array of (from, to) strings
+ * @since 1.21
+ */
+ public function getConvertedTitlesAsResult( $result = null ) {
+ $values = array();
+ foreach ( $this->getConvertedTitles() as $rawTitleStr => $titleStr ) {
+ $values[] = array(
+ 'from' => $rawTitleStr,
+ 'to' => $titleStr
+ );
+ }
+ if ( !empty( $values ) && $result ) {
+ $result->setIndexedTagName( $values, 'c' );
+ }
+ return $values;
+ }
+
+ /**
* Get a list of interwiki titles - maps a title to its interwiki
* prefix.
* @return array raw_prefixed_title (string) => interwiki_prefix (string)
@@ -242,6 +464,33 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Get a list of interwiki titles - maps a title to its interwiki
+ * prefix as result.
+ * @param $result ApiResult
+ * @param $iwUrl boolean
+ * @return array raw_prefixed_title (string) => interwiki_prefix (string)
+ * @since 1.21
+ */
+ public function getInterwikiTitlesAsResult( $result = null, $iwUrl = false ) {
+ $values = array();
+ foreach ( $this->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) {
+ $item = array(
+ 'title' => $rawTitleStr,
+ 'iw' => $interwikiStr,
+ );
+ if ( $iwUrl ) {
+ $title = Title::newFromText( $rawTitleStr );
+ $item['url'] = $title->getFullURL( '', false, PROTO_CURRENT );
+ }
+ $values[] = $item;
+ }
+ if ( !empty( $values ) && $result ) {
+ $result->setIndexedTagName( $values, 'i' );
+ }
+ return $values;
+ }
+
+ /**
* Get the list of revision IDs (requested with the revids= parameter)
* @return array revID (int) => pageID (int)
*/
@@ -258,6 +507,25 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Revision IDs that were not found in the database as result array.
+ * @param $result ApiResult
+ * @return array of revision IDs
+ * @since 1.21
+ */
+ public function getMissingRevisionIDsAsResult( $result = null ) {
+ $values = array();
+ foreach ( $this->getMissingRevisionIDs() as $revid ) {
+ $values[$revid] = array(
+ 'revid' => $revid
+ );
+ }
+ if ( !empty( $values ) && $result ) {
+ $result->setIndexedTagName( $values, 'rev' );
+ }
+ return $values;
+ }
+
+ /**
* Get the list of titles with negative namespace
* @return array Title
*/
@@ -274,55 +542,8 @@ class ApiPageSet extends ApiQueryBase {
}
/**
- * Populate the PageSet from the request parameters.
- */
- public function execute() {
- $this->profileIn();
- $params = $this->extractRequestParams();
-
- // Only one of the titles/pageids/revids is allowed at the same time
- $dataSource = null;
- 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' );
- }
- $dataSource = 'pageids';
- }
- if ( isset( $params['revids'] ) ) {
- if ( isset( $dataSource ) ) {
- $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' );
- }
- $dataSource = 'revids';
- }
-
- switch ( $dataSource ) {
- case 'titles':
- $this->initFromTitles( $params['titles'] );
- break;
- case '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.' );
- }
- $this->mResolveRedirects = false;
- $this->initFromRevIDs( $params['revids'] );
- break;
- default:
- // Do nothing - some queries do not need any of the data sources.
- break;
- }
- $this->profileOut();
- }
-
- /**
* Populate this PageSet from a list of Titles
- * @param $titles array of Title objects
+ * @param array $titles of Title objects
*/
public function populateFromTitles( $titles ) {
$this->profileIn();
@@ -332,7 +553,7 @@ class ApiPageSet extends ApiQueryBase {
/**
* Populate this PageSet from a list of page IDs
- * @param $pageIDs array of page IDs
+ * @param array $pageIDs of page IDs
*/
public function populateFromPageIDs( $pageIDs ) {
$this->profileIn();
@@ -353,7 +574,7 @@ class ApiPageSet extends ApiQueryBase {
/**
* Populate this PageSet from a list of revision IDs
- * @param $revIDs array of revision IDs
+ * @param array $revIDs of revision IDs
*/
public function populateFromRevisionIDs( $revIDs ) {
$this->profileIn();
@@ -380,17 +601,16 @@ class ApiPageSet extends ApiQueryBase {
}
foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) {
- $fieldValues[$pageId] = $row-> $fieldName;
+ $fieldValues[$pageId] = $row->$fieldName;
}
}
/**
- * Resolve redirects, if applicable
+ * Do not use, does nothing, will be removed
+ * @deprecated since 1.21
*/
public function finishPageSetGeneration() {
- $this->profileIn();
- $this->resolvePendingRedirects();
- $this->profileOut();
+ wfDeprecated( __METHOD__, '1.21' );
}
/**
@@ -407,7 +627,7 @@ class ApiPageSet extends ApiQueryBase {
* #5 Substitute the original LinkBatch object with the new list
* #6 Repeat from step #1
*
- * @param $titles array of Title objects or strings
+ * @param array $titles of Title objects or strings
*/
private function initFromTitles( $titles ) {
// Get validated and normalized title objects
@@ -434,10 +654,10 @@ class ApiPageSet extends ApiQueryBase {
/**
* Does the same as initFromTitles(), but is based on page IDs instead
- * @param $pageids array of page IDs
+ * @param array $pageids of page IDs
*/
private function initFromPageIds( $pageids ) {
- if ( !count( $pageids ) ) {
+ if ( !$pageids ) {
return;
}
@@ -447,7 +667,7 @@ class ApiPageSet extends ApiQueryBase {
$pageids = self::getPositiveIntegers( $pageids );
$res = null;
- if ( count( $pageids ) ) {
+ if ( !empty( $pageids ) ) {
$set = array(
'page_id' => $pageids
);
@@ -460,7 +680,7 @@ class ApiPageSet extends ApiQueryBase {
$this->profileDBOut();
}
- $this->initFromQueryResult( $res, $remaining, false ); // process PageIDs
+ $this->initFromQueryResult( $res, $remaining, false ); // process PageIDs
// Resolve any found redirects
$this->resolvePendingRedirects();
@@ -470,9 +690,9 @@ class ApiPageSet extends ApiQueryBase {
* Iterate through the result of the query on 'page' table,
* and for each row create and store title object and save any extra fields requested.
* @param $res ResultWrapper DB Query result
- * @param $remaining array of either pageID or ns/title elements (optional).
+ * @param array $remaining of either pageID or ns/title elements (optional).
* If given, any missing items will go to $mMissingPageIDs and $mMissingTitles
- * @param $processTitles bool Must be provided together with $remaining.
+ * @param bool $processTitles Must be provided together with $remaining.
* If true, treat $remaining as an array of [ns][title]
* If false, treat it as an array of [pageIDs]
*/
@@ -499,7 +719,7 @@ class ApiPageSet extends ApiQueryBase {
$this->processDbRow( $row );
// Need gender information
- if( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
+ if ( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
$usernames[] = $row->page_title;
}
}
@@ -518,7 +738,7 @@ class ApiPageSet extends ApiQueryBase {
$this->mTitles[] = $title;
// need gender information
- if( MWNamespace::hasGenderDistinction( $ns ) ) {
+ if ( MWNamespace::hasGenderDistinction( $ns ) ) {
$usernames[] = $dbkey;
}
}
@@ -541,10 +761,10 @@ class ApiPageSet extends ApiQueryBase {
/**
* Does the same as initFromTitles(), but is based on revision IDs
* instead
- * @param $revids array of revision IDs
+ * @param array $revids of revision IDs
*/
private function initFromRevIDs( $revids ) {
- if ( !count( $revids ) ) {
+ if ( !$revids ) {
return;
}
@@ -555,14 +775,14 @@ class ApiPageSet extends ApiQueryBase {
$revids = self::getPositiveIntegers( $revids );
- if ( count( $revids ) ) {
+ if ( !empty( $revids ) ) {
$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__ );
+ $res = $db->select( $tables, $fields, $where, __METHOD__ );
foreach ( $res as $row ) {
$revid = intval( $row->rev_id );
$pageid = intval( $row->rev_page );
@@ -645,7 +865,7 @@ class ApiPageSet extends ApiQueryBase {
$from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
$to = Title::makeTitle( $row->rd_namespace, $row->rd_title, $row->rd_fragment, $row->rd_interwiki );
unset( $this->mPendingRedirectIDs[$rdfrom] );
- if ( !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) {
+ if ( !$to->isExternal() && !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) {
$lb->add( $row->rd_namespace, $row->rd_title );
}
$this->mRedirectTitles[$from] = $to;
@@ -670,44 +890,63 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Get the cache mode for the data generated by this module.
+ * All PageSet users should take into account whether this returns a more-restrictive
+ * cache mode than the using module itself. For possible return values and other
+ * details about cache modes, see ApiMain::setCacheMode()
+ *
+ * Public caching will only be allowed if *all* the modules that supply
+ * data for a given request return a cache mode of public.
+ *
+ * @param $params
+ * @return string
+ * @since 1.21
+ */
+ public function getCacheMode( $params = null ) {
+ return $this->mCacheMode;
+ }
+
+ /**
* Given an array of title strings, convert them into Title objects.
- * Alternativelly, an array of Title objects may be given.
+ * Alternatively, an array of Title objects may be given.
* This method validates access rights for the title,
* and appends normalization values to the output.
*
- * @param $titles array of Title objects or strings
+ * @param array $titles of Title objects or strings
* @return LinkBatch
*/
private function processTitlesArray( $titles ) {
- $genderCache = GenderCache::singleton();
- $genderCache->doTitlesArray( $titles, __METHOD__ );
-
+ $usernames = array();
$linkBatch = new LinkBatch();
foreach ( $titles as $title ) {
- $titleObj = is_string( $title ) ? Title::newFromText( $title ) : $title;
+ if ( is_string( $title ) ) {
+ $titleObj = Title::newFromText( $title, $this->mDefaultNamespace );
+ } else {
+ $titleObj = $title;
+ }
if ( !$titleObj ) {
// Handle invalid titles gracefully
- $this->mAllpages[0][$title] = $this->mFakePageId;
+ $this->mAllPages[0][$title] = $this->mFakePageId;
$this->mInvalidTitles[$this->mFakePageId] = $title;
$this->mFakePageId--;
continue; // There's nothing else we can do
}
$unconvertedTitle = $titleObj->getPrefixedText();
$titleWasConverted = false;
- $iw = $titleObj->getInterwiki();
- if ( strval( $iw ) !== '' ) {
+ if ( $titleObj->isExternal() ) {
// This title is an interwiki link.
- $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw;
+ $this->mInterwikiTitles[$unconvertedTitle] = $titleObj->getInterwiki();
} else {
// Variants checking
global $wgContLang;
if ( $this->mConvertTitles &&
- count( $wgContLang->getVariants() ) > 1 &&
+ count( $wgContLang->getVariants() ) > 1 &&
!$titleObj->exists() ) {
- // Language::findVariantLink will modify titleObj into
+ // Language::findVariantLink will modify titleText and titleObj into
// the canonical variant if possible
- $wgContLang->findVariantLink( $title, $titleObj );
+ $titleText = is_string( $title ) ? $title : $titleObj->getPrefixedText();
+ $wgContLang->findVariantLink( $titleText, $titleObj );
$titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText();
}
@@ -728,16 +967,36 @@ class ApiPageSet extends ApiQueryBase {
// namespace is localized or the capitalization is
// different
if ( $titleWasConverted ) {
- $this->mConvertedTitles[$title] = $titleObj->getPrefixedText();
+ $this->mConvertedTitles[$unconvertedTitle] = $titleObj->getPrefixedText();
+ // In this case the page can't be Special.
+ if ( is_string( $title ) && $title !== $unconvertedTitle ) {
+ $this->mNormalizedTitles[$title] = $unconvertedTitle;
+ }
} elseif ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) {
$this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
}
+
+ // Need gender information
+ if ( MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
+ $usernames[] = $titleObj->getText();
+ }
}
+ // Get gender information
+ $genderCache = GenderCache::singleton();
+ $genderCache->doQuery( $usernames, __METHOD__ );
return $linkBatch;
}
/**
+ * Get the database connection (read-only)
+ * @return DatabaseBase
+ */
+ protected function getDB() {
+ return $this->mDbSource->getDB();
+ }
+
+ /**
* Returns the input array of integers with all values < 0 removed
*
* @param $array array
@@ -747,7 +1006,7 @@ class ApiPageSet extends ApiQueryBase {
// bug 25734 API: possible issue with revids validation
// It seems with a load of revision rows, MySQL gets upset
// Remove any < 0 integers, as they can't be valid
- foreach( $array as $i => $int ) {
+ foreach ( $array as $i => $int ) {
if ( $int < 0 ) {
unset( $array[$i] );
}
@@ -756,8 +1015,8 @@ class ApiPageSet extends ApiQueryBase {
return $array;
}
- public function getAllowedParams() {
- return array(
+ public function getAllowedParams( $flags = 0 ) {
+ $result = array(
'titles' => array(
ApiBase::PARAM_ISMULTI => true
),
@@ -768,15 +1027,59 @@ class ApiPageSet extends ApiQueryBase {
'revids' => array(
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_ISMULTI => true
- )
+ ),
+ 'redirects' => false,
+ 'converttitles' => false,
);
+ if ( $this->mAllowGenerator ) {
+ if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
+ $result['generator'] = array(
+ ApiBase::PARAM_TYPE => $this->getGenerators()
+ );
+ } else {
+ $result['generator'] = null;
+ }
+ }
+ return $result;
+ }
+
+ private static $generators = null;
+
+ /**
+ * Get an array of all available generators
+ * @return array
+ */
+ private function getGenerators() {
+ if ( self::$generators === null ) {
+ $query = $this->mDbSource;
+ if ( !( $query instanceof ApiQuery ) ) {
+ // If the parent container of this pageset is not ApiQuery,
+ // we must create it to get module manager
+ $query = $this->getMain()->getModuleManager()->getModule( 'query' );
+ }
+ $gens = array();
+ $mgr = $query->getModuleManager();
+ foreach ( $mgr->getNamesWithClasses() as $name => $class ) {
+ if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
+ $gens[] = $name;
+ }
+ }
+ sort( $gens );
+ self::$generators = $gens;
+ }
+ return self::$generators;
}
public function getParamDescription() {
return array(
'titles' => 'A list of titles to work on',
'pageids' => 'A list of page IDs to work on',
- 'revids' => 'A list of revision IDs to work on'
+ 'revids' => 'A list of revision IDs to work on',
+ 'generator' => array( 'Get the list of pages to work on by executing the specified query module.',
+ 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' ),
+ 'redirects' => 'Automatically resolve redirects',
+ 'converttitles' => array( 'Convert titles to other variants if necessary. Only works if the wiki\'s content language supports variant conversion.',
+ 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ),
);
}
@@ -784,10 +1087,7 @@ class ApiPageSet extends ApiQueryBase {
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'" ),
+ array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ),
) );
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index 343a2625..3e1a7531 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -42,42 +42,13 @@ class ApiParamInfo extends ApiBase {
public function execute() {
// Get parameters
$params = $this->extractRequestParams();
- $result = $this->getResult();
+ $resultObj = $this->getResult();
$res = array();
- if ( is_array( $params['modules'] ) ) {
- $modules = $this->getMain()->getModules();
- $res['modules'] = array();
- foreach ( $params['modules'] as $mod ) {
- if ( !isset( $modules[$mod] ) ) {
- $res['modules'][] = array( 'name' => $mod, 'missing' => '' );
- continue;
- }
- $obj = new $modules[$mod]( $this->getMain(), $mod );
- $item = $this->getClassInfo( $obj );
- $item['name'] = $mod;
- $res['modules'][] = $item;
- }
- $result->setIndexedTagName( $res['modules'], 'module' );
- }
+ $this->addModulesInfo( $params, 'modules', $res, $resultObj );
- if ( is_array( $params['querymodules'] ) ) {
- $queryModules = $this->queryObj->getModules();
- $res['querymodules'] = array();
- foreach ( $params['querymodules'] as $qm ) {
- if ( !isset( $queryModules[$qm] ) ) {
- $res['querymodules'][] = array( 'name' => $qm, 'missing' => '' );
- continue;
- }
- $obj = new $queryModules[$qm]( $this, $qm );
- $item = $this->getClassInfo( $obj );
- $item['name'] = $qm;
- $item['querytype'] = $this->queryObj->getModuleType( $qm );
- $res['querymodules'][] = $item;
- }
- $result->setIndexedTagName( $res['querymodules'], 'module' );
- }
+ $this->addModulesInfo( $params, 'querymodules', $res, $resultObj );
if ( $params['mainmodule'] ) {
$res['mainmodule'] = $this->getClassInfo( $this->getMain() );
@@ -88,36 +59,57 @@ class ApiParamInfo extends ApiBase {
$res['pagesetmodule'] = $this->getClassInfo( $pageSet );
}
- if ( is_array( $params['formatmodules'] ) ) {
- $formats = $this->getMain()->getFormats();
- $res['formatmodules'] = array();
- foreach ( $params['formatmodules'] as $f ) {
- if ( !isset( $formats[$f] ) ) {
- $res['formatmodules'][] = array( 'name' => $f, 'missing' => '' );
- continue;
- }
- $obj = new $formats[$f]( $this, $f );
- $item = $this->getClassInfo( $obj );
- $item['name'] = $f;
- $res['formatmodules'][] = $item;
+ $this->addModulesInfo( $params, 'formatmodules', $res, $resultObj );
+
+ $resultObj->addValue( null, $this->getModuleName(), $res );
+ }
+
+ /**
+ * If the type is requested in parameters, adds a section to res with module info.
+ * @param array $params user parameters array
+ * @param string $type parameter name
+ * @param array $res store results in this array
+ * @param ApiResult $resultObj results object to set indexed tag.
+ */
+ private function addModulesInfo( $params, $type, &$res, $resultObj ) {
+ if ( !is_array( $params[$type] ) ) {
+ return;
+ }
+ $isQuery = ( $type === 'querymodules' );
+ if ( $isQuery ) {
+ $mgr = $this->queryObj->getModuleManager();
+ } else {
+ $mgr = $this->getMain()->getModuleManager();
+ }
+ $res[$type] = array();
+ foreach ( $params[$type] as $mod ) {
+ if ( !$mgr->isDefined( $mod ) ) {
+ $res[$type][] = array( 'name' => $mod, 'missing' => '' );
+ continue;
+ }
+ $obj = $mgr->getModule( $mod );
+ $item = $this->getClassInfo( $obj );
+ $item['name'] = $mod;
+ if ( $isQuery ) {
+ $item['querytype'] = $mgr->getModuleGroup( $mod );
}
- $result->setIndexedTagName( $res['formatmodules'], 'module' );
+ $res[$type][] = $item;
}
- $result->addValue( null, $this->getModuleName(), $res );
+ $resultObj->setIndexedTagName( $res[$type], 'module' );
}
/**
* @param $obj ApiBase
* @return ApiResult
*/
- function getClassInfo( $obj ) {
+ private function getClassInfo( $obj ) {
$result = $this->getResult();
$retval['classname'] = get_class( $obj );
$retval['description'] = implode( "\n", (array)$obj->getFinalDescription() );
-
$retval['examples'] = '';
- $retval['version'] = implode( "\n", (array)$obj->getVersion() );
+ // version is deprecated since 1.21, but needs to be returned for v1
+ $retval['version'] = '';
$retval['prefix'] = $obj->getModulePrefix();
if ( $obj->isReadMode() ) {
@@ -133,7 +125,7 @@ class ApiParamInfo extends ApiBase {
$retval['generator'] = '';
}
- $allowedParams = $obj->getFinalParams();
+ $allowedParams = $obj->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
if ( !is_array( $allowedParams ) ) {
return $retval;
}
@@ -150,14 +142,14 @@ class ApiParamInfo extends ApiBase {
if ( is_string( $examples ) ) {
$examples = array( $examples );
}
- foreach( $examples as $k => $v ) {
+ foreach ( $examples as $k => $v ) {
if ( strlen( $retval['examples'] ) ) {
$retval['examples'] .= ' ';
}
$item = array();
if ( is_numeric( $k ) ) {
$retval['examples'] .= $v;
- $result->setContent( $item, $v );
+ ApiResult::setContent( $item, $v );
} else {
if ( !is_array( $v ) ) {
$item['description'] = $v;
@@ -165,7 +157,7 @@ class ApiParamInfo extends ApiBase {
$item['description'] = implode( $v, "\n" );
}
$retval['examples'] .= $item['description'] . ' ' . $k;
- $result->setContent( $item, $k );
+ ApiResult::setContent( $item, $k );
}
$retval['allexamples'][] = $item;
}
@@ -181,7 +173,7 @@ class ApiParamInfo extends ApiBase {
}
//handle shorthand
- if( !is_array( $p ) ) {
+ if ( !is_array( $p ) ) {
$p = array(
ApiBase::PARAM_DFLT => $p,
);
@@ -208,11 +200,11 @@ class ApiParamInfo extends ApiBase {
if ( isset( $p[ApiBase::PARAM_DFLT] ) ) {
$type = $p[ApiBase::PARAM_TYPE];
- if( $type === 'boolean' ) {
+ if ( $type === 'boolean' ) {
$a['default'] = ( $p[ApiBase::PARAM_DFLT] ? 'true' : 'false' );
- } elseif( $type === 'string' ) {
+ } elseif ( $type === 'string' ) {
$a['default'] = strval( $p[ApiBase::PARAM_DFLT] );
- } elseif( $type === 'integer' ) {
+ } elseif ( $type === 'integer' ) {
$a['default'] = intval( $p[ApiBase::PARAM_DFLT] );
} else {
$a['default'] = $p[ApiBase::PARAM_DFLT];
@@ -299,7 +291,7 @@ class ApiParamInfo extends ApiBase {
$retval['props'][] = $propResult;
}
- // default is true for query modules, false for other modules, overriden by ApiBase::PROP_LIST
+ // default is true for query modules, false for other modules, overridden by ApiBase::PROP_LIST
if ( $listResult === true || ( $listResult !== false && $obj instanceof ApiQueryBase ) ) {
$retval['listresult'] = '';
}
@@ -308,7 +300,7 @@ class ApiParamInfo extends ApiBase {
}
// Errors
- $retval['errors'] = $this->parseErrors( $obj->getPossibleErrors() );
+ $retval['errors'] = $this->parseErrors( $obj->getFinalPossibleErrors() );
$result->setIndexedTagName( $retval['errors'], 'error' );
return $retval;
@@ -319,11 +311,11 @@ class ApiParamInfo extends ApiBase {
}
public function getAllowedParams() {
- $modules = array_keys( $this->getMain()->getModules() );
+ $modules = $this->getMain()->getModuleManager()->getNames( 'action' );
sort( $modules );
- $querymodules = array_keys( $this->queryObj->getModules() );
+ $querymodules = $this->queryObj->getModuleManager()->getNames();
sort( $querymodules );
- $formatmodules = array_keys( $this->getMain()->getFormats() );
+ $formatmodules = $this->getMain()->getModuleManager()->getNames( 'format' );
sort( $formatmodules );
return array(
'modules' => array(
@@ -366,8 +358,4 @@ class ApiParamInfo extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Parameter_information';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index db6e2bb8..a369994b 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -26,11 +26,15 @@
* @ingroup API
*/
class ApiParse extends ApiBase {
- private $section, $text, $pstText = null;
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
+ /** @var String $section */
+ private $section = null;
+
+ /** @var Content $content */
+ private $content = null;
+
+ /** @var Content $pstContent */
+ private $pstContent = null;
public function execute() {
// The data is hot but user-dependent, like page views, so we set vary cookies
@@ -40,11 +44,22 @@ class ApiParse extends ApiBase {
$params = $this->extractRequestParams();
$text = $params['text'];
$title = $params['title'];
+ if ( $title === null ) {
+ $titleProvided = false;
+ // A title is needed for parsing, so arbitrarily choose one
+ $title = 'API';
+ } else {
+ $titleProvided = true;
+ }
+
$page = $params['page'];
$pageid = $params['pageid'];
$oldid = $params['oldid'];
- if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) {
+ $model = $params['contentmodel'];
+ $format = $params['contentformat'];
+
+ if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) {
$this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' );
}
@@ -61,7 +76,7 @@ class ApiParse extends ApiBase {
// TODO: Does this still need $wgTitle?
global $wgParser, $wgTitle;
- // Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks
+ // Currently unnecessary, code to act as a safeguard against any change in current behavior of uselang
$oldLang = null;
if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) {
$oldLang = $this->getContext()->getLanguage(); // Backup language
@@ -87,23 +102,22 @@ class ApiParse extends ApiBase {
$titleObj = $rev->getTitle();
$wgTitle = $titleObj;
$pageObj = WikiPage::factory( $titleObj );
- $popts = $pageObj->makeParserOptions( $this->getContext() );
- $popts->enableLimitReport( !$params['disablepp'] );
+ $popts = $this->makeParserOptions( $pageObj, $params );
// If for some reason the "oldid" is actually the current revision, it may be cached
- if ( $titleObj->getLatestRevID() === intval( $oldid ) ) {
+ if ( $rev->isCurrent() ) {
// May get from/save to parser cache
- $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
- isset( $prop['wikitext'] ) ) ;
+ $p_result = $this->getParsedContent( $pageObj, $popts,
+ $pageid, isset( $prop['wikitext'] ) );
} else { // This is an old revision, so get the text differently
- $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() );
+ $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
if ( $this->section !== false ) {
- $this->text = $this->getSectionText( $this->text, 'r' . $rev->getId() );
+ $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
}
// Should we save old revision parses to the parser cache?
- $p_result = $wgParser->parse( $this->text, $titleObj, $popts );
+ $p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts );
}
} else { // Not $oldid, but $pageid or $page
if ( $params['redirects'] ) {
@@ -136,56 +150,84 @@ class ApiParse extends ApiBase {
$pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
$titleObj = $pageObj->getTitle();
+ if ( !$titleObj || !$titleObj->exists() ) {
+ $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
+ }
$wgTitle = $titleObj;
if ( isset( $prop['revid'] ) ) {
$oldid = $pageObj->getLatest();
}
- $popts = $pageObj->makeParserOptions( $this->getContext() );
- $popts->enableLimitReport( !$params['disablepp'] );
+ $popts = $this->makeParserOptions( $pageObj, $params );
// Potentially cached
- $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
- isset( $prop['wikitext'] ) ) ;
+ $p_result = $this->getParsedContent( $pageObj, $popts, $pageid,
+ isset( $prop['wikitext'] ) );
}
} else { // Not $oldid, $pageid, $page. Hence based on $text
-
- if ( is_null( $text ) ) {
- $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
- }
- $this->text = $text;
$titleObj = Title::newFromText( $title );
- if ( !$titleObj ) {
+ if ( !$titleObj || $titleObj->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $title ) );
}
+ if ( !$titleObj->canExist() ) {
+ $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' );
+ }
$wgTitle = $titleObj;
$pageObj = WikiPage::factory( $titleObj );
- $popts = $pageObj->makeParserOptions( $this->getContext() );
- $popts->enableLimitReport( !$params['disablepp'] );
+ $popts = $this->makeParserOptions( $pageObj, $params );
+
+ if ( is_null( $text ) ) {
+ if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
+ $this->setWarning(
+ "'title' used without 'text', and parsed page properties were requested " .
+ "(did you mean to use 'page' instead of 'title'?)"
+ );
+ }
+ // Prevent warning from ContentHandler::makeContent()
+ $text = '';
+ }
+
+ // If we are parsing text, do not use the content model of the default
+ // API title, but default to wikitext to keep BC.
+ if ( !$titleProvided && is_null( $model ) ) {
+ $model = CONTENT_MODEL_WIKITEXT;
+ $this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." );
+ }
+
+ try {
+ $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
+ } catch ( MWContentSerializationException $ex ) {
+ $this->dieUsage( $ex->getMessage(), 'parseerror' );
+ }
if ( $this->section !== false ) {
- $this->text = $this->getSectionText( $this->text, $titleObj->getText() );
+ $this->content = $this->getSectionContent( $this->content, $titleObj->getText() );
}
if ( $params['pst'] || $params['onlypst'] ) {
- $this->pstText = $wgParser->preSaveTransform( $this->text, $titleObj, $this->getUser(), $popts );
+ $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
}
if ( $params['onlypst'] ) {
// Build a result and bail out
$result_array = array();
$result_array['text'] = array();
- $result->setContent( $result_array['text'], $this->pstText );
+ ApiResult::setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
if ( isset( $prop['wikitext'] ) ) {
$result_array['wikitext'] = array();
- $result->setContent( $result_array['wikitext'], $this->text );
+ ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
}
$result->addValue( null, $this->getModuleName(), $result_array );
return;
}
+
// Not cached (save or load)
- $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
+ if ( $params['pst'] ) {
+ $p_result = $this->pstContent->getParserOutput( $titleObj, null, $popts );
+ } else {
+ $p_result = $this->content->getParserOutput( $titleObj, null, $popts );
+ }
}
$result_array = array();
@@ -202,21 +244,35 @@ class ApiParse extends ApiBase {
if ( isset( $prop['text'] ) ) {
$result_array['text'] = array();
- $result->setContent( $result_array['text'], $p_result->getText() );
+ ApiResult::setContent( $result_array['text'], $p_result->getText() );
}
if ( !is_null( $params['summary'] ) ) {
$result_array['parsedsummary'] = array();
- $result->setContent( $result_array['parsedsummary'], Linker::formatComment( $params['summary'], $titleObj ) );
+ ApiResult::setContent( $result_array['parsedsummary'], Linker::formatComment( $params['summary'], $titleObj ) );
+ }
+
+ if ( isset( $prop['langlinks'] ) || isset( $prop['languageshtml'] ) ) {
+ $langlinks = $p_result->getLanguageLinks();
+
+ if ( $params['effectivelanglinks'] ) {
+ // Link flags are ignored for now, but may in the future be
+ // included in the result.
+ $linkFlags = array();
+ wfRunHooks( 'LanguageLinks', array( $titleObj, &$langlinks, &$linkFlags ) );
+ }
+ } else {
+ $langlinks = false;
}
if ( isset( $prop['langlinks'] ) ) {
- $result_array['langlinks'] = $this->formatLangLinks( $p_result->getLanguageLinks() );
+ $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
}
if ( isset( $prop['languageshtml'] ) ) {
- $languagesHtml = $this->languagesHtml( $p_result->getLanguageLinks() );
+ $languagesHtml = $this->languagesHtml( $langlinks );
+
$result_array['languageshtml'] = array();
- $result->setContent( $result_array['languageshtml'], $languagesHtml );
+ ApiResult::setContent( $result_array['languageshtml'], $languagesHtml );
}
if ( isset( $prop['categories'] ) ) {
$result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
@@ -224,7 +280,7 @@ class ApiParse extends ApiBase {
if ( isset( $prop['categorieshtml'] ) ) {
$categoriesHtml = $this->categoriesHtml( $p_result->getCategories() );
$result_array['categorieshtml'] = array();
- $result->setContent( $result_array['categorieshtml'], $categoriesHtml );
+ ApiResult::setContent( $result_array['categorieshtml'], $categoriesHtml );
}
if ( isset( $prop['links'] ) ) {
$result_array['links'] = $this->formatLinks( $p_result->getLinks() );
@@ -265,7 +321,7 @@ class ApiParse extends ApiBase {
if ( isset( $prop['headhtml'] ) ) {
$result_array['headhtml'] = array();
- $result->setContent( $result_array['headhtml'], $context->getOutput()->headElement( $context->getSkin() ) );
+ ApiResult::setContent( $result_array['headhtml'], $context->getOutput()->headElement( $context->getSkin() ) );
}
}
@@ -275,10 +331,10 @@ class ApiParse extends ApiBase {
if ( isset( $prop['wikitext'] ) ) {
$result_array['wikitext'] = array();
- $result->setContent( $result_array['wikitext'], $this->text );
- if ( !is_null( $this->pstText ) ) {
+ ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+ if ( !is_null( $this->pstContent ) ) {
$result_array['psttext'] = array();
- $result->setContent( $result_array['psttext'], $this->pstText );
+ ApiResult::setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
}
}
if ( isset( $prop['properties'] ) ) {
@@ -286,15 +342,19 @@ class ApiParse extends ApiBase {
}
if ( $params['generatexml'] ) {
+ if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
+ $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" );
+ }
+
$wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS );
- $dom = $wgParser->preprocessToDom( $this->text );
+ $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
if ( is_callable( array( $dom, 'saveXML' ) ) ) {
$xml = $dom->saveXML();
} else {
$xml = $dom->__toString();
}
$result_array['parsetree'] = array();
- $result->setContent( $result_array['parsetree'], $xml );
+ ApiResult::setContent( $result_array['parsetree'], $xml );
}
$result_mapping = array(
@@ -319,21 +379,42 @@ class ApiParse extends ApiBase {
}
/**
+ * Constructs a ParserOptions object
+ *
+ * @param WikiPage $pageObj
+ * @param array $params
+ *
+ * @return ParserOptions
+ */
+ protected function makeParserOptions( WikiPage $pageObj, array $params ) {
+ wfProfileIn( __METHOD__ );
+
+ $popts = $pageObj->makeParserOptions( $this->getContext() );
+ $popts->enableLimitReport( !$params['disablepp'] );
+ $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
+ $popts->setIsSectionPreview( $params['sectionpreview'] );
+
+ wfProfileOut( __METHOD__ );
+ return $popts;
+ }
+
+ /**
* @param $page WikiPage
* @param $popts ParserOptions
* @param $pageId Int
* @param $getWikitext Bool
* @return ParserOutput
*/
- private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) {
- global $wgParser;
+ private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
+ $this->content = $page->getContent( Revision::RAW ); //XXX: really raw?
- if ( $this->section !== false ) {
- $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
- ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() );
+ if ( $this->section !== false && $this->content !== null ) {
+ $this->content = $this->getSectionContent(
+ $this->content,
+ !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getText() );
// Not cached (save or load)
- return $wgParser->parse( $this->text, $page->getTitle(), $popts );
+ return $this->content->getParserOutput( $page->getTitle(), null, $popts );
} else {
// Try the parser cache first
// getParserOutput will save to Parser cache if able
@@ -342,20 +423,23 @@ class ApiParse extends ApiBase {
$this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
}
if ( $getWikitext ) {
- $this->text = $page->getRawText();
+ $this->content = $page->getContent( Revision::RAW );
}
return $pout;
}
}
- private function getSectionText( $text, $what ) {
- global $wgParser;
+ private function getSectionContent( Content $content, $what ) {
// Not cached (save or load)
- $text = $wgParser->getSection( $text, $this->section, false );
- if ( $text === false ) {
+ $section = $content->getSection( $this->section );
+ if ( $section === false ) {
$this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
}
- return $text;
+ if ( $section === null ) {
+ $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
+ $section = false;
+ }
+ return $section;
}
private function formatLangLinks( $links ) {
@@ -369,7 +453,7 @@ class ApiParse extends ApiBase {
if ( $title ) {
$entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
}
- $this->getResult()->setContent( $entry, $bits[1] );
+ ApiResult::setContent( $entry, $bits[1] );
$result[] = $entry;
}
return $result;
@@ -380,7 +464,7 @@ class ApiParse extends ApiBase {
foreach ( $links as $link => $sortkey ) {
$entry = array();
$entry['sortkey'] = $sortkey;
- $this->getResult()->setContent( $entry, $link );
+ ApiResult::setContent( $entry, $link );
$result[] = $entry;
}
return $result;
@@ -415,14 +499,14 @@ class ApiParse extends ApiBase {
$text = Language::fetchLanguageName( $nt->getInterwiki() );
$langs[] = Html::element( 'a',
- array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ),
+ array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => 'external' ),
$text == '' ? $l : $text );
}
$s .= implode( wfMessage( 'pipe-separator' )->escaped(), $langs );
if ( $wgContLang->isRTL() ) {
- $s = Html::rawElement( 'span', array( 'dir' => "LTR" ), $s );
+ $s = Html::rawElement( 'span', array( 'dir' => 'LTR' ), $s );
}
return $s;
@@ -434,7 +518,7 @@ class ApiParse extends ApiBase {
foreach ( $nslinks as $title => $id ) {
$entry = array();
$entry['ns'] = $ns;
- $this->getResult()->setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
+ ApiResult::setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
if ( $id != 0 ) {
$entry['exists'] = '';
}
@@ -456,7 +540,7 @@ class ApiParse extends ApiBase {
$entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
}
- $this->getResult()->setContent( $entry, $title->getFullText() );
+ ApiResult::setContent( $entry, $title->getFullText() );
$result[] = $entry;
}
}
@@ -468,7 +552,7 @@ class ApiParse extends ApiBase {
foreach ( $headItems as $tag => $content ) {
$entry = array();
$entry['tag'] = $tag;
- $this->getResult()->setContent( $entry, $content );
+ ApiResult::setContent( $entry, $content );
$result[] = $entry;
}
return $result;
@@ -479,7 +563,7 @@ class ApiParse extends ApiBase {
foreach ( $properties as $name => $value ) {
$entry = array();
$entry['name'] = $name;
- $this->getResult()->setContent( $entry, $value );
+ ApiResult::setContent( $entry, $value );
$result[] = $entry;
}
return $result;
@@ -490,7 +574,7 @@ class ApiParse extends ApiBase {
foreach ( $css as $file => $link ) {
$entry = array();
$entry['file'] = $file;
- $this->getResult()->setContent( $entry, $link );
+ ApiResult::setContent( $entry, $link );
$result[] = $entry;
}
return $result;
@@ -506,9 +590,7 @@ class ApiParse extends ApiBase {
public function getAllowedParams() {
return array(
- 'title' => array(
- ApiBase::PARAM_DFLT => 'API',
- ),
+ 'title' => null,
'text' => null,
'summary' => null,
'page' => null,
@@ -544,20 +626,31 @@ class ApiParse extends ApiBase {
),
'pst' => false,
'onlypst' => false,
+ 'effectivelanglinks' => false,
'uselang' => null,
'section' => null,
'disablepp' => false,
'generatexml' => false,
+ 'preview' => false,
+ 'sectionpreview' => false,
+ 'contentformat' => array(
+ ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+ ),
+ 'contentmodel' => array(
+ ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+ )
);
}
public function getParamDescription() {
$p = $this->getModulePrefix();
+ $wikitext = CONTENT_MODEL_WIKITEXT;
return array(
- 'text' => 'Wikitext to parse',
+ 'text' => "Text to parse. Use {$p}title or {$p}contentmodel to control the content model",
'summary' => 'Summary to parse',
'redirects' => "If the {$p}page or the {$p}pageid parameter is set to a redirect, resolve it",
- 'title' => 'Title of page the text belongs to',
+ 'title' => "Title of page the text belongs to. " .
+ "If omitted, \"API\" is used as the title with content model $wikitext",
'page' => "Parse the content of this page. Cannot be used together with {$p}text and {$p}title",
'pageid' => "Parse the content of this page. Overrides {$p}page",
'oldid' => "Parse the content of this revision. Overrides {$p}page and {$p}pageid",
@@ -581,52 +674,74 @@ class ApiParse extends ApiBase {
' wikitext - Gives the original wikitext that was parsed',
' properties - Gives various properties defined in the parsed wikitext',
),
+ 'effectivelanglinks' => array(
+ 'Includes language links supplied by extensions',
+ '(for use with prop=langlinks|languageshtml)',
+ ),
'pst' => array(
'Do a pre-save transform on the input before parsing it',
- 'Ignored if page, pageid or oldid is used'
+ "Only valid when used with {$p}text",
),
'onlypst' => array(
'Do a pre-save transform (PST) on the input, but don\'t parse it',
- 'Returns the same wikitext, after a PST has been applied. Ignored if page, pageid or oldid is used'
+ 'Returns the same wikitext, after a PST has been applied.',
+ "Only valid when used with {$p}text",
),
'uselang' => 'Which language to parse the request in',
'section' => 'Only retrieve the content of this section number',
'disablepp' => 'Disable the PP Report from the parser output',
- 'generatexml' => 'Generate XML parse tree',
+ 'generatexml' => "Generate XML parse tree (requires contentmodel=$wikitext)",
+ 'preview' => 'Parse in preview mode',
+ 'sectionpreview' => 'Parse in section preview mode (enables preview mode too)',
+ 'contentformat' => array(
+ 'Content serialization format used for the input text',
+ "Only valid when used with {$p}text",
+ ),
+ 'contentmodel' => array(
+ "Content model of the input text. Default is the model of the " .
+ "specified ${p}title, or $wikitext if ${p}title is not specified",
+ "Only valid when used with {$p}text",
+ ),
);
}
public function getDescription() {
+ $p = $this->getModulePrefix();
return array(
- 'Parses wikitext and returns parser output',
+ 'Parses content and returns parser output',
'See the various prop-Modules of action=query to get information from the current version of a page',
+ 'There are several ways to specify the text to parse:',
+ "1) Specify a page or revision, using {$p}page, {$p}pageid, or {$p}oldid.",
+ "2) Specify content explicitly, using {$p}text, {$p}title, and {$p}contentmodel.",
+ "3) Specify only a summary to parse. {$p}prop should be given an empty value.",
);
}
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' => 'params', 'info' => 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?' ),
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' ),
array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ),
array( 'nosuchpageid' ),
array( 'invalidtitle', 'title' ),
+ array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
+ array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ),
+ array( 'code' => 'pagecannotexist', 'info' => "Namespace doesn't allow actual pages" ),
) );
}
public function getExamples() {
return array(
- 'api.php?action=parse&text={{Project:Sandbox}}'
+ 'api.php?action=parse&page=Project:Sandbox' => 'Parse a page',
+ 'api.php?action=parse&text={{Project:Sandbox}}' => 'Parse wikitext',
+ 'api.php?action=parse&text={{PAGENAME}}&title=Test' => 'Parse wikitext, specifying the page title',
+ 'api.php?action=parse&summary=Some+[[link]]&prop=' => 'Parse a summary',
);
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php
index cb5e081a..bd2fde2b 100644
--- a/includes/api/ApiPatrol.php
+++ b/includes/api/ApiPatrol.php
@@ -30,20 +30,32 @@
*/
class ApiPatrol extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* Patrols the article or provides the reason the patrol failed.
*/
public function execute() {
$params = $this->extractRequestParams();
-
- $rc = RecentChange::newFromID( $params['rcid'] );
- if ( !$rc instanceof RecentChange ) {
- $this->dieUsageMsg( array( 'nosuchrcid', $params['rcid'] ) );
+ $this->requireOnlyOneParameter( $params, 'rcid', 'revid' );
+
+ if ( isset( $params['rcid'] ) ) {
+ $rc = RecentChange::newFromID( $params['rcid'] );
+ if ( !$rc ) {
+ $this->dieUsageMsg( array( 'nosuchrcid', $params['rcid'] ) );
+ }
+ } else {
+ $rev = Revision::newFromId( $params['revid'] );
+ if ( !$rev ) {
+ $this->dieUsageMsg( array( 'nosuchrevid', $params['revid'] ) );
+ }
+ $rc = $rev->getRecentChange();
+ if ( !$rc ) {
+ $this->dieUsage(
+ 'The revision ' . $params['revid'] . " can't be patrolled as it's too old",
+ 'notpatrollable'
+ );
+ }
}
+
$retval = $rc->doMarkPatrolled( $this->getUser() );
if ( $retval ) {
@@ -70,8 +82,10 @@ class ApiPatrol extends ApiBase {
ApiBase::PARAM_REQUIRED => true
),
'rcid' => array(
- ApiBase::PARAM_TYPE => 'integer',
- ApiBase::PARAM_REQUIRED => true
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
+ 'revid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
),
);
}
@@ -80,6 +94,7 @@ class ApiPatrol extends ApiBase {
return array(
'token' => 'Patrol token obtained from list=recentchanges',
'rcid' => 'Recentchanges ID to patrol',
+ 'revid' => 'Revision ID to patrol',
);
}
@@ -98,8 +113,16 @@ class ApiPatrol extends ApiBase {
}
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
- array( 'nosuchrcid', 'rcid' ),
+ return array_merge(
+ parent::getPossibleErrors(),
+ parent::getRequireOnlyOneParameterErrorMessages( array( 'rcid', 'revid' ) ),
+ array(
+ array( 'nosuchrcid', 'rcid' ),
+ array( 'nosuchrevid', 'revid' ),
+ array(
+ 'code' => 'notpatrollable',
+ 'info' => "The revision can't be patrolled as it's too old"
+ )
) );
}
@@ -113,15 +136,12 @@ class ApiPatrol extends ApiBase {
public function getExamples() {
return array(
- 'api.php?action=patrol&token=123abc&rcid=230672766'
+ 'api.php?action=patrol&token=123abc&rcid=230672766',
+ 'api.php?action=patrol&token=123abc&revid=230672766'
);
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Patrol';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index b3ca67e6..7830c8b4 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -29,10 +29,6 @@
*/
class ApiProtect extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
global $wgRestrictionLevels;
$params = $this->extractRequestParams();
@@ -107,8 +103,7 @@ class ApiProtect extends ApiBase {
$status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() );
if ( !$status->isOK() ) {
- $errors = $status->getErrorsArray();
- $this->dieUsageMsg( $errors[0] );
+ $this->dieStatus( $status );
}
$res = array(
'title' => $titleObj->getPrefixedText(),
@@ -178,7 +173,7 @@ class ApiProtect extends ApiBase {
'token' => 'A protect token previously retrieved through prop=info',
'protections' => '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.' ),
+ 'Use \'infinite\', \'indefinite\' or \'never\', for a never-expiring protection.' ),
'reason' => 'Reason for (un)protecting',
'cascade' => array( 'Enable cascading protection (i.e. protect pages included in this page)',
'Ignored if not all protection levels are \'sysop\' or \'protect\'' ),
@@ -234,8 +229,4 @@ class ApiProtect extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Protect';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php
index 9fedaf1b..0812ba51 100644
--- a/includes/api/ApiPurge.php
+++ b/includes/api/ApiPurge.php
@@ -31,69 +31,71 @@
*/
class ApiPurge extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
+ private $mPageSet;
+
+ /**
+ * Add all items from $values into the result
+ * @param array $result output
+ * @param array $values values to add
+ * @param string $flag the name of the boolean flag to mark this element
+ * @param string $name if given, name of the value
+ */
+ private static function addValues( array &$result, $values, $flag = null, $name = null ) {
+ foreach ( $values as $val ) {
+ if ( $val instanceof Title ) {
+ $v = array();
+ ApiQueryBase::addTitleInfo( $v, $val );
+ } elseif ( $name !== null ) {
+ $v = array( $name => $val );
+ } else {
+ $v = $val;
+ }
+ if ( $flag !== null ) {
+ $v[$flag] = '';
+ }
+ $result[] = $v;
+ }
}
/**
* Purges the cache of a page
*/
public function execute() {
- $user = $this->getUser();
$params = $this->extractRequestParams();
- if ( !$user->isAllowed( 'purge' ) && !$this->getMain()->isInternalMode() &&
- !$this->getRequest()->wasPosted() ) {
- $this->dieUsageMsg( array( 'mustbeposted', $this->getModuleName() ) );
- }
$forceLinkUpdate = $params['forcelinkupdate'];
- $pageSet = new ApiPageSet( $this );
+ $forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate'];
+ $pageSet = $this->getPageSet();
$pageSet->execute();
$result = array();
- foreach( $pageSet->getInvalidTitles() as $title ) {
- $r = array();
- $r['title'] = $title;
- $r['invalid'] = '';
- $result[] = $r;
- }
- foreach( $pageSet->getMissingPageIDs() as $p ) {
- $page = array();
- $page['pageid'] = $p;
- $page['missing'] = '';
- $result[] = $page;
- }
- foreach( $pageSet->getMissingRevisionIDs() as $r ) {
- $rev = array();
- $rev['revid'] = $r;
- $rev['missing'] = '';
- $result[] = $rev;
- }
-
- foreach ( $pageSet->getTitles() as $title ) {
+ self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' );
+ self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' );
+ self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' );
+ self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' );
+ self::addValues( $result, $pageSet->getMissingTitles(), 'missing' );
+ self::addValues( $result, $pageSet->getInterwikiTitlesAsResult() );
+
+ foreach ( $pageSet->getGoodTitles() as $title ) {
$r = array();
-
ApiQueryBase::addTitleInfo( $r, $title );
- if ( !$title->exists() ) {
- $r['missing'] = '';
- $result[] = $r;
- continue;
- }
-
$page = WikiPage::factory( $title );
$page->doPurge(); // Directly purge and skip the UI part of purge().
$r['purged'] = '';
- if( $forceLinkUpdate ) {
- if ( !$user->pingLimiter() ) {
- global $wgParser, $wgEnableParserCache;
+ if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) {
+ if ( !$this->getUser()->pingLimiter( 'linkpurge' ) ) {
+ global $wgEnableParserCache;
$popts = $page->makeParserOptions( 'canonical' );
- $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
- true, true, $page->getLatest() );
+
+ # Parse content; note that HTML generation is only needed if we want to cache the result.
+ $content = $page->getContent( Revision::RAW );
+ $p_result = $content->getParserOutput( $title, $page->getLatest(), $popts, $wgEnableParserCache );
# Update the links tables
- $updates = $p_result->getSecondaryDataUpdates( $title );
+ $updates = $content->getSecondaryDataUpdates(
+ $title, null, $forceRecursiveLinkUpdate, $p_result );
DataUpdate::runUpdates( $updates );
$r['linkupdate'] = '';
@@ -114,24 +116,59 @@ class ApiPurge extends ApiBase {
$apiResult = $this->getResult();
$apiResult->setIndexedTagName( $result, 'page' );
$apiResult->addValue( null, $this->getModuleName(), $result );
+
+ $values = $pageSet->getNormalizedTitlesAsResult( $apiResult );
+ if ( $values ) {
+ $apiResult->addValue( null, 'normalized', $values );
+ }
+ $values = $pageSet->getConvertedTitlesAsResult( $apiResult );
+ if ( $values ) {
+ $apiResult->addValue( null, 'converted', $values );
+ }
+ $values = $pageSet->getRedirectTitlesAsResult( $apiResult );
+ if ( $values ) {
+ $apiResult->addValue( null, 'redirects', $values );
+ }
+ }
+
+ /**
+ * Get a cached instance of an ApiPageSet object
+ * @return ApiPageSet
+ */
+ private function getPageSet() {
+ if ( !isset( $this->mPageSet ) ) {
+ $this->mPageSet = new ApiPageSet( $this );
+ }
+ return $this->mPageSet;
}
public function isWriteMode() {
return true;
}
- public function getAllowedParams() {
- $psModule = new ApiPageSet( $this );
- return $psModule->getAllowedParams() + array(
+ public function mustBePosted() {
+ // Anonymous users are not allowed a non-POST request
+ return !$this->getUser()->isAllowed( 'purge' );
+ }
+
+ public function getAllowedParams( $flags = 0 ) {
+ $result = array(
'forcelinkupdate' => false,
+ 'forcerecursivelinkupdate' => false
);
+ if ( $flags ) {
+ $result += $this->getPageSet()->getFinalParams( $flags );
+ }
+ return $result;
}
public function getParamDescription() {
- $psModule = new ApiPageSet( $this );
- return $psModule->getParamDescription() + array(
- 'forcelinkupdate' => 'Update the links tables',
- );
+ return $this->getPageSet()->getFinalParamDescription()
+ + array(
+ 'forcelinkupdate' => 'Update the links tables',
+ 'forcerecursivelinkupdate' => 'Update the links table, and update ' .
+ 'the links tables for any page that uses this page as a template',
+ );
}
public function getResultProperties() {
@@ -155,9 +192,14 @@ class ApiPurge extends ApiBase {
ApiBase::PROP_NULLABLE => true
),
'invalid' => 'boolean',
+ 'special' => 'boolean',
'missing' => 'boolean',
'purged' => 'boolean',
- 'linkupdate' => 'boolean'
+ 'linkupdate' => 'boolean',
+ 'iw' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
)
);
}
@@ -169,10 +211,9 @@ class ApiPurge extends ApiBase {
}
public function getPossibleErrors() {
- $psModule = new ApiPageSet( $this );
return array_merge(
parent::getPossibleErrors(),
- $psModule->getPossibleErrors()
+ $this->getPageSet()->getFinalPossibleErrors()
);
}
@@ -185,8 +226,4 @@ class ApiPurge extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Purge';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index 554aae5a..e03837fc 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -37,16 +37,11 @@
*/
class ApiQuery extends ApiBase {
- private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames;
-
/**
- * @var ApiPageSet
+ * List of Api Query prop modules
+ * @var array
*/
- private $mPageSet;
-
- private $params, $redirects, $convertTitles, $iwUrl;
-
- private $mQueryPropModules = array(
+ private static $QueryPropModules = array(
'categories' => 'ApiQueryCategories',
'categoryinfo' => 'ApiQueryCategoryInfo',
'duplicatefiles' => 'ApiQueryDuplicateFiles',
@@ -63,11 +58,17 @@ class ApiQuery extends ApiBase {
'templates' => 'ApiQueryLinks',
);
- private $mQueryListModules = array(
+ /**
+ * List of Api Query list modules
+ * @var array
+ */
+ private static $QueryListModules = array(
'allcategories' => 'ApiQueryAllCategories',
+ 'allfileusages' => 'ApiQueryAllLinks',
'allimages' => 'ApiQueryAllImages',
'alllinks' => 'ApiQueryAllLinks',
'allpages' => 'ApiQueryAllPages',
+ 'alltransclusions' => 'ApiQueryAllLinks',
'allusers' => 'ApiQueryAllUsers',
'backlinks' => 'ApiQueryBacklinks',
'blocks' => 'ApiQueryBlocks',
@@ -80,6 +81,8 @@ class ApiQuery extends ApiBase {
'iwbacklinks' => 'ApiQueryIWBacklinks',
'langbacklinks' => 'ApiQueryLangBacklinks',
'logevents' => 'ApiQueryLogEvents',
+ 'pageswithprop' => 'ApiQueryPagesWithProp',
+ 'pagepropnames' => 'ApiQueryPagePropNames',
'protectedtitles' => 'ApiQueryProtectedTitles',
'querypage' => 'ApiQueryQueryPage',
'random' => 'ApiQueryRandom',
@@ -92,16 +95,27 @@ class ApiQuery extends ApiBase {
'watchlistraw' => 'ApiQueryWatchlistRaw',
);
- private $mQueryMetaModules = array(
+ /**
+ * List of Api Query meta modules
+ * @var array
+ */
+ private static $QueryMetaModules = array(
'allmessages' => 'ApiQueryAllMessages',
'siteinfo' => 'ApiQuerySiteinfo',
'userinfo' => 'ApiQueryUserInfo',
+ 'filerepoinfo' => 'ApiQueryFileRepoInfo',
);
- private $mSlaveDB = null;
- private $mNamedDB = array();
+ /**
+ * @var ApiPageSet
+ */
+ private $mPageSet;
- protected $mAllowedGenerators = array();
+ private $mParams;
+ private $mNamedDB = array();
+ private $mModuleMgr;
+ private $mGeneratorContinue;
+ private $mUseLegacyContinue;
/**
* @param $main ApiMain
@@ -110,59 +124,27 @@ class ApiQuery extends ApiBase {
public function __construct( $main, $action ) {
parent::__construct( $main, $action );
- // Allow custom modules to be added in LocalSettings.php
- global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules,
- $wgMemc, $wgAPICacheHelpTimeout;
- 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 );
-
- // Get array of query generators from cache if present
- $key = wfMemcKey( 'apiquerygenerators', SpecialVersion::getVersion( 'nodb' ) );
-
- if ( $wgAPICacheHelpTimeout > 0 ) {
- $cached = $wgMemc->get( $key );
- if ( $cached ) {
- $this->mAllowedGenerators = $cached;
- return;
- }
- }
- $this->makeGeneratorList( $this->mQueryPropModules );
- $this->makeGeneratorList( $this->mQueryListModules );
+ $this->mModuleMgr = new ApiModuleManager( $this );
- if ( $wgAPICacheHelpTimeout > 0 ) {
- $wgMemc->set( $key, $this->mAllowedGenerators, $wgAPICacheHelpTimeout );
- }
+ // Allow custom modules to be added in LocalSettings.php
+ global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules;
+ $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' );
+ $this->mModuleMgr->addModules( $wgAPIPropModules, 'prop' );
+ $this->mModuleMgr->addModules( self::$QueryListModules, 'list' );
+ $this->mModuleMgr->addModules( $wgAPIListModules, 'list' );
+ $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' );
+ $this->mModuleMgr->addModules( $wgAPIMetaModules, 'meta' );
+
+ // Create PageSet that will process titles/pageids/revids/generator
+ $this->mPageSet = new ApiPageSet( $this );
}
/**
- * Helper function to append any add-in modules to the list
- * @param $modules array Module array
- * @param $newModules array Module array to add to $modules
+ * Overrides to return this instance's module manager.
+ * @return ApiModuleManager
*/
- private static function appendUserModules( &$modules, $newModules ) {
- if ( is_array( $newModules ) ) {
- foreach ( $newModules as $moduleName => $moduleClass ) {
- $modules[$moduleName] = $moduleClass;
- }
- }
- }
-
- /**
- * Gets a default slave database connection object
- * @return DatabaseBase
- */
- public function getDB() {
- if ( !isset( $this->mSlaveDB ) ) {
- $this->profileDBIn();
- $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' );
- $this->profileDBOut();
- }
- return $this->mSlaveDB;
+ public function getModuleManager() {
+ return $this->mModuleMgr;
}
/**
@@ -170,9 +152,9 @@ class ApiQuery extends ApiBase {
* If no such connection has been requested before, it will be created.
* Subsequent calls with the same $name will return the same connection
* as the first, regardless of the values of $db and $groups
- * @param $name string Name to assign to the database connection
- * @param $db int One of the DB_* constants
- * @param $groups array Query groups
+ * @param string $name Name to assign to the database connection
+ * @param int $db One of the DB_* constants
+ * @param array $groups Query groups
* @return DatabaseBase
*/
public function getNamedDB( $name, $db, $groups ) {
@@ -194,31 +176,38 @@ class ApiQuery extends ApiBase {
/**
* Get the array mapping module names to class names
+ * @deprecated since 1.21, use getModuleManager()'s methods instead
+ * @return array array(modulename => classname)
+ */
+ public function getModules() {
+ wfDeprecated( __METHOD__, '1.21' );
+ return $this->getModuleManager()->getNamesWithClasses();
+ }
+
+ /**
+ * Get the generators array mapping module names to class names
+ * @deprecated since 1.21, list of generators is maintained by ApiPageSet
* @return array array(modulename => classname)
*/
- function getModules() {
- return array_merge( $this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules );
+ public function getGenerators() {
+ wfDeprecated( __METHOD__, '1.21' );
+ $gens = array();
+ foreach ( $this->mModuleMgr->getNamesWithClasses() as $name => $class ) {
+ if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
+ $gens[$name] = $class;
+ }
+ }
+ return $gens;
}
/**
* Get whether the specified module is a prop, list or a meta query module
- * @param $moduleName string Name of the module to find type for
+ * @deprecated since 1.21, use getModuleManager()->getModuleGroup()
+ * @param string $moduleName Name of the module to find type for
* @return mixed string or null
*/
function getModuleType( $moduleName ) {
- if ( isset( $this->mQueryPropModules[$moduleName] ) ) {
- return 'prop';
- }
-
- if ( isset( $this->mQueryListModules[$moduleName] ) ) {
- return 'list';
- }
-
- if ( isset( $this->mQueryMetaModules[$moduleName] ) ) {
- return 'meta';
- }
-
- return null;
+ return $this->getModuleManager()->getModuleGroup( $moduleName );
}
/**
@@ -247,42 +236,37 @@ class ApiQuery extends ApiBase {
* #5 Execute all requested modules
*/
public function execute() {
- $this->params = $this->extractRequestParams();
- $this->redirects = $this->params['redirects'];
- $this->convertTitles = $this->params['converttitles'];
- $this->iwUrl = $this->params['iwurl'];
+ $this->mParams = $this->extractRequestParams();
- // Create PageSet
- $this->mPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles );
+ // $pagesetParams is a array of parameter names used by the pageset generator
+ // or null if pageset has already finished and is no longer needed
+ // $completeModules is a set of complete modules with the name as key
+ $this->initContinue( $pagesetParams, $completeModules );
// Instantiate requested modules
- $modules = array();
- $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();
- $cacheMode = $this->mergeCacheMode( $cacheMode,
- $generator->getCacheMode( $params ) );
- $this->executeGeneratorModule( $generator, $modules );
- } else {
- // Append custom fields and populate page/revision information
- $this->addCustomFldsToPageSet( $modules, $this->mPageSet );
+ $allModules = array();
+ $this->instantiateModules( $allModules, 'prop' );
+ $propModules = $allModules; // Keep a copy
+ $this->instantiateModules( $allModules, 'list' );
+ $this->instantiateModules( $allModules, 'meta' );
+
+ // Filter modules based on continue parameter
+ $modules = $this->initModules( $allModules, $completeModules, $pagesetParams !== null );
+
+ // Execute pageset if in legacy mode or if pageset is not done
+ if ( $completeModules === null || $pagesetParams !== null ) {
+ // Populate page/revision information
$this->mPageSet->execute();
+ // Record page information (title, namespace, if exists, etc)
+ $this->outputGeneralPageInfo();
+ } else {
+ $this->mPageSet->executeDryRun();
}
- // Record page information (title, namespace, if exists, etc)
- $this->outputGeneralPageInfo();
+ $cacheMode = $this->mPageSet->getCacheMode();
- // Execute all requested modules.
- /**
- * @var $module ApiQueryBase
- */
+ // Execute all unfinished modules
+ /** @var $module ApiQueryBase */
foreach ( $modules as $module ) {
$params = $module->extractRequestParams();
$cacheMode = $this->mergeCacheMode(
@@ -295,6 +279,135 @@ class ApiQuery extends ApiBase {
// Set the cache mode
$this->getMain()->setCacheMode( $cacheMode );
+
+ if ( $completeModules === null ) {
+ return; // Legacy continue, we are done
+ }
+
+ // Reformat query-continue result section
+ $result = $this->getResult();
+ $qc = $result->getData();
+ if ( isset( $qc['query-continue'] ) ) {
+ $qc = $qc['query-continue'];
+ $result->unsetValue( null, 'query-continue' );
+ } elseif ( $this->mGeneratorContinue !== null ) {
+ $qc = array();
+ } else {
+ // no more "continue"s, we are done!
+ return;
+ }
+
+ // we are done with all the modules that do not have result in query-continue
+ $completeModules = array_merge( $completeModules, array_diff_key( $modules, $qc ) );
+ if ( $pagesetParams !== null ) {
+ // The pageset is still in use, check if all props have finished
+ $incompleteProps = array_intersect_key( $propModules, $qc );
+ if ( count( $incompleteProps ) > 0 ) {
+ // Properties are not done, continue with the same pageset state - copy current parameters
+ $main = $this->getMain();
+ $contValues = array();
+ foreach ( $pagesetParams as $param ) {
+ // The param name is already prefix-encoded
+ $contValues[$param] = $main->getVal( $param );
+ }
+ } elseif ( $this->mGeneratorContinue !== null ) {
+ // Move to the next set of pages produced by pageset, properties need to be restarted
+ $contValues = $this->mGeneratorContinue;
+ $pagesetParams = array_keys( $contValues );
+ $completeModules = array_diff_key( $completeModules, $propModules );
+ } else {
+ // Done with the pageset, finish up with the the lists and meta modules
+ $pagesetParams = null;
+ }
+ }
+
+ $continue = '||' . implode( '|', array_keys( $completeModules ) );
+ if ( $pagesetParams !== null ) {
+ // list of all pageset parameters to use in the next request
+ $continue = implode( '|', $pagesetParams ) . $continue;
+ } else {
+ // we are done with the pageset
+ $contValues = array();
+ $continue = '-' . $continue;
+ }
+ $contValues['continue'] = $continue;
+ foreach ( $qc as $qcModule ) {
+ foreach ( $qcModule as $qcKey => $qcValue ) {
+ $contValues[$qcKey] = $qcValue;
+ }
+ }
+ $this->getResult()->addValue( null, 'continue', $contValues );
+ }
+
+ /**
+ * Parse 'continue' parameter into the list of complete modules and a list of generator parameters
+ * @param array|null $pagesetParams returns list of generator params or null if pageset is done
+ * @param array|null $completeModules returns list of finished modules (as keys), or null if legacy
+ */
+ private function initContinue( &$pagesetParams, &$completeModules ) {
+ $pagesetParams = array();
+ $continue = $this->mParams['continue'];
+ if ( $continue !== null ) {
+ $this->mUseLegacyContinue = false;
+ if ( $continue !== '' ) {
+ // Format: ' pagesetParam1 | pagesetParam2 || module1 | module2 | module3 | ...
+ // If pageset is done, use '-'
+ $continue = explode( '||', $continue );
+ $this->dieContinueUsageIf( count( $continue ) !== 2 );
+ if ( $continue[0] === '-' ) {
+ $pagesetParams = null; // No need to execute pageset
+ } elseif ( $continue[0] !== '' ) {
+ // list of pageset params that might need to be repeated
+ $pagesetParams = explode( '|', $continue[0] );
+ }
+ $continue = $continue[1];
+ }
+ if ( $continue !== '' ) {
+ $completeModules = array_flip( explode( '|', $continue ) );
+ } else {
+ $completeModules = array();
+ }
+ } else {
+ $this->mUseLegacyContinue = true;
+ $completeModules = null;
+ }
+ }
+
+ /**
+ * Validate sub-modules, filter out completed ones, and do requestExtraData()
+ * @param array $allModules An dict of name=>instance of all modules requested by the client
+ * @param array|null $completeModules list of finished modules, or null if legacy continue
+ * @param bool $usePageset True if pageset will be executed
+ * @return array of modules to be processed during this execution
+ */
+ private function initModules( $allModules, $completeModules, $usePageset ) {
+ $modules = $allModules;
+ $tmp = $completeModules;
+ $wasPosted = $this->getRequest()->wasPosted();
+
+ /** @var $module ApiQueryBase */
+ foreach ( $allModules as $moduleName => $module ) {
+ if ( !$wasPosted && $module->mustBePosted() ) {
+ $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) );
+ }
+ if ( $completeModules !== null && array_key_exists( $moduleName, $completeModules ) ) {
+ // If this module is done, mark all its params as used
+ $module->extractRequestParams();
+ // Make sure this module is not used during execution
+ unset( $modules[$moduleName] );
+ unset( $tmp[$moduleName] );
+ } elseif ( $completeModules === null || $usePageset ) {
+ // Query modules may optimize data requests through the $this->getPageSet()
+ // object by adding extra fields from the page table.
+ // This function will gather all the extra request fields from the modules.
+ $module->requestExtraData( $this->mPageSet );
+ } else {
+ // Error - this prop module must have finished before generator is done
+ $this->dieContinueUsageIf( $this->mModuleMgr->getModuleGroup( $moduleName ) === 'prop' );
+ }
+ }
+ $this->dieContinueUsageIf( $completeModules !== null && count( $tmp ) !== 0 );
+ return $modules;
}
/**
@@ -320,32 +433,21 @@ class ApiQuery extends ApiBase {
}
/**
- * Query modules may optimize data requests through the $this->getPageSet() object
- * by adding extra fields from the page table.
- * This function will gather all the extra request fields from the modules.
- * @param $modules array of module objects
- * @param $pageSet ApiPageSet
- */
- private function addCustomFldsToPageSet( $modules, $pageSet ) {
- // Query all requested modules.
- /**
- * @var $module ApiQueryBase
- */
- foreach ( $modules as $module ) {
- $module->requestExtraData( $pageSet );
- }
- }
-
- /**
* Create instances of all modules requested by the client
- * @param $modules Array to append instantiated modules to
- * @param $param string Parameter name to read modules from
- * @param $moduleList Array array(modulename => classname)
+ * @param array $modules to append instantiated modules to
+ * @param string $param Parameter name to read modules from
*/
- private function instantiateModules( &$modules, $param, $moduleList ) {
- if ( isset( $this->params[$param] ) ) {
- foreach ( $this->params[$param] as $moduleName ) {
- $modules[] = new $moduleList[$moduleName] ( $this, $moduleName );
+ private function instantiateModules( &$modules, $param ) {
+ if ( isset( $this->mParams[$param] ) ) {
+ foreach ( $this->mParams[$param] as $moduleName ) {
+ $instance = $this->mModuleMgr->getModule( $moduleName, $param );
+ if ( $instance === null ) {
+ ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
+ }
+ // Ignore duplicates. TODO 2.0: die()?
+ if ( !array_key_exists( $moduleName, $modules ) ) {
+ $modules[$moduleName] = $instance;
+ }
}
}
}
@@ -363,85 +465,25 @@ class ApiQuery extends ApiBase {
// 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 ) {
- $normValues[] = array(
- 'from' => $rawTitleStr,
- 'to' => $titleStr
- );
+ $values = $pageSet->getNormalizedTitlesAsResult( $result );
+ if ( $values ) {
+ $result->addValue( 'query', 'normalized', $values );
}
-
- if ( count( $normValues ) ) {
- $result->setIndexedTagName( $normValues, 'n' );
- $result->addValue( 'query', 'normalized', $normValues );
+ $values = $pageSet->getConvertedTitlesAsResult( $result );
+ if ( $values ) {
+ $result->addValue( 'query', 'converted', $values );
}
-
- // Title conversions
- $convValues = array();
- foreach ( $pageSet->getConvertedTitles() as $rawTitleStr => $titleStr ) {
- $convValues[] = array(
- 'from' => $rawTitleStr,
- 'to' => $titleStr
- );
+ $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] );
+ if ( $values ) {
+ $result->addValue( 'query', 'interwiki', $values );
}
-
- if ( count( $convValues ) ) {
- $result->setIndexedTagName( $convValues, 'c' );
- $result->addValue( 'query', 'converted', $convValues );
+ $values = $pageSet->getRedirectTitlesAsResult( $result );
+ if ( $values ) {
+ $result->addValue( 'query', 'redirects', $values );
}
-
- // Interwiki titles
- $intrwValues = array();
- foreach ( $pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) {
- $item = array(
- 'title' => $rawTitleStr,
- 'iw' => $interwikiStr,
- );
- if ( $this->iwUrl ) {
- $title = Title::newFromText( $rawTitleStr );
- $item['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
- }
- $intrwValues[] = $item;
- }
-
- if ( count( $intrwValues ) ) {
- $result->setIndexedTagName( $intrwValues, 'i' );
- $result->addValue( 'query', 'interwiki', $intrwValues );
- }
-
- // Show redirect information
- $redirValues = array();
- /**
- * @var $titleTo Title
- */
- foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleTo ) {
- $r = array(
- 'from' => strval( $titleStrFrom ),
- 'to' => $titleTo->getPrefixedText(),
- );
- if ( $titleTo->getFragment() !== '' ) {
- $r['tofragment'] = $titleTo->getFragment();
- }
- $redirValues[] = $r;
- }
-
- if ( count( $redirValues ) ) {
- $result->setIndexedTagName( $redirValues, 'r' );
- $result->addValue( 'query', 'redirects', $redirValues );
- }
-
- // Missing revision elements
- $missingRevIDs = $pageSet->getMissingRevisionIDs();
- if ( count( $missingRevIDs ) ) {
- $revids = array();
- foreach ( $missingRevIDs as $revid ) {
- $revids[$revid] = array(
- 'revid' => $revid
- );
- }
- $result->setIndexedTagName( $revids, 'rev' );
- $result->addValue( 'query', 'badrevids', $revids );
+ $values = $pageSet->getMissingRevisionIDsAsResult( $result );
+ if ( $values ) {
+ $result->addValue( 'query', 'badrevids', $values );
}
// Page elements
@@ -466,12 +508,13 @@ class ApiQuery extends ApiBase {
);
}
// Report special pages
+ /** @var $title Title */
foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) {
$vals = array();
ApiQueryBase::addTitleInfo( $vals, $title );
$vals['special'] = '';
if ( $title->isSpecialPage() &&
- !SpecialPageFactory::exists( $title->getDbKey() ) ) {
+ !SpecialPageFactory::exists( $title->getDBkey() ) ) {
$vals['missing'] = '';
} elseif ( $title->getNamespace() == NS_MEDIA &&
!wfFindFile( $title ) ) {
@@ -489,7 +532,7 @@ class ApiQuery extends ApiBase {
}
if ( count( $pages ) ) {
- if ( $this->params['indexpageids'] ) {
+ if ( $this->mParams['indexpageids'] ) {
$pageIDs = array_keys( $pages );
// json treats all map keys as strings - converting to match
$pageIDs = array_map( 'strval', $pageIDs );
@@ -500,21 +543,44 @@ class ApiQuery extends ApiBase {
$result->setIndexedTagName( $pages, 'page' );
$result->addValue( 'query', 'pages', $pages );
}
- if ( $this->params['export'] ) {
+ if ( $this->mParams['export'] ) {
$this->doExport( $pageSet, $result );
}
}
/**
- * @param $pageSet ApiPageSet Pages to be exported
- * @param $result ApiResult Result to output to
+ * This method is called by the generator base when generator in the smart-continue
+ * mode tries to set 'query-continue' value. ApiQuery stores those values separately
+ * until the post-processing when it is known if the generation should continue or repeat.
+ * @param ApiQueryGeneratorBase $module generator module
+ * @param string $paramName
+ * @param mixed $paramValue
+ * @return bool true if processed, false if this is a legacy continue
+ */
+ public function setGeneratorContinue( $module, $paramName, $paramValue ) {
+ if ( $this->mUseLegacyContinue ) {
+ return false;
+ }
+ $paramName = $module->encodeParamName( $paramName );
+ if ( $this->mGeneratorContinue === null ) {
+ $this->mGeneratorContinue = array();
+ }
+ $this->mGeneratorContinue[$paramName] = $paramValue;
+ return true;
+ }
+
+ /**
+ * @param $pageSet ApiPageSet Pages to be exported
+ * @param $result ApiResult Result to output to
*/
- private function doExport( $pageSet, $result ) {
+ private function doExport( $pageSet, $result ) {
$exportTitles = array();
$titles = $pageSet->getGoodTitles();
if ( count( $titles ) ) {
+ $user = $this->getUser();
+ /** @var $title Title */
foreach ( $titles as $title ) {
- if ( $title->userCan( 'read' ) ) {
+ if ( $title->userCan( 'read', $user ) ) {
$exportTitles[] = $title;
}
}
@@ -536,7 +602,7 @@ class ApiQuery extends ApiBase {
// It's not continuable, so it would cause more
// problems than it'd solve
$result->disableSizeCheck();
- if ( $this->params['exportnowrap'] ) {
+ if ( $this->mParams['exportnowrap'] ) {
$result->reset();
// Raw formatter will handle this
$result->addValue( null, 'text', $exportxml );
@@ -549,80 +615,30 @@ class ApiQuery extends ApiBase {
$result->enableSizeCheck();
}
- /**
- * Create a generator object of the given type and return it
- * @param $generatorName string Module name
- * @return ApiQueryGeneratorBase
- */
- public function newGenerator( $generatorName ) {
- // Find class that implements requested generator
- if ( isset( $this->mQueryListModules[$generatorName] ) ) {
- $className = $this->mQueryListModules[$generatorName];
- } elseif ( isset( $this->mQueryPropModules[$generatorName] ) ) {
- $className = $this->mQueryPropModules[$generatorName];
- } else {
- ApiBase::dieDebug( __METHOD__, "Unknown generator=$generatorName" );
- }
- $generator = new $className ( $this, $generatorName );
- if ( !$generator instanceof ApiQueryGeneratorBase ) {
- $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' );
- }
- $generator->setGeneratorMode();
- return $generator;
- }
-
- /**
- * For generator mode, execute generator, and use its output as new
- * ApiPageSet
- * @param $generator ApiQueryGeneratorBase Generator Module
- * @param $modules array of module objects
- */
- protected function executeGeneratorModule( $generator, $modules ) {
- // Generator results
- $resultPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles );
-
- // Add any additional fields modules may need
- $generator->requestExtraData( $this->mPageSet );
- $this->addCustomFldsToPageSet( $modules, $resultPageSet );
-
- // Populate page information with the original user input
- $this->mPageSet->execute();
-
- // populate resultPageSet with the generator output
- $generator->profileIn();
- $generator->executeGenerator( $resultPageSet );
- wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$resultPageSet ) );
- $resultPageSet->finishPageSetGeneration();
- $generator->profileOut();
-
- // Swap the resulting pageset back in
- $this->mPageSet = $resultPageSet;
- }
-
- public function getAllowedParams() {
- return array(
+ public function getAllowedParams( $flags = 0 ) {
+ $result = array(
'prop' => array(
ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => $this->mPropModuleNames
+ ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'prop' )
),
'list' => array(
ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => $this->mListModuleNames
+ ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'list' )
),
'meta' => array(
ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => $this->mMetaModuleNames
+ ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'meta' )
),
- 'generator' => array(
- ApiBase::PARAM_TYPE => $this->mAllowedGenerators
- ),
- 'redirects' => false,
- 'converttitles' => false,
'indexpageids' => false,
'export' => false,
'exportnowrap' => false,
'iwurl' => false,
+ 'continue' => null,
);
+ if ( $flags ) {
+ $result += $this->getPageSet()->getFinalParams( $flags );
+ }
+ return $result;
}
/**
@@ -630,42 +646,40 @@ class ApiQuery extends ApiBase {
* @return string
*/
public function makeHelpMsg() {
- // Make sure the internal object is empty
- // (just in case a sub-module decides to optimize during instantiation)
- $this->mPageSet = null;
+
+ // Use parent to make default message for the query module
+ $msg = parent::makeHelpMsg();
$querySeparator = str_repeat( '--- ', 12 );
$moduleSeparator = str_repeat( '*** ', 14 );
- $msg = "\n$querySeparator Query: Prop $querySeparator\n\n";
- $msg .= $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' );
+ $msg .= "\n$querySeparator Query: Prop $querySeparator\n\n";
+ $msg .= $this->makeHelpMsgHelper( 'prop' );
$msg .= "\n$querySeparator Query: List $querySeparator\n\n";
- $msg .= $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' );
+ $msg .= $this->makeHelpMsgHelper( 'list' );
$msg .= "\n$querySeparator Query: Meta $querySeparator\n\n";
- $msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' );
+ $msg .= $this->makeHelpMsgHelper( 'meta' );
$msg .= "\n\n$moduleSeparator Modules: continuation $moduleSeparator\n\n";
- // Use parent to make default message for the query module
- $msg = parent::makeHelpMsg() . $msg;
-
return $msg;
}
/**
- * For all modules in $moduleList, generate help messages and join them together
- * @param $moduleList Array array(modulename => classname)
- * @param $paramName string Parameter name
+ * For all modules of a given group, generate help messages and join them together
+ * @param string $group Module group
* @return string
*/
- private function makeHelpMsgHelper( $moduleList, $paramName ) {
+ private function makeHelpMsgHelper( $group ) {
$moduleDescriptions = array();
- foreach ( $moduleList as $moduleName => $moduleClass ) {
+ $moduleNames = $this->mModuleMgr->getNames( $group );
+ sort( $moduleNames );
+ foreach ( $moduleNames as $name ) {
/**
* @var $module ApiQueryBase
*/
- $module = new $moduleClass( $this, $moduleName, null );
+ $module = $this->mModuleMgr->getModule( $name );
- $msg = ApiMain::makeHelpMsgHeader( $module, $paramName );
+ $msg = ApiMain::makeHelpMsgHeader( $module, $group );
$msg2 = $module->makeHelpMsg();
if ( $msg2 !== false ) {
$msg .= $msg2;
@@ -679,46 +693,23 @@ class ApiQuery extends ApiBase {
return implode( "\n", $moduleDescriptions );
}
- /**
- * Adds any classes that are a subclass of ApiQueryGeneratorBase
- * to the allowed generator list
- * @param $moduleList array()
- */
- private function makeGeneratorList( $moduleList ) {
- foreach( $moduleList as $moduleName => $moduleClass ) {
- if ( is_subclass_of( $moduleClass, 'ApiQueryGeneratorBase' ) ) {
- $this->mAllowedGenerators[] = $moduleName;
- }
- }
- }
-
- /**
- * Override to add extra parameters from PageSet
- * @return string
- */
- public function makeHelpMsgParameters() {
- $psModule = new ApiPageSet( $this );
- return $psModule->makeHelpMsgParameters() . parent::makeHelpMsgParameters();
- }
-
public function shouldCheckMaxlag() {
return true;
}
public function getParamDescription() {
- return array(
+ return $this->getPageSet()->getFinalParamDescription() + array(
'prop' => 'Which properties to get for the titles/revisions/pageids. Module help is available below',
'list' => 'Which lists to get. Module help is available below',
'meta' => 'Which metadata to get about the site. Module help is available below',
- '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',
- 'converttitles' => array( "Convert titles to other variants if necessary. Only works if the wiki's content language supports variant conversion.",
- 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ),
'indexpageids' => 'Include an additional pageids section listing all returned page IDs',
'export' => 'Export the current revisions of all given or generated pages',
'exportnowrap' => 'Return the export XML without wrapping it in an XML result (same format as Special:Export). Can only be used with export',
'iwurl' => 'Whether to get the full URL if the title is an interwiki link',
+ 'continue' => array(
+ 'When present, formats query-continue as key-value pairs that should simply be merged into the original request.',
+ 'This parameter must be set to an empty string in the initial query.',
+ 'This parameter is recommended for all new development, and will be made default in the next API version.' ),
);
}
@@ -731,31 +722,25 @@ class ApiQuery extends ApiBase {
}
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
- array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ),
- ) );
+ return array_merge(
+ parent::getPossibleErrors(),
+ $this->getPageSet()->getFinalPossibleErrors()
+ );
}
public function getExamples() {
return array(
- 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment',
- 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions',
+ 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment&continue=',
+ 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=',
);
}
public function getHelpUrls() {
return array(
+ 'https://www.mediawiki.org/wiki/API:Query',
'https://www.mediawiki.org/wiki/API:Meta',
'https://www.mediawiki.org/wiki/API:Properties',
'https://www.mediawiki.org/wiki/API:Lists',
);
}
-
- public function getVersion() {
- $psModule = new ApiPageSet( $this );
- $vers = array();
- $vers[] = __CLASS__ . ': $Id$';
- $vers[] = $psModule->getVersion();
- return $vers;
- }
}
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
index 4f4c77f0..3f5c6ee7 100644
--- a/includes/api/ApiQueryAllCategories.php
+++ b/includes/api/ApiQueryAllCategories.php
@@ -60,10 +60,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
if ( !is_null( $params['continue'] ) ) {
$cont = explode( '|', $params['continue'] );
- if ( count( $cont ) != 1 ) {
- $this->dieUsage( "Invalid continue param. You should pass the " .
- "original value returned by the previous query", "_badcontinue" );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 1 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$cont_from = $db->addQuotes( $cont[0] );
$this->addWhere( "cat_title $op= $cont_from" );
@@ -79,9 +76,8 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
if ( $dir == 'newer' ) {
$this->addWhereRange( 'cat_pages', 'newer', $min, $max );
} else {
- $this->addWhereRange( 'cat_pages', 'older', $max, $min);
+ $this->addWhereRange( 'cat_pages', 'older', $max, $min );
}
-
if ( isset( $params['prefix'] ) ) {
$this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
@@ -125,7 +121,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
$pages[] = $titleObj;
} else {
$item = array();
- $result->setContent( $item, $titleObj->getText() );
+ ApiResult::setContent( $item, $titleObj->getText() );
if ( isset( $prop['size'] ) ) {
$item['size'] = intval( $row->cat_pages );
$item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files;
@@ -225,12 +221,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
return 'Enumerate all categories';
}
- 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' ),
- ) );
- }
-
public function getExamples() {
return array(
'api.php?action=query&list=allcategories&acprop=size',
@@ -241,8 +231,4 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Allcategories';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php
index b562da8e..ccc7a3a2 100644
--- a/includes/api/ApiQueryAllImages.php
+++ b/includes/api/ApiQueryAllImages.php
@@ -41,8 +41,8 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
}
/**
- * Override 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.
+ * Override parent method to make sure the repo's DB is used
+ * which may not necessarily be the same as the local DB.
*
* TODO: allow querying non-local repos.
* @return DatabaseBase
@@ -93,7 +93,10 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$prop = array_flip( $params['prop'] );
$this->addFields( LocalFile::selectFields() );
- $dir = ( in_array( $params['dir'], array( 'descending', 'older' ) ) ? 'older' : 'newer' );
+ $ascendingOrder = true;
+ if ( $params['dir'] == 'descending' || $params['dir'] == 'older' ) {
+ $ascendingOrder = false;
+ }
if ( $params['sort'] == 'name' ) {
// Check mutually exclusive params
@@ -110,19 +113,16 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
// Pagination
if ( !is_null( $params['continue'] ) ) {
$cont = explode( '|', $params['continue'] );
- if ( count( $cont ) != 1 ) {
- $this->dieUsage( 'Invalid continue param. You should pass the ' .
- 'original value returned by the previous query', '_badcontinue' );
- }
- $op = ( $dir == 'older' ? '<' : '>' );
- $cont_from = $db->addQuotes( $cont[0] );
- $this->addWhere( "img_name $op= $cont_from" );
+ $this->dieContinueUsageIf( count( $cont ) != 1 );
+ $op = ( $ascendingOrder ? '>' : '<' );
+ $continueFrom = $db->addQuotes( $cont[0] );
+ $this->addWhere( "img_name $op= $continueFrom" );
}
// Image filters
$from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
$to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
- $this->addWhereRange( 'img_name', $dir, $from, $to );
+ $this->addWhereRange( 'img_name', ( $ascendingOrder ? 'newer' : 'older' ), $from, $to );
if ( isset( $params['prefix'] ) ) {
$this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
@@ -135,13 +135,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", 'badparams' );
}
}
- if (!is_null( $params['user'] ) && $params['filterbots'] != 'all') {
+ if ( !is_null( $params['user'] ) && $params['filterbots'] != 'all' ) {
// Since filterbots checks if each user has the bot right, it doesn't make sense to use it with user
- $this->dieUsage( "Parameters 'user' and 'filterbots' cannot be used together", 'badparams' );
+ $this->dieUsage( "Parameters '{$prefix}user' and '{$prefix}filterbots' cannot be used together", 'badparams' );
}
// Pagination
- $this->addTimestampWhereRange( 'img_timestamp', $dir, $params['start'], $params['end'] );
+ $this->addTimestampWhereRange( 'img_timestamp', ( $ascendingOrder ? 'newer' : 'older' ), $params['start'], $params['end'] );
// Image filters
if ( !is_null( $params['user'] ) ) {
@@ -149,8 +149,6 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
}
if ( $params['filterbots'] != 'all' ) {
$this->addTables( 'user_groups' );
- $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL': 'NOT NULL' );
- $this->addWhere( "ug_group IS $groupCond" );
$this->addJoinConds( array( 'user_groups' => array(
'LEFT JOIN',
array(
@@ -158,6 +156,8 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
'ug_user = img_user'
)
) ) );
+ $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL': 'NOT NULL' );
+ $this->addWhere( "ug_group IS $groupCond" );
}
}
@@ -172,12 +172,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$sha1 = false;
if ( isset( $params['sha1'] ) ) {
- if ( !$this->validateSha1Hash( $params['sha1'] ) ) {
+ $sha1 = strtolower( $params['sha1'] );
+ if ( !$this->validateSha1Hash( $sha1 ) ) {
$this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' );
}
- $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 );
+ $sha1 = wfBaseConvert( $sha1, 16, 36, 31 );
} elseif ( isset( $params['sha1base36'] ) ) {
- $sha1 = $params['sha1base36'];
+ $sha1 = strtolower( $params['sha1base36'] );
if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
$this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' );
}
@@ -188,7 +189,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
if ( !is_null( $params['mime'] ) ) {
global $wgMiserMode;
- if ( $wgMiserMode ) {
+ if ( $wgMiserMode ) {
$this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' );
}
@@ -200,16 +201,19 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$limit = $params['limit'];
$this->addOption( 'LIMIT', $limit + 1 );
- $sort = ( $dir == 'older' ? ' DESC' : '' );
+ $sortFlag = '';
+ if ( !$ascendingOrder ) {
+ $sortFlag = ' DESC';
+ }
if ( $params['sort'] == 'timestamp' ) {
- $this->addOption( 'ORDER BY', 'img_timestamp' . $sort );
- if ( $params['filterbots'] == 'all' ) {
- $this->addOption( 'USE INDEX', array( 'image' => 'img_timestamp' ) );
- } else {
+ $this->addOption( 'ORDER BY', 'img_timestamp' . $sortFlag );
+ if ( !is_null( $params['user'] ) ) {
$this->addOption( 'USE INDEX', array( 'image' => 'img_usertext_timestamp' ) );
+ } else {
+ $this->addOption( 'USE INDEX', array( 'image' => 'img_timestamp' ) );
}
} else {
- $this->addOption( 'ORDER BY', 'img_name' . $sort );
+ $this->addOption( 'ORDER BY', 'img_name' . $sortFlag );
}
$res = $this->select( __METHOD__ );
@@ -256,7 +260,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
}
public function getAllowedParams() {
- return array (
+ return array(
'sort' => array(
ApiBase::PARAM_DFLT => 'name',
ApiBase::PARAM_TYPE => array(
@@ -272,7 +276,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
'descending',
// sort=timestamp
'newer',
- 'older',
+ 'older'
)
),
'from' => null,
@@ -373,12 +377,11 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
array( 'code' => 'badparams', 'info' => "Parameter'{$p}from' can only be used with {$p}sort=name" ),
array( 'code' => 'badparams', 'info' => "Parameter'{$p}to' can only be used with {$p}sort=name" ),
array( 'code' => 'badparams', 'info' => "Parameter'{$p}prefix' can only be used with {$p}sort=name" ),
- array( 'code' => 'badparams', 'info' => "Parameters 'user' and 'filterbots' cannot be used together" ),
+ array( 'code' => 'badparams', 'info' => "Parameters '{$p}user' and '{$p}filterbots' cannot be used together" ),
array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ),
array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ),
array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ),
array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -402,8 +405,4 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Allimages';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index da4840f0..47d1bcef 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -32,7 +32,51 @@
class ApiQueryAllLinks extends ApiQueryGeneratorBase {
public function __construct( $query, $moduleName ) {
- parent::__construct( $query, $moduleName, 'al' );
+ switch ( $moduleName ) {
+ case 'alllinks':
+ $prefix = 'al';
+ $this->table = 'pagelinks';
+ $this->tablePrefix = 'pl_';
+ $this->fieldTitle = 'title';
+ $this->dfltNamespace = NS_MAIN;
+ $this->hasNamespace = true;
+ $this->indexTag = 'l';
+ $this->description = 'Enumerate all links that point to a given namespace';
+ $this->descriptionWhat = 'link';
+ $this->descriptionTargets = 'linked titles';
+ $this->descriptionLinking = 'linking';
+ break;
+ case 'alltransclusions':
+ $prefix = 'at';
+ $this->table = 'templatelinks';
+ $this->tablePrefix = 'tl_';
+ $this->fieldTitle = 'title';
+ $this->dfltNamespace = NS_TEMPLATE;
+ $this->hasNamespace = true;
+ $this->indexTag = 't';
+ $this->description = 'List all transclusions (pages embedded using {{x}}), including non-existing';
+ $this->descriptionWhat = 'transclusion';
+ $this->descriptionTargets = 'transcluded titles';
+ $this->descriptionLinking = 'transcluding';
+ break;
+ case 'allfileusages':
+ $prefix = 'af';
+ $this->table = 'imagelinks';
+ $this->tablePrefix = 'il_';
+ $this->fieldTitle = 'to';
+ $this->dfltNamespace = NS_FILE;
+ $this->hasNamespace = false;
+ $this->indexTag = 'f';
+ $this->description = 'List all file usages, including non-existing';
+ $this->descriptionWhat = 'file';
+ $this->descriptionTargets = 'file titles';
+ $this->descriptionLinking = 'using';
+ break;
+ default:
+ ApiBase::dieDebug( __METHOD__, 'Unknown module name' );
+ }
+
+ parent::__construct( $query, $moduleName, $prefix );
}
public function execute() {
@@ -55,75 +99,81 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$db = $this->getDB();
$params = $this->extractRequestParams();
+ $pfx = $this->tablePrefix;
+ $fieldTitle = $this->fieldTitle;
$prop = array_flip( $params['prop'] );
$fld_ids = isset( $prop['ids'] );
$fld_title = isset( $prop['title'] );
+ if ( $this->hasNamespace ) {
+ $namespace = $params['namespace'];
+ } else {
+ $namespace = $this->dfltNamespace;
+ }
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->dieUsage(
+ "{$this->getModuleName()} cannot return corresponding page ids in unique {$this->descriptionWhat}s mode",
+ 'params' );
}
$this->addOption( 'DISTINCT' );
}
- $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' );
+ $this->addTables( $this->table );
+ if ( $this->hasNamespace ) {
+ $this->addWhereFld( $pfx . 'namespace', $namespace );
}
- if ( !is_null( $params['continue'] ) ) {
+
+ $continue = !is_null( $params['continue'] );
+ if ( $continue ) {
$continueArr = explode( '|', $params['continue'] );
$op = $params['dir'] == 'descending' ? '<' : '>';
if ( $params['unique'] ) {
- if ( count( $continueArr ) != 1 ) {
- $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
- }
+ $this->dieContinueUsageIf( count( $continueArr ) != 1 );
$continueTitle = $db->addQuotes( $continueArr[0] );
- $this->addWhere( "pl_title $op= $continueTitle" );
+ $this->addWhere( "{$pfx}{$fieldTitle} $op= $continueTitle" );
} else {
- if ( count( $continueArr ) != 2 ) {
- $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
- }
+ $this->dieContinueUsageIf( count( $continueArr ) != 2 );
$continueTitle = $db->addQuotes( $continueArr[0] );
$continueFrom = intval( $continueArr[1] );
$this->addWhere(
- "pl_title $op $continueTitle OR " .
- "(pl_title = $continueTitle AND " .
- "pl_from $op= $continueFrom)"
+ "{$pfx}{$fieldTitle} $op $continueTitle OR " .
+ "({$pfx}{$fieldTitle} = $continueTitle AND " .
+ "{$pfx}from $op= $continueFrom)"
);
}
}
- $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
+ // 'continue' always overrides 'from'
+ $from = ( $continue || is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
$to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
- $this->addWhereRange( 'pl_title', 'newer', $from, $to );
+ $this->addWhereRange( $pfx . $fieldTitle, 'newer', $from, $to );
if ( isset( $params['prefix'] ) ) {
- $this->addWhere( 'pl_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
+ $this->addWhere( $pfx . $fieldTitle . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
}
- $this->addFields( 'pl_title' );
- $this->addFieldsIf( 'pl_from', !$params['unique'] );
+ $this->addFields( array( 'pl_title' => $pfx . $fieldTitle ) );
+ $this->addFieldsIf( array( 'pl_from' => $pfx . 'from' ), !$params['unique'] );
- $this->addOption( 'USE INDEX', 'pl_namespace' );
+ if ( $this->hasNamespace ) {
+ $this->addOption( 'USE INDEX', $pfx . 'namespace' );
+ }
$limit = $params['limit'];
$this->addOption( 'LIMIT', $limit + 1 );
$sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
$orderBy = array();
- $orderBy[] = 'pl_title' . $sort;
+ $orderBy[] = $pfx . $fieldTitle . $sort;
if ( !$params['unique'] ) {
- $orderBy[] = 'pl_from' . $sort;
+ $orderBy[] = $pfx . 'from' . $sort;
}
$this->addOption( 'ORDER BY', $orderBy );
$res = $this->select( __METHOD__ );
$pageids = array();
+ $titles = array();
$count = 0;
$result = $this->getResult();
foreach ( $res as $row ) {
@@ -132,7 +182,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
if ( $params['unique'] ) {
$this->setContinueEnumParameter( 'continue', $row->pl_title );
} else {
- $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from );
+ $this->setContinueEnumParameter( 'continue', $row->pl_title . '|' . $row->pl_from );
}
break;
}
@@ -143,7 +193,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$vals['fromid'] = intval( $row->pl_from );
}
if ( $fld_title ) {
- $title = Title::makeTitle( $params['namespace'], $row->pl_title );
+ $title = Title::makeTitle( $namespace, $row->pl_title );
ApiQueryBase::addTitleInfo( $vals, $title );
}
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
@@ -151,24 +201,28 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
if ( $params['unique'] ) {
$this->setContinueEnumParameter( 'continue', $row->pl_title );
} else {
- $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from );
+ $this->setContinueEnumParameter( 'continue', $row->pl_title . '|' . $row->pl_from );
}
break;
}
+ } elseif ( $params['unique'] ) {
+ $titles[] = Title::makeTitle( $namespace, $row->pl_title );
} else {
$pageids[] = $row->pl_from;
}
}
if ( is_null( $resultPageSet ) ) {
- $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'l' );
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $this->indexTag );
+ } elseif ( $params['unique'] ) {
+ $resultPageSet->populateFromTitles( $titles );
} else {
$resultPageSet->populateFromPageIDs( $pageids );
}
}
public function getAllowedParams() {
- return array(
+ $allowedParams = array(
'continue' => null,
'from' => null,
'to' => null,
@@ -183,7 +237,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
)
),
'namespace' => array(
- ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_DFLT => $this->dfltNamespace,
ApiBase::PARAM_TYPE => 'namespace'
),
'limit' => array(
@@ -201,25 +255,39 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
)
),
);
+ if ( !$this->hasNamespace ) {
+ unset( $allowedParams['namespace'] );
+ }
+ return $allowedParams;
}
public function getParamDescription() {
$p = $this->getModulePrefix();
- return array(
- 'from' => 'The page title to start enumerating from',
- 'to' => 'The page title to stop enumerating at',
- 'prefix' => 'Search for all page titles that begin with this value',
- 'unique' => "Only show unique links. Cannot be used with generator or {$p}prop=ids",
+ $what = $this->descriptionWhat;
+ $targets = $this->descriptionTargets;
+ $linking = $this->descriptionLinking;
+ $paramDescription = array(
+ 'from' => "The title of the $what to start enumerating from",
+ 'to' => "The title of the $what to stop enumerating at",
+ 'prefix' => "Search for all $targets that begin with this value",
+ 'unique' => array(
+ "Only show distinct $targets. Cannot be used with {$p}prop=ids.",
+ 'When used as a generator, yields target pages instead of source pages.',
+ ),
'prop' => array(
'What pieces of information to include',
- " ids - Adds pageid of where the link is from (Cannot be used with {$p}unique)",
- ' title - Adds the title of the link',
+ " ids - Adds the pageid of the $linking page (Cannot be used with {$p}unique)",
+ " title - Adds the title of the $what",
),
'namespace' => 'The namespace to enumerate',
- 'limit' => 'How many total links to return',
+ 'limit' => 'How many total items to return',
'continue' => 'When more results are available, use this to continue',
'dir' => 'The direction in which to list',
);
+ if ( !$this->hasNamespace ) {
+ unset( $paramDescription['namespace'] );
+ }
+ return $paramDescription;
}
public function getResultProperties() {
@@ -235,30 +303,36 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
}
public function getDescription() {
- return 'Enumerate all links that point to a given namespace';
+ return $this->description;
}
public function getPossibleErrors() {
$m = $this->getModuleName();
+ $what = $this->descriptionWhat;
return array_merge( parent::getPossibleErrors(), array(
- array( 'code' => 'params', 'info' => "{$m} cannot be used as a generator in unique links mode" ),
- array( 'code' => 'params', 'info' => "{$m} 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' ),
+ array( 'code' => 'params', 'info' => "{$m} cannot return corresponding page ids in unique {$what}s mode" ),
) );
}
public function getExamples() {
+ $p = $this->getModulePrefix();
+ $name = $this->getModuleName();
+ $what = $this->descriptionWhat;
+ $targets = $this->descriptionTargets;
return array(
- 'api.php?action=query&list=alllinks&alunique=&alfrom=B',
+ "api.php?action=query&list={$name}&{$p}from=B&{$p}prop=ids|title"
+ => "List $targets with page ids they are from, including missing ones. Start at B",
+ "api.php?action=query&list={$name}&{$p}unique=&{$p}from=B"
+ => "List unique $targets",
+ "api.php?action=query&generator={$name}&g{$p}unique=&g{$p}from=B"
+ => "Gets all $targets, marking the missing ones",
+ "api.php?action=query&generator={$name}&g{$p}from=B"
+ => "Gets pages containing the {$what}s",
);
}
public function getHelpUrls() {
- return 'https://www.mediawiki.org/wiki/API:Alllinks';
- }
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ $name = ucfirst( $this->getModuleName() );
+ return "https://www.mediawiki.org/wiki/API:{$name}";
}
}
diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php
index f5e1146b..d47c7b76 100644
--- a/includes/api/ApiQueryAllMessages.php
+++ b/includes/api/ApiQueryAllMessages.php
@@ -39,8 +39,9 @@ class ApiQueryAllMessages extends ApiQueryBase {
$params = $this->extractRequestParams();
if ( is_null( $params['lang'] ) ) {
- global $wgLang;
- $langObj = $wgLang;
+ $langObj = $this->getLanguage();
+ } elseif ( !Language::isValidCode( $params['lang'] ) ) {
+ $this->dieUsage( 'Invalid language code for parameter lang', 'invalidlang' );
} else {
$langObj = Language::factory( $params['lang'] );
}
@@ -48,7 +49,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
if ( $params['enableparser'] ) {
if ( !is_null( $params['title'] ) ) {
$title = Title::newFromText( $params['title'] );
- if ( !$title ) {
+ if ( !$title || $title->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
} else {
@@ -86,7 +87,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
foreach ( $messages_target as $message ) {
// === 0: must be at beginning of string (position 0)
if ( strpos( $message, $params['prefix'] ) === 0 ) {
- if( !$skip ) {
+ if ( !$skip ) {
$skip = true;
}
$messages_filtered[] = $message;
@@ -116,7 +117,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
$lang = $langObj->getCode();
$customisedMessages = AllmessagesTablePager::getCustomisedStatuses(
- array_map( array( $langObj, 'ucfirst'), $messages_target ), $lang, $lang != $wgContLang->getCode() );
+ array_map( array( $langObj, 'ucfirst' ), $messages_target ), $lang, $lang != $wgContLang->getCode() );
$customised = $params['customised'] === 'modified';
}
@@ -143,7 +144,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
}
if ( $customiseFilterEnabled ) {
- $messageIsCustomised = isset( $customisedMessages['pages'][ $langObj->ucfirst( $message ) ] );
+ $messageIsCustomised = isset( $customisedMessages['pages'][$langObj->ucfirst( $message )] );
if ( $customised === $messageIsCustomised ) {
if ( $customised ) {
$a['customised'] = '';
@@ -256,6 +257,12 @@ class ApiQueryAllMessages extends ApiQueryBase {
);
}
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'invalidlang', 'info' => 'Invalid language code for parameter lang' ),
+ ) );
+ }
+
public function getResultProperties() {
return array(
'' => array(
@@ -291,8 +298,4 @@ class ApiQueryAllMessages extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Meta#allmessages_.2F_am';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php
index 16cc31d2..d95980c2 100644
--- a/includes/api/ApiQueryAllPages.php
+++ b/includes/api/ApiQueryAllPages.php
@@ -69,10 +69,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
if ( !is_null( $params['continue'] ) ) {
$cont = explode( '|', $params['continue'] );
- if ( count( $cont ) != 1 ) {
- $this->dieUsage( "Invalid continue param. You should pass the " .
- "original value returned by the previous query", "_badcontinue" );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 1 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$cont_from = $db->addQuotes( $cont[0] );
$this->addWhere( "page_title $op= $cont_from" );
@@ -120,7 +117,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
if ( count( $params['prtype'] ) || $params['prexpiry'] != 'all' ) {
$this->addTables( 'page_restrictions' );
$this->addWhere( 'page_id=pr_page' );
- $this->addWhere( 'pr_expiry>' . $db->addQuotes( $db->timestamp() ) );
+ $this->addWhere( "pr_expiry > {$db->addQuotes( $db->timestamp() )} OR pr_expiry IS NULL" );
if ( count( $params['prtype'] ) ) {
$this->addWhereFld( 'pr_type', $params['prtype'] );
@@ -138,8 +135,6 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
} elseif ( $params['prfiltercascade'] == 'noncascading' ) {
$this->addWhereFld( 'pr_cascade', 0 );
}
-
- $this->addOption( 'DISTINCT' );
}
$forceNameTitleIndex = false;
@@ -149,6 +144,8 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
$this->addWhere( "pr_expiry != {$db->addQuotes( $db->getInfinity() )}" );
}
+ $this->addOption( 'DISTINCT' );
+
} elseif ( isset( $params['prlevel'] ) ) {
$this->dieUsage( 'prlevel may not be used without prtype', 'params' );
}
@@ -177,7 +174,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
$res = $this->select( __METHOD__ );
//Get gender information
- if( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) {
+ if ( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) {
$users = array();
foreach ( $res as $row ) {
$users[] = $row->page_title;
@@ -226,7 +223,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
'to' => null,
'prefix' => null,
'namespace' => array(
- ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_DFLT => NS_MAIN,
ApiBase::PARAM_TYPE => 'namespace',
),
'filterredir' => array(
@@ -307,7 +304,10 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
'prtype' => 'Limit to protected pages only',
'prlevel' => "The protection level (must be used with {$p}prtype= parameter)",
'prfiltercascade' => "Filter protections based on cascadingness (ignored when {$p}prtype isn't set)",
- 'filterlanglinks' => 'Filter based on whether a page has langlinks',
+ 'filterlanglinks' => array(
+ 'Filter based on whether a page has langlinks',
+ 'Note that this may not consider langlinks added by extensions.',
+ ),
'limit' => 'How many total pages to return.',
'prexpiry' => array(
'Which protection expiry to filter the page on',
@@ -336,7 +336,6 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
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' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -351,7 +350,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
'Show info about 4 pages starting at the letter "T"',
),
'api.php?action=query&generator=allpages&gaplimit=2&gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content' => array(
- 'Show content of first 2 non-redirect pages begining at "Re"',
+ 'Show content of first 2 non-redirect pages beginning at "Re"',
)
);
}
@@ -359,8 +358,4 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Allpages';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index 7f50cbad..1948a51a 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -37,7 +37,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
/**
* This function converts the user name to a canonical form
* which is stored in the database.
- * @param String $name
+ * @param string $name
* @return String
*/
private function getCanonicalUserName( $name ) {
@@ -81,12 +81,18 @@ class ApiQueryAllUsers extends ApiQueryBase {
$db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) );
}
- if ( !is_null( $params['rights'] ) ) {
+ if ( !is_null( $params['rights'] ) && count( $params['rights'] ) ) {
$groups = array();
- foreach( $params['rights'] as $r ) {
+ foreach ( $params['rights'] as $r ) {
$groups = array_merge( $groups, User::getGroupsWithPermission( $r ) );
}
+ // no group with the given right(s) exists, no need for a query
+ if ( !count( $groups ) ) {
+ $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), '' );
+ return;
+ }
+
$groups = array_unique( $groups );
if ( is_null( $params['group'] ) ) {
@@ -155,7 +161,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
$this->addFields( array( 'recentedits' => 'COUNT(*)' ) );
$this->addWhere( 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ) );
- $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 );
+ $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays * 24 * 3600 );
$this->addWhere( 'rc_timestamp >= ' . $db->addQuotes( $timestamp ) );
$this->addOption( 'GROUP BY', $userFieldToSort );
@@ -273,7 +279,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
if ( $fld_rights ) {
if ( !isset( $lastUserData['rights'] ) ) {
if ( $lastUserObj ) {
- $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+ $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
} else {
// This should not normally happen
$lastUserData['rights'] = array();
@@ -438,8 +444,4 @@ class ApiQueryAllUsers extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Allusers';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index 06db87bf..2d1089a7 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -188,6 +188,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$titleWhere = array();
$allRedirNs = array();
$allRedirDBkey = array();
+ /** @var $t Title */
foreach ( $this->redirTitles as $t ) {
$redirNs = $t->getNamespace();
$redirDBkey = $t->getDBkey();
@@ -201,6 +202,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if ( !is_null( $this->redirID ) ) {
$op = $this->params['dir'] == 'descending' ? '<' : '>';
+ /** @var $first Title */
$first = $this->redirTitles[0];
$title = $db->addQuotes( $first->getDBkey() );
$ns = $first->getNamespace();
@@ -227,10 +229,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$orderBy = array();
$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
// Don't order by namespace/title if it's constant in the WHERE clause
- if( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) {
+ if ( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) {
$orderBy[] = $this->bl_ns . $sort;
}
- if( count( array_unique( $allRedirDBkey ) ) != 1 ) {
+ if ( count( array_unique( $allRedirDBkey ) ) != 1 ) {
$orderBy[] = $this->bl_title . $sort;
}
$orderBy[] = $this->bl_from . $sort;
@@ -246,13 +248,16 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$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 );
+ $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2 / 2 : ApiBase::LIMIT_BIG2 );
$result = $this->getResult();
if ( $this->params['limit'] == 'max' ) {
$this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$result->setParsedLimit( $this->getModuleName(), $this->params['limit'] );
+ } else {
+ $this->params['limit'] = intval( $this->params['limit'] );
+ $this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax );
}
$this->processContinue();
@@ -292,9 +297,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
// 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 } ];
+ $parentID = $this->pageMap[$row->{$this->bl_ns}][$row->{$this->bl_title}];
} else {
- $parentID = $this->pageMap[NS_FILE][$row-> { $this->bl_title } ];
+ $parentID = $this->pageMap[NS_FILE][$row->{$this->bl_title}];
}
$this->continueStr = $this->getContinueRedirStr( $parentID, $row->page_id );
break;
@@ -375,8 +380,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
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 );
@@ -406,20 +411,14 @@ 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' ) {
- // Illegal continue parameter
- $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', '_badcontinue' );
- }
+ $this->dieContinueUsageIf( $rootNs === 0 && $continueList[0] !== '0' );
+
$this->rootTitle = Title::makeTitleSafe( $rootNs, $continueList[1] );
+ $this->dieContinueUsageIf( !$this->rootTitle );
- if ( !$this->rootTitle ) {
- $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', '_badcontinue' );
- }
$contID = intval( $continueList[2] );
+ $this->dieContinueUsageIf( $contID === 0 && $continueList[2] !== '0' );
- 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;
$id2 = isset( $continueList[3] ) ? $continueList[3] : null;
$redirID = intval( $id2 );
@@ -455,12 +454,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace'
),
- 'dir' => array(
- ApiBase::PARAM_DFLT => 'ascending',
- ApiBase::PARAM_TYPE => array(
- 'ascending',
- 'descending'
- )
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
),
'filterredir' => array(
ApiBase::PARAM_DFLT => 'all',
@@ -535,7 +534,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->getTitleOrPageIdErrorMessage(),
array(
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' ),
)
);
}
@@ -562,8 +560,4 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return $this->helpUrl;
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index 2c48aca0..8668e04b 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -112,7 +112,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add a set of fields to select to the internal array
- * @param $value array|string Field name or array of field names
+ * @param array|string $value Field name or array of field names
*/
protected function addFields( $value ) {
if ( is_array( $value ) ) {
@@ -124,8 +124,8 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Same as addFields(), but add the fields only if a condition is met
- * @param $value array|string See addFields()
- * @param $condition bool If false, do nothing
+ * @param array|string $value See addFields()
+ * @param bool $condition If false, do nothing
* @return bool $condition
*/
protected function addFieldsIf( $value, $condition ) {
@@ -162,7 +162,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Same as addWhere(), but add the WHERE clauses only if a condition is met
* @param $value mixed See addWhere()
- * @param $condition bool If false, do nothing
+ * @param bool $condition If false, do nothing
* @return bool $condition
*/
protected function addWhereIf( $value, $condition ) {
@@ -175,8 +175,8 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Equivalent to addWhere(array($field => $value))
- * @param $field string Field name
- * @param $value string Value; ignored if null or empty array;
+ * @param string $field Field name
+ * @param string $value Value; ignored if null or empty array;
*/
protected function addWhereFld( $field, $value ) {
// Use count() to its full documented capabilities to simultaneously
@@ -189,14 +189,14 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add a WHERE clause corresponding to a range, and an ORDER BY
* clause to sort in the right direction
- * @param $field string Field name
- * @param $dir string If 'newer', sort in ascending order, otherwise
+ * @param string $field Field name
+ * @param string $dir If 'newer', sort in ascending order, otherwise
* sort in descending order
- * @param $start string Value to start the list at. If $dir == 'newer'
+ * @param string $start Value to start the list at. If $dir == 'newer'
* this is the lower boundary, otherwise it's the upper boundary
- * @param $end string Value to end the list at. If $dir == 'newer' this
+ * @param string $end Value to end the list at. If $dir == 'newer' this
* is the upper boundary, otherwise it's the lower boundary
- * @param $sort bool If false, don't add an ORDER BY clause
+ * @param bool $sort If false, don't add an ORDER BY clause
*/
protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
$isDirNewer = ( $dir === 'newer' );
@@ -240,8 +240,8 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add an option such as LIMIT or USE INDEX. If an option was set
* before, the old value will be overwritten
- * @param $name string Option name
- * @param $value string Option value
+ * @param string $name Option name
+ * @param string $value Option value
*/
protected function addOption( $name, $value = null ) {
if ( is_null( $value ) ) {
@@ -253,9 +253,9 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Execute a SELECT query based on the values in the internal arrays
- * @param $method string Function the query should be attributed to.
+ * @param string $method Function the query should be attributed to.
* You should usually use __METHOD__ here
- * @param $extraQuery array Query data to add but not store in the object
+ * @param array $extraQuery Query data to add but not store in the object
* Format is array( 'tables' => ..., 'fields' => ..., 'where' => ..., 'options' => ..., 'join_conds' => ... )
* @return ResultWrapper
*/
@@ -298,9 +298,9 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add information (title and namespace) about a Title object to a
* result array
- * @param $arr array Result array à la ApiResult
+ * @param array $arr Result array à la ApiResult
* @param $title Title
- * @param $prefix string Module prefix
+ * @param string $prefix Module prefix
*/
public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
$arr[$prefix . 'ns'] = intval( $title->getNamespace() );
@@ -325,8 +325,8 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add a sub-element under the page element with the given page ID
- * @param $pageId int Page ID
- * @param $data array Data array à la ApiResult
+ * @param int $pageId Page ID
+ * @param array $data Data array à la ApiResult
* @return bool Whether the element fit in the result
*/
protected function addPageSubItems( $pageId, $data ) {
@@ -339,9 +339,9 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Same as addPageSubItems(), but one element of $data at a time
- * @param $pageId int Page ID
- * @param $item array Data array à la ApiResult
- * @param $elemname string XML element name. If null, getModuleName()
+ * @param int $pageId Page ID
+ * @param array $item Data array à la ApiResult
+ * @param string $elemname XML element name. If null, getModuleName()
* is used
* @return bool Whether the element fit in the result
*/
@@ -351,7 +351,7 @@ abstract class ApiQueryBase extends ApiBase {
}
$result = $this->getResult();
$fit = $result->addValue( array( 'query', 'pages', $pageId,
- $this->getModuleName() ), null, $item );
+ $this->getModuleName() ), null, $item );
if ( !$fit ) {
return false;
}
@@ -362,15 +362,15 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Set a query-continue value
- * @param $paramName string Parameter name
- * @param $paramValue string Parameter value
+ * @param string $paramName Parameter name
+ * @param string $paramValue Parameter value
*/
protected function setContinueEnumParameter( $paramName, $paramValue ) {
$paramName = $this->encodeParamName( $paramName );
$msg = array( $paramName => $paramValue );
$result = $this->getResult();
$result->disableSizeCheck();
- $result->addValue( 'query-continue', $this->getModuleName(), $msg );
+ $result->addValue( 'query-continue', $this->getModuleName(), $msg, ApiResult::ADD_ON_TOP );
$result->enableSizeCheck();
}
@@ -380,8 +380,7 @@ abstract class ApiQueryBase extends ApiBase {
*/
protected function getDB() {
if ( is_null( $this->mDb ) ) {
- $apiQuery = $this->getQuery();
- $this->mDb = $apiQuery->getDB();
+ $this->mDb = $this->getQuery()->getDB();
}
return $this->mDb;
}
@@ -389,9 +388,9 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Selects the query database connection with the given name.
* See ApiQuery::getNamedDB() for more information
- * @param $name string Name to assign to the database connection
- * @param $db int One of the DB_* constants
- * @param $groups array Query groups
+ * @param string $name Name to assign to the database connection
+ * @param int $db One of the DB_* constants
+ * @param array $groups Query groups
* @return DatabaseBase
*/
public function selectNamedDB( $name, $db, $groups ) {
@@ -408,7 +407,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Convert a title to a DB key
- * @param $title string Page title with spaces
+ * @param string $title Page title with spaces
* @return string Page title with underscores
*/
public function titleToKey( $title ) {
@@ -420,12 +419,12 @@ abstract class ApiQueryBase extends ApiBase {
if ( !$t ) {
$this->dieUsageMsg( array( 'invalidtitle', $title ) );
}
- return $t->getPrefixedDbKey();
+ return $t->getPrefixedDBkey();
}
/**
* The inverse of titleToKey()
- * @param $key string Page title with underscores
+ * @param string $key Page title with underscores
* @return string Page title with spaces
*/
public function keyToTitle( $key ) {
@@ -433,7 +432,7 @@ abstract class ApiQueryBase extends ApiBase {
if ( trim( $key ) == '' ) {
return '';
}
- $t = Title::newFromDbKey( $key );
+ $t = Title::newFromDBkey( $key );
// This really shouldn't happen but we gotta check anyway
if ( !$t ) {
$this->dieUsageMsg( array( 'invalidtitle', $key ) );
@@ -443,7 +442,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* An alternative to titleToKey() that doesn't trim trailing spaces
- * @param $titlePart string Title part with spaces
+ * @param string $titlePart Title part with spaces
* @return string Title part with underscores
*/
public function titlePartToKey( $titlePart ) {
@@ -452,7 +451,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* An alternative to keyToTitle() that doesn't trim trailing spaces
- * @param $keyPart string Key part with spaces
+ * @param string $keyPart Key part with spaces
* @return string Key part with underscores
*/
public function keyPartToTitle( $keyPart ) {
@@ -479,7 +478,7 @@ abstract class ApiQueryBase extends ApiBase {
* @param $protocol String
* @return null|string
*/
- public function prepareUrlQuerySearchString( $query = null, $protocol = null) {
+ public function prepareUrlQuerySearchString( $query = null, $protocol = null ) {
$db = $this->getDb();
if ( !is_null( $query ) || $query != '' ) {
if ( is_null( $protocol ) ) {
@@ -534,7 +533,7 @@ abstract class ApiQueryBase extends ApiBase {
* @return bool
*/
public function validateSha1Hash( $hash ) {
- return preg_match( '/[a-fA-F0-9]{40}/', $hash );
+ return preg_match( '/^[a-f0-9]{40}$/', $hash );
}
/**
@@ -542,25 +541,19 @@ abstract class ApiQueryBase extends ApiBase {
* @return bool
*/
public function validateSha1Base36Hash( $hash ) {
- return preg_match( '/[a-zA-Z0-9]{31}/', $hash );
+ return preg_match( '/^[a-z0-9]{31}$/', $hash );
}
/**
* @return array
*/
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
+ $errors = parent::getPossibleErrors();
+ $errors = array_merge( $errors, array(
array( 'invalidtitle', 'title' ),
array( 'invalidtitle', 'key' ),
) );
- }
-
- /**
- * Get version string for use in the API help output
- * @return string
- */
- public static function getBaseVersion() {
- return __CLASS__ . ': $Id$';
+ return $errors;
}
}
@@ -569,33 +562,41 @@ abstract class ApiQueryBase extends ApiBase {
*/
abstract class ApiQueryGeneratorBase extends ApiQueryBase {
- private $mIsGenerator;
+ private $mGeneratorPageSet = null;
/**
- * @param $query ApiBase
- * @param $moduleName string
- * @param $paramPrefix string
+ * Switch this module to generator mode. By default, generator mode is
+ * switched off and the module acts like a normal query module.
+ * @since 1.21 requires pageset parameter
+ * @param $generatorPageSet ApiPageSet object that the module will get
+ * by calling getPageSet() when in generator mode.
*/
- public function __construct( $query, $moduleName, $paramPrefix = '' ) {
- parent::__construct( $query, $moduleName, $paramPrefix );
- $this->mIsGenerator = false;
+ public function setGeneratorMode( ApiPageSet $generatorPageSet ) {
+ if ( $generatorPageSet === null ) {
+ ApiBase::dieDebug( __METHOD__, 'Required parameter missing - $generatorPageSet' );
+ }
+ $this->mGeneratorPageSet = $generatorPageSet;
}
/**
- * Switch this module to generator mode. By default, generator mode is
- * switched off and the module acts like a normal query module.
+ * Get the PageSet object to work on.
+ * If this module is generator, the pageSet object is different from other module's
+ * @return ApiPageSet
*/
- public function setGeneratorMode() {
- $this->mIsGenerator = true;
+ protected function getPageSet() {
+ if ( $this->mGeneratorPageSet !== null ) {
+ return $this->mGeneratorPageSet;
+ }
+ return parent::getPageSet();
}
/**
* Overrides base class to prepend 'g' to every generator parameter
- * @param $paramName string Parameter name
+ * @param string $paramName Parameter name
* @return string Prefixed parameter name
*/
public function encodeParamName( $paramName ) {
- if ( $this->mIsGenerator ) {
+ if ( $this->mGeneratorPageSet !== null ) {
return 'g' . parent::encodeParamName( $paramName );
} else {
return parent::encodeParamName( $paramName );
@@ -603,9 +604,24 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
}
/**
+ * Overrides base in case of generator & smart continue to
+ * notify ApiQueryMain instead of adding them to the result right away.
+ * @param string $paramName Parameter name
+ * @param string $paramValue Parameter value
+ */
+ protected function setContinueEnumParameter( $paramName, $paramValue ) {
+ // If this is a generator and query->setGeneratorContinue() returns false, treat as before
+ if ( $this->mGeneratorPageSet === null
+ || !$this->getQuery()->setGeneratorContinue( $this, $paramName, $paramValue )
+ ) {
+ parent::setContinueEnumParameter( $paramName, $paramValue );
+ }
+ }
+
+ /**
* Execute this module as a generator
* @param $resultPageSet ApiPageSet: All output should be appended to
* this object
*/
- public abstract function executeGenerator( $resultPageSet );
+ abstract public function executeGenerator( $resultPageSet );
}
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index 96b86962..e3c27f5e 100644
--- a/includes/api/ApiQueryBlocks.php
+++ b/includes/api/ApiQueryBlocks.php
@@ -63,7 +63,7 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->addTables( 'ipblocks' );
$this->addFields( 'ipb_auto' );
- $this->addFieldsIf ( 'ipb_id', $fld_id );
+ $this->addFieldsIf( 'ipb_id', $fld_id );
$this->addFieldsIf( array( 'ipb_address', 'ipb_user' ), $fld_user || $fld_userid );
$this->addFieldsIf( 'ipb_by_text', $fld_by );
$this->addFieldsIf( 'ipb_by', $fld_byid );
@@ -91,17 +91,30 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->addWhereFld( 'ipb_auto', 0 );
}
if ( isset( $params['ip'] ) ) {
- list( $ip, $range ) = IP::parseCIDR( $params['ip'] );
- if ( $ip && $range ) {
- // We got a CIDR range
- if ( $range < 16 )
- $this->dieUsage( 'CIDR ranges broader than /16 are not accepted', 'cidrtoobroad' );
- $lower = wfBaseConvert( $ip, 10, 16, 8, false );
- $upper = wfBaseConvert( $ip + pow( 2, 32 - $range ) - 1, 10, 16, 8, false );
+ global $wgBlockCIDRLimit;
+ if ( IP::isIPv4( $params['ip'] ) ) {
+ $type = 'IPv4';
+ $cidrLimit = $wgBlockCIDRLimit['IPv4'];
+ $prefixLen = 0;
+ } elseif ( IP::isIPv6( $params['ip'] ) ) {
+ $type = 'IPv6';
+ $cidrLimit = $wgBlockCIDRLimit['IPv6'];
+ $prefixLen = 3; // IP::toHex output is prefixed with "v6-"
} else {
- $lower = $upper = IP::toHex( $params['ip'] );
+ $this->dieUsage( 'IP parameter is not valid', 'param_ip' );
+ }
+
+ # Check range validity, if it's a CIDR
+ list( $ip, $range ) = IP::parseCIDR( $params['ip'] );
+ if ( $ip !== false && $range !== false && $range < $cidrLimit ) {
+ $this->dieUsage( "$type CIDR ranges broader than /$cidrLimit are not accepted", 'cidrtoobroad' );
}
- $prefix = substr( $lower, 0, 4 );
+
+ # Let IP::parseRange handle calculating $upper, instead of duplicating the logic here.
+ list( $lower, $upper ) = IP::parseRange( $params['ip'] );
+
+ # Extract the common prefix to any rangeblock affecting this IP/CIDR
+ $prefix = substr( $lower, 0, $prefixLen + floor( $cidrLimit / 4 ) );
# Fairly hard to make a malicious SQL statement out of hex characters,
# but it is good practice to add quotes
@@ -120,10 +133,10 @@ class ApiQueryBlocks extends ApiQueryBase {
$show = array_flip( $params['show'] );
/* Check for conflicting parameters. */
- if ( ( isset ( $show['account'] ) && isset ( $show['!account'] ) )
- || ( isset ( $show['ip'] ) && isset ( $show['!ip'] ) )
- || ( isset ( $show['range'] ) && isset ( $show['!range'] ) )
- || ( isset ( $show['temp'] ) && isset ( $show['!temp'] ) )
+ if ( ( isset( $show['account'] ) && isset( $show['!account'] ) )
+ || ( isset( $show['ip'] ) && isset( $show['!ip'] ) )
+ || ( isset( $show['range'] ) && isset( $show['!range'] ) )
+ || ( isset( $show['temp'] ) && isset( $show['!temp'] ) )
) {
$this->dieUsageMsg( 'show' );
}
@@ -132,10 +145,10 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->addWhereIf( 'ipb_user != 0', isset( $show['account'] ) );
$this->addWhereIf( 'ipb_user != 0 OR ipb_range_end > ipb_range_start', isset( $show['!ip'] ) );
$this->addWhereIf( 'ipb_user = 0 AND ipb_range_end = ipb_range_start', isset( $show['ip'] ) );
- $this->addWhereIf( 'ipb_expiry = '.$db->addQuotes($db->getInfinity()), isset( $show['!temp'] ) );
- $this->addWhereIf( 'ipb_expiry != '.$db->addQuotes($db->getInfinity()), isset( $show['temp'] ) );
- $this->addWhereIf( "ipb_range_end = ipb_range_start", isset( $show['!range'] ) );
- $this->addWhereIf( "ipb_range_end > ipb_range_start", isset( $show['range'] ) );
+ $this->addWhereIf( 'ipb_expiry = ' . $db->addQuotes( $db->getInfinity() ), isset( $show['!temp'] ) );
+ $this->addWhereIf( 'ipb_expiry != ' . $db->addQuotes( $db->getInfinity() ), isset( $show['temp'] ) );
+ $this->addWhereIf( 'ipb_range_end = ipb_range_start', isset( $show['!range'] ) );
+ $this->addWhereIf( 'ipb_range_end > ipb_range_start', isset( $show['range'] ) );
}
if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
@@ -182,8 +195,8 @@ class ApiQueryBlocks extends ApiQueryBase {
$block['reason'] = $row->ipb_reason;
}
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::formatHex( $row->ipb_range_start );
+ $block['rangeend'] = IP::formatHex( $row->ipb_range_end );
}
if ( $fld_flags ) {
// For clarity, these flags use the same names as their action=block counterparts
@@ -294,6 +307,7 @@ class ApiQueryBlocks extends ApiQueryBase {
}
public function getParamDescription() {
+ global $wgBlockCIDRLimit;
$p = $this->getModulePrefix();
return array(
'start' => 'The timestamp to start enumerating from',
@@ -301,8 +315,12 @@ class ApiQueryBlocks extends ApiQueryBase {
'dir' => $this->getDirectionDescription( $p ),
'ids' => 'List of block IDs to list (optional)',
'users' => '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' ),
+ '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 " .
+ "IPv4/{$wgBlockCIDRLimit['IPv4']} or IPv6/{$wgBlockCIDRLimit['IPv6']} " .
+ "are not accepted"
+ ),
'limit' => 'The maximum amount of blocks to list',
'prop' => array(
'Which properties to get',
@@ -383,10 +401,19 @@ class ApiQueryBlocks extends ApiQueryBase {
}
public function getPossibleErrors() {
+ global $wgBlockCIDRLimit;
return array_merge( parent::getPossibleErrors(),
$this->getRequireOnlyOneParameterErrorMessages( array( 'users', 'ip' ) ),
array(
- array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ),
+ array(
+ 'code' => 'cidrtoobroad',
+ 'info' => "IPv4 CIDR ranges broader than /{$wgBlockCIDRLimit['IPv4']} are not accepted"
+ ),
+ array(
+ 'code' => 'cidrtoobroad',
+ 'info' => "IPv6 CIDR ranges broader than /{$wgBlockCIDRLimit['IPv6']} are not accepted"
+ ),
+ array( 'code' => 'param_ip', 'info' => 'IP parameter is not valid' ),
array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ),
array( 'code' => 'param_user', 'info' => 'User name user is not valid' ),
array( 'show' ),
@@ -404,8 +431,4 @@ class ApiQueryBlocks extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Blocks';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 309c2ce9..5d714f57 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -49,11 +49,10 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
/**
* @param $resultPageSet ApiPageSet
- * @return
*/
private function run( $resultPageSet = null ) {
if ( $this->getPageSet()->getGoodTitleCount() == 0 ) {
- return; // nothing to do
+ return; // nothing to do
}
$params = $this->extractRequestParams();
@@ -85,10 +84,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
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" );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$clfrom = intval( $cont[0] );
$clto = $this->getDB()->addQuotes( $cont[1] );
@@ -177,7 +173,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
break;
}
- $titles[] = Title :: makeTitle( NS_CATEGORY, $row->cl_to );
+ $titles[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
}
$resultPageSet->populateFromTitles( $titles );
}
@@ -187,7 +183,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
return array(
'prop' => array(
ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => array (
+ ApiBase::PARAM_TYPE => array(
'sortkey',
'timestamp',
'hidden',
@@ -276,8 +272,4 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#categories_.2F_cl';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php
index 31517fab..a889272e 100644
--- a/includes/api/ApiQueryCategoryInfo.php
+++ b/includes/api/ApiQueryCategoryInfo.php
@@ -48,6 +48,7 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
$this->getPageSet()->getMissingTitles();
$cattitles = array();
foreach ( $categories as $c ) {
+ /** @var $t Title */
$t = $titles[$c];
$cattitles[$c] = $t->getDBkey();
}
@@ -146,8 +147,4 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#categoryinfo_.2F_ci';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 55ce0234..704d108a 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -78,7 +78,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$this->addFieldsIf( 'cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp' );
- $this->addTables( array( 'page', 'categorylinks' ) ); // must be in this order for 'USE INDEX'
+ $this->addTables( array( 'page', 'categorylinks' ) ); // must be in this order for 'USE INDEX'
$this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() );
$queryTypes = $params['type'];
@@ -106,11 +106,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
} else {
if ( $params['continue'] ) {
$cont = explode( '|', $params['continue'], 3 );
- if ( count( $cont ) != 3 ) {
- $this->dieUsage( 'Invalid continue param. You should pass the original value returned '.
- 'by the previous query', '_badcontinue'
- );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 3 );
// Remove the types to skip from $queryTypes
$contTypeIndex = array_search( $cont[0], $queryTypes );
@@ -118,7 +114,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
// Add a WHERE clause for sortkey and from
// pack( "H*", $foo ) is used to convert hex back to binary
- $escSortkey = $this->getDB()->addQuotes( pack( "H*", $cont[1] ) );
+ $escSortkey = $this->getDB()->addQuotes( pack( 'H*', $cont[1] ) );
$from = intval( $cont[2] );
$op = $dir == 'newer' ? '>' : '<';
// $contWhere is used further down
@@ -221,7 +217,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
if ( $fld_sortkeyprefix ) {
$vals['sortkeyprefix'] = $row->cl_sortkey_prefix;
}
- if ( $fld_type ) {
+ if ( $fld_type ) {
$vals['type'] = $row->cl_type;
}
if ( $fld_timestamp ) {
@@ -247,7 +243,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
if ( is_null( $resultPageSet ) ) {
$result->setIndexedTagName_internal(
- array( 'query', $this->getModuleName() ), 'cm' );
+ array( 'query', $this->getModuleName() ), 'cm' );
}
}
@@ -262,7 +258,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'prop' => array(
ApiBase::PARAM_DFLT => 'ids|title',
ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => array (
+ ApiBase::PARAM_TYPE => array(
'ids',
'title',
'sortkey',
@@ -271,7 +267,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'timestamp',
)
),
- 'namespace' => array (
+ 'namespace' => array(
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace',
),
@@ -403,7 +399,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$this->getTitleOrPageIdErrorMessage(),
array(
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' ),
)
);
}
@@ -418,8 +413,4 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Categorymembers';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index e69ccbd6..82733133 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -50,13 +50,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$fld_user = isset( $prop['user'] );
$fld_userid = isset( $prop['userid'] );
$fld_comment = isset( $prop['comment'] );
- $fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $fld_parsedcomment = isset( $prop['parsedcomment'] );
$fld_minor = isset( $prop['minor'] );
$fld_len = isset( $prop['len'] );
$fld_sha1 = isset( $prop['sha1'] );
$fld_content = isset( $prop['content'] );
$fld_token = isset( $prop['token'] );
+ // If we're in JSON callback mode, no tokens can be obtained
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) {
+ $fld_token = false;
+ }
+
$result = $this->getResult();
$pageSet = $this->getPageSet();
$titles = $pageSet->getTitles();
@@ -74,15 +79,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
if ( $mode == 'revs' || $mode == 'user' ) {
// Ignore namespace and unique due to inability to know whether they were purposely set
- foreach( array( 'from', 'to', 'prefix', /*'namespace',*/ 'continue', /*'unique'*/ ) as $p ) {
+ foreach ( array( 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ) as $p ) {
if ( !is_null( $params[$p] ) ) {
- $this->dieUsage( "The '{$p}' parameter cannot be used in modes 1 or 2", 'badparams');
+ $this->dieUsage( "The '{$p}' parameter cannot be used in modes 1 or 2", 'badparams' );
}
}
} else {
- foreach( array( 'start', 'end' ) as $p ) {
+ foreach ( array( 'start', 'end' ) as $p ) {
if ( !is_null( $params[$p] ) ) {
- $this->dieUsage( "The {$p} parameter cannot be used in mode 3", 'badparams');
+ $this->dieUsage( "The {$p} parameter cannot be used in mode 3", 'badparams' );
}
}
}
@@ -116,7 +121,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
// Check limits
$userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
- $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
+ $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
$limit = $params['limit'];
@@ -160,10 +165,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 3 );
$ns = intval( $cont[0] );
+ $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
$title = $db->addQuotes( $cont[1] );
$ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
$op = ( $dir == 'newer' ? '>' : '<' );
@@ -307,7 +311,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
),
'namespace' => array(
ApiBase::PARAM_TYPE => 'namespace',
- ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_DFLT => NS_MAIN,
),
'limit' => array(
ApiBase::PARAM_DFLT => 10,
@@ -362,7 +366,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
'namespace' => 'Only list pages in this namespace (3)',
'user' => 'Only list revisions by this user',
'excludeuser' => 'Don\'t list revisions by this user',
- 'continue' => 'When more results are available, use this to continue (3)',
+ 'continue' => 'When more results are available, use this to continue (1, 3)',
'unique' => 'List only one revision for each page (3)',
);
}
@@ -397,11 +401,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
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' ),
array( 'code' => 'badparams', 'info' => "The 'from' parameter cannot be used in modes 1 or 2" ),
array( 'code' => 'badparams', 'info' => "The 'to' parameter cannot be used in modes 1 or 2" ),
array( 'code' => 'badparams', 'info' => "The 'prefix' parameter cannot be used in modes 1 or 2" ),
- array( 'code' => 'badparams', 'info' => "The 'continue' parameter cannot be used in modes 1 or 2" ),
array( 'code' => 'badparams', 'info' => "The 'start' parameter cannot be used in mode 3" ),
array( 'code' => 'badparams', 'info' => "The 'end' parameter cannot be used in mode 3" ),
) );
@@ -423,8 +425,4 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Deletedrevs';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php
index 6715969a..cf0d841e 100644
--- a/includes/api/ApiQueryDisabled.php
+++ b/includes/api/ApiQueryDisabled.php
@@ -36,10 +36,6 @@
*/
class ApiQueryDisabled extends ApiQueryBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$this->setWarning( "The \"{$this->getModuleName()}\" module has been disabled." );
}
@@ -61,8 +57,4 @@ class ApiQueryDisabled extends ApiQueryBase {
public function getExamples() {
return array();
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php
index 8f0fd3be..0311fa7f 100644
--- a/includes/api/ApiQueryDuplicateFiles.php
+++ b/includes/api/ApiQueryDuplicateFiles.php
@@ -48,8 +48,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
/**
- * @param $resultPageSet ApiPageSet
- * @return
+ * @param ApiPageSet $resultPageSet
*/
private function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
@@ -59,17 +58,14 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
$images = $namespaces[NS_FILE];
- if( $params['dir'] == 'descending' ) {
+ if ( $params['dir'] == 'descending' ) {
$images = array_reverse( $images );
}
$skipUntilThisDup = false;
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$fromImage = $cont[0];
$skipUntilThisDup = $cont[1];
// Filter out any images before $fromImage
@@ -83,7 +79,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
$filesToFind = array_keys( $images );
- if( $params['localonly'] ) {
+ if ( $params['localonly'] ) {
$files = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToFind );
} else {
$files = RepoGroup::singleton()->findFiles( $filesToFind );
@@ -95,33 +91,35 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
$sha1s = array();
foreach ( $files as $file ) {
+ /** @var $file File */
$sha1s[$file->getName()] = $file->getSha1();
}
// find all files with the hashes, result format is: array( hash => array( dup1, dup2 ), hash1 => ... )
$filesToFindBySha1s = array_unique( array_values( $sha1s ) );
- if( $params['localonly'] ) {
+ if ( $params['localonly'] ) {
$filesBySha1s = RepoGroup::singleton()->getLocalRepo()->findBySha1s( $filesToFindBySha1s );
} else {
$filesBySha1s = RepoGroup::singleton()->findBySha1s( $filesToFindBySha1s );
}
// iterate over $images to handle continue param correct
- foreach( $images as $image => $pageId ) {
- if( !isset( $sha1s[$image] ) ) {
+ foreach ( $images as $image => $pageId ) {
+ if ( !isset( $sha1s[$image] ) ) {
continue; //file does not exist
}
$sha1 = $sha1s[$image];
$dupFiles = $filesBySha1s[$sha1];
- if( $params['dir'] == 'descending' ) {
+ if ( $params['dir'] == 'descending' ) {
$dupFiles = array_reverse( $dupFiles );
}
+ /** @var $dupFile File */
foreach ( $dupFiles as $dupFile ) {
$dupName = $dupFile->getName();
- if( $image == $dupName && $dupFile->isLocal() ) {
+ if ( $image == $dupName && $dupFile->isLocal() ) {
continue; //ignore the local file itself
}
- if( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) {
+ if ( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) {
continue; //skip to pos after the image from continue param
}
$skipUntilThisDup = false;
@@ -133,14 +131,14 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
break;
}
if ( !is_null( $resultPageSet ) ) {
- $titles[] = $file->getTitle();
+ $titles[] = $dupFile->getTitle();
} else {
$r = array(
'name' => $dupName,
'user' => $dupFile->getUser( 'text' ),
'timestamp' => wfTimestamp( TS_ISO_8601, $dupFile->getTimestamp() )
);
- if( !$dupFile->isLocal() ) {
+ if ( !$dupFile->isLocal() ) {
$r['shared'] = '';
}
$fit = $this->addPageSubItem( $pageId, $r );
@@ -150,7 +148,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
}
}
- if( !$fit ) {
+ if ( !$fit ) {
break;
}
}
@@ -204,12 +202,6 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
return 'List all files that are duplicates of the given file(s) based on hash values';
}
- 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' ),
- ) );
- }
-
public function getExamples() {
return array(
'api.php?action=query&titles=File:Albert_Einstein_Head.jpg&prop=duplicatefiles',
@@ -220,8 +212,4 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#duplicatefiles_.2F_df';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index 42b398ba..456e87ba 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -55,7 +55,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
$query = $params['query'];
$protocol = self::getProtocolPrefix( $params['protocol'] );
- $this->addTables( array( 'page', 'externallinks' ) ); // must be in this order for 'USE INDEX'
+ $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' );
@@ -121,8 +121,12 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
ApiQueryBase::addTitleInfo( $vals, $title );
}
if ( $fld_url ) {
- // We *could* run this through wfExpandUrl() but I think it's better to output the link verbatim, even if it's protocol-relative --Roan
- $vals['url'] = $row->el_to;
+ $to = $row->el_to;
+ // expand protocol-relative urls
+ if ( $params['expandurl'] ) {
+ $to = wfExpandUrl( $to, PROTO_CANONICAL );
+ }
+ $vals['url'] = $to;
}
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
if ( !$fit ) {
@@ -169,7 +173,8 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
ApiBase::PARAM_MIN => 1,
ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
- )
+ ),
+ 'expandurl' => false,
);
}
@@ -213,12 +218,13 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
),
'offset' => 'Used for paging. Use the value returned for "continue"',
'protocol' => array(
- "Protocol of the url. If empty and {$p}query set, the protocol is http.",
+ "Protocol of the URL. If empty and {$p}query set, the protocol is http.",
"Leave both this and {$p}query 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.'
+ 'limit' => 'How many pages to return.',
+ 'expandurl' => 'Expand protocol-relative URLs with the canonical protocol',
);
if ( $wgMiserMode ) {
@@ -266,8 +272,4 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Exturlusage';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
index 9365a9b8..583ef697 100644
--- a/includes/api/ApiQueryExternalLinks.php
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -86,8 +86,12 @@ class ApiQueryExternalLinks extends ApiQueryBase {
break;
}
$entry = array();
- // We *could* run this through wfExpandUrl() but I think it's better to output the link verbatim, even if it's protocol-relative --Roan
- ApiResult::setContent( $entry, $row->el_to );
+ $to = $row->el_to;
+ // expand protocol-relative urls
+ if ( $params['expandurl'] ) {
+ $to = wfExpandUrl( $to, PROTO_CANONICAL );
+ }
+ ApiResult::setContent( $entry, $to );
$fit = $this->addPageSubItem( $row->el_from, $entry );
if ( !$fit ) {
$this->setContinueEnumParameter( 'offset', $offset + $count - 1 );
@@ -117,6 +121,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
ApiBase::PARAM_DFLT => '',
),
'query' => null,
+ 'expandurl' => false,
);
}
@@ -126,10 +131,11 @@ class ApiQueryExternalLinks extends ApiQueryBase {
'limit' => 'How many links to return',
'offset' => 'When more results are available, use this to continue',
'protocol' => array(
- "Protocol of the url. If empty and {$p}query set, the protocol is http.",
+ "Protocol of the URL. If empty and {$p}query set, the protocol is http.",
"Leave both this and {$p}query empty to list all external links"
),
'query' => 'Search string without protocol. Useful for checking whether a certain page contains a certain external url',
+ 'expandurl' => 'Expand protocol-relative URLs with the canonical protocol',
);
}
@@ -142,7 +148,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
}
public function getDescription() {
- return 'Returns all external urls (not interwikies) from the given page(s)';
+ return 'Returns all external URLs (not interwikis) from the given page(s)';
}
public function getPossibleErrors() {
@@ -160,8 +166,4 @@ class ApiQueryExternalLinks extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#extlinks_.2F_el';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryFileRepoInfo.php b/includes/api/ApiQueryFileRepoInfo.php
new file mode 100644
index 00000000..3a353533
--- /dev/null
+++ b/includes/api/ApiQueryFileRepoInfo.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Copyright © 2013 Mark Holmquist <mtraceur@member.fsf.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
+ * @since 1.22
+ */
+
+/**
+ * A query action to return meta information about the foreign file repos
+ * configured on the wiki.
+ *
+ * @ingroup API
+ */
+class ApiQueryFileRepoInfo extends ApiQueryBase {
+
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'fri' );
+ }
+
+ protected function getInitialisedRepoGroup() {
+ $repoGroup = RepoGroup::singleton();
+
+ if ( !$repoGroup->reposInitialised ) {
+ $repoGroup->initialiseRepos();
+ }
+
+ return $repoGroup;
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $props = array_flip( $params['prop'] );
+
+ $repos = array();
+
+ $repoGroup = $this->getInitialisedRepoGroup();
+
+ $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$repos, $props ) {
+ $repos[] = array_intersect_key( $repo->getInfo(), $props );
+ } );
+
+ $repos[] = array_intersect_key( $repoGroup->localRepo->getInfo(), $props );
+
+ $result = $this->getResult();
+ $result->setIndexedTagName( $repos, 'repo' );
+ $result->addValue( array( 'query' ), 'repos', $repos );
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ public function getAllowedParams() {
+ $props = $this->getProps();
+
+ return array(
+ 'prop' => array(
+ ApiBase::PARAM_DFLT => join( '|', $props ),
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TYPE => $props,
+ ),
+ );
+ }
+
+ public function getProps() {
+ $props = array();
+ $repoGroup = $this->getInitialisedRepoGroup();
+
+ $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$props ) {
+ $props = array_merge( $props, array_keys( $repo->getInfo() ) );
+ } );
+
+ return array_values( array_unique( array_merge( $props, array_keys( $repoGroup->localRepo->getInfo() ) ) ) );
+ }
+
+ public function getParamDescription() {
+ $p = $this->getModulePrefix();
+ return array(
+ 'prop' => array(
+ 'Which repository properties to get (there may be more available on some wikis):',
+ ' apiurl - URL to the repository API - helpful for getting image info from the host.',
+ ' name - The key of the repository - used in e.g. $wgForeignFileRepos and imageinfo return values.',
+ ' displayname - The human-readable name of the repository wiki.',
+ ' rooturl - Root URL for image paths.',
+ ' local - Whether that repository is the local one or not.',
+ ),
+ );
+ }
+
+ public function getDescription() {
+ return 'Return meta information about image repositories configured on the wiki.';
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=query&meta=filerepoinfo&friprop=apiurl|name|displayname',
+ );
+ }
+}
diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php
index a5486ef4..f53cd386 100644
--- a/includes/api/ApiQueryFilearchive.php
+++ b/includes/api/ApiQueryFilearchive.php
@@ -64,7 +64,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
$this->addTables( 'filearchive' );
$this->addFields( array( 'fa_name', 'fa_deleted' ) );
- $this->addFieldsIf( 'fa_storage_key', $fld_sha1 );
+ $this->addFieldsIf( 'fa_sha1', $fld_sha1 );
$this->addFieldsIf( 'fa_timestamp', $fld_timestamp );
$this->addFieldsIf( array( 'fa_user', 'fa_user_text' ), $fld_user );
$this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size );
@@ -77,10 +77,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
if ( !is_null( $params['continue'] ) ) {
$cont = explode( '|', $params['continue'] );
- if ( count( $cont ) != 1 ) {
- $this->dieUsage( "Invalid continue param. You should pass the " .
- "original value returned by the previous query", "_badcontinue" );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 1 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$cont_from = $db->addQuotes( $cont[0] );
$this->addWhere( "fa_name $op= $cont_from" );
@@ -101,25 +98,21 @@ class ApiQueryFilearchive extends ApiQueryBase {
$sha1Set = isset( $params['sha1'] );
$sha1base36Set = isset( $params['sha1base36'] );
if ( $sha1Set || $sha1base36Set ) {
- global $wgMiserMode;
- if ( $wgMiserMode ) {
- $this->dieUsage( 'Search by hash disabled in Miser Mode', 'hashsearchdisabled' );
- }
-
$sha1 = false;
if ( $sha1Set ) {
- if ( !$this->validateSha1Hash( $params['sha1'] ) ) {
+ $sha1 = strtolower( $params['sha1'] );
+ if ( !$this->validateSha1Hash( $sha1 ) ) {
$this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' );
}
- $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 );
+ $sha1 = wfBaseConvert( $sha1, 16, 36, 31 );
} elseif ( $sha1base36Set ) {
- if ( !$this->validateSha1Base36Hash( $params['sha1base36'] ) ) {
+ $sha1 = strtolower( $params['sha1base36'] );
+ if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
$this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' );
}
- $sha1 = $params['sha1base36'];
}
if ( $sha1 ) {
- $this->addWhere( 'fa_storage_key ' . $db->buildLike( "{$sha1}.", $db->anyString() ) );
+ $this->addWhereFld( 'fa_sha1', $sha1 );
}
}
@@ -155,7 +148,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
self::addTitleInfo( $file, $title );
if ( $fld_sha1 ) {
- $file['sha1'] = wfBaseConvert( LocalRepo::getHashFromKey( $row->fa_storage_key ), 36, 16, 40 );
+ $file['sha1'] = wfBaseConvert( $row->fa_sha1, 36, 16, 40 );
}
if ( $fld_timestamp ) {
$file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp );
@@ -214,7 +207,6 @@ class ApiQueryFilearchive extends ApiQueryBase {
$file['suppressed'] = '';
}
-
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file );
if ( !$fit ) {
$this->setContinueEnumParameter( 'continue', $row->fa_name );
@@ -226,7 +218,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
}
public function getAllowedParams() {
- return array (
+ return array(
'from' => null,
'continue' => null,
'to' => null,
@@ -276,8 +268,8 @@ class ApiQueryFilearchive extends ApiQueryBase {
'prefix' => 'Search for all image titles that begin with this value',
'dir' => 'The direction in which to list',
'limit' => 'How many images to return in total',
- 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36. Disabled in Miser Mode",
- 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki). Disabled in Miser Mode',
+ 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36",
+ 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)',
'prop' => array(
'What image information to get:',
' sha1 - Adds SHA-1 hash for the image',
@@ -289,7 +281,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
' parseddescription - Parse the description on the version',
' mime - Adds MIME of the image',
' mediatype - Adds the media type of the image',
- ' metadata - Lists EXIF metadata for the version of the image',
+ ' metadata - Lists Exif metadata for the version of the image',
' bitdepth - Adds the bit depth of the version',
' archivename - Adds the file name of the archive version for non-latest versions'
),
@@ -370,7 +362,6 @@ class ApiQueryFilearchive extends ApiQueryBase {
array( 'code' => 'hashsearchdisabled', 'info' => 'Search by hash disabled in Miser Mode' ),
array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ),
array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -383,7 +374,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Filearchive';
}
}
diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php
index c5012f08..ebae3e76 100644
--- a/includes/api/ApiQueryIWBacklinks.php
+++ b/includes/api/ApiQueryIWBacklinks.php
@@ -56,10 +56,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 3 );
$db = $this->getDB();
$op = $params['dir'] == 'descending' ? '<' : '>';
@@ -233,7 +230,6 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(), array(
array( 'missingparam', 'prefix' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -244,7 +240,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Iwbacklinks';
}
}
diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php
index 30c7f5a8..be539311 100644
--- a/includes/api/ApiQueryIWLinks.php
+++ b/includes/api/ApiQueryIWLinks.php
@@ -58,10 +58,7 @@ class ApiQueryIWLinks extends ApiQueryBase {
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 3 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$db = $this->getDB();
$iwlfrom = intval( $cont[0] );
@@ -84,8 +81,8 @@ class ApiQueryIWLinks extends ApiQueryBase {
$this->addOption( 'ORDER BY', 'iwl_from' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'iwl_title' . $sort,
- 'iwl_from' . $sort
+ 'iwl_from' . $sort,
+ 'iwl_title' . $sort
));
}
} else {
@@ -93,9 +90,10 @@ class ApiQueryIWLinks extends ApiQueryBase {
if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
$this->addOption( 'ORDER BY', 'iwl_prefix' . $sort );
} else {
- $this->addOption( 'ORDER BY', array (
+ $this->addOption( 'ORDER BY', array(
'iwl_from' . $sort,
- 'iwl_prefix' . $sort
+ 'iwl_prefix' . $sort,
+ 'iwl_title' . $sort
));
}
}
@@ -187,7 +185,6 @@ class ApiQueryIWLinks extends ApiQueryBase {
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(), array(
array( 'missingparam', 'prefix' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -197,7 +194,7 @@ class ApiQueryIWLinks extends ApiQueryBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Iwlinks';
}
}
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index d822eed5..0ea28684 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -30,6 +30,8 @@
* @ingroup API
*/
class ApiQueryImageInfo extends ApiQueryBase {
+ const TRANSFORM_LIMIT = 50;
+ private static $transformCount = 0;
public function __construct( $query, $moduleName, $prefix = 'ii' ) {
// We allow a subclass to override the prefix, to create a related API module.
@@ -52,14 +54,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
$titles = array_keys( $pageIds[NS_FILE] );
asort( $titles ); // Ensure the order is always the same
- $skip = false;
+ $fromTitle = null;
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$fromTitle = strval( $cont[0] );
$fromTimestamp = $cont[1];
// Filter out any titles before $fromTitle
@@ -74,19 +72,44 @@ class ApiQueryImageInfo extends ApiQueryBase {
$result = $this->getResult();
//search only inside the local repo
- if( $params['localonly'] ) {
+ if ( $params['localonly'] ) {
$images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles );
} else {
$images = RepoGroup::singleton()->findFiles( $titles );
}
- foreach ( $images as $img ) {
- // Skip redirects
- if ( $img->getOriginalTitle()->isRedirect() ) {
- continue;
+ foreach ( $titles as $title ) {
+ $pageId = $pageIds[NS_FILE][$title];
+ $start = $title === $fromTitle ? $fromTimestamp : $params['start'];
+
+ if ( !isset( $images[$title] ) ) {
+ if ( isset( $prop['uploadwarning'] ) ) {
+ // Uploadwarning needs info about non-existing files
+ $images[$title] = wfLocalFile( $title );
+ } else {
+ $result->addValue(
+ array( 'query', 'pages', intval( $pageId ) ),
+ 'imagerepository', ''
+ );
+ // The above can't fail because it doesn't increase the result size
+ continue;
+ }
}
- $start = $skip ? $fromTimestamp : $params['start'];
- $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ];
+ /** @var $img File */
+ $img = $images[$title];
+
+ if ( self::getTransformCount() >= self::TRANSFORM_LIMIT ) {
+ if ( count( $pageIds[NS_FILE] ) == 1 ) {
+ // See the 'the user is screwed' comment below
+ $this->setContinueEnumParameter( 'start',
+ $start !== null ? $start : wfTimestamp( TS_ISO_8601, $img->getTimestamp() )
+ );
+ } else {
+ $this->setContinueEnumParameter( 'continue',
+ $this->getContinueStr( $img, $start ) );
+ }
+ break;
+ }
$fit = $result->addValue(
array( 'query', 'pages', intval( $pageId ) ),
@@ -100,10 +123,11 @@ class ApiQueryImageInfo extends ApiQueryBase {
// thing again. When the violating queries have been
// out-continued, the result will get through
$this->setContinueEnumParameter( 'start',
- wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) );
+ $start !== null ? $start : wfTimestamp( TS_ISO_8601, $img->getTimestamp() )
+ );
} else {
$this->setContinueEnumParameter( 'continue',
- $this->getContinueStr( $img ) );
+ $this->getContinueStr( $img, $start ) );
}
break;
}
@@ -140,6 +164,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
// Get one more to facilitate query-continue functionality
$count = ( $gotOne ? 1 : 0 );
$oldies = $img->getHistory( $params['limit'] - $count + 1, $start, $params['end'] );
+ /** @var $oldie File */
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...
@@ -150,9 +175,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
break;
}
- $fit = $this->addPageSubItem( $pageId,
- self::getInfo( $oldie, $prop, $result,
- $finalThumbParams, $params['metadataversion'] ) );
+ $fit = self::getTransformCount() < self::TRANSFORM_LIMIT &&
+ $this->addPageSubItem( $pageId,
+ self::getInfo( $oldie, $prop, $result,
+ $finalThumbParams, $params['metadataversion']
+ )
+ );
if ( !$fit ) {
if ( count( $pageIds[NS_FILE] ) == 1 ) {
$this->setContinueEnumParameter( 'start',
@@ -167,45 +195,32 @@ class ApiQueryImageInfo extends ApiQueryBase {
if ( !$fit ) {
break;
}
- $skip = false;
- }
-
- $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
}
}
}
/**
* From parameters, construct a 'scale' array
- * @param $params Array: Parameters passed to api.
+ * @param array $params Parameters passed to api.
* @return Array or Null: key-val array of 'width' and 'height', or null
*/
public function getScale( $params ) {
$p = $this->getModulePrefix();
- // Height and width.
- if ( $params['urlheight'] != -1 && $params['urlwidth'] == -1 ) {
- $this->dieUsage( "{$p}urlheight cannot be used without {$p}urlwidth", "{$p}urlwidth" );
- }
-
if ( $params['urlwidth'] != -1 ) {
$scale = array();
$scale['width'] = $params['urlwidth'];
$scale['height'] = $params['urlheight'];
+ } elseif ( $params['urlheight'] != -1 ) {
+ // Height is specified but width isn't
+ // Don't set $scale['width']; this signals mergeThumbParams() to fill it with the image's width
+ $scale = array();
+ $scale['height'] = $params['urlheight'];
} else {
$scale = null;
if ( $params['urlparam'] ) {
$this->dieUsage( "{$p}urlparam requires {$p}urlwidth", "urlparam_no_width" );
}
- return $scale;
}
return $scale;
@@ -216,11 +231,25 @@ class ApiQueryImageInfo extends ApiQueryBase {
* We do this later than getScale, since we need the image
* to know which handler, since handlers can make their own parameters.
* @param File $image Image that params are for.
- * @param Array $thumbParams thumbnail parameters from getScale
- * @param String $otherParams of otherParams (iiurlparam).
+ * @param array $thumbParams thumbnail parameters from getScale
+ * @param string $otherParams of otherParams (iiurlparam).
* @return Array of parameters for transform.
*/
- protected function mergeThumbParams ( $image, $thumbParams, $otherParams ) {
+ protected function mergeThumbParams( $image, $thumbParams, $otherParams ) {
+ global $wgThumbLimits;
+
+ if ( !isset( $thumbParams['width'] ) && isset( $thumbParams['height'] ) ) {
+ // We want to limit only by height in this situation, so pass the
+ // image's full width as the limiting width. But some file types
+ // don't have a width of their own, so pick something arbitrary so
+ // thumbnailing the default icon works.
+ if ( $image->getWidth() <= 0 ) {
+ $thumbParams['width'] = max( $wgThumbLimits );
+ } else {
+ $thumbParams['width'] = $image->getWidth();
+ }
+ }
+
if ( !$otherParams ) {
return $thumbParams;
}
@@ -246,8 +275,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
if ( isset( $paramList['width'] ) ) {
if ( intval( $paramList['width'] ) != intval( $thumbParams['width'] ) ) {
- $this->dieUsage( "{$p}urlparam had width of {$paramList['width']} but "
- . "{$p}urlwidth was {$thumbParams['width']}", "urlparam_urlwidth_mismatch" );
+ $this->setWarning( "Ignoring width value set in {$p}urlparam ({$paramList['width']}) "
+ . "in favor of width value derived from {$p}urlwidth/{$p}urlheight ({$thumbParams['width']})" );
}
}
@@ -264,10 +293,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
* Get result information for an image revision
*
* @param $file File object
- * @param $prop Array of properties to get (in the keys)
+ * @param array $prop of properties to get (in the keys)
* @param $result ApiResult object
- * @param $thumbParams Array containing 'width' and 'height' items, or null
- * @param $version string Version of image metadata (for things like jpeg which have different versions).
+ * @param array $thumbParams containing 'width' and 'height' items, or null
+ * @param string $version Version of image metadata (for things like jpeg which have different versions).
* @return Array: result array
*/
static function getInfo( $file, $prop, $result, $thumbParams = null, $version = 'latest' ) {
@@ -334,6 +363,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
$mediatype = isset( $prop['mediatype'] );
$archive = isset( $prop['archivename'] );
$bitdepth = isset( $prop['bitdepth'] );
+ $uploadwarning = isset( $prop['uploadwarning'] );
if ( ( $url || $sha1 || $meta || $mime || $mediatype || $archive || $bitdepth )
&& $file->isDeleted( File::DELETED_FILE ) ) {
@@ -346,6 +376,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
if ( $url ) {
if ( !is_null( $thumbParams ) ) {
$mto = $file->transform( $thumbParams );
+ self::$transformCount++;
if ( $mto && !$mto->isError() ) {
$vals['thumburl'] = wfExpandUrl( $mto->getUrl(), PROTO_CURRENT );
@@ -360,7 +391,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) {
- list( $ext, $mime ) = $file->getHandler()->getThumbType(
+ list( , $mime ) = $file->getHandler()->getThumbType(
$mto->getExtension(), $file->getMimeType(), $thumbParams );
$vals['thumbmime'] = $mime;
}
@@ -377,8 +408,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
if ( $meta ) {
+ wfSuppressWarnings();
$metadata = unserialize( $file->getMetadata() );
- if ( $version !== 'latest' ) {
+ wfRestoreWarnings();
+ if ( $metadata && $version !== 'latest' ) {
$metadata = $file->convertMetadataVersion( $metadata, $version );
}
$vals['metadata'] = $metadata ? self::processMetaData( $metadata, $result ) : null;
@@ -400,10 +433,25 @@ class ApiQueryImageInfo extends ApiQueryBase {
$vals['bitdepth'] = $file->getBitDepth();
}
+ if ( $uploadwarning ) {
+ $vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) );
+ }
+
return $vals;
}
/**
+ * Get the count of image transformations performed
+ *
+ * If this is >= TRANSFORM_LIMIT, you should probably stop processing images.
+ *
+ * @return integer count
+ */
+ static function getTransformCount() {
+ return self::$transformCount;
+ }
+
+ /**
*
* @param $metadata Array
* @param $result ApiResult
@@ -432,11 +480,14 @@ class ApiQueryImageInfo extends ApiQueryBase {
/**
* @param $img File
+ * @param null|string $start
* @return string
*/
- protected function getContinueStr( $img ) {
- return $img->getOriginalTitle()->getText() .
- '|' . $img->getTimestamp();
+ protected function getContinueStr( $img, $start = null ) {
+ if ( $start === null ) {
+ $start = $img->getTimestamp();
+ }
+ return $img->getOriginalTitle()->getDBkey() . '|' . $start;
}
public function getAllowedParams() {
@@ -494,6 +545,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
/**
* Returns array key value pairs of properties and their descriptions
*
+ * @param string $modulePrefix
* @return array
*/
private static function getProperties( $modulePrefix = '' ) {
@@ -511,9 +563,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail' .
' (requires url and param ' . $modulePrefix . 'urlwidth)',
'mediatype' => ' mediatype - Adds the media type of the image',
- 'metadata' => ' metadata - Lists EXIF metadata for the version of the image',
+ 'metadata' => ' metadata - Lists Exif metadata for the version of the image',
'archivename' => ' archivename - Adds the file name of the archive version for non-latest versions',
'bitdepth' => ' bitdepth - Adds the bit depth of the version',
+ 'uploadwarning' => ' uploadwarning - Used by the Special:Upload page to get information about an existing file. Not intended for use outside MediaWiki core',
);
}
@@ -521,7 +574,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
* Returns the descriptions for the properties provided by getPropertyNames()
*
* @param array $filter List of properties to filter out
- *
+ * @param string $modulePrefix
* @return array
*/
public static function getPropertyDescriptions( $filter = array(), $modulePrefix = '' ) {
@@ -540,11 +593,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
return array(
'prop' => self::getPropertyDescriptions( array(), $p ),
'urlwidth' => array( "If {$p}prop=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 {$p}urlwidth. Cannot be used without {$p}urlwidth",
+ 'For performance reasons if this option is used, ' .
+ 'no more than ' . self::TRANSFORM_LIMIT . ' scaled images will be returned.' ),
+ 'urlheight' => "Similar to {$p}urlwidth.",
'urlparam' => array( "A handler specific parameter string. For example, pdf's ",
"might use 'page15-100px'. {$p}urlwidth must be used and be consistent with {$p}urlparam" ),
- 'limit' => 'How many image revisions to return',
+ 'limit' => 'How many image revisions to return per image',
'start' => 'Timestamp to start listing from',
'end' => 'Timestamp to stop listing at',
'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.",
@@ -578,6 +632,15 @@ class ApiQueryImageInfo extends ApiQueryBase {
ApiBase::PROP_NULLABLE => true
)
),
+ 'dimensions' => array(
+ 'size' => 'integer',
+ 'width' => 'integer',
+ 'height' => 'integer',
+ 'pagecount' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
'comment' => array(
'commenthidden' => 'boolean',
'comment' => array(
@@ -633,6 +696,13 @@ class ApiQueryImageInfo extends ApiQueryBase {
ApiBase::PROP_NULLABLE => true
)
),
+ 'thumbmime' => array(
+ 'filehidden' => 'boolean',
+ 'thumbmime' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
'mediatype' => array(
'filehidden' => 'boolean',
'mediatype' => array(
@@ -672,8 +742,6 @@ class ApiQueryImageInfo extends ApiQueryBase {
array( 'code' => "{$p}urlwidth", 'info' => "{$p}urlheight cannot be used without {$p}urlwidth" ),
array( 'code' => 'urlparam', 'info' => "Invalid value for {$p}urlparam" ),
array( 'code' => 'urlparam_no_width', 'info' => "{$p}urlparam requires {$p}urlwidth" ),
- array( 'code' => 'urlparam_urlwidth_mismatch', 'info' => "The width set in {$p}urlparm doesnt't " .
- "match the one in {$p}urlwidth" ),
) );
}
@@ -687,8 +755,4 @@ class ApiQueryImageInfo extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#imageinfo_.2F_ii';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index 6052a75f..f2bf0a7b 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -49,7 +49,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
*/
private function run( $resultPageSet = null ) {
if ( $this->getPageSet()->getGoodTitleCount() == 0 ) {
- return; // nothing to do
+ return; // nothing to do
}
$params = $this->extractRequestParams();
@@ -62,10 +62,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$ilfrom = intval( $cont[0] );
$ilto = $this->getDB()->addQuotes( $cont[1] );
@@ -185,12 +182,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
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' ),
- ) );
- }
-
public function getExamples() {
return array(
'api.php?action=query&prop=images&titles=Main%20Page' => 'Get a list of images used in the [[Main Page]]',
@@ -201,8 +192,4 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#images_.2F_im';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index 5d4f0346..017684ed 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -33,7 +33,8 @@ class ApiQueryInfo extends ApiQueryBase {
private $fld_protection = false, $fld_talkid = false,
$fld_subjectid = false, $fld_url = false,
- $fld_readable = false, $fld_watched = false, $fld_notificationtimestamp = false,
+ $fld_readable = false, $fld_watched = false, $fld_watchers = false,
+ $fld_notificationtimestamp = false,
$fld_preload = false, $fld_displaytitle = false;
private $params, $titles, $missing, $everything, $pageCounter;
@@ -41,7 +42,8 @@ class ApiQueryInfo extends ApiQueryBase {
private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched,
$pageLatest, $pageLength;
- private $protections, $watched, $notificationtimestamps, $talkids, $subjectids, $displaytitles;
+ private $protections, $watched, $watchers, $notificationtimestamps, $talkids, $subjectids, $displaytitles;
+ private $showZeroWatchers = false;
private $tokenFunctions;
@@ -54,11 +56,11 @@ class ApiQueryInfo extends ApiQueryBase {
* @return void
*/
public function requestExtraData( $pageSet ) {
- global $wgDisableCounters;
+ global $wgDisableCounters, $wgContentHandlerUseDB;
$pageSet->requestField( 'page_restrictions' );
// when resolving redirects, no page will have this field
- if( !$pageSet->isResolvingRedirects() ) {
+ if ( !$pageSet->isResolvingRedirects() ) {
$pageSet->requestField( 'page_is_redirect' );
}
$pageSet->requestField( 'page_is_new' );
@@ -68,6 +70,9 @@ class ApiQueryInfo extends ApiQueryBase {
$pageSet->requestField( 'page_touched' );
$pageSet->requestField( 'page_latest' );
$pageSet->requestField( 'page_len' );
+ if ( $wgContentHandlerUseDB ) {
+ $pageSet->requestField( 'page_content_model' );
+ }
}
/**
@@ -96,7 +101,7 @@ class ApiQueryInfo extends ApiQueryBase {
'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ),
'email' => array( 'ApiQueryInfo', 'getEmailToken' ),
'import' => array( 'ApiQueryInfo', 'getImportToken' ),
- 'watch' => array( 'ApiQueryInfo', 'getWatchToken'),
+ 'watch' => array( 'ApiQueryInfo', 'getWatchToken' ),
);
wfRunHooks( 'APIQueryInfoTokens', array( &$this->tokenFunctions ) );
return $this->tokenFunctions;
@@ -118,11 +123,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'edit' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'edit' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['edit'] ) ) {
+ ApiQueryInfo::$cachedTokens['edit'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'edit' ];
+ return ApiQueryInfo::$cachedTokens['edit'];
}
public static function getDeleteToken( $pageid, $title ) {
@@ -132,11 +137,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'delete' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'delete' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['delete'] ) ) {
+ ApiQueryInfo::$cachedTokens['delete'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'delete' ];
+ return ApiQueryInfo::$cachedTokens['delete'];
}
public static function getProtectToken( $pageid, $title ) {
@@ -146,11 +151,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'protect' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'protect' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['protect'] ) ) {
+ ApiQueryInfo::$cachedTokens['protect'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'protect' ];
+ return ApiQueryInfo::$cachedTokens['protect'];
}
public static function getMoveToken( $pageid, $title ) {
@@ -160,11 +165,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'move' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'move' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['move'] ) ) {
+ ApiQueryInfo::$cachedTokens['move'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'move' ];
+ return ApiQueryInfo::$cachedTokens['move'];
}
public static function getBlockToken( $pageid, $title ) {
@@ -174,11 +179,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'block' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'block' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['block'] ) ) {
+ ApiQueryInfo::$cachedTokens['block'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'block' ];
+ return ApiQueryInfo::$cachedTokens['block'];
}
public static function getUnblockToken( $pageid, $title ) {
@@ -193,11 +198,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'email' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'email' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['email'] ) ) {
+ ApiQueryInfo::$cachedTokens['email'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'email' ];
+ return ApiQueryInfo::$cachedTokens['email'];
}
public static function getImportToken( $pageid, $title ) {
@@ -207,11 +212,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'import' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'import' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['import'] ) ) {
+ ApiQueryInfo::$cachedTokens['import'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'import' ];
+ return ApiQueryInfo::$cachedTokens['import'];
}
public static function getWatchToken( $pageid, $title ) {
@@ -221,11 +226,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'watch' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'watch' ] = $wgUser->getEditToken( 'watch' );
+ if ( !isset( ApiQueryInfo::$cachedTokens['watch'] ) ) {
+ ApiQueryInfo::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
}
- return ApiQueryInfo::$cachedTokens[ 'watch' ];
+ return ApiQueryInfo::$cachedTokens['watch'];
}
public static function getOptionsToken( $pageid, $title ) {
@@ -235,11 +240,11 @@ class ApiQueryInfo extends ApiQueryBase {
}
// The token is always the same, let's exploit that
- if ( !isset( ApiQueryInfo::$cachedTokens[ 'options' ] ) ) {
- ApiQueryInfo::$cachedTokens[ 'options' ] = $wgUser->getEditToken();
+ if ( !isset( ApiQueryInfo::$cachedTokens['options'] ) ) {
+ ApiQueryInfo::$cachedTokens['options'] = $wgUser->getEditToken();
}
- return ApiQueryInfo::$cachedTokens[ 'options' ];
+ return ApiQueryInfo::$cachedTokens['options'];
}
public function execute() {
@@ -248,6 +253,7 @@ class ApiQueryInfo extends ApiQueryBase {
$prop = array_flip( $this->params['prop'] );
$this->fld_protection = isset( $prop['protection'] );
$this->fld_watched = isset( $prop['watched'] );
+ $this->fld_watchers = isset( $prop['watchers'] );
$this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
$this->fld_talkid = isset( $prop['talkid'] );
$this->fld_subjectid = isset( $prop['subjectid'] );
@@ -268,10 +274,7 @@ class ApiQueryInfo extends ApiQueryBase {
// 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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
foreach ( $this->everything as $pageid => $title ) {
if ( Title::compare( $title, $conttitle ) >= 0 ) {
@@ -308,6 +311,10 @@ class ApiQueryInfo extends ApiQueryBase {
$this->getWatchedInfo();
}
+ if ( $this->fld_watchers ) {
+ $this->getWatcherInfo();
+ }
+
// Run the talkid/subjectid query if requested
if ( $this->fld_talkid || $this->fld_subjectid ) {
$this->getTSIDs();
@@ -317,6 +324,7 @@ class ApiQueryInfo extends ApiQueryBase {
$this->getDisplayTitle();
}
+ /** @var $title Title */
foreach ( $this->everything as $pageid => $title ) {
$pageInfo = $this->extractPageInfo( $pageid, $title );
$fit = $result->addValue( array(
@@ -334,7 +342,7 @@ class ApiQueryInfo extends ApiQueryBase {
/**
* Get a result array with information about a title
- * @param $pageid int Page ID (negative for missing titles)
+ * @param int $pageid Page ID (negative for missing titles)
* @param $title Title object
* @return array
*/
@@ -343,13 +351,17 @@ class ApiQueryInfo extends ApiQueryBase {
$titleExists = $pageid > 0; //$title->exists() needs pageid, which is not set for all title objects
$ns = $title->getNamespace();
$dbkey = $title->getDBkey();
+
+ $pageInfo['contentmodel'] = $title->getContentModel();
+ $pageInfo['pagelanguage'] = $title->getPageLanguage()->getCode();
+
if ( $titleExists ) {
global $wgDisableCounters;
$pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
$pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] );
$pageInfo['counter'] = $wgDisableCounters
- ? ""
+ ? ''
: intval( $this->pageCounter[$pageid] );
$pageInfo['length'] = intval( $this->pageLength[$pageid] );
@@ -387,6 +399,14 @@ class ApiQueryInfo extends ApiQueryBase {
$pageInfo['watched'] = '';
}
+ if ( $this->fld_watchers ) {
+ if ( isset( $this->watchers[$ns][$dbkey] ) ) {
+ $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
+ } elseif ( $this->showZeroWatchers ) {
+ $pageInfo['watchers'] = 0;
+ }
+ }
+
if ( $this->fld_notificationtimestamp ) {
$pageInfo['notificationtimestamp'] = '';
if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
@@ -394,7 +414,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
}
- if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
+ if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
$pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
}
@@ -406,7 +426,7 @@ class ApiQueryInfo extends ApiQueryBase {
$pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
$pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
}
- if ( $this->fld_readable && $title->userCan( 'read' ) ) {
+ if ( $this->fld_readable && $title->userCan( 'read', $this->getUser() ) ) {
$pageInfo['readable'] = '';
}
@@ -450,6 +470,7 @@ class ApiQueryInfo extends ApiQueryBase {
$res = $this->select( __METHOD__ );
foreach ( $res as $row ) {
+ /** @var $title Title */
$title = $this->titles[$row->pr_page];
$a = array(
'type' => $row->pr_type,
@@ -462,7 +483,7 @@ class ApiQueryInfo extends ApiQueryBase {
$this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
}
// Also check old restrictions
- foreach( $this->titles as $pageId => $title ) {
+ foreach ( $this->titles as $pageId => $title ) {
if ( $this->pageRestrictions[$pageId] ) {
$namespace = $title->getNamespace();
$dbKey = $title->getDBkey();
@@ -585,6 +606,7 @@ class ApiQueryInfo extends ApiQueryBase {
private function getTSIDs() {
$getTitles = $this->talkids = $this->subjectids = array();
+ /** @var $t Title */
foreach ( $this->everything as $t ) {
if ( MWNamespace::isTalk( $t->getNamespace() ) ) {
if ( $this->fld_subjectid ) {
@@ -647,7 +669,9 @@ class ApiQueryInfo extends ApiQueryBase {
private function getWatchedInfo() {
$user = $this->getUser();
- if ( $user->isAnon() || count( $this->everything ) == 0 ) {
+ if ( $user->isAnon() || count( $this->everything ) == 0
+ || !$user->isAllowed( 'viewmywatchlist' )
+ ) {
return;
}
@@ -678,6 +702,46 @@ class ApiQueryInfo extends ApiQueryBase {
}
}
+ /**
+ * Get the count of watchers and put it in $this->watchers
+ */
+ private function getWatcherInfo() {
+ global $wgUnwatchedPageThreshold;
+
+ if ( count( $this->everything ) == 0 ) {
+ return;
+ }
+
+ $user = $this->getUser();
+ $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
+ if ( !$canUnwatchedpages && !is_int( $wgUnwatchedPageThreshold ) ) {
+ return;
+ }
+
+ $this->watchers = array();
+ $this->showZeroWatchers = $canUnwatchedpages;
+ $db = $this->getDB();
+
+ $lb = new LinkBatch( $this->everything );
+
+ $this->resetQueryParams();
+ $this->addTables( array( 'watchlist' ) );
+ $this->addFields( array( 'wl_title', 'wl_namespace', 'count' => 'COUNT(*)' ) );
+ $this->addWhere( array(
+ $lb->constructSet( 'wl', $db )
+ ) );
+ $this->addOption( 'GROUP BY', array( 'wl_namespace', 'wl_title' ) );
+ if ( !$canUnwatchedpages ) {
+ $this->addOption( 'HAVING', "COUNT(*) >= $wgUnwatchedPageThreshold" );
+ }
+
+ $res = $this->select( __METHOD__ );
+
+ foreach ( $res as $row ) {
+ $this->watchers[$row->wl_namespace][$row->wl_title] = (int)$row->count;
+ }
+ }
+
public function getCacheMode( $params ) {
$publicProps = array(
'protection',
@@ -709,6 +773,7 @@ class ApiQueryInfo extends ApiQueryBase {
'protection',
'talkid',
'watched', # private
+ 'watchers', # private
'notificationtimestamp', # private
'subjectid',
'url',
@@ -734,6 +799,7 @@ class ApiQueryInfo extends ApiQueryBase {
' protection - List the protection level of each page',
' talkid - The page ID of the talk page for each non-talk page',
' watched - List the watched status of each page',
+ ' watchers - The number of watchers, if allowed',
' notificationtimestamp - The watchlist notification timestamp 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',
@@ -762,11 +828,18 @@ class ApiQueryInfo extends ApiQueryBase {
'starttimestamp' => array(
ApiBase::PROP_TYPE => 'timestamp',
ApiBase::PROP_NULLABLE => true
- )
+ ),
+ 'contentmodel' => 'string',
),
'watched' => array(
'watched' => 'boolean'
),
+ 'watchers' => array(
+ 'watchers' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
'notificationtimestamp' => array(
'notificationtimestamp' => array(
ApiBase::PROP_TYPE => 'timestamp',
@@ -809,12 +882,6 @@ class ApiQueryInfo extends ApiQueryBase {
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' ),
- ) );
- }
-
public function getExamples() {
return array(
'api.php?action=query&prop=info&titles=Main%20Page',
@@ -825,8 +892,4 @@ class ApiQueryInfo extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#info_.2F_in';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php
index 3920407b..5bd451b6 100644
--- a/includes/api/ApiQueryLangBacklinks.php
+++ b/includes/api/ApiQueryLangBacklinks.php
@@ -56,10 +56,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 3 );
$db = $this->getDB();
$op = $params['dir'] == 'descending' ? '<' : '>';
@@ -198,7 +195,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
'prop' => array(
'Which properties to get',
' lllang - Adds the language code of the language link',
- ' lltitle - Adds the title of the language ink',
+ ' lltitle - Adds the title of the language link',
),
'limit' => 'How many total pages to return',
'dir' => 'The direction in which to list',
@@ -226,14 +223,14 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
return array( 'Find all pages that link to the given language link.',
'Can be used to find all links with a language code, or',
'all links to a title (with a given language).',
- 'Using neither parameter is effectively "All Language Links"',
+ 'Using neither parameter is effectively "All Language Links".',
+ 'Note that this may not consider language links added by extensions.',
);
}
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(), array(
array( 'missingparam', 'lang' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -244,7 +241,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Langbacklinks';
}
}
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index 3109a090..aa796e31 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -25,7 +25,7 @@
*/
/**
- * A query module to list all langlinks (links to correspanding foreign language pages).
+ * A query module to list all langlinks (links to corresponding foreign language pages).
*
* @ingroup API
*/
@@ -56,10 +56,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
$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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$llfrom = intval( $cont[0] );
$lllang = $this->getDB()->addQuotes( $cont[1] );
@@ -70,18 +67,19 @@ class ApiQueryLangLinks extends ApiQueryBase {
);
}
- $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
- if ( isset( $params['lang'] ) ) {
+ //FIXME: (follow-up) To allow extensions to add to the language links, we need
+ // to load them all, add the extra links, then apply paging.
+ // Should not be terrible, it's not going to be more than a few hundred links.
+
+ // Note that, since (ll_from, ll_lang) is a unique key, we don't need
+ // to sort by ll_title to ensure deterministic ordering.
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ if ( isset( $params['lang'] ) ) {
$this->addWhereFld( 'll_lang', $params['lang'] );
if ( isset( $params['title'] ) ) {
$this->addWhereFld( 'll_title', $params['title'] );
- $this->addOption( 'ORDER BY', 'll_from' . $sort );
- } else {
- $this->addOption( 'ORDER BY', array(
- 'll_title' . $sort,
- 'll_from' . $sort
- ));
}
+ $this->addOption( 'ORDER BY', 'll_from' . $sort );
} else {
// Don't order by ll_from if it's constant in the WHERE clause
if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
@@ -179,7 +177,6 @@ class ApiQueryLangLinks extends ApiQueryBase {
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(), array(
array( 'missingparam', 'lang' ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
) );
}
@@ -192,8 +189,4 @@ class ApiQueryLangLinks extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#langlinks_.2F_ll';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index 9e4b7ebb..937f4f13 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -79,7 +79,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
*/
private function run( $resultPageSet = null ) {
if ( $this->getPageSet()->getGoodTitleCount() == 0 ) {
- return; // nothing to do
+ return; // nothing to do
}
$params = $this->extractRequestParams();
@@ -112,10 +112,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 3 );
$op = $params['dir'] == 'descending' ? '<' : '>';
$plfrom = intval( $cont[0] );
$plns = intval( $cont[1] );
@@ -241,17 +238,13 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$desc = $this->description;
$name = $this->getModuleName();
return array(
- "api.php?action=query&prop={$name}&titles=Main%20Page" => "Get {$desc}s from the [[Main Page]]:",
- "api.php?action=query&generator={$name}&titles=Main%20Page&prop=info" => "Get information about the {$desc} pages in the [[Main Page]]:",
- "api.php?action=query&prop={$name}&titles=Main%20Page&{$this->prefix}namespace=2|10" => "Get {$desc}s from the Main Page in the User and Template namespaces:",
+ "api.php?action=query&prop={$name}&titles=Main%20Page" => "Get {$desc}s from the [[Main Page]]",
+ "api.php?action=query&generator={$name}&titles=Main%20Page&prop=info" => "Get information about the {$desc} pages in the [[Main Page]]",
+ "api.php?action=query&prop={$name}&titles=Main%20Page&{$this->prefix}namespace=2|10" => "Get {$desc}s from the Main Page in the User and Template namespaces",
);
}
public function getHelpUrls() {
return $this->helpUrl;
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 5d85c221..26774ef4 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -49,16 +49,16 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->fld_ids = isset( $prop['ids'] );
$this->fld_title = isset( $prop['title'] );
$this->fld_type = isset( $prop['type'] );
- $this->fld_action = isset ( $prop['action'] );
+ $this->fld_action = isset( $prop['action'] );
$this->fld_user = isset( $prop['user'] );
$this->fld_userid = isset( $prop['userid'] );
$this->fld_timestamp = isset( $prop['timestamp'] );
$this->fld_comment = isset( $prop['comment'] );
- $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
$this->fld_details = isset( $prop['details'] );
$this->fld_tags = isset( $prop['tags'] );
- $hideLogs = LogEventsList::getExcludeClause( $db );
+ $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getUser() );
if ( $hideLogs !== false ) {
$this->addWhere( $hideLogs );
}
@@ -67,10 +67,10 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->addTables( array( 'logging', 'user', 'page' ) );
$this->addOption( 'STRAIGHT_JOIN' );
$this->addJoinConds( array(
- 'user' => array( 'JOIN',
+ 'user' => array( 'LEFT JOIN',
'user_id=log_user' ),
'page' => array( 'LEFT JOIN',
- array( 'log_namespace=page_namespace',
+ array( 'log_namespace=page_namespace',
'log_title=page_title' ) ) ) );
$index = array( 'logging' => 'times' ); // default, may change
@@ -82,8 +82,8 @@ class ApiQueryLogEvents extends ApiQueryBase {
) );
$this->addFieldsIf( array( 'log_id', 'page_id' ), $this->fld_ids );
- $this->addFieldsIf( array( 'log_user', 'user_name' ), $this->fld_user );
- $this->addFieldsIf( 'user_id', $this->fld_userid );
+ $this->addFieldsIf( array( 'log_user', 'log_user_text', 'user_name' ), $this->fld_user );
+ $this->addFieldsIf( 'log_user', $this->fld_userid );
$this->addFieldsIf( array( 'log_namespace', 'log_title' ), $this->fld_title || $this->fld_parsedcomment );
$this->addFieldsIf( 'log_comment', $this->fld_comment || $this->fld_parsedcomment );
$this->addFieldsIf( 'log_params', $this->fld_details );
@@ -98,8 +98,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
$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';
+ $index['change_tag'] = 'change_tag_tag_id';
}
if ( !is_null( $params['action'] ) ) {
@@ -151,7 +150,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( is_null( $title ) ) {
$this->dieUsage( "Bad title value '$prefix'", 'param_prefix' );
}
- $this->addWhereFld( 'log_namespace', $title->getNamespace() );
+ $this->addWhereFld( 'log_namespace', $title->getNamespace() );
$this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) );
}
@@ -201,7 +200,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
public static function addLogParams( $result, &$vals, $params, $type, $action, $ts, $legacy = false ) {
switch ( $type ) {
case 'move':
- if ( $legacy ){
+ if ( $legacy ) {
$targetKey = 0;
$noredirKey = 1;
} else {
@@ -209,21 +208,21 @@ class ApiQueryLogEvents extends ApiQueryBase {
$noredirKey = '5::noredir';
}
- if ( isset( $params[ $targetKey ] ) ) {
- $title = Title::newFromText( $params[ $targetKey ] );
+ if ( isset( $params[$targetKey] ) ) {
+ $title = Title::newFromText( $params[$targetKey] );
if ( $title ) {
$vals2 = array();
ApiQueryBase::addTitleInfo( $vals2, $title, 'new_' );
$vals[$type] = $vals2;
}
}
- if ( isset( $params[ $noredirKey ] ) && $params[ $noredirKey ] ) {
+ if ( isset( $params[$noredirKey] ) && $params[$noredirKey] ) {
$vals[$type]['suppressedredirect'] = '';
}
$params = null;
break;
case 'patrol':
- if ( $legacy ){
+ if ( $legacy ) {
$cur = 0;
$prev = 1;
$auto = 2;
@@ -241,7 +240,12 @@ class ApiQueryLogEvents extends ApiQueryBase {
break;
case 'rights':
$vals2 = array();
- list( $vals2['old'], $vals2['new'] ) = $params;
+ if ( $legacy ) {
+ list( $vals2['old'], $vals2['new'] ) = $params;
+ } else {
+ $vals2['new'] = implode( ', ', $params['5::newgroups'] );
+ $vals2['old'] = implode( ', ', $params['4::oldgroups'] );
+ }
$vals[$type] = $vals2;
$params = null;
break;
@@ -260,11 +264,26 @@ class ApiQueryLogEvents extends ApiQueryBase {
$vals[$type] = $vals2;
$params = null;
break;
+ case 'upload':
+ if ( isset( $params['img_timestamp'] ) ) {
+ $params['img_timestamp'] = wfTimestamp( TS_ISO_8601, $params['img_timestamp'] );
+ }
+ break;
}
if ( !is_null( $params ) ) {
- $result->setIndexedTagName( $params, 'param' );
- $result->setIndexedTagName_recursive( $params, 'param' );
- $vals = array_merge( $vals, $params );
+ $logParams = array();
+ // Keys like "4::paramname" can't be used for output so we change them to "paramname"
+ foreach ( $params as $key => $value ) {
+ if ( strpos( $key, ':' ) === false ) {
+ $logParams[$key] = $value;
+ continue;
+ }
+ $logParam = explode( ':', $key, 3 );
+ $logParams[$logParam[2]] = $value;
+ }
+ $result->setIndexedTagName( $logParams, 'param' );
+ $result->setIndexedTagName_recursive( $logParams, 'param' );
+ $vals = array_merge( $vals, $logParams );
}
return $vals;
}
@@ -275,18 +294,22 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( $this->fld_ids ) {
$vals['logid'] = intval( $row->log_id );
- $vals['pageid'] = intval( $row->page_id );
}
if ( $this->fld_title || $this->fld_parsedcomment ) {
$title = Title::makeTitle( $row->log_namespace, $row->log_title );
}
- if ( $this->fld_title ) {
+ if ( $this->fld_title || $this->fld_ids ) {
if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
$vals['actionhidden'] = '';
} else {
- ApiQueryBase::addTitleInfo( $vals, $title );
+ if ( $this->fld_title ) {
+ ApiQueryBase::addTitleInfo( $vals, $title );
+ }
+ if ( $this->fld_ids ) {
+ $vals['pageid'] = intval( $row->page_id );
+ }
}
}
@@ -316,10 +339,10 @@ class ApiQueryLogEvents extends ApiQueryBase {
$vals['userhidden'] = '';
} else {
if ( $this->fld_user ) {
- $vals['user'] = $row->user_name;
+ $vals['user'] = $row->user_name === null ? $row->log_user_text : $row->user_name;
}
if ( $this->fld_userid ) {
- $vals['userid'] = $row->user_id;
+ $vals['userid'] = $row->log_user;
}
if ( !$row->log_user ) {
@@ -362,8 +385,12 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
// formatComment() calls wfMessage() among other things
return 'anon-public-user-private';
- } else {
+ } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getUser() )
+ === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
+ ) { // Output can only contain public data.
return 'public';
+ } else {
+ return 'anon-public-user-private';
}
}
@@ -432,7 +459,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
' timestamp - Adds the timestamp for the event',
' comment - Adds the comment of the event',
' parsedcomment - Adds the parsed comment of the event',
- ' details - Lists addtional details about the event',
+ ' details - Lists additional details about the event',
' tags - Lists tags for the event',
),
'type' => 'Filter log entries to only this type',
@@ -526,8 +553,4 @@ class ApiQueryLogEvents extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Logevents';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryORM.php b/includes/api/ApiQueryORM.php
new file mode 100644
index 00000000..a23ff06b
--- /dev/null
+++ b/includes/api/ApiQueryORM.php
@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * Base query module for querying results from ORMTables.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup API
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class ApiQueryORM extends ApiQueryBase {
+
+ /**
+ * Returns an instance of the IORMTable table being queried.
+ *
+ * @since 1.21
+ *
+ * @return IORMTable
+ */
+ abstract protected function getTable();
+
+ /**
+ * Returns the name of the individual rows.
+ * For example: page, user, contest, campaign, etc.
+ * This is used to appropriately name elements in XML.
+ * Deriving classes typically override this method.
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ protected function getRowName() {
+ return 'item';
+ }
+
+ /**
+ * Returns the name of the list of rows.
+ * For example: pages, users, contests, campaigns, etc.
+ * This is used to appropriately name nodes in the output.
+ * Deriving classes typically override this method.
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ protected function getListName() {
+ return 'items';
+ }
+
+ /**
+ * Returns the path to where the items results should be added in the result.
+ *
+ * @since 1.21
+ *
+ * @return null|string|array
+ */
+ protected function getResultPath() {
+ return null;
+ }
+
+ /**
+ * Get the parameters, find out what the conditions for the query are,
+ * run it, and add the results.
+ *
+ * @since 1.21
+ */
+ public function execute() {
+ $params = $this->getParams();
+
+ if ( !in_array( 'id', $params['props'] ) ) {
+ $params['props'][] = 'id';
+ }
+
+ $results = $this->getResults( $params, $this->getConditions( $params ) );
+ $this->addResults( $params, $results );
+ }
+
+ /**
+ * Get the request parameters and remove all params set
+ * to null (ie those that are not actually provided).
+ *
+ * @since 1.21
+ *
+ * @return array
+ */
+ protected function getParams() {
+ return array_filter(
+ $this->extractRequestParams(),
+ function( $prop ) {
+ return isset( $prop );
+ }
+ );
+ }
+
+ /**
+ * Get the conditions for the query. These will be provided as
+ * regular parameters, together with limit, props, continue,
+ * and possibly others which we need to get rid off.
+ *
+ * @since 1.21
+ *
+ * @param array $params
+ *
+ * @return array
+ */
+ protected function getConditions( array $params ) {
+ $conditions = array();
+ $fields = $this->getTable()->getFields();
+
+ foreach ( $params as $name => $value ) {
+ if ( array_key_exists( $name, $fields ) ) {
+ $conditions[$name] = $value;
+ }
+ }
+
+ return $conditions;
+ }
+
+ /**
+ * Get the actual results.
+ *
+ * @since 1.21
+ *
+ * @param array $params
+ * @param array $conditions
+ *
+ * @return ORMResult
+ */
+ protected function getResults( array $params, array $conditions ) {
+ return $this->getTable()->select(
+ $params['props'],
+ $conditions,
+ array(
+ 'LIMIT' => $params['limit'] + 1,
+ 'ORDER BY' => $this->getTable()->getPrefixedField( 'id' ) . ' ASC',
+ ),
+ __METHOD__
+ );
+ }
+
+ /**
+ * Serialize the results and add them to the result object.
+ *
+ * @since 1.21
+ *
+ * @param array $params
+ * @param ORMResult $results
+ */
+ protected function addResults( array $params, ORMResult $results ) {
+ $serializedResults = array();
+ $count = 0;
+
+ foreach ( $results as /* IORMRow */ $result ) {
+ 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', $result->getId() );
+ break;
+ }
+
+ $serializedResults[] = $this->formatRow( $result, $params );
+ }
+
+ $this->setIndexedTagNames( $serializedResults );
+ $this->addSerializedResults( $serializedResults );
+ }
+
+ /**
+ * Formats a row to it's desired output format.
+ *
+ * @since 1.21
+ *
+ * @param IORMRow $result
+ * @param array $params
+ *
+ * @return mixed
+ */
+ protected function formatRow( IORMRow $result, array $params ) {
+ return $result->toArray( $params['props'] );
+ }
+
+ /**
+ * Set the tag names for formats such as XML.
+ *
+ * @since 1.21
+ *
+ * @param array $serializedResults
+ */
+ protected function setIndexedTagNames( array &$serializedResults ) {
+ $this->getResult()->setIndexedTagName( $serializedResults, $this->getRowName() );
+ }
+
+ /**
+ * Add the serialized results to the result object.
+ *
+ * @since 1.21
+ *
+ * @param array $serializedResults
+ */
+ protected function addSerializedResults( array $serializedResults ) {
+ $this->getResult()->addValue(
+ $this->getResultPath(),
+ $this->getListName(),
+ $serializedResults
+ );
+ }
+
+ /**
+ * @see ApiBase::getAllowedParams()
+ * @return array
+ */
+ public function getAllowedParams() {
+ $params = array(
+ 'props' => array(
+ ApiBase::PARAM_TYPE => $this->getTable()->getFieldNames(),
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'limit' => array(
+ ApiBase::PARAM_DFLT => 20,
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
+ ),
+ 'continue' => null,
+ );
+
+ return array_merge( $this->getTable()->getAPIParams(), $params );
+ }
+
+ /**
+ * @see ApiBase::getParamDescription()
+ * @return array
+ */
+ public function getParamDescription() {
+ $descriptions = array(
+ 'props' => 'Fields to query',
+ 'continue' => 'Offset number from where to continue the query',
+ 'limit' => 'Max amount of rows to return',
+ );
+
+ return array_merge( $this->getTable()->getFieldDescriptions(), $descriptions );
+ }
+
+}
diff --git a/includes/api/ApiQueryPagePropNames.php b/includes/api/ApiQueryPagePropNames.php
new file mode 100644
index 00000000..08c883d8
--- /dev/null
+++ b/includes/api/ApiQueryPagePropNames.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Created on January 21, 2013
+ *
+ * Copyright © 2013 Brad Jorsch <bjorsch@wikimedia.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
+ * @since 1.21
+ * @author Brad Jorsch
+ */
+
+/**
+ * A query module to list used page props
+ *
+ * @ingroup API
+ * @since 1.21
+ */
+class ApiQueryPagePropNames extends ApiQueryBase {
+
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'ppn' );
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $this->addTables( 'page_props' );
+ $this->addFields( 'pp_propname' );
+ $this->addOption( 'DISTINCT' );
+ $this->addOption( 'ORDER BY', 'pp_propname' );
+
+ if ( $params['continue'] ) {
+ $cont = explode( '|', $params['continue'] );
+ $this->dieContinueUsageIf( count( $cont ) != 1 );
+
+ // Add a WHERE clause
+ $this->addWhereRange( 'pp_propname', 'newer', $cont[0], null );
+ }
+
+ $limit = $params['limit'];
+ $this->addOption( 'LIMIT', $limit + 1 );
+
+ $result = $this->getResult();
+ $count = 0;
+ foreach ( $this->select( __METHOD__ ) as $row ) {
+ if ( ++$count > $limit ) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ $this->setContinueEnumParameter( 'continue', $row->pp_propname );
+ break;
+ }
+
+ $vals = array();
+ $vals['propname'] = $row->pp_propname;
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'continue', $row->pp_propname );
+ break;
+ }
+ }
+
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'p' );
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'continue' => null,
+ 'limit' => array(
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_DFLT => 10,
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'continue' => 'When more results are available, use this to continue',
+ 'limit' => 'The maximum number of pages to return',
+ );
+ }
+
+ public function getDescription() {
+ return 'List all page prop names in use on the wiki';
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=query&list=pagepropnames' => 'Get first 10 prop names',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Pagepropnames';
+ }
+}
diff --git a/includes/api/ApiQueryPageProps.php b/includes/api/ApiQueryPageProps.php
index 1eef67e6..2de57106 100644
--- a/includes/api/ApiQueryPageProps.php
+++ b/includes/api/ApiQueryPageProps.php
@@ -49,7 +49,7 @@ class ApiQueryPageProps extends ApiQueryBase {
$this->addTables( 'page_props' );
$this->addFields( array( 'pp_page', 'pp_propname', 'pp_value' ) );
- $this->addWhereFld( 'pp_page', array_keys( $pages ) );
+ $this->addWhereFld( 'pp_page', array_keys( $pages ) );
if ( $this->params['continue'] ) {
$this->addWhere( 'pp_page >=' . intval( $this->params['continue'] ) );
@@ -60,7 +60,10 @@ class ApiQueryPageProps extends ApiQueryBase {
}
# Force a sort order to ensure that properties are grouped by page
- $this->addOption( 'ORDER BY', 'pp_page' );
+ # But only if pp_page is not constant in the WHERE clause.
+ if ( count( $pages ) > 1 ) {
+ $this->addOption( 'ORDER BY', 'pp_page' );
+ }
$res = $this->select( __METHOD__ );
$currentPage = 0; # Id of the page currently processed
@@ -122,14 +125,16 @@ class ApiQueryPageProps extends ApiQueryBase {
public function getAllowedParams() {
return array(
'continue' => null,
- 'prop' => null,
+ 'prop' => array(
+ ApiBase::PARAM_ISMULTI => true,
+ ),
);
}
public function getParamDescription() {
return array(
'continue' => 'When more results are available, use this to continue',
- 'prop' => 'Page prop to look on the page for. Useful for checking whether a certain page uses a certain page prop.'
+ 'prop' => 'Only list these props. Useful for checking whether a certain page uses a certain page prop',
);
}
@@ -146,8 +151,4 @@ class ApiQueryPageProps extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#pageprops_.2F_pp';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryPagesWithProp.php b/includes/api/ApiQueryPagesWithProp.php
new file mode 100644
index 00000000..6f2f02e4
--- /dev/null
+++ b/includes/api/ApiQueryPagesWithProp.php
@@ -0,0 +1,189 @@
+<?php
+/**
+ * Created on December 31, 2012
+ *
+ * Copyright © 2012 Brad Jorsch <bjorsch@wikimedia.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
+ * @since 1.21
+ * @author Brad Jorsch
+ */
+
+/**
+ * A query module to enumerate pages that use a particular prop
+ *
+ * @ingroup API
+ * @since 1.21
+ */
+class ApiQueryPagesWithProp extends ApiQueryGeneratorBase {
+
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'pwp' );
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
+ }
+
+ /**
+ * @param $resultPageSet ApiPageSet
+ * @return void
+ */
+ private function run( $resultPageSet = null ) {
+ $params = $this->extractRequestParams();
+
+ $prop = array_flip( $params['prop'] );
+ $fld_ids = isset( $prop['ids'] );
+ $fld_title = isset( $prop['title'] );
+ $fld_value = isset( $prop['value'] );
+
+ if ( $resultPageSet === null ) {
+ $this->addFields( array( 'page_id' ) );
+ $this->addFieldsIf( array( 'page_title', 'page_namespace' ), $fld_title );
+ $this->addFieldsIf( 'pp_value', $fld_value );
+ } else {
+ $this->addFields( $resultPageSet->getPageTableFields() );
+ }
+ $this->addTables( array( 'page_props', 'page' ) );
+ $this->addWhere( 'pp_page=page_id' );
+ $this->addWhereFld( 'pp_propname', $params['propname'] );
+
+ $dir = ( $params['dir'] == 'ascending' ) ? 'newer' : 'older';
+
+ if ( $params['continue'] ) {
+ $cont = explode( '|', $params['continue'] );
+ $this->dieContinueUsageIf( count( $cont ) != 1 );
+
+ // Add a WHERE clause
+ $from = (int)$cont[0];
+ $this->addWhereRange( 'pp_page', $dir, $from, null );
+ }
+
+ $sort = ( $params['dir'] === 'descending' ? ' DESC' : '' );
+ $this->addOption( 'ORDER BY', 'pp_page' . $sort );
+
+ $limit = $params['limit'];
+ $this->addOption( 'LIMIT', $limit + 1 );
+
+ $result = $this->getResult();
+ $count = 0;
+ foreach ( $this->select( __METHOD__ ) as $row ) {
+ if ( ++$count > $limit ) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ $this->setContinueEnumParameter( 'continue', $row->page_id );
+ break;
+ }
+
+ if ( $resultPageSet === null ) {
+ $vals = array();
+ if ( $fld_ids ) {
+ $vals['pageid'] = (int)$row->page_id;
+ }
+ if ( $fld_title ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ ApiQueryBase::addTitleInfo( $vals, $title );
+ }
+ if ( $fld_value ) {
+ $vals['value'] = $row->pp_value;
+ }
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'continue', $row->page_id );
+ break;
+ }
+ } else {
+ $resultPageSet->processDbRow( $row );
+ }
+ }
+
+ if ( $resultPageSet === null ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
+ }
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'propname' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'prop' => array(
+ ApiBase::PARAM_DFLT => 'ids|title',
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TYPE => array(
+ 'ids',
+ 'title',
+ 'value',
+ )
+ ),
+ 'continue' => null,
+ 'limit' => array(
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_DFLT => 10,
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
+ ),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending',
+ )
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'propname' => 'Page prop for which to enumerate pages',
+ 'prop' => array(
+ 'What pieces of information to include',
+ ' ids - Adds the page ID',
+ ' title - Adds the title and namespace ID of the page',
+ ' value - Adds the value of the page prop',
+ ),
+ 'dir' => 'In which direction to sort',
+ 'continue' => 'When more results are available, use this to continue',
+ 'limit' => 'The maximum number of pages to return',
+ );
+ }
+
+ public function getDescription() {
+ return 'List all pages using a given page prop';
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=query&list=pageswithprop&pwppropname=displaytitle&pwpprop=ids|title|value' => 'Get first 10 pages using {{DISPLAYTITLE:}}',
+ 'api.php?action=query&generator=pageswithprop&gpwppropname=notoc&prop=info' => 'Get page info about first 10 pages using __NOTOC__',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Pageswithprop';
+ }
+}
diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php
index 14aed28d..222ad074 100644
--- a/includes/api/ApiQueryProtectedTitles.php
+++ b/includes/api/ApiQueryProtectedTitles.php
@@ -98,7 +98,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
$vals['user'] = $row->user_name;
}
- if ( isset( $prop['user'] ) ) {
+ if ( isset( $prop['userid'] ) || /*B/C*/isset( $prop['user'] ) ) {
$vals['userid'] = $row->pt_user;
}
@@ -157,7 +157,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => array_diff( $wgRestrictionLevels, array( '' ) )
),
- 'limit' => array (
+ 'limit' => array(
ApiBase::PARAM_DFLT => 10,
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MIN => 1,
@@ -231,6 +231,9 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
),
'userid' => 'integer'
),
+ 'userid' => array(
+ 'userid' => 'integer'
+ ),
'comment' => array(
'comment' => 'string'
),
@@ -261,8 +264,4 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Protectedtitles';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php
index a8be26d3..79fe0498 100644
--- a/includes/api/ApiQueryQueryPage.php
+++ b/includes/api/ApiQueryQueryPage.php
@@ -32,27 +32,18 @@
class ApiQueryQueryPage extends ApiQueryGeneratorBase {
private $qpMap;
- /**
- * Some query pages are useless because they're available elsewhere in the API
- */
- private $uselessQueryPages = array(
- 'MIMEsearch', // aiprop=mime
- 'LinkSearch', // list=exturlusage
- 'FileDuplicateSearch', // prop=duplicatefiles
- );
-
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'qp' );
// We need to do this to make sure $wgQueryPages is set up
// This SUCKS
global $IP;
- require_once( "$IP/includes/QueryPage.php" );
+ require_once "$IP/includes/QueryPage.php";
// Build mapping from special page names to QueryPage classes
- global $wgQueryPages;
+ global $wgQueryPages, $wgAPIUselessQueryPages;
$this->qpMap = array();
foreach ( $wgQueryPages as $page ) {
- if( !in_array( $page[1], $this->uselessQueryPages ) ) {
+ if ( !in_array( $page[1], $wgAPIUselessQueryPages ) ) {
$this->qpMap[$page[1]] = $page[0];
}
}
@@ -75,6 +66,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
$result = $this->getResult();
+ /** @var $qp QueryPage */
$qp = new $this->qpMap[$params['page']]();
if ( !$qp->userCanExecute( $this->getUser() ) ) {
$this->dieUsageMsg( 'specialpage-cantexecute' );
@@ -141,6 +133,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
}
public function getCacheMode( $params ) {
+ /** @var $qp QueryPage */
$qp = new $this->qpMap[$params['page']]();
if ( $qp->getRestriction() != '' ) {
return 'private';
@@ -211,7 +204,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(), array(
- array( 'specialpage-cantexecute' )
+ array( 'specialpage-cantexecute' )
) );
}
@@ -221,7 +214,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Querypage';
}
}
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
index ddf5841b..2754bdae 100644
--- a/includes/api/ApiQueryRandom.php
+++ b/includes/api/ApiQueryRandom.php
@@ -33,6 +33,8 @@
class ApiQueryRandom extends ApiQueryGeneratorBase {
+ private $pageIDs;
+
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'rn' );
}
@@ -184,7 +186,7 @@ class ApiQueryRandom extends ApiQueryGeneratorBase {
return 'api.php?action=query&list=random&rnnamespace=0&rnlimit=2';
}
- public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRandom.php overlordq$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Random';
}
}
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 7ae4f371..6b10bdc6 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -39,7 +39,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
$fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false,
$fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false,
- $fld_tags = false, $token = array();
+ $fld_tags = false, $fld_sha1 = false, $token = array();
private $tokenFunctions;
@@ -105,7 +105,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/**
* Sets internal state to include the desired properties in the output.
- * @param $prop Array associative array of properties, only keys are used here
+ * @param array $prop associative array of properties, only keys are used here
*/
public function initProperties( $prop ) {
$this->fld_comment = isset( $prop['comment'] );
@@ -121,6 +121,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->fld_patrolled = isset( $prop['patrolled'] );
$this->fld_loginfo = isset( $prop['loginfo'] );
$this->fld_tags = isset( $prop['tags'] );
+ $this->fld_sha1 = isset( $prop['sha1'] );
}
public function execute() {
@@ -149,6 +150,31 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->addTables( 'recentchanges' );
$index = array( 'recentchanges' => 'rc_timestamp' ); // May change
$this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] );
+
+ 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' );
+ }
+
+ $timestamp = $this->getDB()->addQuotes( wfTimestamp( TS_MW, $cont[0] ) );
+ $id = intval( $cont[1] );
+ $op = $params['dir'] === 'older' ? '<' : '>';
+
+ $this->addWhere(
+ "rc_timestamp $op $timestamp OR " .
+ "(rc_timestamp = $timestamp AND " .
+ "rc_id $op= $id)"
+ );
+ }
+
+ $order = $params['dir'] === 'older' ? 'DESC' : 'ASC';
+ $this->addOption( 'ORDER BY', array(
+ "rc_timestamp $order",
+ "rc_id $order",
+ ) );
+
$this->addWhereFld( 'rc_namespace', $params['namespace'] );
$this->addWhereFld( 'rc_deleted', 0 );
@@ -214,8 +240,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
'rc_title',
'rc_cur_id',
'rc_type',
- 'rc_moved_to_ns',
- 'rc_moved_to_title',
'rc_deleted'
) );
@@ -231,12 +255,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' );
}
+ $this->addFields( 'rc_id' );
/* Add fields to our query if they are specified as a needed parameter. */
- $this->addFieldsIf( array( 'rc_id', 'rc_this_oldid', 'rc_last_oldid' ), $this->fld_ids );
+ $this->addFieldsIf( array( 'rc_this_oldid', '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->fld_userid );
- $this->addFieldsIf( array( 'rc_minor', 'rc_type', 'rc_bot' ) , $this->fld_flags );
+ $this->addFieldsIf( array( 'rc_minor', 'rc_type', 'rc_bot' ), $this->fld_flags );
$this->addFieldsIf( array( 'rc_old_len', 'rc_new_len' ), $this->fld_sizes );
$this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
$this->addFieldsIf( array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ), $this->fld_loginfo );
@@ -249,6 +274,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->addFields( 'ts_tags' );
}
+ if ( $this->fld_sha1 ) {
+ $this->addTables( 'revision' );
+ $this->addJoinConds( array( 'revision' => array( 'LEFT JOIN', array( 'rc_this_oldid=rev_id' ) ) ) );
+ $this->addFields( array( 'rev_sha1', 'rev_deleted' ) );
+ }
+
if ( $params['toponly'] || $showRedirects ) {
$this->addTables( 'page' );
$this->addJoinConds( array( 'page' => array( 'LEFT JOIN', array( 'rc_namespace=page_namespace', 'rc_title=page_title' ) ) ) );
@@ -262,9 +293,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
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->addWhereFld( 'ct_tag', $params['tag'] );
+ $index['change_tag'] = 'change_tag_tag_id';
}
$this->token = $params['token'];
@@ -283,7 +313,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
foreach ( $res as $row ) {
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( 'continue', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) . '|' . $row->rc_id );
break;
}
@@ -297,7 +327,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
}
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
+ $this->setContinueEnumParameter( 'continue', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) . '|' . $row->rc_id );
break;
}
} else {
@@ -316,17 +346,11 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/**
* Extracts from a single sql row the data needed to describe one recent change.
*
- * @param $row The row from which to extract the data.
+ * @param mixed $row The row from which to extract the data.
* @return array An array mapping strings (descriptors) to their respective string values.
* @access public
*/
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 );
- }
-
/* Determine the title of the page that has been changed. */
$title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
@@ -349,6 +373,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
case RC_LOG:
$vals['type'] = 'log';
break;
+ case RC_EXTERNAL:
+ $vals['type'] = 'external';
+ break;
case RC_MOVE_OVER_REDIRECT:
$vals['type'] = 'move over redirect';
break;
@@ -359,9 +386,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/* 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_' );
- }
}
/* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */
@@ -457,6 +481,19 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
}
}
+ if ( $this->fld_sha1 && $row->rev_sha1 !== null ) {
+ // The RevDel check should currently never pass due to the
+ // rc_deleted = 0 condition in the WHERE clause, but in case that
+ // ever changes we check it here too.
+ if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
+ $vals['sha1hidden'] = '';
+ } elseif ( $row->rev_sha1 !== '' ) {
+ $vals['sha1'] = wfBaseConvert( $row->rev_sha1, 36, 16, 40 );
+ } else {
+ $vals['sha1'] = '';
+ }
+ }
+
if ( !is_null( $this->token ) ) {
$tokenFunctions = $this->getTokenFunctions();
foreach ( $this->token as $t ) {
@@ -481,13 +518,15 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
}
return $retval;
}
- switch( $type ) {
+ switch ( $type ) {
case 'edit':
return RC_EDIT;
case 'new':
return RC_NEW;
case 'log':
return RC_LOG;
+ case 'external':
+ return RC_EXTERNAL;
}
}
@@ -551,7 +590,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
'redirect',
'patrolled',
'loginfo',
- 'tags'
+ 'tags',
+ 'sha1',
)
),
'token' => array(
@@ -584,11 +624,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => array(
'edit',
+ 'external',
'new',
'log'
)
),
'toponly' => false,
+ 'continue' => null,
);
}
@@ -616,6 +658,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
' patrolled - Tags edits that have been patrolled',
' loginfo - Adds log information (logid, logtype, etc) to log entries',
' tags - Lists tags for the entry',
+ ' sha1 - Adds the content checksum for entries associated with a revision',
),
'token' => 'Which tokens to obtain for each change',
'show' => array(
@@ -626,6 +669,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
'limit' => 'How many total changes to return',
'tag' => 'Only list changes tagged with this tag',
'toponly' => 'Only list changes which are the latest revision',
+ 'continue' => 'When more results are available, use this to continue',
);
}
@@ -712,7 +756,17 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
ApiBase::PROP_TYPE => 'string',
ApiBase::PROP_NULLABLE => true
)
- )
+ ),
+ 'sha1' => array(
+ 'sha1' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'sha1hidden' => array(
+ ApiBase::PROP_TYPE => 'boolean',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ ),
);
self::addTokenProperties( $props, $this->getTokenFunctions() );
@@ -741,8 +795,4 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Recentchanges';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index b89a8ea9..415288ef 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -34,15 +34,15 @@
class ApiQueryRevisions extends ApiQueryBase {
private $diffto, $difftotext, $expandTemplates, $generateXML, $section,
- $token, $parseContent;
+ $token, $parseContent, $contentFormat;
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'rv' );
}
- private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
+ private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false,
$fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
- $fld_content = false, $fld_tags = false;
+ $fld_content = false, $fld_tags = false, $fld_contentmodel = false;
private $tokenFunctions;
@@ -95,7 +95,6 @@ class ApiQueryRevisions extends ApiQueryBase {
!is_null( $params['endid'] ) || $params['dir'] === 'newer' ||
!is_null( $params['start'] ) || !is_null( $params['end'] ) );
-
$pageSet = $this->getPageSet();
$pageCount = $pageSet->getGoodTitleCount();
$revCount = $pageSet->getRevisionCount();
@@ -147,23 +146,28 @@ class ApiQueryRevisions extends ApiQueryBase {
$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_parsedcomment = isset ( $prop['parsedcomment'] );
- $this->fld_size = isset ( $prop['size'] );
- $this->fld_sha1 = isset ( $prop['sha1'] );
+ $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_sha1 = isset( $prop['sha1'] );
+ $this->fld_contentmodel = isset( $prop['contentmodel'] );
$this->fld_userid = isset( $prop['userid'] );
- $this->fld_user = isset ( $prop['user'] );
+ $this->fld_user = isset( $prop['user'] );
$this->token = $params['token'];
+ if ( !empty( $params['contentformat'] ) ) {
+ $this->contentFormat = $params['contentformat'];
+ }
+
// Possible indexes used
$index = array();
$userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
- $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
+ $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
$limit = $params['limit'];
if ( $limit == 'max' ) {
$limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
@@ -184,15 +188,16 @@ class ApiQueryRevisions extends ApiQueryBase {
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';
+ $this->addWhereFld( 'ct_tag', $params['tag'] );
+ $index['change_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
+ $user = $this->getUser();
+ /** @var $title Title */
foreach ( $pageSet->getGoodTitles() as $title ) {
- if ( !$title->userCan( 'read' ) ) {
+ if ( !$title->userCan( 'read', $user ) ) {
$this->dieUsage(
'The current user is not allowed to read ' . $title->getPrefixedText(),
'accessdenied' );
@@ -255,7 +260,7 @@ class ApiQueryRevisions extends ApiQueryBase {
// rvstart and rvstartid when that is supplied.
if ( !is_null( $params['continue'] ) ) {
$params['startid'] = $params['continue'];
- unset( $params['start'] );
+ $params['start'] = null;
}
// This code makes an assumption that sorting by rev_id and rev_timestamp produces
@@ -332,10 +337,7 @@ class ApiQueryRevisions extends ApiQueryBase {
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' );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$pageid = intval( $cont[0] );
$revid = intval( $cont[1] );
$this->addWhere(
@@ -433,12 +435,18 @@ class ApiQueryRevisions extends ApiQueryBase {
}
}
- if ( $this->fld_sha1 ) {
+ if ( $this->fld_sha1 && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
if ( $revision->getSha1() != '' ) {
$vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 );
} else {
$vals['sha1'] = '';
}
+ } elseif ( $this->fld_sha1 ) {
+ $vals['sha1hidden'] = '';
+ }
+
+ if ( $this->fld_contentmodel ) {
+ $vals['contentmodel'] = $revision->getContentModel();
}
if ( $this->fld_comment || $this->fld_parsedcomment ) {
@@ -479,55 +487,121 @@ class ApiQueryRevisions extends ApiQueryBase {
}
}
- $text = null;
+ $content = null;
global $wgParser;
- if ( $this->fld_content || !is_null( $this->difftotext ) ) {
- $text = $revision->getText();
+ if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+ $content = $revision->getContent();
// 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 ) {
+ if ( $content && $this->section !== false ) {
+ $content = $content->getSection( $this->section, false );
+ if ( !$content ) {
$this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' );
}
}
}
- if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
+ if ( $this->fld_content && $content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
+ $text = null;
+
if ( $this->generateXML ) {
- $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
- $dom = $wgParser->preprocessToDom( $text );
- if ( is_callable( array( $dom, 'saveXML' ) ) ) {
- $xml = $dom->saveXML();
+ if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+ $t = $content->getNativeData(); # note: don't set $text
+
+ $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
+ $dom = $wgParser->preprocessToDom( $t );
+ if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+ $xml = $dom->saveXML();
+ } else {
+ $xml = $dom->__toString();
+ }
+ $vals['parsetree'] = $xml;
} else {
- $xml = $dom->__toString();
+ $this->setWarning( "Conversion to XML is supported for wikitext only, " .
+ $title->getPrefixedDBkey() .
+ " uses content model " . $content->getModel() );
}
- $vals['parsetree'] = $xml;
-
}
+
if ( $this->expandTemplates && !$this->parseContent ) {
- $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
+ #XXX: implement template expansion for all content types in ContentHandler?
+ if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+ $text = $content->getNativeData();
+
+ $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
+ } else {
+ $this->setWarning( "Template expansion is supported for wikitext only, " .
+ $title->getPrefixedDBkey() .
+ " uses content model " . $content->getModel() );
+
+ $text = false;
+ }
}
if ( $this->parseContent ) {
- $text = $wgParser->parse( $text, $title, ParserOptions::newFromContext( $this->getContext() ) )->getText();
+ $po = $content->getParserOutput( $title, $revision->getId(), ParserOptions::newFromContext( $this->getContext() ) );
+ $text = $po->getText();
+ }
+
+ if ( $text === null ) {
+ $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat();
+ $model = $content->getModel();
+
+ if ( !$content->isSupportedFormat( $format ) ) {
+ $name = $title->getPrefixedDBkey();
+
+ $this->dieUsage( "The requested format {$this->contentFormat} is not supported " .
+ "for content model $model used by $name", 'badformat' );
+ }
+
+ $text = $content->serialize( $format );
+
+ // always include format and model.
+ // Format is needed to deserialize, model is needed to interpret.
+ $vals['contentformat'] = $format;
+ $vals['contentmodel'] = $model;
+ }
+
+ if ( $text !== false ) {
+ ApiResult::setContent( $vals, $text );
}
- ApiResult::setContent( $vals, $text );
} elseif ( $this->fld_content ) {
- $vals['texthidden'] = '';
+ if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
+ $vals['texthidden'] = '';
+ } else {
+ $vals['textmissing'] = '';
+ }
}
if ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
global $wgAPIMaxUncachedDiffs;
static $n = 0; // Number of uncached diffs we've had
- if ( $n < $wgAPIMaxUncachedDiffs ) {
+
+ if ( is_null( $content ) ) {
+ $vals['textmissing'] = '';
+ } elseif ( $n < $wgAPIMaxUncachedDiffs ) {
$vals['diff'] = array();
$context = new DerivativeContext( $this->getContext() );
$context->setTitle( $title );
+ $handler = $revision->getContentHandler();
+
if ( !is_null( $this->difftotext ) ) {
- $engine = new DifferenceEngine( $context );
- $engine->setText( $text, $this->difftotext );
+ $model = $title->getContentModel();
+
+ if ( $this->contentFormat
+ && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) {
+
+ $name = $title->getPrefixedDBkey();
+
+ $this->dieUsage( "The requested format {$this->contentFormat} is not supported for " .
+ "content model $model used by $name", 'badformat' );
+ }
+
+ $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat );
+
+ $engine = $handler->createDifferenceEngine( $context );
+ $engine->setContent( $content, $difftocontent );
} else {
- $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
+ $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
$vals['diff']['from'] = $engine->getOldid();
$vals['diff']['to'] = $engine->getNewid();
}
@@ -567,6 +641,7 @@ class ApiQueryRevisions extends ApiQueryBase {
'userid',
'size',
'sha1',
+ 'contentmodel',
'comment',
'parsedcomment',
'content',
@@ -616,6 +691,10 @@ class ApiQueryRevisions extends ApiQueryBase {
'continue' => null,
'diffto' => null,
'difftotext' => null,
+ 'contentformat' => array(
+ ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+ ApiBase::PARAM_DFLT => null
+ ),
);
}
@@ -631,6 +710,7 @@ class ApiQueryRevisions extends ApiQueryBase {
' userid - User id of revision creator',
' size - Length (bytes) of the revision',
' sha1 - SHA-1 (base 16) of the revision',
+ ' contentmodel - Content model id',
' comment - Comment by the user for revision',
' parsedcomment - Parsed comment by the user for the revision',
' content - Text of the revision',
@@ -644,9 +724,10 @@ class ApiQueryRevisions extends ApiQueryBase {
'dir' => $this->getDirectionDescription( $p, ' (enum)' ),
'user' => 'Only include revisions made by user (enum)',
'excludeuser' => 'Exclude revisions made by user (enum)',
- 'expandtemplates' => 'Expand templates in revision content',
- 'generatexml' => 'Generate XML parse tree for revision content',
- 'parse' => 'Parse revision content. For performance reasons if this option is used, rvlimit is enforced to 1.',
+ 'expandtemplates' => "Expand templates in revision content (requires {$p}prop=content)",
+ 'generatexml' => "Generate XML parse tree for revision content (requires {$p}prop=content)",
+ 'parse' => array( "Parse revision content (requires {$p}prop=content).",
+ 'For performance reasons if this option is used, rvlimit is enforced to 1.' ),
'section' => 'Only retrieve the content of this section number',
'token' => 'Which tokens to obtain for each revision',
'continue' => 'When more results are available, use this to continue',
@@ -655,6 +736,7 @@ class ApiQueryRevisions extends ApiQueryBase {
'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.',
"Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ),
'tag' => 'Only list revisions tagged with this tag',
+ 'contentformat' => 'Serialization format used for difftotext and expected for output of content',
);
}
@@ -709,8 +791,12 @@ class ApiQueryRevisions extends ApiQueryBase {
ApiBase::PROP_TYPE => 'string',
ApiBase::PROP_NULLABLE => true
),
- 'texthidden' => 'boolean'
- )
+ 'texthidden' => 'boolean',
+ 'textmissing' => 'boolean',
+ ),
+ 'contentmodel' => array(
+ 'contentmodel' => 'string'
+ ),
);
self::addTokenProperties( $props, $this->getTokenFunctions() );
@@ -732,13 +818,18 @@ class ApiQueryRevisions extends ApiQueryBase {
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' => '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' ),
+ array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied '
+ . ' to the page\'s content model' ),
) );
}
@@ -762,8 +853,4 @@ class ApiQueryRevisions extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Properties#revisions_.2F_rv';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index 364433d5..36b55979 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -31,6 +31,14 @@
*/
class ApiQuerySearch extends ApiQueryGeneratorBase {
+ /**
+ * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't
+ * a valid option for an array for PARAM_TYPE, so we'll use a fake name
+ * that can't possibly be a class name and describes what the null behavior
+ * does
+ */
+ const BACKEND_NULL_PARAM = 'database-backed';
+
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'sr' );
}
@@ -59,7 +67,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
$prop = array_flip( $params['prop'] );
// Create search engine instance and set options
- $search = SearchEngine::create();
+ $search = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ?
+ SearchEngine::create( $params['backend'] ) : SearchEngine::create();
$search->setLimitOffset( $limit + 1, $params['offset'] );
$search->setNamespaces( $params['namespace'] );
$search->showRedirects = $params['redirects'];
@@ -93,6 +102,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
if ( is_null( $matches ) ) {
$this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" );
+ } elseif ( $matches instanceof Status && !$matches->isGood() ) {
+ $this->dieUsage( $matches->getWikiText(), 'search-error' );
}
$apiResult = $this->getResult();
@@ -168,7 +179,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
}
if ( isset( $prop['hasrelated'] ) && $result->hasRelated() ) {
- $vals['hasrelated'] = "";
+ $vals['hasrelated'] = '';
}
// Add item to results and see whether it fits
@@ -199,13 +210,15 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
public function getAllowedParams() {
- return array(
+ global $wgSearchType;
+
+ $params = array(
'search' => array(
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
'namespace' => array(
- ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_DFLT => NS_MAIN,
ApiBase::PARAM_TYPE => 'namespace',
ApiBase::PARAM_ISMULTI => true,
),
@@ -252,10 +265,23 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
ApiBase::PARAM_MAX2 => ApiBase::LIMIT_SML2
)
);
+
+ $alternatives = SearchEngine::getSearchTypes();
+ if ( count( $alternatives ) > 1 ) {
+ if ( $alternatives[0] === null ) {
+ $alternatives[0] = self::BACKEND_NULL_PARAM;
+ }
+ $params['backend'] = array(
+ ApiBase::PARAM_DFLT => $wgSearchType,
+ ApiBase::PARAM_TYPE => $alternatives,
+ );
+ }
+
+ return $params;
}
public function getParamDescription() {
- return array(
+ $descriptions = array(
'search' => 'Search for all page titles (or content) that has this value',
'namespace' => 'The namespace(s) to enumerate',
'what' => 'Search inside the text or titles',
@@ -278,6 +304,12 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
'offset' => 'Use this value to continue paging (return by query)',
'limit' => 'How many total pages to return'
);
+
+ if ( count( SearchEngine::getSearchTypes() ) > 1 ) {
+ $descriptions['backend'] = 'Which search backend to use, if not the default';
+ }
+
+ return $descriptions;
}
public function getResultProperties() {
@@ -345,6 +377,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
return array_merge( parent::getPossibleErrors(), array(
array( 'code' => 'search-text-disabled', 'info' => 'text search is disabled' ),
array( 'code' => 'search-title-disabled', 'info' => 'title search is disabled' ),
+ array( 'code' => 'search-error', 'info' => 'search error has occurred' ),
) );
}
@@ -359,8 +392,4 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Search';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index ec503d64..a7767062 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -96,6 +96,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
case 'variables':
$fit = $this->appendVariables( $p );
break;
+ case 'protocols':
+ $fit = $this->appendProtocols( $p );
+ break;
default:
ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" );
}
@@ -111,19 +114,57 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
protected function appendGeneralInfo( $property ) {
- global $wgContLang;
+ global $wgContLang,
+ $wgDisableLangConversion,
+ $wgDisableTitleConversion;
$data = array();
$mainPage = Title::newMainPage();
$data['mainpage'] = $mainPage->getPrefixedText();
- $data['base'] = wfExpandUrl( $mainPage->getFullUrl(), PROTO_CURRENT );
+ $data['base'] = wfExpandUrl( $mainPage->getFullURL(), PROTO_CURRENT );
$data['sitename'] = $GLOBALS['wgSitename'];
+ $data['logo'] = $GLOBALS['wgLogo'];
$data['generator'] = "MediaWiki {$GLOBALS['wgVersion']}";
$data['phpversion'] = phpversion();
- $data['phpsapi'] = php_sapi_name();
+ $data['phpsapi'] = PHP_SAPI;
$data['dbtype'] = $GLOBALS['wgDBtype'];
$data['dbversion'] = $this->getDB()->getServerVersion();
+ $allowFrom = array( '' );
+ $allowException = true;
+ if ( !$GLOBALS['wgAllowExternalImages'] ) {
+ if ( $GLOBALS['wgEnableImageWhitelist'] ) {
+ $data['imagewhitelistenabled'] = '';
+ }
+ $allowFrom = $GLOBALS['wgAllowExternalImagesFrom'];
+ $allowException = !empty( $allowFrom );
+ }
+ if ( $allowException ) {
+ $data['externalimages'] = (array)$allowFrom;
+ $this->getResult()->setIndexedTagName( $data['externalimages'], 'prefix' );
+ }
+
+ if ( !$wgDisableLangConversion ) {
+ $data['langconversion'] = '';
+ }
+
+ if ( !$wgDisableTitleConversion ) {
+ $data['titleconversion'] = '';
+ }
+
+ if ( $wgContLang->linkPrefixExtension() ) {
+ $data['linkprefix'] = wfMessage( 'linkprefix' )->inContentLanguage()->text();
+ } else {
+ $data['linkprefix'] = '';
+ }
+
+ $linktrail = $wgContLang->linkTrail();
+ if ( $linktrail ) {
+ $data['linktrail'] = $linktrail;
+ } else {
+ $data['linktrail'] = '';
+ }
+
$git = SpecialVersion::getGitHeadSha1( $GLOBALS['IP'] );
if ( $git ) {
$data['git-hash'] = $git;
@@ -144,15 +185,15 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['lang'] = $GLOBALS['wgLanguageCode'];
$fallbacks = array();
- foreach( $wgContLang->getFallbackLanguages() as $code ) {
+ foreach ( $wgContLang->getFallbackLanguages() as $code ) {
$fallbacks[] = array( 'code' => $code );
}
$data['fallback'] = $fallbacks;
$this->getResult()->setIndexedTagName( $data['fallback'], 'lang' );
- if( $wgContLang->hasVariants() ) {
+ if ( $wgContLang->hasVariants() ) {
$variants = array();
- foreach( $wgContLang->getVariants() as $code ) {
+ foreach ( $wgContLang->getVariants() as $code ) {
$variants[] = array( 'code' => $code );
}
$data['variants'] = $variants;
@@ -227,6 +268,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
if ( MWNamespace::isNonincludable( $ns ) ) {
$data[$ns]['nonincludable'] = '';
}
+
+ $contentmodel = MWNamespace::getNamespaceContentModel( $ns );
+ if ( $contentmodel ) {
+ $data[$ns]['defaultcontentmodel'] = $contentmodel;
+ }
}
$this->getResult()->setIndexedTagName( $data, 'ns' );
@@ -250,6 +296,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data[] = $item;
}
+ sort( $data );
+
$this->getResult()->setIndexedTagName( $data, 'ns' );
return $this->getResult()->addValue( 'query', $property, $data );
}
@@ -314,10 +362,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$val['language'] = $langNames[$prefix];
}
$val['url'] = wfExpandUrl( $row['iw_url'], PROTO_CURRENT );
- if( isset( $row['iw_wikiid'] ) ) {
+ if ( isset( $row['iw_wikiid'] ) ) {
$val['wikiid'] = $row['iw_wikiid'];
}
- if( isset( $row['iw_api'] ) ) {
+ if ( isset( $row['iw_api'] ) ) {
$val['api'] = $row['iw_api'];
}
@@ -345,7 +393,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
);
}
} else {
- list( $host, $lag, $index ) = $lb->getMaxLag();
+ list( , $lag, $index ) = $lb->getMaxLag();
$data[] = array(
'host' => $wgShowHostnames
? $lb->getServerName( $index )
@@ -373,11 +421,15 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['activeusers'] = intval( SiteStats::activeUsers() );
$data['admins'] = intval( SiteStats::numberingroup( 'sysop' ) );
$data['jobs'] = intval( SiteStats::jobs() );
+
+ wfRunHooks( 'APIQuerySiteInfoStatisticsInfo', array( &$data ) );
+
return $this->getResult()->addValue( 'query', $property, $data );
}
protected function appendUserGroups( $property, $numberInGroup ) {
- global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+ global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups;
+ global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
$data = array();
$result = $this->getResult();
@@ -425,7 +477,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
global $wgFileExtensions;
$data = array();
- foreach ( $wgFileExtensions as $ext ) {
+ foreach ( array_unique( $wgFileExtensions ) as $ext ) {
$data[] = array( 'ext' => $ext );
}
$this->getResult()->setIndexedTagName( $data, 'fe' );
@@ -457,7 +509,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
if ( isset( $ext['author'] ) ) {
$ret['author'] = is_array( $ext['author'] ) ?
- implode( ', ', $ext['author' ] ) : $ext['author'];
+ implode( ', ', $ext['author'] ) : $ext['author'];
}
if ( isset( $ext['url'] ) ) {
$ret['url'] = $ext['url'];
@@ -489,7 +541,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data = array(
'url' => $url ? $url : '',
- 'text' => $text ? $text : ''
+ 'text' => $text ? $text : ''
);
return $this->getResult()->addValue( 'query', $property, $data );
@@ -513,9 +565,17 @@ class ApiQuerySiteinfo extends ApiQueryBase {
public function appendSkins( $property ) {
$data = array();
+ $usable = Skin::getUsableSkins();
+ $default = Skin::normalizeKey( 'default' );
foreach ( Skin::getSkinNames() as $name => $displayName ) {
$skin = array( 'code' => $name );
ApiResult::setContent( $skin, $displayName );
+ if ( !isset( $usable[$name] ) ) {
+ $skin['unusable'] = '';
+ }
+ if ( $name === $default ) {
+ $skin['default'] = '';
+ }
$data[] = $skin;
}
$this->getResult()->setIndexedTagName( $data, 'skin' );
@@ -525,7 +585,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
public function appendExtensionTags( $property ) {
global $wgParser;
$wgParser->firstCallInit();
- $tags = array_map( array( $this, 'formatParserTags'), $wgParser->getTags() );
+ $tags = array_map( array( $this, 'formatParserTags' ), $wgParser->getTags() );
$this->getResult()->setIndexedTagName( $tags, 't' );
return $this->getResult()->addValue( 'query', $property, $tags );
}
@@ -544,6 +604,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
return $this->getResult()->addValue( 'query', $property, $variables );
}
+ public function appendProtocols( $property ) {
+ global $wgUrlProtocols;
+ // Make a copy of the global so we don't try to set the _element key of it - bug 45130
+ $protocols = array_values( $wgUrlProtocols );
+ $this->getResult()->setIndexedTagName( $protocols, 'p' );
+ return $this->getResult()->addValue( 'query', $property, $protocols );
+ }
+
private function formatParserTags( $item ) {
return "<{$item}>";
}
@@ -554,7 +622,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
ksort( $myWgHooks );
$data = array();
- foreach ( $myWgHooks as $hook => $hooks ) {
+ foreach ( $myWgHooks as $hook => $hooks ) {
$arr = array(
'name' => $hook,
'subscribers' => array_map( array( 'SpecialVersion', 'arrayToString' ), $hooks ),
@@ -596,6 +664,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'functionhooks',
'showhooks',
'variables',
+ 'protocols',
)
),
'filteriw' => array(
@@ -621,20 +690,23 @@ class ApiQuerySiteinfo extends ApiQueryBase {
' specialpagealiases - List of special page aliases',
' magicwords - List of magic words and their aliases',
' statistics - Returns site statistics',
- " interwikimap - Returns interwiki map (optionally filtered, (optionally localised by using {$p}inlanguagecode))",
+ " interwikimap - Returns interwiki map " .
+ "(optionally filtered, (optionally localised by using {$p}inlanguagecode))",
' dbrepllag - Returns database server with the highest replication lag',
' usergroups - Returns user groups and the associated permissions',
' 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 (optionally localised by using {$p}inlanguagecode)",
+ " languages - Returns a list of languages MediaWiki supports" .
+ "(optionally localised by using {$p}inlanguagecode)",
' skins - Returns a list of all enabled skins',
' extensiontags - Returns a list of parser extension tags',
' functionhooks - Returns a list of parser function hooks',
' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)',
' variables - Returns a list of variable IDs',
+ ' protocols - Returns a list of protocols that are allowed in external links.',
),
- 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
+ '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',
'inlanguagecode' => 'Language code for localised language names (best effort, use CLDR extension)',
@@ -646,9 +718,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
- array( 'code' => 'includeAllDenied', 'info' => 'Cannot view all servers info unless $wgShowHostnames is true' ),
- ) );
+ return array_merge( parent::getPossibleErrors(), array( array(
+ 'code' => 'includeAllDenied',
+ 'info' => 'Cannot view all servers info unless $wgShowHostnames is true'
+ ), ) );
}
public function getExamples() {
@@ -662,8 +735,4 @@ class ApiQuerySiteinfo extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Meta#siteinfo_.2F_si';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php
index a310d109..6899375a 100644
--- a/includes/api/ApiQueryStashImageInfo.php
+++ b/includes/api/ApiQueryStashImageInfo.php
@@ -42,7 +42,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
$result = $this->getResult();
if ( !$params['filekey'] && !$params['sessionkey'] ) {
- $this->dieUsage( "One of filekey or sessionkey must be supplied", 'nofilekey');
+ $this->dieUsage( "One of filekey or sessionkey must be supplied", 'nofilekey' );
}
// Alias sessionkey to filekey, but give an existing filekey precedence.
@@ -138,9 +138,4 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
-
}
-
diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php
index f97c1b2a..732df9a4 100644
--- a/includes/api/ApiQueryTags.php
+++ b/includes/api/ApiQueryTags.php
@@ -133,8 +133,7 @@ class ApiQueryTags extends ApiQueryBase {
public function getAllowedParams() {
return array(
- 'continue' => array(
- ),
+ 'continue' => null,
'limit' => array(
ApiBase::PARAM_DFLT => 10,
ApiBase::PARAM_TYPE => 'limit',
@@ -162,7 +161,7 @@ class ApiQueryTags extends ApiQueryBase {
'prop' => array(
'Which properties to get',
' name - Adds name of tag',
- ' displayname - Adds system messsage for the tag',
+ ' displayname - Adds system message for the tag',
' description - Adds description of the tag',
' hitcount - Adds the amount of revisions that have this tag',
),
@@ -196,7 +195,7 @@ class ApiQueryTags extends ApiQueryBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Tags';
}
}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index f30b1325..9a9be7b2 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -48,7 +48,7 @@ class ApiQueryContributions extends ApiQueryBase {
$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_parsedcomment = isset( $prop['parsedcomment'] );
$this->fld_size = isset( $prop['size'] );
$this->fld_sizediff = isset( $prop['sizediff'] );
$this->fld_flags = isset( $prop['flags'] );
@@ -83,10 +83,10 @@ class ApiQueryContributions extends ApiQueryBase {
// Do the actual query.
$res = $this->select( __METHOD__ );
- if( $this->fld_sizediff ) {
+ if ( $this->fld_sizediff ) {
$revIds = array();
foreach ( $res as $row ) {
- if( $row->rev_parent_id ) {
+ if ( $row->rev_parent_id ) {
$revIds[] = $row->rev_parent_id;
}
}
@@ -160,10 +160,7 @@ class ApiQueryContributions extends ApiQueryBase {
// Handle continue parameter
if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
$continue = explode( '|', $this->params['continue'] );
- if ( count( $continue ) != 2 ) {
- $this->dieUsage( 'Invalid continue param. You should pass the original ' .
- 'value returned by the previous query', '_badcontinue' );
- }
+ $this->dieContinueUsageIf( count( $continue ) != 2 );
$db = $this->getDB();
$encUser = $db->addQuotes( $continue[0] );
$encTS = $db->addQuotes( $db->timestamp( $continue[1] ) );
@@ -223,7 +220,7 @@ class ApiQueryContributions extends ApiQueryBase {
) );
if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
- $this->fld_patrolled ) {
+ $this->fld_patrolled ) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
$this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' );
}
@@ -258,7 +255,7 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
$this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
$this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
- $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff );
+ $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
$this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
if ( $this->fld_tags ) {
@@ -271,8 +268,7 @@ class ApiQueryContributions extends ApiQueryBase {
$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';
+ $index['change_tag'] = 'change_tag_tag_id';
}
if ( $this->params['toponly'] ) {
@@ -300,6 +296,10 @@ class ApiQueryContributions extends ApiQueryBase {
$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 ( !is_null( $row->rev_parent_id ) ) {
+ $vals['parentid'] = intval( $row->rev_parent_id );
+ }
}
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
@@ -445,7 +445,7 @@ class ApiQueryContributions extends ApiQueryBase {
'end' => 'The end timestamp to return to',
'continue' => 'When more results are available, use this to continue',
'user' => 'The users to retrieve contributions for',
- 'userprefix' => "Retrieve contibutions for all users whose names begin with this value. Overrides {$p}user",
+ 'userprefix' => "Retrieve contributions for all users whose names begin with this value. Overrides {$p}user",
'dir' => $this->getDirectionDescription( $p ),
'namespace' => 'Only list contributions in these namespaces',
'prop' => array(
@@ -477,7 +477,11 @@ class ApiQueryContributions extends ApiQueryBase {
),
'ids' => array(
'pageid' => 'integer',
- 'revid' => 'integer'
+ 'revid' => 'integer',
+ 'parentid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
),
'title' => array(
'ns' => 'namespace',
@@ -546,8 +550,4 @@ class ApiQueryContributions extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Usercontribs';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index 66906659..3c85ea69 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -77,18 +77,18 @@ class ApiQueryUserInfo extends ApiQueryBase {
if ( isset( $this->prop['groups'] ) ) {
$vals['groups'] = $user->getEffectiveGroups();
- $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty
+ $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty
}
if ( isset( $this->prop['implicitgroups'] ) ) {
$vals['implicitgroups'] = $user->getAutomaticGroups();
- $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
+ $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
}
if ( isset( $this->prop['rights'] ) ) {
// User::getRights() may return duplicate values, strip them
$vals['rights'] = array_values( array_unique( $user->getRights() ) );
- $result->setIndexedTagName( $vals['rights'], 'r' ); // even if empty
+ $result->setIndexedTagName( $vals['rights'], 'r' ); // even if empty
}
if ( isset( $this->prop['changeablegroups'] ) ) {
@@ -104,12 +104,15 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
if ( isset( $this->prop['preferencestoken'] ) &&
- is_null( $this->getMain()->getRequest()->getVal( 'callback' ) )
+ is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) &&
+ $user->isAllowed( 'editmyoptions' )
) {
$vals['preferencestoken'] = $user->getEditToken( '', $this->getMain()->getRequest() );
}
if ( isset( $this->prop['editcount'] ) ) {
+ // use intval to prevent null if a non-logged-in user calls
+ // api.php?format=jsonfm&action=query&meta=userinfo&uiprop=editcount
$vals['editcount'] = intval( $user->getEditCount() );
}
@@ -121,11 +124,13 @@ class ApiQueryUserInfo extends ApiQueryBase {
$vals['realname'] = $user->getRealName();
}
- if ( isset( $this->prop['email'] ) ) {
- $vals['email'] = $user->getEmail();
- $auth = $user->getEmailAuthenticationTimestamp();
- if ( !is_null( $auth ) ) {
- $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth );
+ if ( $user->isAllowed( 'viewmyprivateinfo' ) ) {
+ if ( isset( $this->prop['email'] ) ) {
+ $vals['email'] = $user->getEmail();
+ $auth = $user->getEmailAuthenticationTimestamp();
+ if ( !is_null( $auth ) ) {
+ $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth );
+ }
}
}
@@ -167,8 +172,9 @@ class ApiQueryUserInfo extends ApiQueryBase {
if ( $user->isNewbie() ) {
$categories[] = 'ip';
$categories[] = 'subnet';
- if ( !$user->isAnon() )
+ if ( !$user->isAnon() ) {
$categories[] = 'newbie';
+ }
}
$categories = array_merge( $categories, $user->getGroups() );
@@ -303,8 +309,4 @@ class ApiQueryUserInfo extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Meta#userinfo_.2F_ui';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index bf438d1d..dccfee67 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -110,26 +110,46 @@ class ApiQueryUsers extends ApiQueryBase {
$this->addFields( User::selectFields() );
$this->addWhereFld( 'user_name', $goodNames );
- if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) {
- $this->addTables( 'user_groups' );
- $this->addJoinConds( array( 'user_groups' => array( 'LEFT JOIN', 'ug_user=user_id' ) ) );
- $this->addFields( 'ug_group' );
- }
-
$this->showHiddenUsersAddBlockInfo( isset( $this->prop['blockinfo'] ) );
$data = array();
$res = $this->select( __METHOD__ );
+ $this->resetQueryParams();
+
+ // get user groups if needed
+ if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) {
+ $userGroups = array();
+
+ $this->addTables( 'user' );
+ $this->addWhereFld( 'user_name', $goodNames );
+ $this->addTables( 'user_groups' );
+ $this->addJoinConds( array( 'user_groups' => array( 'INNER JOIN', 'ug_user=user_id' ) ) );
+ $this->addFields( array( 'user_name', 'ug_group' ) );
+ $userGroupsRes = $this->select( __METHOD__ );
+
+ foreach ( $userGroupsRes as $row ) {
+ $userGroups[$row->user_name][] = $row->ug_group;
+ }
+ }
foreach ( $res as $row ) {
- $user = User::newFromRow( $row );
+ // create user object and pass along $userGroups if set
+ // that reduces the number of database queries needed in User dramatically
+ if ( !isset( $userGroups ) ) {
+ $user = User::newFromRow( $row );
+ } else {
+ if ( !isset( $userGroups[$row->user_name] ) || !is_array( $userGroups[$row->user_name] ) ) {
+ $userGroups[$row->user_name] = array();
+ }
+ $user = User::newFromRow( $row, array( 'user_groups' => $userGroups[$row->user_name] ) );
+ }
$name = $user->getName();
$data[$name]['userid'] = $user->getId();
$data[$name]['name'] = $name;
if ( isset( $this->prop['editcount'] ) ) {
- $data[$name]['editcount'] = intval( $user->getEditCount() );
+ $data[$name]['editcount'] = $user->getEditCount();
}
if ( isset( $this->prop['registration'] ) ) {
@@ -137,29 +157,15 @@ class ApiQueryUsers extends ApiQueryBase {
}
if ( isset( $this->prop['groups'] ) ) {
- if ( !isset( $data[$name]['groups'] ) ) {
- $data[$name]['groups'] = $user->getAutomaticGroups();
- }
-
- if ( !is_null( $row->ug_group ) ) {
- // This row contains only one group, others will be added from other rows
- $data[$name]['groups'][] = $row->ug_group;
- }
+ $data[$name]['groups'] = $user->getEffectiveGroups();
}
- if ( isset( $this->prop['implicitgroups'] ) && !isset( $data[$name]['implicitgroups'] ) ) {
- $data[$name]['implicitgroups'] = $user->getAutomaticGroups();
+ if ( isset( $this->prop['implicitgroups'] ) ) {
+ $data[$name]['implicitgroups'] = $user->getAutomaticGroups();
}
if ( isset( $this->prop['rights'] ) ) {
- if ( !isset( $data[$name]['rights'] ) ) {
- $data[$name]['rights'] = User::getGroupPermissions( $user->getAutomaticGroups() );
- }
-
- if ( !is_null( $row->ug_group ) ) {
- $data[$name]['rights'] = array_unique( array_merge( $data[$name]['rights'],
- User::getGroupPermissions( array( $row->ug_group ) ) ) );
- }
+ $data[$name]['rights'] = $user->getRights();
}
if ( $row->ipb_deleted ) {
$data[$name]['hidden'] = '';
@@ -198,11 +204,13 @@ class ApiQueryUsers extends ApiQueryBase {
}
}
+ $context = $this->getContext();
// Second pass: add result data to $retval
foreach ( $goodNames as $u ) {
if ( !isset( $data[$u] ) ) {
$data[$u] = array( 'name' => $u );
$urPage = new UserrightsPage;
+ $urPage->setContext( $context );
$iwUser = $urPage->fetchUser( $u );
if ( $iwUser instanceof UserRightsProxy ) {
@@ -244,16 +252,16 @@ class ApiQueryUsers extends ApiQueryBase {
}
$done[] = $u;
}
- return $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'user' );
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'user' );
}
/**
- * Gets all the groups that a user is automatically a member of (implicit groups)
- *
- * @deprecated since 1.20; call User::getAutomaticGroups() directly.
- * @param $user User
- * @return array
- */
+ * Gets all the groups that a user is automatically a member of (implicit groups)
+ *
+ * @deprecated since 1.20; call User::getAutomaticGroups() directly.
+ * @param $user User
+ * @return array
+ */
public static function getAutoGroups( $user ) {
wfDeprecated( __METHOD__, '1.20' );
@@ -304,7 +312,7 @@ class ApiQueryUsers extends ApiQueryBase {
' rights - Lists all the rights the user(s) has',
' 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]]',
+ ' emailable - Tags if the user can and wants to receive email 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',
@@ -390,8 +398,4 @@ class ApiQueryUsers extends ApiQueryBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Users';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index a1a33728..22843f50 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -68,7 +68,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->fld_user = isset( $prop['user'] );
$this->fld_userid = isset( $prop['userid'] );
$this->fld_comment = isset( $prop['comment'] );
- $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
$this->fld_timestamp = isset( $prop['timestamp'] );
$this->fld_sizes = isset( $prop['sizes'] );
$this->fld_patrol = isset( $prop['patrol'] );
@@ -116,7 +116,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
) );
$userId = $user->getId();
- $this->addJoinConds( array( 'watchlist' => array('INNER JOIN',
+ $this->addJoinConds( array( 'watchlist' => array( 'INNER JOIN',
array(
'wl_user' => $userId,
'wl_namespace=rc_namespace',
@@ -135,7 +135,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if ( !$params['allrev'] ) {
$this->addTables( 'page' );
- $this->addJoinConds( array( 'page' => array( 'LEFT JOIN','rc_cur_id=page_id' ) ) );
+ $this->addJoinConds( array( 'page' => array( 'LEFT JOIN', 'rc_cur_id=page_id' ) ) );
$this->addWhere( 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG );
}
@@ -143,12 +143,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$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->dieUsageMsg( 'show' );
}
@@ -171,6 +170,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
}
+ if ( !is_null( $params['type'] ) ) {
+ $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) );
+ }
+
if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
$this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
}
@@ -226,6 +229,32 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
private function extractRowInfo( $row ) {
$vals = array();
+ $type = intval( $row->rc_type );
+
+ /* 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_EXTERNAL:
+ $vals['type'] = 'external';
+ break;
+ case RC_MOVE_OVER_REDIRECT:
+ $vals['type'] = 'move over redirect';
+ break;
+ default:
+ $vals['type'] = $type;
+ }
+
if ( $this->fld_ids ) {
$vals['pageid'] = intval( $row->rc_cur_id );
$vals['revid'] = intval( $row->rc_this_oldid );
@@ -240,14 +269,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if ( $this->fld_user || $this->fld_userid ) {
- if ( $this->fld_user ) {
- $vals['user'] = $row->rc_user_text;
- }
-
if ( $this->fld_userid ) {
+ $vals['userid'] = $row->rc_user;
+ // for backwards compatibility
$vals['user'] = $row->rc_user;
}
+ if ( $this->fld_user ) {
+ $vals['user'] = $row->rc_user_text;
+ }
+
if ( !$row->rc_user ) {
$vals['anon'] = '';
}
@@ -310,6 +341,27 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
return $vals;
}
+ /* Copied from ApiQueryRecentChanges. */
+ private function parseRCType( $type ) {
+ if ( is_array( $type ) ) {
+ $retval = array();
+ foreach ( $type as $t ) {
+ $retval[] = $this->parseRCType( $t );
+ }
+ return $retval;
+ }
+ switch ( $type ) {
+ case 'edit':
+ return RC_EDIT;
+ case 'new':
+ return RC_NEW;
+ case 'log':
+ return RC_LOG;
+ case 'external':
+ return RC_EXTERNAL;
+ }
+ }
+
public function getAllowedParams() {
return array(
'allrev' => false,
@@ -319,7 +371,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'end' => array(
ApiBase::PARAM_TYPE => 'timestamp'
),
- 'namespace' => array (
+ 'namespace' => array(
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace'
),
@@ -374,6 +426,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'!patrolled',
)
),
+ 'type' => array(
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TYPE => array(
+ 'edit',
+ 'external',
+ 'new',
+ 'log',
+ )
+ ),
'owner' => array(
ApiBase::PARAM_TYPE => 'user'
),
@@ -413,6 +474,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'Show only items that meet this criteria.',
"For example, to see only minor edits done by logged-in users, set {$p}show=minor|!anon"
),
+ 'type' => array(
+ 'Which types of changes to show',
+ ' edit - Regular page edits',
+ ' external - External changes',
+ ' new - Page creations',
+ ' log - Log entries',
+ ),
'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'
);
@@ -421,6 +489,17 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
public function getResultProperties() {
global $wgLogTypes;
return array(
+ '' => array(
+ 'type' => array(
+ ApiBase::PROP_TYPE => array(
+ 'edit',
+ 'new',
+ 'move',
+ 'log',
+ 'move over redirect'
+ )
+ )
+ ),
'ids' => array(
'pageid' => 'integer',
'revid' => 'integer',
@@ -511,15 +590,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'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&list=watchlist&wlowner=Bob_Smith&wltoken=d8d562e9725ea1512894cdab28e5ceebc7f20237'
+ 'api.php?action=query&list=watchlist&wlowner=Bob_Smith&wltoken=123ABC'
);
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Watchlist';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php
index 6b24aef3..ea4e724a 100644
--- a/includes/api/ApiQueryWatchlistRaw.php
+++ b/includes/api/ApiQueryWatchlistRaw.php
@@ -71,11 +71,9 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
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" );
- }
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
$ns = intval( $cont[0] );
+ $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
$title = $this->getDB()->addQuotes( $cont[1] );
$op = $params['dir'] == 'ascending' ? '>' : '<';
$this->addWhere(
@@ -225,7 +223,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
);
}
- public function getVersion() {
- return __CLASS__ . ': $Id$';
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Watchlistraw';
}
}
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index 91e20812..39c114b8 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -36,13 +36,26 @@
* There are two special key values that change how XML output is generated:
* '_element' This key sets the tag name for the rest of the elements in the current array.
* It is only inserted if the formatter returned true for getNeedsRawData()
- * '*' This key has special meaning only to the XML formatter, and is outputed as is
- * for all others. In XML it becomes the content of the current element.
+ * '*' This key has special meaning only to the XML formatter, and is outputted as is
+ * for all others. In XML it becomes the content of the current element.
*
* @ingroup API
*/
class ApiResult extends ApiBase {
+ /**
+ * override existing value in addValue() and setElement()
+ * @since 1.21
+ */
+ const OVERRIDE = 1;
+
+ /**
+ * For addValue() and setElement(), if the value does not exist, add it as the first element.
+ * In case the new value has no name (numerical index), all indexes will be renumbered.
+ * @since 1.21
+ */
+ const ADD_ON_TOP = 2;
+
private $mData, $mIsRawMode, $mSize, $mCheckingSize;
/**
@@ -134,18 +147,27 @@ class ApiResult extends ApiBase {
/**
* Add an output value to the array by name.
* Verifies that value with the same name has not been added before.
- * @param $arr array to add $value to
- * @param $name string Index of $arr to add $value at
+ * @param array $arr to add $value to
+ * @param string $name Index of $arr to add $value at
* @param $value mixed
- * @param $overwrite bool Whether overwriting an existing element is allowed
+ * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This parameter used to be
+ * boolean, and the value of OVERRIDE=1 was specifically chosen so that it would be backwards
+ * compatible with the new method signature.
+ *
+ * @since 1.21 int $flags replaced boolean $override
*/
- public static function setElement( &$arr, $name, $value, $overwrite = false ) {
+ public static function setElement( &$arr, $name, $value, $flags = 0 ) {
if ( $arr === null || $name === null || $value === null || !is_array( $arr ) || is_array( $name ) ) {
ApiBase::dieDebug( __METHOD__, 'Bad parameter' );
}
- if ( !isset ( $arr[$name] ) || $overwrite ) {
- $arr[$name] = $value;
+ $exists = isset( $arr[$name] );
+ if ( !$exists || ( $flags & ApiResult::OVERRIDE ) ) {
+ if ( !$exists && ( $flags & ApiResult::ADD_ON_TOP ) ) {
+ $arr = array( $name => $value ) + $arr;
+ } else {
+ $arr[$name] = $value;
+ }
} elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
$merged = array_intersect_key( $arr[$name], $value );
if ( !count( $merged ) ) {
@@ -161,9 +183,9 @@ class ApiResult extends ApiBase {
/**
* Adds a content element to an array.
* Use this function instead of hardcoding the '*' element.
- * @param $arr array to add the content element to
+ * @param array $arr to add the content element to
* @param $value Mixed
- * @param $subElemName string when present, content element is created
+ * @param string $subElemName when present, content element is created
* as a sub item of $arr. Use this parameter to create elements in
* format "<elem>text</elem>" without attributes.
*/
@@ -186,7 +208,7 @@ class ApiResult extends ApiBase {
* give all indexed values the given tag name. This function MUST be
* called on every array that has numerical indexes.
* @param $arr array
- * @param $tag string Tag name
+ * @param string $tag Tag name
*/
public function setIndexedTagName( &$arr, $tag ) {
// In raw mode, add the '_element', otherwise just ignore
@@ -203,7 +225,7 @@ class ApiResult extends ApiBase {
/**
* Calls setIndexedTagName() on each sub-array of $arr
* @param $arr array
- * @param $tag string Tag name
+ * @param string $tag Tag name
*/
public function setIndexedTagName_recursive( &$arr, $tag ) {
if ( !is_array( $arr ) ) {
@@ -222,7 +244,7 @@ class ApiResult extends ApiBase {
* Calls setIndexedTagName() on an array already in the result.
* Don't specify a path to a value that's not in the result, or
* you'll get nasty errors.
- * @param $path array Path to the array, like addValue()'s $path
+ * @param array $path Path to the array, like addValue()'s $path
* @param $tag string
*/
public function setIndexedTagName_internal( $path, $tag ) {
@@ -249,11 +271,14 @@ class ApiResult extends ApiBase {
* @param $path array|string|null
* @param $name string
* @param $value mixed
- * @param $overwrite bool
- *
+ * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This parameter used to be
+ * boolean, and the value of OVERRIDE=1 was specifically chosen so that it would be backwards
+ * compatible with the new method signature.
* @return bool True if $value fits in the result, false if not
+ *
+ * @since 1.21 int $flags replaced boolean $override
*/
- public function addValue( $path, $name, $value, $overwrite = false ) {
+ public function addValue( $path, $name, $value, $flags = 0 ) {
global $wgAPIMaxResultSize;
$data = &$this->mData;
@@ -268,26 +293,34 @@ class ApiResult extends ApiBase {
$this->mSize = $newsize;
}
- if ( !is_null( $path ) ) {
- if ( is_array( $path ) ) {
- foreach ( $path as $p ) {
- if ( !isset( $data[$p] ) ) {
+ $addOnTop = $flags & ApiResult::ADD_ON_TOP;
+ if ( $path !== null ) {
+ foreach ( (array)$path as $p ) {
+ if ( !isset( $data[$p] ) ) {
+ if ( $addOnTop ) {
+ $data = array( $p => array() ) + $data;
+ $addOnTop = false;
+ } else {
$data[$p] = array();
}
- $data = &$data[$p];
- }
- } else {
- if ( !isset( $data[$path] ) ) {
- $data[$path] = array();
}
- $data = &$data[$path];
+ $data = &$data[$p];
}
}
if ( !$name ) {
- $data[] = $value; // Add list element
+ // Add list element
+ if ( $addOnTop ) {
+ // This element needs to be inserted in the beginning
+ // Numerical indexes will be renumbered
+ array_unshift( $data, $value );
+ } else {
+ // Add new value at the end
+ $data[] = $value;
+ }
} else {
- self::setElement( $data, $name, $value, $overwrite ); // Add named element
+ // Add named element
+ self::setElement( $data, $name, $value, $flags );
}
return true;
}
@@ -300,19 +333,19 @@ class ApiResult extends ApiBase {
*/
public function setParsedLimit( $moduleName, $limit ) {
// Add value, allowing overwriting
- $this->addValue( 'limits', $moduleName, $limit, true );
+ $this->addValue( 'limits', $moduleName, $limit, ApiResult::OVERRIDE );
}
/**
* Unset a value previously added to the result set.
* Fails silently if the value isn't found.
* For parameters, see addValue()
- * @param $path array
+ * @param $path array|null
* @param $name string
*/
public function unsetValue( $path, $name ) {
$data = &$this->mData;
- if ( !is_null( $path ) ) {
+ if ( $path !== null ) {
foreach ( (array)$path as $p ) {
if ( !isset( $data[$p] ) ) {
return;
@@ -367,8 +400,4 @@ class ApiResult extends ApiBase {
public function execute() {
ApiBase::dieDebug( __METHOD__, 'execute() is not supported on Result object' );
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index 677df16a..b9873f49 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -29,10 +29,6 @@
*/
class ApiRollback extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* @var Title
*/
@@ -185,7 +181,7 @@ class ApiRollback extends ApiBase {
$this->mTitleObj = Title::newFromText( $params['title'] );
- if ( !$this->mTitleObj ) {
+ if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
if ( !$this->mTitleObj->exists() ) {
@@ -205,8 +201,4 @@ class ApiRollback extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Rollback';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiRsd.php b/includes/api/ApiRsd.php
index f0e1fad6..d219c91c 100644
--- a/includes/api/ApiRsd.php
+++ b/includes/api/ApiRsd.php
@@ -31,10 +31,6 @@
*/
class ApiRsd extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$result = $this->getResult();
@@ -44,7 +40,7 @@ class ApiRsd extends ApiBase {
$service = array( 'apis' => $this->formatRsdApiList() );
ApiResult::setContent( $service, 'MediaWiki', 'engineName' );
ApiResult::setContent( $service, 'https://www.mediawiki.org/', 'engineLink' );
- ApiResult::setContent( $service, Title::newMainPage()->getCanonicalUrl(), 'homePageLink' );
+ ApiResult::setContent( $service, Title::newMainPage()->getCanonicalURL(), 'homePageLink' );
$result->setIndexedTagName( $service['apis'], 'api' );
@@ -155,10 +151,6 @@ class ApiRsd extends ApiBase {
}
return $outputData;
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
class ApiFormatXmlRsd extends ApiFormatXml {
@@ -170,8 +162,4 @@ class ApiFormatXmlRsd extends ApiFormatXml {
public function getMimeType() {
return 'application/rsd+xml';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php
index 098b1a66..53a68fde 100644
--- a/includes/api/ApiSetNotificationTimestamp.php
+++ b/includes/api/ApiSetNotificationTimestamp.php
@@ -31,9 +31,7 @@
*/
class ApiSetNotificationTimestamp extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
+ private $mPageSet;
public function execute() {
$user = $this->getUser();
@@ -41,15 +39,19 @@ class ApiSetNotificationTimestamp extends ApiBase {
if ( $user->isAnon() ) {
$this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' );
}
+ if ( !$user->isAllowed( 'editmywatchlist' ) ) {
+ $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' );
+ }
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' );
- $pageSet = new ApiPageSet( $this );
- $args = array_merge( array( $params, 'entirewatchlist' ), array_keys( $pageSet->getAllowedParams() ) );
- call_user_func_array( array( $this, 'requireOnlyOneParameter' ), $args );
+ $pageSet = $this->getPageSet();
+ if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) {
+ $this->dieUsage( "Cannot use 'entirewatchlist' at the same time as '{$pageSet->getDataSource()}'", 'multisource' );
+ }
- $dbw = $this->getDB( DB_MASTER );
+ $dbw = wfGetDB( DB_MASTER, 'api' );
$timestamp = null;
if ( isset( $params['timestamp'] ) ) {
@@ -96,20 +98,20 @@ class ApiSetNotificationTimestamp extends ApiBase {
$result['notificationtimestamp'] = ( is_null( $timestamp ) ? '' : wfTimestamp( TS_ISO_8601, $timestamp ) );
} else {
// First, log the invalid titles
- foreach( $pageSet->getInvalidTitles() as $title ) {
+ foreach ( $pageSet->getInvalidTitles() as $title ) {
$r = array();
$r['title'] = $title;
$r['invalid'] = '';
$result[] = $r;
}
- foreach( $pageSet->getMissingPageIDs() as $p ) {
+ foreach ( $pageSet->getMissingPageIDs() as $p ) {
$page = array();
$page['pageid'] = $p;
$page['missing'] = '';
$page['notwatched'] = '';
$result[] = $page;
}
- foreach( $pageSet->getMissingRevisionIDs() as $r ) {
+ foreach ( $pageSet->getMissingRevisionIDs() as $r ) {
$rev = array();
$rev['revid'] = $r;
$rev['missing'] = '';
@@ -135,6 +137,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
}
// Now, put the valid titles into the result
+ /** @var $title Title */
foreach ( $pageSet->getTitles() as $title ) {
$ns = $title->getNamespace();
$dbkey = $title->getDBkey();
@@ -161,6 +164,17 @@ class ApiSetNotificationTimestamp extends ApiBase {
$apiResult->addValue( null, $this->getModuleName(), $result );
}
+ /**
+ * Get a cached instance of an ApiPageSet object
+ * @return ApiPageSet
+ */
+ private function getPageSet() {
+ if ( !isset( $this->mPageSet ) ) {
+ $this->mPageSet = new ApiPageSet( $this );
+ }
+ return $this->mPageSet;
+ }
+
public function mustBePosted() {
return true;
}
@@ -177,9 +191,8 @@ class ApiSetNotificationTimestamp extends ApiBase {
return '';
}
- public function getAllowedParams() {
- $psModule = new ApiPageSet( $this );
- return $psModule->getAllowedParams() + array(
+ public function getAllowedParams( $flags = 0 ) {
+ $result = array(
'entirewatchlist' => array(
ApiBase::PARAM_TYPE => 'boolean'
),
@@ -194,11 +207,15 @@ class ApiSetNotificationTimestamp extends ApiBase {
ApiBase::PARAM_TYPE => 'integer'
),
);
+ if ( $flags ) {
+ $result += $this->getPageSet()->getFinalParams( $flags );
+ }
+ return $result;
+
}
public function getParamDescription() {
- $psModule = new ApiPageSet( $this );
- return $psModule->getParamDescription() + array(
+ return $this->getPageSet()->getFinalParamDescription() + array(
'entirewatchlist' => 'Work on all watched pages',
'timestamp' => 'Timestamp to which to set the notification timestamp',
'torevid' => 'Revision to set the notification timestamp to (one page only)',
@@ -247,18 +264,20 @@ class ApiSetNotificationTimestamp extends ApiBase {
public function getDescription() {
return array( 'Update the notification timestamp for watched pages.',
'This affects the highlighting of changed pages in the watchlist and history,',
- 'and the sending of email when the "E-mail me when a page on my watchlist is',
+ 'and the sending of email when the "Email me when a page on my watchlist is',
'changed" preference is enabled.'
);
}
public function getPossibleErrors() {
- $psModule = new ApiPageSet( $this );
+ $ps = $this->getPageSet();
return array_merge(
parent::getPossibleErrors(),
- $psModule->getPossibleErrors(),
- $this->getRequireMaxOneParameterErrorMessages( array( 'timestamp', 'torevid', 'newerthanrevid' ) ),
- $this->getRequireOnlyOneParameterErrorMessages( array_merge( array( 'entirewatchlist' ), array_keys( $psModule->getAllowedParams() ) ) ),
+ $ps->getFinalPossibleErrors(),
+ $this->getRequireMaxOneParameterErrorMessages(
+ array( 'timestamp', 'torevid', 'newerthanrevid' ) ),
+ $this->getRequireOnlyOneParameterErrorMessages(
+ array_merge( array( 'entirewatchlist' ), array_keys( $ps->getFinalParams() ) ) ),
array(
array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot use watchlist change notifications' ),
array( 'code' => 'multpages', 'info' => 'torevid may only be used with a single page' ),
@@ -269,17 +288,13 @@ class ApiSetNotificationTimestamp extends ApiBase {
public function getExamples() {
return array(
- 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=ABC123' => 'Reset the notification status for the entire watchlist',
- 'api.php?action=setnotificationtimestamp&titles=Main_page&token=ABC123' => 'Reset the notification status for "Main page"',
- 'api.php?action=setnotificationtimestamp&titles=Main_page&timestamp=2012-01-01T00:00:00Z&token=ABC123' => 'Set the notification timestamp for "Main page" so all edits since 1 January 2012 are unviewed',
+ 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=123ABC' => 'Reset the notification status for the entire watchlist',
+ 'api.php?action=setnotificationtimestamp&titles=Main_page&token=123ABC' => 'Reset the notification status for "Main page"',
+ 'api.php?action=setnotificationtimestamp&titles=Main_page&timestamp=2012-01-01T00:00:00Z&token=123ABC' => 'Set the notification timestamp for "Main page" so all edits since 1 January 2012 are unviewed',
);
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:SetNotificationTimestamp';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php
index 2c9b482c..d220a5e6 100644
--- a/includes/api/ApiTokens.php
+++ b/includes/api/ApiTokens.php
@@ -24,25 +24,17 @@
* @file
*/
-
/**
* @ingroup API
*/
class ApiTokens extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
- wfProfileIn( __METHOD__ );
$params = $this->extractRequestParams();
$res = array();
$types = $this->getTokenTypes();
foreach ( $params['type'] as $type ) {
- $type = strtolower( $type );
-
$val = call_user_func( $types[$type], null, null );
if ( $val === false ) {
@@ -53,20 +45,24 @@ class ApiTokens extends ApiBase {
}
$this->getResult()->addValue( null, $this->getModuleName(), $res );
- wfProfileOut( __METHOD__ );
}
private function getTokenTypes() {
+ // If we're in JSON callback mode, no tokens can be obtained
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) {
+ return array();
+ }
+
static $types = null;
if ( $types ) {
return $types;
}
wfProfileIn( __METHOD__ );
- $types = array( 'patrol' => 'ApiQueryRecentChanges::getPatrolToken' );
+ $types = array( 'patrol' => array( 'ApiQueryRecentChanges', 'getPatrolToken' ) );
$names = array( 'edit', 'delete', 'protect', 'move', 'block', 'unblock',
'email', 'import', 'watch', 'options' );
foreach ( $names as $name ) {
- $types[$name] = 'ApiQueryInfo::get' . ucfirst( $name ) . 'Token';
+ $types[$name] = array( 'ApiQueryInfo', 'get' . ucfirst( $name ) . 'Token' );
}
wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) );
ksort( $types );
@@ -85,54 +81,13 @@ class ApiTokens extends ApiBase {
}
public function getResultProperties() {
- return array(
- '' => array(
- 'patroltoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'edittoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'deletetoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'protecttoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'movetoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'blocktoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'unblocktoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'emailtoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'importtoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'watchtoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
- 'optionstoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- )
- )
+ $props = array(
+ '' => array(),
);
+
+ self::addTokenProperties( $props, $this->getTokenTypes() );
+
+ return $props;
}
public function getParamDescription() {
@@ -151,8 +106,4 @@ class ApiTokens extends ApiBase {
'api.php?action=tokens&type=email|move' => 'Retrieve an email token and a move token'
);
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php
index ff9ac474..6a739a2f 100644
--- a/includes/api/ApiUnblock.php
+++ b/includes/api/ApiUnblock.php
@@ -32,10 +32,6 @@
*/
class ApiUnblock extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
/**
* Unblocks the specified user or provides the reason the unblock failed.
*/
@@ -43,12 +39,6 @@ class ApiUnblock extends ApiBase {
$user = $this->getUser();
$params = $this->extractRequestParams();
- if ( $params['gettoken'] ) {
- $res['unblocktoken'] = $user->getEditToken();
- $this->getResult()->addValue( null, $this->getModuleName(), $res );
- return;
- }
-
if ( is_null( $params['id'] ) && is_null( $params['user'] ) ) {
$this->dieUsageMsg( 'unblock-notarget' );
}
@@ -100,10 +90,6 @@ class ApiUnblock extends ApiBase {
),
'user' => null,
'token' => null,
- 'gettoken' => array(
- ApiBase::PARAM_DFLT => false,
- ApiBase::PARAM_DEPRECATED => true,
- ),
'reason' => '',
);
}
@@ -114,7 +100,6 @@ class ApiUnblock extends ApiBase {
'id' => "ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with {$p}user",
'user' => "Username, IP address or IP range you want to unblock. Cannot be used together with {$p}id",
'token' => "An unblock token previously obtained through prop=info",
- 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken',
'reason' => 'Reason for unblock',
);
}
@@ -122,10 +107,6 @@ class ApiUnblock extends ApiBase {
public function getResultProperties() {
return array(
'' => array(
- 'unblocktoken' => array(
- ApiBase::PROP_TYPE => 'string',
- ApiBase::PROP_NULLABLE => true
- ),
'id' => array(
ApiBase::PROP_TYPE => 'integer',
ApiBase::PROP_NULLABLE => true
@@ -178,8 +159,4 @@ class ApiUnblock extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Block';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
index c9962517..4bbe568d 100644
--- a/includes/api/ApiUndelete.php
+++ b/includes/api/ApiUndelete.php
@@ -29,10 +29,6 @@
*/
class ApiUndelete extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$params = $this->extractRequestParams();
@@ -45,7 +41,7 @@ class ApiUndelete extends ApiBase {
}
$titleObj = Title::newFromText( $params['title'] );
- if ( !$titleObj ) {
+ if ( !$titleObj || $titleObj->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
@@ -61,7 +57,13 @@ class ApiUndelete extends ApiBase {
}
$pa = new PageArchive( $titleObj );
- $retval = $pa->undelete( ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), $params['reason'] );
+ $retval = $pa->undelete(
+ ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ),
+ $params['reason'],
+ array(),
+ false,
+ $this->getUser()
+ );
if ( !is_array( $retval ) ) {
$this->dieUsageMsg( 'cannotundelete' );
}
@@ -170,8 +172,4 @@ class ApiUndelete extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Undelete';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php
index e7a7849b..467eccf8 100644
--- a/includes/api/ApiUpload.php
+++ b/includes/api/ApiUpload.php
@@ -36,11 +36,9 @@ class ApiUpload extends ApiBase {
protected $mParams;
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
+ global $wgEnableAsyncUploads;
+
// Check whether upload is enabled
if ( !UploadBase::isEnabled() ) {
$this->dieUsageMsg( 'uploaddisabled' );
@@ -51,28 +49,33 @@ class ApiUpload extends ApiBase {
// Parameter handling
$this->mParams = $this->extractRequestParams();
$request = $this->getMain()->getRequest();
+ // Check if async mode is actually supported (jobs done in cli mode)
+ $this->mParams['async'] = ( $this->mParams['async'] && $wgEnableAsyncUploads );
// Add the uploaded file to the params array
$this->mParams['file'] = $request->getFileName( 'file' );
$this->mParams['chunk'] = $request->getFileName( 'chunk' );
// Copy the session key to the file key, for backward compatibility.
- if( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
+ if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
$this->mParams['filekey'] = $this->mParams['sessionkey'];
}
// Select an upload module
- if ( !$this->selectUploadModule() ) {
- // This is not a true upload, but a status request or similar
- return;
- }
- if ( !isset( $this->mUpload ) ) {
- $this->dieUsage( 'No upload module set', 'nomodule' );
+ try {
+ if ( !$this->selectUploadModule() ) {
+ return; // not a true upload, but a status request or similar
+ } elseif ( !isset( $this->mUpload ) ) {
+ $this->dieUsage( 'No upload module set', 'nomodule' );
+ }
+ } catch ( UploadStashException $e ) { // XXX: don't spam exception log
+ $this->dieUsage( get_class( $e ) . ": " . $e->getMessage(), 'stasherror' );
}
// First check permission to upload
$this->checkPermissions( $user );
- // Fetch the file
+ // Fetch the file (usually a no-op)
+ /** @var $status Status */
$status = $this->mUpload->fetchFile();
if ( !$status->isGood() ) {
$errors = $status->getErrorsArray();
@@ -82,28 +85,38 @@ class ApiUpload extends ApiBase {
// Check if the uploaded file is sane
if ( $this->mParams['chunk'] ) {
- $maxSize = $this->mUpload->getMaxUploadSize( );
- if( $this->mParams['filesize'] > $maxSize ) {
+ $maxSize = $this->mUpload->getMaxUploadSize();
+ if ( $this->mParams['filesize'] > $maxSize ) {
$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
}
+ if ( !$this->mUpload->getTitle() ) {
+ $this->dieUsage( 'Invalid file title supplied', 'internal-error' );
+ }
+ } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
+ // defer verification to background process
} else {
+ wfDebug( __METHOD__ . 'about to verify' );
$this->verifyUpload();
}
-
+
// Check if the user has the rights to modify or overwrite the requested title
// (This check is irrelevant if stashing is already requested, since the errors
// can always be fixed by changing the title)
- if ( ! $this->mParams['stash'] ) {
+ if ( !$this->mParams['stash'] ) {
$permErrors = $this->mUpload->verifyTitlePermissions( $user );
if ( $permErrors !== true ) {
$this->dieRecoverableError( $permErrors[0], 'filename' );
}
}
- // Get the result based on the current upload context:
- $result = $this->getContextResult();
- if ( $result['result'] === 'Success' ) {
- $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
+ // Get the result based on the current upload context:
+ try {
+ $result = $this->getContextResult();
+ if ( $result['result'] === 'Success' ) {
+ $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
+ }
+ } catch ( UploadStashException $e ) { // XXX: don't spam exception log
+ $this->dieUsage( get_class( $e ) . ": " . $e->getMessage(), 'stasherror' );
}
$this->getResult()->addValue( null, $this->getModuleName(), $result );
@@ -111,14 +124,15 @@ class ApiUpload extends ApiBase {
// Cleanup any temporary mess
$this->mUpload->cleanupTempFile();
}
+
/**
- * Get an uplaod result based on upload context
+ * Get an upload result based on upload context
* @return array
*/
- private function getContextResult(){
+ private function getContextResult() {
$warnings = $this->getApiWarnings();
if ( $warnings && !$this->mParams['ignorewarnings'] ) {
- // Get warnings formated in result array format
+ // Get warnings formatted in result array format
return $this->getWarningsResult( $warnings );
} elseif ( $this->mParams['chunk'] ) {
// Add chunk, and get result
@@ -131,13 +145,14 @@ class ApiUpload extends ApiBase {
// performUpload will return a formatted properly for the API with status
return $this->performUpload( $warnings );
}
+
/**
- * Get Stash Result, throws an expetion if the file could not be stashed.
- * @param $warnings array Array of Api upload warnings
+ * Get Stash Result, throws an exception if the file could not be stashed.
+ * @param array $warnings Array of Api upload warnings
* @return array
*/
- private function getStashResult( $warnings ){
- $result = array ();
+ private function getStashResult( $warnings ) {
+ $result = array();
// Some uploads can request they be stashed, so as not to publish them immediately.
// In this case, a failure to stash ought to be fatal
try {
@@ -152,12 +167,13 @@ class ApiUpload extends ApiBase {
}
return $result;
}
+
/**
* Get Warnings Result
- * @param $warnings array Array of Api upload warnings
+ * @param array $warnings Array of Api upload warnings
* @return array
*/
- private function getWarningsResult( $warnings ){
+ private function getWarningsResult( $warnings ) {
$result = array();
$result['result'] = 'Warning';
$result['warnings'] = $warnings;
@@ -171,12 +187,13 @@ class ApiUpload extends ApiBase {
}
return $result;
}
+
/**
* Get the result of a chunk upload.
- * @param $warnings array Array of Api upload warnings
+ * @param array $warnings Array of Api upload warnings
* @return array
*/
- private function getChunkResult( $warnings ){
+ private function getChunkResult( $warnings ) {
$result = array();
$result['result'] = 'Continue';
@@ -186,55 +203,78 @@ class ApiUpload extends ApiBase {
$request = $this->getMain()->getRequest();
$chunkPath = $request->getFileTempname( 'chunk' );
$chunkSize = $request->getUpload( 'chunk' )->getSize();
- if ($this->mParams['offset'] == 0) {
+ if ( $this->mParams['offset'] == 0 ) {
try {
- $result['filekey'] = $this->performStash();
+ $filekey = $this->performStash();
} catch ( MWException $e ) {
// FIXME: Error handling here is wrong/different from rest of this
$this->dieUsage( $e->getMessage(), 'stashfailed' );
}
} else {
- $status = $this->mUpload->addChunk($chunkPath, $chunkSize,
- $this->mParams['offset']);
+ $filekey = $this->mParams['filekey'];
+ /** @var $status Status */
+ $status = $this->mUpload->addChunk(
+ $chunkPath, $chunkSize, $this->mParams['offset'] );
if ( !$status->isGood() ) {
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
return array();
}
+ }
- // Check we added the last chunk:
- if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) {
+ // Check we added the last chunk:
+ if ( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) {
+ if ( $this->mParams['async'] ) {
+ $progress = UploadBase::getSessionStatus( $filekey );
+ if ( $progress && $progress['result'] === 'Poll' ) {
+ $this->dieUsage( "Chunk assembly already in progress.", 'stashfailed' );
+ }
+ UploadBase::setSessionStatus(
+ $filekey,
+ array( 'result' => 'Poll',
+ 'stage' => 'queued', 'status' => Status::newGood() )
+ );
+ $ok = JobQueueGroup::singleton()->push( new AssembleUploadChunksJob(
+ Title::makeTitle( NS_FILE, $filekey ),
+ array(
+ 'filename' => $this->mParams['filename'],
+ 'filekey' => $filekey,
+ 'session' => $this->getContext()->exportSession()
+ )
+ ) );
+ if ( $ok ) {
+ $result['result'] = 'Poll';
+ } else {
+ UploadBase::setSessionStatus( $filekey, false );
+ $this->dieUsage(
+ "Failed to start AssembleUploadChunks.php", 'stashfailed' );
+ }
+ } else {
$status = $this->mUpload->concatenateChunks();
-
if ( !$status->isGood() ) {
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
return array();
}
- // We have a new filekey for the fully concatenated file.
- $result['filekey'] = $this->mUpload->getLocalFile()->getFileKey();
-
- // Remove chunk from stash. (Checks against user ownership of chunks.)
- $this->mUpload->stash->removeFile( $this->mParams['filekey'] );
+ // The fully concatenated file has a new filekey. So remove
+ // the old filekey and fetch the new one.
+ $this->mUpload->stash->removeFile( $filekey );
+ $filekey = $this->mUpload->getLocalFile()->getFileKey();
$result['result'] = 'Success';
-
- } else {
-
- // Continue passing through the filekey for adding further chunks.
- $result['filekey'] = $this->mParams['filekey'];
}
}
+ $result['filekey'] = $filekey;
$result['offset'] = $this->mParams['offset'] + $chunkSize;
return $result;
}
-
+
/**
* Stash the file and return the file key
* Also re-raises exceptions with slightly more informative message strings (useful for API)
* @throws MWException
* @return String file key
*/
- function performStash() {
+ private function performStash() {
try {
$stashFile = $this->mUpload->stashFile();
@@ -244,7 +284,7 @@ class ApiUpload extends ApiBase {
$fileKey = $stashFile->getFileKey();
} catch ( MWException $e ) {
$message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
- wfDebug( __METHOD__ . ' ' . $message . "\n");
+ wfDebug( __METHOD__ . ' ' . $message . "\n" );
throw new MWException( $message );
}
return $fileKey;
@@ -254,12 +294,12 @@ class ApiUpload extends ApiBase {
* Throw an error that the user can recover from by providing a better
* value for $parameter
*
- * @param $error array Error array suitable for passing to dieUsageMsg()
- * @param $parameter string Parameter that needs revising
- * @param $data array Optional extra data to pass to the user
+ * @param array $error Error array suitable for passing to dieUsageMsg()
+ * @param string $parameter Parameter that needs revising
+ * @param array $data Optional extra data to pass to the user
* @throws UsageException
*/
- function dieRecoverableError( $error, $parameter, $data = array() ) {
+ private function dieRecoverableError( $error, $parameter, $data = array() ) {
try {
$data['filekey'] = $this->performStash();
$data['sessionkey'] = $data['filekey'];
@@ -283,11 +323,27 @@ class ApiUpload extends ApiBase {
$request = $this->getMain()->getRequest();
// chunk or one and only one of the following parameters is needed
- if( !$this->mParams['chunk'] ) {
+ if ( !$this->mParams['chunk'] ) {
$this->requireOnlyOneParameter( $this->mParams,
'filekey', 'file', 'url', 'statuskey' );
}
+ // Status report for "upload to stash"/"upload from stash"
+ if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
+ $progress = UploadBase::getSessionStatus( $this->mParams['filekey'] );
+ if ( !$progress ) {
+ $this->dieUsage( 'No result in status data', 'missingresult' );
+ } elseif ( !$progress['status']->isGood() ) {
+ $this->dieUsage( $progress['status']->getWikiText(), 'stashfailed' );
+ }
+ if ( isset( $progress['status']->value['verification'] ) ) {
+ $this->checkVerification( $progress['status']->value['verification'] );
+ }
+ unset( $progress['status'] ); // remove Status object
+ $this->getResult()->addValue( null, $this->getModuleName(), $progress );
+ return false;
+ }
+
if ( $this->mParams['statuskey'] ) {
$this->checkAsyncDownloadEnabled();
@@ -302,7 +358,6 @@ class ApiUpload extends ApiBase {
}
$this->getResult()->addValue( null, $this->getModuleName(), $sessionData );
return false;
-
}
// The following modules all require the filename parameter to be set
@@ -313,7 +368,7 @@ class ApiUpload extends ApiBase {
if ( $this->mParams['chunk'] ) {
// Chunk upload
$this->mUpload = new UploadFromChunks();
- if( isset( $this->mParams['filekey'] ) ){
+ if ( isset( $this->mParams['filekey'] ) ) {
// handle new chunk
$this->mUpload->continueChunks(
$this->mParams['filename'],
@@ -334,8 +389,11 @@ class ApiUpload extends ApiBase {
}
$this->mUpload = new UploadFromStash( $this->getUser() );
-
- $this->mUpload->initialize( $this->mParams['filekey'], $this->mParams['filename'] );
+ // This will not download the temp file in initialize() in async mode.
+ // We still have enough information to call checkWarnings() and such.
+ $this->mUpload->initialize(
+ $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
+ );
} elseif ( isset( $this->mParams['file'] ) ) {
$this->mUpload = new UploadFromFile();
$this->mUpload->initialize(
@@ -352,6 +410,10 @@ class ApiUpload extends ApiBase {
$this->dieUsageMsg( 'copyuploadbaddomain' );
}
+ if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
+ $this->dieUsageMsg( 'copyuploadbadurl' );
+ }
+
$async = false;
if ( $this->mParams['asyncdownload'] ) {
$this->checkAsyncDownloadEnabled();
@@ -396,16 +458,23 @@ class ApiUpload extends ApiBase {
/**
* Performs file verification, dies on error.
*/
- protected function verifyUpload( ) {
- global $wgFileExtensions;
-
- $verification = $this->mUpload->verifyUpload( );
+ protected function verifyUpload() {
+ $verification = $this->mUpload->verifyUpload();
if ( $verification['status'] === UploadBase::OK ) {
return;
}
- // TODO: Move them to ApiBase's message map
- switch( $verification['status'] ) {
+ $this->checkVerification( $verification );
+ }
+
+ /**
+ * Performs file verification, dies on error.
+ */
+ protected function checkVerification( array $verification ) {
+ global $wgFileExtensions;
+
+ // @todo Move them to ApiBase's message map
+ switch ( $verification['status'] ) {
// Recoverable errors
case UploadBase::MIN_LENGTH_PARTNAME:
$this->dieRecoverableError( 'filename-tooshort', 'filename' );
@@ -435,7 +504,7 @@ class ApiUpload extends ApiBase {
case UploadBase::FILETYPE_BADTYPE:
$extradata = array(
'filetype' => $verification['finalExt'],
- 'allowed' => $wgFileExtensions
+ 'allowed' => array_values( array_unique( $wgFileExtensions ) )
);
$this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' );
@@ -460,12 +529,11 @@ class ApiUpload extends ApiBase {
break;
default:
$this->dieUsage( 'An unknown error occurred', 'unknown-error',
- 0, array( 'code' => $verification['status'] ) );
+ 0, array( 'code' => $verification['status'] ) );
break;
}
}
-
/**
* Check warnings.
* Returns a suitable array for inclusion into API results if there were warnings
@@ -497,18 +565,18 @@ class ApiUpload extends ApiBase {
if ( isset( $warnings['exists'] ) ) {
$warning = $warnings['exists'];
unset( $warnings['exists'] );
- $warnings[$warning['warning']] = $warning['file']->getName();
+ $localFile = isset( $warning['normalizedFile'] ) ? $warning['normalizedFile'] : $warning['file'];
+ $warnings[$warning['warning']] = $localFile->getName();
}
}
return $warnings;
}
-
/**
* Perform the actual upload. Returns a suitable result array on success;
* dies on failure.
*
- * @param $warnings array Array of Api upload warnings
+ * @param array $warnings Array of Api upload warnings
* @return array
*/
protected function performUpload( $warnings ) {
@@ -517,6 +585,7 @@ class ApiUpload extends ApiBase {
$this->mParams['text'] = $this->mParams['comment'];
}
+ /** @var $file File */
$file = $this->mUpload->getLocalFile();
$watch = $this->getWatchlistValue( $this->mParams['watchlist'], $file->getTitle() );
@@ -526,29 +595,57 @@ class ApiUpload extends ApiBase {
}
// No errors, no warnings: do the upload
- $status = $this->mUpload->performUpload( $this->mParams['comment'],
- $this->mParams['text'], $watch, $this->getUser() );
-
- if ( !$status->isGood() ) {
- $error = $status->getErrorsArray();
-
- if ( count( $error ) == 1 && $error[0][0] == 'async' ) {
- // The upload can not be performed right now, because the user
- // requested so
- return array(
- 'result' => 'Queued',
- 'statuskey' => $error[0][1],
- );
+ if ( $this->mParams['async'] ) {
+ $progress = UploadBase::getSessionStatus( $this->mParams['filekey'] );
+ if ( $progress && $progress['result'] === 'Poll' ) {
+ $this->dieUsage( "Upload from stash already in progress.", 'publishfailed' );
+ }
+ UploadBase::setSessionStatus(
+ $this->mParams['filekey'],
+ array( 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() )
+ );
+ $ok = JobQueueGroup::singleton()->push( new PublishStashedFileJob(
+ Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
+ array(
+ 'filename' => $this->mParams['filename'],
+ 'filekey' => $this->mParams['filekey'],
+ 'comment' => $this->mParams['comment'],
+ 'text' => $this->mParams['text'],
+ 'watch' => $watch,
+ 'session' => $this->getContext()->exportSession()
+ )
+ ) );
+ if ( $ok ) {
+ $result['result'] = 'Poll';
} else {
- $this->getResult()->setIndexedTagName( $error, 'error' );
+ UploadBase::setSessionStatus( $this->mParams['filekey'], false );
+ $this->dieUsage(
+ "Failed to start PublishStashedFile.php", 'publishfailed' );
+ }
+ } else {
+ /** @var $status Status */
+ $status = $this->mUpload->performUpload( $this->mParams['comment'],
+ $this->mParams['text'], $watch, $this->getUser() );
+
+ if ( !$status->isGood() ) {
+ $error = $status->getErrorsArray();
+
+ if ( count( $error ) == 1 && $error[0][0] == 'async' ) {
+ // The upload can not be performed right now, because the user
+ // requested so
+ return array(
+ 'result' => 'Queued',
+ 'statuskey' => $error[0][1],
+ );
+ } else {
+ $this->getResult()->setIndexedTagName( $error, 'error' );
- $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
+ $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
+ }
}
+ $result['result'] = 'Success';
}
- $file = $this->mUpload->getLocalFile();
-
- $result['result'] = 'Success';
$result['filename'] = $file->getName();
if ( $warnings && count( $warnings ) > 0 ) {
$result['warnings'] = $warnings;
@@ -563,7 +660,7 @@ class ApiUpload extends ApiBase {
protected function checkAsyncDownloadEnabled() {
global $wgAllowAsyncCopyUploads;
if ( !$wgAllowAsyncCopyUploads ) {
- $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled');
+ $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled' );
}
}
@@ -601,7 +698,9 @@ class ApiUpload extends ApiBase {
),
),
'ignorewarnings' => false,
- 'file' => null,
+ 'file' => array(
+ ApiBase::PARAM_TYPE => 'upload',
+ ),
'url' => null,
'filekey' => null,
'sessionkey' => array(
@@ -612,11 +711,15 @@ class ApiUpload extends ApiBase {
'filesize' => null,
'offset' => null,
- 'chunk' => null,
+ 'chunk' => array(
+ ApiBase::PARAM_TYPE => 'upload',
+ ),
+ 'async' => false,
'asyncdownload' => false,
'leavemessage' => false,
'statuskey' => null,
+ 'checkstatus' => false,
);
return $params;
@@ -641,9 +744,11 @@ class ApiUpload extends ApiBase {
'offset' => 'Offset of chunk in bytes',
'filesize' => 'Filesize of entire upload',
+ 'async' => 'Make potentially large file operations asynchronous when possible',
'asyncdownload' => 'Make fetching a URL asynchronous',
'leavemessage' => 'If asyncdownload is used, leave a message on the user talk page if finished',
- 'statuskey' => 'Fetch the upload status for this file key',
+ 'statuskey' => 'Fetch the upload status for this file key (upload by URL)',
+ 'checkstatus' => 'Only fetch the upload status for the given file key',
);
return $params;
@@ -692,7 +797,7 @@ class ApiUpload extends ApiBase {
' * 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 "filekey" parameter',
'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when',
- 'sending the "file". Also you must get and send an edit token before doing any upload stuff'
+ 'sending the "file". Also you must get and send an edit token before doing any upload stuff'
);
}
@@ -712,8 +817,10 @@ class ApiUpload extends ApiBase {
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' => 'publishfailed', 'info' => 'Publishing of stashed file failed' ),
array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ),
array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ),
+ array( 'code' => 'stasherror', 'info' => 'An upload stash error occurred' ),
array( 'fileexists-forbidden' ),
array( 'fileexists-shared-forbidden' ),
)
@@ -740,8 +847,4 @@ class ApiUpload extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Upload';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php
index cbb66a41..7d308285 100644
--- a/includes/api/ApiUserrights.php
+++ b/includes/api/ApiUserrights.php
@@ -30,10 +30,6 @@
*/
class ApiUserrights extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
private $mUser = null;
public function execute() {
@@ -42,6 +38,7 @@ class ApiUserrights extends ApiBase {
$user = $this->getUrUser();
$form = new UserrightsPage;
+ $form->setContext( $this->getContext() );
$r['user'] = $user->getName();
$r['userid'] = $user->getId();
list( $r['added'], $r['removed'] ) =
@@ -66,10 +63,10 @@ class ApiUserrights extends ApiBase {
$params = $this->extractRequestParams();
$form = new UserrightsPage;
+ $form->setContext( $this->getContext() );
$status = $form->fetchUser( $params['user'] );
if ( !$status->isOK() ) {
- $errors = $status->getErrorsArray();
- $this->dieUsageMsg( $errors[0] );
+ $this->dieStatus( $status );
} else {
$user = $status->value;
}
@@ -87,7 +84,7 @@ class ApiUserrights extends ApiBase {
}
public function getAllowedParams() {
- return array (
+ return array(
'user' => array(
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
@@ -141,8 +138,4 @@ class ApiUserrights extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:User_group_membership';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php
index 0509f1f8..c7d636a1 100644
--- a/includes/api/ApiWatch.php
+++ b/includes/api/ApiWatch.php
@@ -31,36 +31,48 @@
*/
class ApiWatch extends ApiBase {
- public function __construct( $main, $action ) {
- parent::__construct( $main, $action );
- }
-
public function execute() {
$user = $this->getUser();
if ( !$user->isLoggedIn() ) {
$this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
}
+ if ( !$user->isAllowed( 'editmywatchlist' ) ) {
+ $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' );
+ }
$params = $this->extractRequestParams();
$title = Title::newFromText( $params['title'] );
- if ( !$title || $title->getNamespace() < 0 ) {
+ if ( !$title || $title->isExternal() || !$title->canExist() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
$res = array( 'title' => $title->getPrefixedText() );
+ // Currently unnecessary, code to act as a safeguard against any change in current behavior of uselang
+ // Copy from ApiParse
+ $oldLang = null;
+ if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) {
+ $oldLang = $this->getContext()->getLanguage(); // Backup language
+ $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) );
+ }
+
if ( $params['unwatch'] ) {
$res['unwatched'] = '';
$res['message'] = $this->msg( 'removedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock();
- $success = UnwatchAction::doUnwatch( $title, $user );
+ $status = UnwatchAction::doUnwatch( $title, $user );
} else {
$res['watched'] = '';
$res['message'] = $this->msg( 'addedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock();
- $success = WatchAction::doWatch( $title, $user );
+ $status = WatchAction::doWatch( $title, $user );
}
- if ( !$success ) {
- $this->dieUsageMsg( 'hookaborted' );
+
+ if ( !is_null( $oldLang ) ) {
+ $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang
+ }
+
+ if ( !$status->isOK() ) {
+ $this->dieStatus( $status );
}
$this->getResult()->addValue( null, $this->getModuleName(), $res );
}
@@ -88,6 +100,7 @@ class ApiWatch extends ApiBase {
ApiBase::PARAM_REQUIRED => true
),
'unwatch' => false,
+ 'uselang' => null,
'token' => array(
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
@@ -99,6 +112,7 @@ class ApiWatch extends ApiBase {
return array(
'title' => 'The page to (un)watch',
'unwatch' => 'If set the page will be unwatched rather than watched',
+ 'uselang' => 'Language to show the message in',
'token' => 'A token previously acquired via prop=info',
);
}
@@ -136,8 +150,4 @@ class ApiWatch extends ApiBase {
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Watch';
}
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
}
diff --git a/includes/BacklinkCache.php b/includes/cache/BacklinkCache.php
index 05bf3186..193f20fe 100644
--- a/includes/BacklinkCache.php
+++ b/includes/cache/BacklinkCache.php
@@ -20,6 +20,7 @@
*
* @file
* @author Tim Starling
+ * @author Aaron Schulz
* @copyright © 2009, Tim Starling, Domas Mituzas
* @copyright © 2010, Max Sem
* @copyright © 2011, Antoine Musso
@@ -103,7 +104,7 @@ class BacklinkCache {
self::$cache = new ProcessCacheLRU( 1 );
}
$dbKey = $title->getPrefixedDBkey();
- if ( !self::$cache->has( $dbKey, 'obj' ) ) {
+ if ( !self::$cache->has( $dbKey, 'obj', 3600 ) ) {
self::$cache->set( $dbKey, 'obj', new self( $title ) );
}
return self::$cache->get( $dbKey, 'obj' );
@@ -147,85 +148,88 @@ class BacklinkCache {
if ( !isset( $this->db ) ) {
$this->db = wfGetDB( DB_SLAVE );
}
-
return $this->db;
}
/**
* Get the backlinks for a given table. Cached in process memory only.
* @param $table String
- * @param $startId Integer or false
- * @param $endId Integer or false
+ * @param $startId Integer|false
+ * @param $endId Integer|false
+ * @param $max Integer|INF
* @return TitleArrayFromResult
*/
- public function getLinks( $table, $startId = false, $endId = false ) {
+ public function getLinks( $table, $startId = false, $endId = false, $max = INF ) {
+ return TitleArray::newFromResult( $this->queryLinks( $table, $startId, $endId, $max ) );
+ }
+
+ /**
+ * Get the backlinks for a given table. Cached in process memory only.
+ * @param $table String
+ * @param $startId Integer|false
+ * @param $endId Integer|false
+ * @param $max Integer|INF
+ * @return ResultWrapper
+ */
+ protected function queryLinks( $table, $startId, $endId, $max ) {
wfProfileIn( __METHOD__ );
$fromField = $this->getPrefix( $table ) . '_from';
- if ( $startId || $endId ) {
- // Partial range, not cached
- wfDebug( __METHOD__ . ": from DB (uncacheable range)\n" );
+ if ( !$startId && !$endId && is_infinite( $max )
+ && isset( $this->fullResultCache[$table] ) )
+ {
+ wfDebug( __METHOD__ . ": got results from cache\n" );
+ $res = $this->fullResultCache[$table];
+ } else {
+ wfDebug( __METHOD__ . ": got results from DB\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.
if ( $startId ) {
$conds[] = "$fromField >= " . intval( $startId );
}
-
if ( $endId ) {
$conds[] = "$fromField <= " . intval( $endId );
}
+ $options = array( 'STRAIGHT_JOIN', 'ORDER BY' => $fromField );
+ if ( is_finite( $max ) && $max > 0 ) {
+ $options['LIMIT'] = $max;
+ }
$res = $this->getDB()->select(
array( $table, 'page' ),
array( 'page_namespace', 'page_title', 'page_id' ),
$conds,
__METHOD__,
- array(
- 'STRAIGHT_JOIN',
- 'ORDER BY' => $fromField
- ) );
- $ta = TitleArray::newFromResult( $res );
-
- wfProfileOut( __METHOD__ );
- return $ta;
- }
+ $options
+ );
- // @todo FIXME: Make this a function?
- if ( !isset( $this->fullResultCache[$table] ) ) {
- 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',
- 'ORDER BY' => $fromField,
- ) );
- $this->fullResultCache[$table] = $res;
+ if ( !$startId && !$endId && $res->numRows() < $max ) {
+ // The full results fit within the limit, so cache them
+ $this->fullResultCache[$table] = $res;
+ } else {
+ wfDebug( __METHOD__ . ": results from DB were uncacheable\n" );
+ }
}
- $ta = TitleArray::newFromResult( $this->fullResultCache[$table] );
-
wfProfileOut( __METHOD__ );
- return $ta;
+ return $res;
}
/**
* Get the field name prefix for a given table
* @param $table String
+ * @throws MWException
* @return null|string
*/
protected function getPrefix( $table ) {
static $prefixes = array(
- 'pagelinks' => 'pl',
- 'imagelinks' => 'il',
+ 'pagelinks' => 'pl',
+ 'imagelinks' => 'il',
'categorylinks' => 'cl',
'templatelinks' => 'tl',
- 'redirect' => 'rd',
+ 'redirect' => 'rd',
);
if ( isset( $prefixes[$table] ) ) {
@@ -233,7 +237,7 @@ class BacklinkCache {
} else {
$prefix = null;
wfRunHooks( 'BacklinkCacheGetPrefix', array( $table, &$prefix ) );
- if( $prefix ) {
+ if ( $prefix ) {
return $prefix;
} else {
throw new MWException( "Invalid table \"$table\" in " . __CLASS__ );
@@ -245,6 +249,7 @@ class BacklinkCache {
* Get the SQL condition array for selecting backlinks, with a join
* on the page table.
* @param $table String
+ * @throws MWException
* @return array|null
*/
protected function getConditions( $table ) {
@@ -257,17 +262,17 @@ class BacklinkCache {
case 'templatelinks':
$conds = array(
"{$prefix}_namespace" => $this->title->getNamespace(),
- "{$prefix}_title" => $this->title->getDBkey(),
+ "{$prefix}_title" => $this->title->getDBkey(),
"page_id={$prefix}_from"
);
break;
case 'redirect':
$conds = array(
"{$prefix}_namespace" => $this->title->getNamespace(),
- "{$prefix}_title" => $this->title->getDBkey(),
+ "{$prefix}_title" => $this->title->getDBkey(),
$this->getDb()->makeList( array(
- "{$prefix}_interwiki = ''",
- "{$prefix}_interwiki is null",
+ "{$prefix}_interwiki" => '',
+ "{$prefix}_interwiki IS NULL",
), LIST_OR ),
"page_id={$prefix}_from"
);
@@ -287,31 +292,58 @@ class BacklinkCache {
default:
$conds = null;
wfRunHooks( 'BacklinkCacheGetConditions', array( $table, $this->title, &$conds ) );
- if( !$conds )
+ if ( !$conds ) {
throw new MWException( "Invalid table \"$table\" in " . __CLASS__ );
+ }
}
return $conds;
}
/**
+ * Check if there are any backlinks
+ * @param $table String
+ * @return bool
+ */
+ public function hasLinks( $table ) {
+ return ( $this->getNumLinks( $table, 1 ) > 0 );
+ }
+
+ /**
* Get the approximate number of backlinks
* @param $table String
+ * @param $max integer|INF Only count up to this many backlinks
* @return integer
*/
- public function getNumLinks( $table ) {
- if ( isset( $this->fullResultCache[$table] ) ) {
- return $this->fullResultCache[$table]->numRows();
- }
+ public function getNumLinks( $table, $max = INF ) {
+ global $wgMemc;
+ // 1) try partition cache ...
if ( isset( $this->partitionCache[$table] ) ) {
$entry = reset( $this->partitionCache[$table] );
- return $entry['numRows'];
+ return min( $max, $entry['numRows'] );
}
- $titleArray = $this->getLinks( $table );
+ // 2) ... then try full result cache ...
+ if ( isset( $this->fullResultCache[$table] ) ) {
+ return min( $max, $this->fullResultCache[$table]->numRows() );
+ }
- return $titleArray->count();
+ $memcKey = wfMemcKey( 'numbacklinks', md5( $this->title->getPrefixedDBkey() ), $table );
+
+ // 3) ... fallback to memcached ...
+ $count = $wgMemc->get( $memcKey );
+ if ( $count ) {
+ return min( $max, $count );
+ }
+
+ // 4) fetch from the database ...
+ $count = $this->getLinks( $table, false, false, $max )->count();
+ if ( $count < $max ) { // full count
+ $wgMemc->set( $memcKey, $count, self::CACHE_EXPIRY );
+ }
+
+ return min( $max, $count );
}
/**
@@ -319,14 +351,14 @@ 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 $table String: the links table name
+ * @param string $table the links table name
* @param $batchSize Integer
* @return Array
*/
public function partition( $table, $batchSize ) {
+ global $wgMemc;
// 1) try partition cache ...
-
if ( isset( $this->partitionCache[$table][$batchSize] ) ) {
wfDebug( __METHOD__ . ": got from partition cache\n" );
return $this->partitionCache[$table][$batchSize]['batches'];
@@ -336,18 +368,12 @@ class BacklinkCache {
$cacheEntry =& $this->partitionCache[$table][$batchSize];
// 2) ... then try full result cache ...
-
if ( isset( $this->fullResultCache[$table] ) ) {
$cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
wfDebug( __METHOD__ . ": got from full result cache\n" );
-
return $cacheEntry['batches'];
}
- // 3) ... fallback to memcached ...
-
- global $wgMemc;
-
$memcKey = wfMemcKey(
'backlinks',
md5( $this->title->getPrefixedDBkey() ),
@@ -355,23 +381,44 @@ class BacklinkCache {
$batchSize
);
+ // 3) ... fallback to memcached ...
$memcValue = $wgMemc->get( $memcKey );
-
if ( is_array( $memcValue ) ) {
$cacheEntry = $memcValue;
wfDebug( __METHOD__ . ": got from memcached $memcKey\n" );
-
return $cacheEntry['batches'];
}
-
// 4) ... finally fetch from the slow database :(
+ $cacheEntry = array( 'numRows' => 0, 'batches' => array() ); // final result
+ // Do the selects in batches to avoid client-side OOMs (bug 43452).
+ // Use a LIMIT that plays well with $batchSize to keep equal sized partitions.
+ $selectSize = max( $batchSize, 200000 - ( 200000 % $batchSize ) );
+ $start = false;
+ do {
+ $res = $this->queryLinks( $table, $start, false, $selectSize );
+ $partitions = $this->partitionResult( $res, $batchSize, false );
+ // Merge the link count and range partitions for this chunk
+ $cacheEntry['numRows'] += $partitions['numRows'];
+ $cacheEntry['batches'] = array_merge( $cacheEntry['batches'], $partitions['batches'] );
+ if ( count( $partitions['batches'] ) ) {
+ list( $lStart, $lEnd ) = end( $partitions['batches'] );
+ $start = $lEnd + 1; // pick up after this inclusive range
+ }
+ } while ( $partitions['numRows'] >= $selectSize );
+ // Make sure the first range has start=false and the last one has end=false
+ if ( count( $cacheEntry['batches'] ) ) {
+ $cacheEntry['batches'][0][0] = false;
+ $cacheEntry['batches'][count( $cacheEntry['batches'] ) - 1][1] = false;
+ }
- $this->getLinks( $table );
- $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
- // Save to memcached
+ // Save partitions to memcached
$wgMemc->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY );
+ // Save backlink count to memcached
+ $memcKey = wfMemcKey( 'numbacklinks', md5( $this->title->getPrefixedDBkey() ), $table );
+ $wgMemc->set( $memcKey, $cacheEntry['numRows'], self::CACHE_EXPIRY );
+
wfDebug( __METHOD__ . ": got from database\n" );
return $cacheEntry['batches'];
}
@@ -380,30 +427,32 @@ class BacklinkCache {
* Partition a DB result with backlinks in it into batches
* @param $res ResultWrapper database result
* @param $batchSize integer
- * @return array @see
+ * @param $isComplete bool Whether $res includes all the backlinks
+ * @throws MWException
+ * @return array
*/
- protected function partitionResult( $res, $batchSize ) {
+ protected function partitionResult( $res, $batchSize, $isComplete = true ) {
$batches = array();
$numRows = $res->numRows();
$numBatches = ceil( $numRows / $batchSize );
for ( $i = 0; $i < $numBatches; $i++ ) {
- if ( $i == 0 ) {
+ if ( $i == 0 && $isComplete ) {
$start = false;
} else {
- $rowNum = intval( $numRows * $i / $numBatches );
+ $rowNum = $i * $batchSize;
$res->seek( $rowNum );
$row = $res->fetchObject();
- $start = $row->page_id;
+ $start = (int)$row->page_id;
}
- if ( $i == $numBatches - 1 ) {
+ if ( $i == ( $numBatches - 1 ) && $isComplete ) {
$end = false;
} else {
- $rowNum = intval( $numRows * ( $i + 1 ) / $numBatches );
+ $rowNum = min( $numRows - 1, ( $i + 1 ) * $batchSize - 1 );
$res->seek( $rowNum );
$row = $res->fetchObject();
- $end = $row->page_id - 1;
+ $end = (int)$row->page_id;
}
# Sanity check order
diff --git a/includes/cache/CacheDependency.php b/includes/cache/CacheDependency.php
index a3c2b52a..32bcdf7f 100644
--- a/includes/cache/CacheDependency.php
+++ b/includes/cache/CacheDependency.php
@@ -74,7 +74,7 @@ class DependencyWrapper {
/**
* Get the user-defined value
- * @return bool|\Mixed
+ * @return bool|Mixed
*/
function getValue() {
return $this->value;
@@ -98,11 +98,11 @@ class DependencyWrapper {
* calculated value will be stored to the cache in a wrapper.
*
* @param $cache BagOStuff a cache object such as $wgMemc
- * @param $key String: the cache key
+ * @param string $key 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
+ * @param array $callbackParams the function parameters for the callback
+ * @param array $deps the dependencies to store on a cache miss. Note: these
* are not the dependencies used on a cache hit! Cache hits use the stored
* dependency array.
*
@@ -153,7 +153,7 @@ class FileDependency extends CacheDependency {
/**
* Create a file dependency
*
- * @param $filename String: the name of the file, preferably fully qualified
+ * @param string $filename 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.
@@ -404,7 +404,7 @@ class GlobalDependency extends CacheDependency {
* @return bool
*/
function isExpired() {
- if( !isset($GLOBALS[$this->name]) ) {
+ if ( !isset( $GLOBALS[$this->name] ) ) {
return true;
}
return $GLOBALS[$this->name] != $this->value;
diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php
index c0c5609c..d4bf5ee6 100644
--- a/includes/cache/FileCacheBase.php
+++ b/includes/cache/FileCacheBase.php
@@ -35,7 +35,7 @@ abstract class FileCacheBase {
/* lazy loaded */
protected $mCached;
- /* @TODO: configurable? */
+ /* @todo configurable? */
const MISS_FACTOR = 15; // log 1 every MISS_FACTOR cache misses
const MISS_TTL_SEC = 3600; // how many seconds ago is "recent"
@@ -107,7 +107,7 @@ abstract class FileCacheBase {
/**
* Check if up to date cache file exists
- * @param $timestamp string MW_TS timestamp
+ * @param string $timestamp MW_TS timestamp
*
* @return bool
*/
@@ -138,7 +138,7 @@ abstract class FileCacheBase {
* @return string
*/
public function fetchText() {
- if( $this->useGzip() ) {
+ if ( $this->useGzip() ) {
$fh = gzopen( $this->cachePath(), 'rb' );
return stream_get_contents( $fh );
} else {
@@ -163,7 +163,7 @@ abstract class FileCacheBase {
$this->checkCacheDirs(); // build parent dir
if ( !file_put_contents( $this->cachePath(), $text, LOCK_EX ) ) {
- wfDebug( __METHOD__ . "() failed saving ". $this->cachePath() . "\n");
+ wfDebug( __METHOD__ . "() failed saving " . $this->cachePath() . "\n" );
$this->mCached = null;
return false;
}
@@ -229,7 +229,7 @@ abstract class FileCacheBase {
public function incrMissesRecent( WebRequest $request ) {
global $wgMemc;
if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) {
- # Get a large IP range that should include the user even if that
+ # Get a large IP range that should include the user even if that
# person's IP address changes
$ip = $request->getIP();
if ( !IP::isValid( $ip ) ) {
diff --git a/includes/cache/GenderCache.php b/includes/cache/GenderCache.php
index 2a169bb3..a933527a 100644
--- a/includes/cache/GenderCache.php
+++ b/includes/cache/GenderCache.php
@@ -59,22 +59,21 @@ class GenderCache {
/**
* Returns the gender for given username.
- * @param $username String or User: username
- * @param $caller String: the calling method
+ * @param string $username or User: username
+ * @param string $caller the calling method
* @return String
*/
public function getGenderOf( $username, $caller = '' ) {
global $wgUser;
- if( $username instanceof User ) {
+ if ( $username instanceof User ) {
$username = $username->getName();
}
$username = self::normalizeUsername( $username );
if ( !isset( $this->cache[$username] ) ) {
-
if ( $this->misses >= $this->missLimit && $wgUser->getName() !== $username ) {
- if( $this->misses === $this->missLimit ) {
+ if ( $this->misses === $this->missLimit ) {
$this->misses++;
wfDebug( __METHOD__ . ": too many misses, returning default onwards\n" );
}
@@ -84,7 +83,6 @@ class GenderCache {
$this->misses++;
$this->doQuery( $username, $caller );
}
-
}
/* Undefined if there is a valid username which for some reason doesn't
@@ -102,7 +100,9 @@ class GenderCache {
public function doLinkBatch( $data, $caller = '' ) {
$users = array();
foreach ( $data as $ns => $pagenames ) {
- if ( !MWNamespace::hasGenderDistinction( $ns ) ) continue;
+ if ( !MWNamespace::hasGenderDistinction( $ns ) ) {
+ continue;
+ }
foreach ( array_keys( $pagenames ) as $username ) {
$users[$username] = true;
}
@@ -116,7 +116,7 @@ class GenderCache {
*
* @since 1.20
* @param $titles List: array of Title objects or strings
- * @param $caller String: the calling method
+ * @param string $caller the calling method
*/
public function doTitlesArray( $titles, $caller = '' ) {
$users = array();
@@ -137,20 +137,20 @@ class GenderCache {
/**
* Preloads genders for given list of users.
* @param $users List|String: usernames
- * @param $caller String: the calling method
+ * @param string $caller the calling method
*/
public function doQuery( $users, $caller = '' ) {
$default = $this->getDefault();
$usersToCheck = array();
- foreach ( (array) $users as $value ) {
+ foreach ( (array)$users as $value ) {
$name = self::normalizeUsername( $value );
// Skip users whose gender setting we already know
if ( !isset( $this->cache[$name] ) ) {
// For existing users, this value will be overwritten by the correct value
$this->cache[$name] = $default;
// query only for valid names, which can be in the database
- if( User::isValidUserName( $name ) ) {
+ if ( User::isValidUserName( $name ) ) {
$usersToCheck[] = $name;
}
}
diff --git a/includes/cache/HTMLCacheUpdate.php b/includes/cache/HTMLCacheUpdate.php
index 0a3c0023..992809ef 100644
--- a/includes/cache/HTMLCacheUpdate.php
+++ b/includes/cache/HTMLCacheUpdate.php
@@ -23,24 +23,6 @@
/**
* Class to invalidate the HTML cache of all the pages linking to a given title.
- * Small numbers of links will be done immediately, large numbers are pushed onto
- * the job queue.
- *
- * This class is designed to work efficiently with small numbers of links, and
- * to work reasonably well with up to ~10^5 links. Above ~10^6 links, the memory
- * and time requirements of loading all backlinked IDs in doUpdate() might become
- * prohibitive. The requirements measured at Wikimedia are approximately:
- *
- * memory: 48 bytes per row
- * time: 16us per row for the query plus processing
- *
- * The reason this query is done is to support partitioning of the job
- * by backlinked ID. The memory issue could be allieviated by doing this query in
- * batches, but of course LIMIT with an offset is inefficient on the DB side.
- *
- * The class is nevertheless a vast improvement on the previous method of using
- * File::getLinksTo() and Title::touchArray(), which uses about 2KB of memory per
- * link.
*
* @ingroup Cache
*/
@@ -50,8 +32,7 @@ class HTMLCacheUpdate implements DeferrableUpdate {
*/
public $mTitle;
- public $mTable, $mPrefix, $mStart, $mEnd;
- public $mRowsPerJob, $mRowsPerQuery;
+ public $mTable;
/**
* @param $titleTo
@@ -59,202 +40,34 @@ class HTMLCacheUpdate implements DeferrableUpdate {
* @param $start bool
* @param $end bool
*/
- function __construct( $titleTo, $table, $start = false, $end = false ) {
- global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
-
+ function __construct( Title $titleTo, $table ) {
$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() {
- if ( $this->mStart || $this->mEnd ) {
- $this->doPartialUpdate();
- return;
- }
-
- # 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->invalidateTitles( $titleArray );
- }
- }
- }
-
- /**
- * 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 );
- }
- }
+ wfProfileIn( __METHOD__ );
- /**
- * 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.
- *
- * @param $titleArray array
- */
- 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 );
- }
-
- /**
- * @return mixed
- */
- protected function insertJobs() {
- $batches = $this->mCache->partition( $this->mTable, $this->mRowsPerJob );
- if ( !$batches ) {
- return;
- }
- $jobs = array();
- foreach ( $batches as $batch ) {
- $params = array(
+ $job = new HTMLCacheUpdateJob(
+ $this->mTitle,
+ array(
'table' => $this->mTable,
- 'start' => $batch[0],
- 'end' => $batch[1],
- );
- $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params );
- }
- Job::batchInsert( $jobs );
- }
-
- /**
- * Invalidate an array (or iterator) of Title objects, right now
- * @param $titleArray array
- */
- protected function invalidateTitles( $titleArray ) {
- global $wgUseFileCache, $wgUseSquid;
-
- $dbw = wfGetDB( DB_MASTER );
- $timestamp = $dbw->timestamp();
-
- # Get all IDs in this query into an array
- $ids = array();
- foreach ( $titleArray as $title ) {
- $ids[] = $title->getArticleID();
- }
-
- if ( !$ids ) {
- return;
- }
-
- # Update page_touched
- $batches = array_chunk( $ids, $this->mRowsPerQuery );
- foreach ( $batches as $batch ) {
- $dbw->update( 'page',
- array( 'page_touched' => $timestamp ),
- array( 'page_id' => $batch ),
- __METHOD__
- );
- }
-
- # Update squid
- if ( $wgUseSquid ) {
- $u = SquidUpdate::newFromTitles( $titleArray );
- $u->doUpdate();
- }
+ ) + Job::newRootJobParams( // "overall" refresh links job info
+ "htmlCacheUpdate:{$this->mTable}:{$this->mTitle->getPrefixedText()}"
+ )
+ );
- # Update file cache
- if ( $wgUseFileCache ) {
- foreach ( $titleArray as $title ) {
- HTMLFileCache::clearFileCache( $title );
- }
+ $count = $this->mTitle->getBacklinkCache()->getNumLinks( $this->mTable, 200 );
+ if ( $count >= 200 ) { // many backlinks
+ JobQueueGroup::singleton()->push( $job );
+ JobQueueGroup::singleton()->deduplicateRootJob( $job );
+ } else { // few backlinks ($count might be off even if 0)
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->onTransactionIdle( function() use ( $job ) {
+ $job->run(); // just do the purge query now
+ } );
}
- }
-}
-
-
-/**
- * Job wrapper for HTMLCacheUpdate. Gets run whenever a related
- * job gets called from the queue.
- *
- * @ingroup JobQueue
- */
-class HTMLCacheUpdateJob extends Job {
- var $table, $start, $end;
-
- /**
- * Construct a job
- * @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 );
- $this->table = $params['table'];
- $this->start = $params['start'];
- $this->end = $params['end'];
- }
- public function run() {
- $update = new HTMLCacheUpdate( $this->title, $this->table, $this->start, $this->end );
- $update->doUpdate();
- return true;
+ wfProfileOut( __METHOD__ );
}
}
diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php
index 6bfeed32..ab379116 100644
--- a/includes/cache/HTMLFileCache.php
+++ b/includes/cache/HTMLFileCache.php
@@ -33,6 +33,7 @@ class HTMLFileCache extends FileCacheBase {
* Construct an ObjectFileCache from a Title and an action
* @param $title Title|string Title object or prefixed DB key string
* @param $action string
+ * @throws MWException
* @return HTMLFileCache
*/
public static function newFromTitle( $title, $action ) {
@@ -127,7 +128,7 @@ class HTMLFileCache extends FileCacheBase {
public function loadFromFileCache( IContextSource $context ) {
global $wgMimeType, $wgLanguageCode;
- wfDebug( __METHOD__ . "()\n");
+ wfDebug( __METHOD__ . "()\n" );
$filename = $this->cachePath();
$context->getOutput()->sendCacheControl();
@@ -162,15 +163,15 @@ class HTMLFileCache extends FileCacheBase {
return $text;
}
- wfDebug( __METHOD__ . "()\n", false);
+ wfDebug( __METHOD__ . "()\n", false );
$now = wfTimestampNow();
if ( $this->useGzip() ) {
$text = str_replace(
- '</html>', '<!-- Cached/compressed '.$now." -->\n</html>", $text );
+ '</html>', '<!-- Cached/compressed ' . $now . " -->\n</html>", $text );
} else {
$text = str_replace(
- '</html>', '<!-- Cached '.$now." -->\n</html>", $text );
+ '</html>', '<!-- Cached ' . $now . " -->\n</html>", $text );
}
// Store text to FS...
@@ -181,7 +182,7 @@ class HTMLFileCache extends FileCacheBase {
// gzip output to buffer as needed and set headers...
if ( $this->useGzip() ) {
- // @TODO: ugly wfClientAcceptsGzip() function - use context!
+ // @todo Ugly wfClientAcceptsGzip() function - use context!
if ( wfClientAcceptsGzip() ) {
header( 'Content-Encoding: gzip' );
return $compressed;
diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php
index 372f983b..48b60aa9 100644
--- a/includes/cache/LinkBatch.php
+++ b/includes/cache/LinkBatch.php
@@ -39,7 +39,7 @@ class LinkBatch {
protected $caller;
function __construct( $arr = array() ) {
- foreach( $arr as $item ) {
+ foreach ( $arr as $item ) {
$this->addObj( $item );
}
}
@@ -98,7 +98,7 @@ class LinkBatch {
* @return bool
*/
public function isEmpty() {
- return ($this->getSize() == 0);
+ return $this->getSize() == 0;
}
/**
@@ -223,7 +223,7 @@ class LinkBatch {
/**
* Construct a WHERE clause which will match all the given titles.
*
- * @param $prefix String: the appropriate table's field name prefix ('page', 'pl', etc)
+ * @param string $prefix the appropriate table's field name prefix ('page', 'pl', etc)
* @param $db DatabaseBase object to use
* @return mixed string with SQL where clause fragment, or false if no items.
*/
diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php
index f759c020..54de1989 100644
--- a/includes/cache/LinkCache.php
+++ b/includes/cache/LinkCache.php
@@ -35,18 +35,44 @@ class LinkCache {
private $mGoodLinkFields = array();
private $mBadLinks = array();
private $mForUpdate = false;
+ private $useDatabase = true;
/**
- * Get an instance of this class
+ * @var LinkCache
+ */
+ protected static $instance;
+
+ /**
+ * Get an instance of this class.
*
* @return LinkCache
*/
static function &singleton() {
- static $instance;
- if ( !isset( $instance ) ) {
- $instance = new LinkCache;
+ if ( self::$instance ) {
+ return self::$instance;
}
- return $instance;
+ self::$instance = new LinkCache;
+ return self::$instance;
+ }
+
+ /**
+ * Destroy the singleton instance, a new one will be created next time
+ * singleton() is called.
+ * @since 1.22
+ */
+ static function destroySingleton() {
+ self::$instance = null;
+ }
+
+ /**
+ * Set the singleton instance to a given object.
+ * Since we do not have an interface for LinkCache, you have to be sure the
+ * given object implements all the LinkCache public methods.
+ * @param LinkCache $instance
+ * @since 1.22
+ */
+ static function setSingleton( LinkCache $instance ) {
+ self::$instance = $instance;
}
/**
@@ -74,11 +100,11 @@ class LinkCache {
* Get a field of a title object from cache.
* If this link is not good, it will return NULL.
* @param $title Title
- * @param $field String: ('length','redirect','revision')
+ * @param string $field ('length','redirect','revision','model')
* @return mixed
*/
public function getGoodLinkFieldObj( $title, $field ) {
- $dbkey = $title->getPrefixedDbKey();
+ $dbkey = $title->getPrefixedDBkey();
if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) {
return $this->mGoodLinkFields[$dbkey][$field];
} else {
@@ -102,14 +128,16 @@ class LinkCache {
* @param $len Integer: text's length
* @param $redir Integer: whether the page is a redirect
* @param $revision Integer: latest revision's ID
+ * @param $model Integer: latest revision's content model ID
*/
- public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false ) {
- $dbkey = $title->getPrefixedDbKey();
+ public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false, $model = false ) {
+ $dbkey = $title->getPrefixedDBkey();
$this->mGoodLinks[$dbkey] = intval( $id );
$this->mGoodLinkFields[$dbkey] = array(
'length' => intval( $len ),
'redirect' => intval( $redir ),
- 'revision' => intval( $revision ) );
+ 'revision' => intval( $revision ),
+ 'model' => intval( $model ) );
}
/**
@@ -117,15 +145,16 @@ class LinkCache {
* @since 1.19
* @param $title Title
* @param $row object which has the fields page_id, page_is_redirect,
- * page_latest
+ * page_latest and page_content_model
*/
public function addGoodLinkObjFromRow( $title, $row ) {
- $dbkey = $title->getPrefixedDbKey();
+ $dbkey = $title->getPrefixedDBkey();
$this->mGoodLinks[$dbkey] = intval( $row->page_id );
$this->mGoodLinkFields[$dbkey] = array(
'length' => intval( $row->page_len ),
'redirect' => intval( $row->page_is_redirect ),
'revision' => intval( $row->page_latest ),
+ 'model' => !empty( $row->page_content_model ) ? strval( $row->page_content_model ) : null,
);
}
@@ -133,7 +162,7 @@ class LinkCache {
* @param $title Title
*/
public function addBadLinkObj( $title ) {
- $dbkey = $title->getPrefixedDbKey();
+ $dbkey = $title->getPrefixedDBkey();
if ( !$this->isBadLink( $dbkey ) ) {
$this->mBadLinks[$dbkey] = 1;
}
@@ -147,24 +176,29 @@ class LinkCache {
* @param $title Title
*/
public function clearLink( $title ) {
- $dbkey = $title->getPrefixedDbKey();
+ $dbkey = $title->getPrefixedDBkey();
unset( $this->mBadLinks[$dbkey] );
unset( $this->mGoodLinks[$dbkey] );
unset( $this->mGoodLinkFields[$dbkey] );
}
- public function getGoodLinks() { return $this->mGoodLinks; }
- public function getBadLinks() { return array_keys( $this->mBadLinks ); }
+ public function getGoodLinks() {
+ return $this->mGoodLinks;
+ }
+
+ public function getBadLinks() {
+ return array_keys( $this->mBadLinks );
+ }
/**
* Add a title to the link cache, return the page_id or zero if non-existent
*
- * @param $title String: title to add
+ * @param string $title title to add
* @return Integer
*/
public function addLink( $title ) {
$nt = Title::newFromDBkey( $title );
- if( $nt ) {
+ if ( $nt ) {
return $this->addLinkObj( $nt );
} else {
return 0;
@@ -172,13 +206,27 @@ class LinkCache {
}
/**
+ * Enable or disable database use.
+ * @since 1.22
+ * @param $value Boolean
+ * @return Boolean
+ */
+ public function useDatabase( $value = null ) {
+ if ( $value !== null ) {
+ $this->useDatabase = (bool)$value;
+ }
+ return $this->useDatabase;
+ }
+
+ /**
* Add a title to the link cache, return the page_id or zero if non-existent
*
* @param $nt Title object to add
* @return Integer
*/
public function addLinkObj( $nt ) {
- global $wgAntiLockFlags;
+ global $wgAntiLockFlags, $wgContentHandlerUseDB;
+
wfProfileIn( __METHOD__ );
$key = $nt->getPrefixedDBkey();
@@ -197,6 +245,10 @@ class LinkCache {
return 0;
}
+ if( !$this->useDatabase ) {
+ return 0;
+ }
+
# Some fields heavily used for linking...
if ( $this->mForUpdate ) {
$db = wfGetDB( DB_MASTER );
@@ -210,8 +262,12 @@ class LinkCache {
$options = array();
}
- $s = $db->selectRow( 'page',
- array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+ $f = array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+ if ( $wgContentHandlerUseDB ) {
+ $f[] = 'page_content_model';
+ }
+
+ $s = $db->selectRow( 'page', $f,
array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ),
__METHOD__, $options );
# Set fields...
diff --git a/includes/LocalisationCache.php b/includes/cache/LocalisationCache.php
index d8e5d3a3..25a1e196 100644
--- a/includes/LocalisationCache.php
+++ b/includes/cache/LocalisationCache.php
@@ -110,7 +110,7 @@ class LocalisationCache {
'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
- 'digitGroupingPattern', 'pluralRules', 'compiledPluralRules',
+ 'digitGroupingPattern', 'pluralRules', 'pluralRuleTypes', 'compiledPluralRules',
);
/**
@@ -160,6 +160,20 @@ class LocalisationCache {
*/
var $pluralRules = null;
+ /**
+ * Associative array of cached plural rule types. The key is the language
+ * code, the value is an array of plural rule types for that language. For
+ * example, $pluralRuleTypes['ar'] = ['zero', 'one', 'two', 'few', 'many'].
+ * The index for each rule type matches the index for the rule in
+ * $pluralRules, thus allowing correlation between the two. The reason we
+ * don't just use the type names as the keys in $pluralRules is because
+ * Language::convertPlural applies the rules based on numeric order (or
+ * explicit numeric parameter), not based on the name of the rule type. For
+ * example, {{plural:count|wordform1|wordform2|wordform3}}, rather than
+ * {{plural:count|one=wordform1|two=wordform2|many=wordform3}}.
+ */
+ var $pluralRuleTypes = null;
+
var $mergeableKeys = null;
/**
@@ -168,6 +182,7 @@ class LocalisationCache {
* for $wgLocalisationCacheConf.
*
* @param $conf Array
+ * @throws MWException
*/
function __construct( $conf ) {
global $wgCacheDirectory;
@@ -339,7 +354,6 @@ class LocalisationCache {
* @param $code
* @param $key
* @param $subkey
- * @return
*/
protected function loadSubitem( $code, $key, $subkey ) {
if ( !in_array( $key, self::$splitKeys ) ) {
@@ -369,6 +383,9 @@ class LocalisationCache {
/**
* Returns true if the cache identified by $code is missing or expired.
+ *
+ * @param string $code
+ *
* @return bool
*/
public function isExpired( $code ) {
@@ -378,7 +395,7 @@ class LocalisationCache {
}
$deps = $this->store->get( $code, 'deps' );
- $keys = $this->store->get( $code, 'list', 'messages' );
+ $keys = $this->store->get( $code, 'list' );
$preload = $this->store->get( $code, 'preload' );
// Different keys may expire separately, at least in LCStore_Accel
if ( $deps === null || $keys === null || $preload === null ) {
@@ -404,6 +421,7 @@ class LocalisationCache {
/**
* Initialise a language in this object. Rebuild the cache if necessary.
* @param $code
+ * @throws MWException
*/
protected function initLanguage( $code ) {
if ( isset( $this->initialisedLangs[$code] ) ) {
@@ -474,12 +492,14 @@ class LocalisationCache {
* Read a PHP file containing localisation data.
* @param $_fileName
* @param $_fileType
+ * @throws MWException
* @return array
*/
protected function readPHPFile( $_fileName, $_fileType ) {
+ wfProfileIn( __METHOD__ );
// Disable APC caching
$_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
- include( $_fileName );
+ include $_fileName;
ini_set( 'apc.cache_by_default', $_apcEnabled );
if ( $_fileType == 'core' || $_fileType == 'extension' ) {
@@ -487,8 +507,10 @@ class LocalisationCache {
} elseif ( $_fileType == 'aliases' ) {
$data = compact( 'aliases' );
} else {
+ wfProfileOut( __METHOD__ );
throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
}
+ wfProfileOut( __METHOD__ );
return $data;
}
@@ -503,7 +525,7 @@ class LocalisationCache {
}
try {
$compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
- } catch( CLDRPluralRuleError $e ) {
+ } catch ( CLDRPluralRuleError $e ) {
wfDebugLog( 'l10n', $e->getMessage() . "\n" );
return array();
}
@@ -517,14 +539,7 @@ class LocalisationCache {
*/
public function getPluralRules( $code ) {
if ( $this->pluralRules === null ) {
- $cldrPlural = __DIR__ . "/../languages/data/plurals.xml";
- $mwPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml";
- // Load CLDR plural rules
- $this->loadPluralFile( $cldrPlural );
- if ( file_exists( $mwPlural ) ) {
- // Override or extend
- $this->loadPluralFile( $mwPlural );
- }
+ $this->loadPluralFiles();
}
if ( !isset( $this->pluralRules[$code] ) ) {
return null;
@@ -533,6 +548,36 @@ class LocalisationCache {
}
}
+ /**
+ * Get the plural rule types for a given language from the XML files.
+ * Cached.
+ * @since 1.22
+ */
+ public function getPluralRuleTypes( $code ) {
+ if ( $this->pluralRuleTypes === null ) {
+ $this->loadPluralFiles();
+ }
+ if ( !isset( $this->pluralRuleTypes[$code] ) ) {
+ return null;
+ } else {
+ return $this->pluralRuleTypes[$code];
+ }
+ }
+
+ /**
+ * Load the plural XML files.
+ */
+ protected function loadPluralFiles() {
+ global $IP;
+ $cldrPlural = "$IP/languages/data/plurals.xml";
+ $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
+ // Load CLDR plural rules
+ $this->loadPluralFile( $cldrPlural );
+ if ( file_exists( $mwPlural ) ) {
+ // Override or extend
+ $this->loadPluralFile( $mwPlural );
+ }
+ }
/**
* Load a plural XML file with the given filename, compile the relevant
@@ -545,12 +590,20 @@ class LocalisationCache {
foreach ( $rulesets as $ruleset ) {
$codes = $ruleset->getAttribute( 'locales' );
$rules = array();
+ $ruleTypes = array();
$ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
foreach ( $ruleElements as $elt ) {
+ $ruleType = $elt->getAttribute( 'count' );
+ if ( $ruleType === 'other' ) {
+ // Don't record "other" rules, which have an empty condition
+ continue;
+ }
$rules[] = $elt->nodeValue;
+ $ruleTypes[] = $ruleType;
}
foreach ( explode( ' ', $codes ) as $code ) {
$this->pluralRules[$code] = $rules;
+ $this->pluralRuleTypes[$code] = $ruleTypes;
}
}
}
@@ -561,8 +614,12 @@ class LocalisationCache {
* exists, the data array is returned, otherwise false is returned.
*/
protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
+ global $IP;
+ wfProfileIn( __METHOD__ );
+
$fileName = Language::getMessagesFileName( $code );
if ( !file_exists( $fileName ) ) {
+ wfProfileOut( __METHOD__ );
return false;
}
@@ -573,9 +630,13 @@ class LocalisationCache {
$data['pluralRules'] = $this->getPluralRules( $code );
# And for PHP
$data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
+ # Load plural rule types
+ $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
+
+ $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
+ $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
- $deps['plurals'] = new FileDependency( __DIR__ . "/../languages/data/plurals.xml" );
- $deps['plurals-mw'] = new FileDependency( __DIR__ . "/../languages/data/plurals-mediawiki.xml" );
+ wfProfileOut( __METHOD__ );
return $data;
}
@@ -596,7 +657,7 @@ class LocalisationCache {
} elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
$value = array_merge_recursive( $value, $fallbackValue );
} elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
- if ( !empty( $value['inherit'] ) ) {
+ if ( !empty( $value['inherit'] ) ) {
$value = array_merge( $fallbackValue, $value );
}
@@ -659,12 +720,14 @@ class LocalisationCache {
* Load localisation data for a given language for both core and extensions
* and save it to the persistent cache store and the process cache
* @param $code
+ * @throws MWException
*/
public function recache( $code ) {
global $wgExtensionMessagesFiles;
wfProfileIn( __METHOD__ );
if ( !$code ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Invalid language code requested" );
}
$this->recachedLangs[$code] = true;
@@ -733,6 +796,7 @@ class LocalisationCache {
# This is done after the core because we know the fallback sequence now.
# But it has a higher precedence for merging so that we can support things
# like site-specific message overrides.
+ wfProfileIn( __METHOD__ . '-extensions' );
$allData = $initialData;
foreach ( $wgExtensionMessagesFiles as $fileName ) {
$data = $this->readPHPFile( $fileName, 'extension' );
@@ -753,6 +817,7 @@ class LocalisationCache {
foreach ( $coreData as $key => $item ) {
$this->mergeItem( $key, $allData[$key], $item );
}
+ wfProfileOut( __METHOD__ . '-extensions' );
# Add cache dependencies for any referenced globals
$deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
@@ -778,6 +843,10 @@ class LocalisationCache {
if ( $allData['compiledPluralRules'] === null ) {
$allData['compiledPluralRules'] = array();
}
+ # If there were no plural rule types, return an empty array
+ if ( $allData['pluralRuleTypes'] === null ) {
+ $allData['pluralRuleTypes'] = array();
+ }
# Set the list keys
$allData['list'] = array();
@@ -788,6 +857,7 @@ class LocalisationCache {
wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
if ( is_null( $allData['namespaceNames'] ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
'Check that your languages/messages/MessagesEn.php file is intact.' );
}
@@ -802,6 +872,7 @@ class LocalisationCache {
}
# Save to the persistent cache
+ wfProfileIn( __METHOD__ . '-write' );
$this->store->startWrite( $code );
foreach ( $allData as $key => $value ) {
if ( in_array( $key, self::$splitKeys ) ) {
@@ -813,6 +884,7 @@ class LocalisationCache {
}
}
$this->store->finishWrite();
+ wfProfileOut( __METHOD__ . '-write' );
# Clear out the MessageBlobStore
# HACK: If using a null (i.e. disabled) storage backend, we
@@ -860,6 +932,7 @@ class LocalisationCache {
unset( $this->loadedItems[$code] );
unset( $this->loadedSubitems[$code] );
unset( $this->initialisedLangs[$code] );
+ unset( $this->shallowFallbacks[$code] );
foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
if ( $fbCode === $code ) {
@@ -884,6 +957,25 @@ class LocalisationCache {
$this->store = new LCStore_Null;
$this->manualRecache = false;
}
+
+ /**
+ * Return an array with initialised languages.
+ *
+ * @return array
+ */
+ public function getInitialisedLanguages() {
+ return $this->initialisedLangs;
+ }
+
+ /**
+ * Set initialised languages.
+ *
+ * @param array $languages Optional array of initialised languages.
+ */
+ public function setInitialisedLanguages( $languages = array() ) {
+ $this->initialisedLangs = $languages;
+ }
+
}
/**
@@ -906,14 +998,14 @@ class LocalisationCache {
interface LCStore {
/**
* Get a value.
- * @param $code string Language code
- * @param $key string Cache key
+ * @param string $code Language code
+ * @param string $key Cache key
*/
function get( $code, $key );
/**
* Start a write transaction.
- * @param $code Language code
+ * @param string $code Language code
*/
function startWrite( $code );
@@ -925,8 +1017,8 @@ interface LCStore {
/**
* Set a key to a given value. startWrite() must be called before this
* is called, and finishWrite() must be called afterwards.
- * @param $key
- * @param $value
+ * @param string $key
+ * @param mixed $value
*/
function set( $key, $value );
}
@@ -1027,7 +1119,6 @@ class LCStore_DB implements LCStore {
if ( $this->dbw->wasReadOnlyError() ) {
$this->readOnly = true;
$this->dbw->rollback( __METHOD__ );
- $this->dbw->ignoreErrors( false );
return;
} else {
throw $e;
@@ -1156,7 +1247,7 @@ class LCStore_CDB implements LCStore {
}
protected function getFileName( $code ) {
- if ( !$code || strpos( $code, '/' ) !== false ) {
+ if ( strval( $code ) === '' || strpos( $code, '/' ) !== false ) {
throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
}
return "{$this->directory}/l10n_cache-$code.cdb";
@@ -1277,5 +1368,4 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
$this->unload( $code );
}
}
-
}
diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php
index b854a2ec..a92c87f4 100644
--- a/includes/cache/MessageCache.php
+++ b/includes/cache/MessageCache.php
@@ -22,14 +22,29 @@
*/
/**
- *
+ * MediaWiki message cache structure version.
+ * Bump this whenever the message cache format has changed.
*/
-define( 'MSG_LOAD_TIMEOUT', 60 );
-define( 'MSG_LOCK_TIMEOUT', 10 );
-define( 'MSG_WAIT_TIMEOUT', 10 );
define( 'MSG_CACHE_VERSION', 1 );
/**
+ * Memcached timeout when loading a key.
+ * See MessageCache::load()
+ */
+define( 'MSG_LOAD_TIMEOUT', 60 );
+
+/**
+ * Memcached timeout when locking a key for a writing operation.
+ * See MessageCache::lock()
+ */
+define( 'MSG_LOCK_TIMEOUT', 30 );
+/**
+ * Number of times we will try to acquire a lock from Memcached.
+ * This comes in addition to MSG_LOCK_TIMEOUT.
+ */
+define( 'MSG_WAIT_TIMEOUT', 30 );
+
+/**
* Message cache
* Performs various MediaWiki namespace-related functions
* @ingroup Cache
@@ -44,10 +59,16 @@ class MessageCache {
*/
protected $mCache;
- // Should mean that database cannot be used, but check
+ /**
+ * Should mean that database cannot be used, but check
+ * @var bool $mDisable
+ */
protected $mDisable;
- /// Lifetime for cache, used by object caching
+ /**
+ * Lifetime for cache, used by object caching.
+ * Set on construction, see __construct().
+ */
protected $mExpiry;
/**
@@ -56,38 +77,21 @@ class MessageCache {
*/
protected $mParserOptions, $mParser;
- /// Variable for tracking which variables are already loaded
- protected $mLoadedLanguages = array();
-
- /**
- * Used for automatic detection of most used messages.
- */
- protected $mRequestedMessages = array();
-
/**
- * How long the message request counts are stored. Longer period gives
- * better sample, but also takes longer to adapt changes. The counts
- * are aggregrated per day, regardless of the value of this variable.
+ * Variable for tracking which variables are already loaded
+ * @var array $mLoadedLanguages
*/
- protected static $mAdaptiveDataAge = 604800; // Is 7*24*3600
-
- /**
- * Filter the tail of less used messages that are requested more seldom
- * than this factor times the number of request of most requested message.
- * These messages are not loaded in the default set, but are still cached
- * individually on demand with the normal cache expiry time.
- */
- protected static $mAdaptiveInclusionThreshold = 0.05;
+ protected $mLoadedLanguages = array();
/**
* Singleton instance
*
- * @var MessageCache
+ * @var MessageCache $instance
*/
private static $instance;
/**
- * @var bool
+ * @var bool $mInParser
*/
protected $mInParser = false;
@@ -95,12 +99,16 @@ class MessageCache {
* Get the signleton instance of this class
*
* @since 1.18
- * @return MessageCache object
+ * @return MessageCache
*/
public static function singleton() {
if ( is_null( self::$instance ) ) {
global $wgUseDatabaseMessages, $wgMsgCacheExpiry;
- self::$instance = new self( wfGetMessageCacheStorage(), $wgUseDatabaseMessages, $wgMsgCacheExpiry );
+ self::$instance = new self(
+ wfGetMessageCacheStorage(),
+ $wgUseDatabaseMessages,
+ $wgMsgCacheExpiry
+ );
}
return self::$instance;
}
@@ -114,6 +122,11 @@ class MessageCache {
self::$instance = null;
}
+ /**
+ * @param ObjectCache $memCached A cache instance. If none, fall back to CACHE_NONE.
+ * @param bool $useDB
+ * @param int $expiry Lifetime for cache. @see $mExpiry.
+ */
function __construct( $memCached, $useDB, $expiry ) {
if ( !$memCached ) {
$memCached = wfGetCache( CACHE_NONE );
@@ -139,15 +152,13 @@ class MessageCache {
/**
* Try to load the cache from a local file.
- * Actual format of the file depends on the $wgLocalMessageCacheSerialized
- * setting.
*
- * @param $hash String: the hash of contents, to check validity.
- * @param $code Mixed: Optional language code, see documenation of load().
- * @return bool on failure.
+ * @param string $hash the hash of contents, to check validity.
+ * @param Mixed $code Optional language code, see documenation of load().
+ * @return array The cache array
*/
- function loadFromLocal( $hash, $code ) {
- global $wgCacheDirectory, $wgLocalMessageCacheSerialized;
+ function getLocalCache( $hash, $code ) {
+ global $wgCacheDirectory;
$filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
@@ -159,31 +170,19 @@ class MessageCache {
return false; // No cache file
}
- if ( $wgLocalMessageCacheSerialized ) {
- // Check to see if the file has the hash specified
- $localHash = fread( $file, 32 );
- if ( $hash === $localHash ) {
- // All good, get the rest of it
- $serialized = '';
- while ( !feof( $file ) ) {
- $serialized .= fread( $file, 100000 );
- }
- fclose( $file );
- return $this->setCache( unserialize( $serialized ), $code );
- } else {
- fclose( $file );
- return false; // Wrong hash
+ // Check to see if the file has the hash specified
+ $localHash = fread( $file, 32 );
+ if ( $hash === $localHash ) {
+ // All good, get the rest of it
+ $serialized = '';
+ while ( !feof( $file ) ) {
+ $serialized .= fread( $file, 100000 );
}
+ fclose( $file );
+ return unserialize( $serialized );
} else {
- $localHash = substr( fread( $file, 40 ), 8 );
fclose( $file );
- if ( $hash != $localHash ) {
- return false; // Wrong hash
- }
-
- # Require overwrites the member variable or just shadows it?
- require( $filename );
- return $this->setCache( $this->mCache, $code );
+ return false; // Wrong hash
}
}
@@ -212,55 +211,6 @@ class MessageCache {
wfRestoreWarnings();
}
- function saveToScript( $array, $hash, $code ) {
- global $wgCacheDirectory;
-
- $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
- $tempFilename = $filename . '.tmp';
- wfMkdirParents( $wgCacheDirectory, null, __METHOD__ ); // might fail
-
- wfSuppressWarnings();
- $file = fopen( $tempFilename, 'w' );
- wfRestoreWarnings();
-
- if ( !$file ) {
- wfDebug( "Unable to open local cache file for writing\n" );
- return;
- }
-
- fwrite( $file, "<?php\n//$hash\n\n \$this->mCache = array(" );
-
- foreach ( $array as $key => $message ) {
- $key = $this->escapeForScript( $key );
- $message = $this->escapeForScript( $message );
- fwrite( $file, "'$key' => '$message',\n" );
- }
-
- fwrite( $file, ");\n?>" );
- fclose( $file);
- rename( $tempFilename, $filename );
- }
-
- function escapeForScript( $string ) {
- $string = str_replace( '\\', '\\\\', $string );
- $string = str_replace( '\'', '\\\'', $string );
- return $string;
- }
-
- /**
- * Set the cache to $cache, if it is valid. Otherwise set the cache to false.
- *
- * @return bool
- */
- function setCache( $cache, $code ) {
- if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) {
- $this->mCache[$code] = $cache;
- return true;
- } else {
- return false;
- }
- }
-
/**
* Loads messages from caches or from database in this order:
* (1) local message cache (if $wgUseLocalMessageCache is enabled)
@@ -277,13 +227,14 @@ class MessageCache {
* or false if populating empty cache fails. Also returns true if MessageCache
* is disabled.
*
- * @param $code String: language to which load messages
+ * @param bool|String $code Language to which load messages
+ * @throws MWException
* @return bool
*/
function load( $code = false ) {
global $wgUseLocalMessageCache;
- if( !is_string( $code ) ) {
+ if ( !is_string( $code ) ) {
# This isn't really nice, so at least make a note about it and try to
# fall back
wfDebug( __METHOD__ . " called without providing a language code\n" );
@@ -308,77 +259,161 @@ class MessageCache {
# Loading code starts
wfProfileIn( __METHOD__ );
$success = false; # Keep track of success
+ $staleCache = false; # a cache array with expired data, or false if none has been loaded
$where = array(); # Debug info, delayed to avoid spamming debug log too much
$cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
- # (1) local cache
+ # Local cache
# Hash of the contents is stored in memcache, to detect if local cache goes
- # out of date (due to update in other thread?)
+ # out of date (e.g. due to replace() on some other server)
if ( $wgUseLocalMessageCache ) {
wfProfileIn( __METHOD__ . '-fromlocal' );
$hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) );
if ( $hash ) {
- $success = $this->loadFromLocal( $hash, $code );
- if ( $success ) $where[] = 'got from local cache';
+ $cache = $this->getLocalCache( $hash, $code );
+ if ( !$cache ) {
+ $where[] = 'local cache is empty or has the wrong hash';
+ } elseif ( $this->isCacheExpired( $cache ) ) {
+ $where[] = 'local cache is expired';
+ $staleCache = $cache;
+ } else {
+ $where[] = 'got from local cache';
+ $success = true;
+ $this->mCache[$code] = $cache;
+ }
}
wfProfileOut( __METHOD__ . '-fromlocal' );
}
- # (2) memcache
- # Fails if nothing in cache, or in the wrong version.
if ( !$success ) {
- wfProfileIn( __METHOD__ . '-fromcache' );
- $cache = $this->mMemc->get( $cacheKey );
- $success = $this->setCache( $cache, $code );
- if ( $success ) {
- $where[] = 'got from global cache';
- $this->saveToCaches( $cache, false, $code );
- }
- wfProfileOut( __METHOD__ . '-fromcache' );
- }
+ # Try the global cache. If it is empty, try to acquire a lock. If
+ # the lock can't be acquired, wait for the other thread to finish
+ # and then try the global cache a second time.
+ for ( $failedAttempts = 0; $failedAttempts < 2; $failedAttempts++ ) {
+ wfProfileIn( __METHOD__ . '-fromcache' );
+ $cache = $this->mMemc->get( $cacheKey );
+ if ( !$cache ) {
+ $where[] = 'global cache is empty';
+ } elseif ( $this->isCacheExpired( $cache ) ) {
+ $where[] = 'global cache is expired';
+ $staleCache = $cache;
+ } else {
+ $where[] = 'got from global cache';
+ $this->mCache[$code] = $cache;
+ $this->saveToCaches( $cache, 'local-only', $code );
+ $success = true;
+ }
+
+ wfProfileOut( __METHOD__ . '-fromcache' );
- # (3)
- # Nothing in caches... so we need create one and store it in caches
- if ( !$success ) {
- $where[] = 'cache is empty';
- $where[] = 'loading from database';
-
- $this->lock( $cacheKey );
-
- # Limit the concurrency of loadFromDB to a single process
- # This prevents the site from going down when the cache expires
- $statusKey = wfMemcKey( 'messages', $code, 'status' );
- $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
- if ( $success ) {
- $cache = $this->loadFromDB( $code );
- $success = $this->setCache( $cache, $code );
- }
- if ( $success ) {
- $success = $this->saveToCaches( $cache, true, $code );
if ( $success ) {
- $this->mMemc->delete( $statusKey );
+ # Done, no need to retry
+ break;
+ }
+
+ # We need to call loadFromDB. Limit the concurrency to a single
+ # process. This prevents the site from going down when the cache
+ # expires.
+ $statusKey = wfMemcKey( 'messages', $code, 'status' );
+ $acquired = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
+ if ( $acquired ) {
+ # Unlock the status key if there is an exception
+ $that = $this;
+ $statusUnlocker = new ScopedCallback( function () use ( $that, $statusKey ) {
+ $that->mMemc->delete( $statusKey );
+ } );
+
+ # Now let's regenerate
+ $where[] = 'loading from database';
+
+ # Lock the cache to prevent conflicting writes
+ # If this lock fails, it doesn't really matter, it just means the
+ # write is potentially non-atomic, e.g. the results of a replace()
+ # may be discarded.
+ if ( $this->lock( $cacheKey ) ) {
+ $mainUnlocker = new ScopedCallback( function () use ( $that, $cacheKey ) {
+ $that->unlock( $cacheKey );
+ } );
+ } else {
+ $mainUnlocker = null;
+ $where[] = 'could not acquire main lock';
+ }
+
+ $cache = $this->loadFromDB( $code );
+ $this->mCache[$code] = $cache;
+ $success = true;
+ $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
+
+ # Unlock
+ ScopedCallback::consume( $mainUnlocker );
+ ScopedCallback::consume( $statusUnlocker );
+
+ if ( !$saveSuccess ) {
+ # Cache save has failed.
+ # There are two main scenarios where this could be a problem:
+ #
+ # - The cache is more than the maximum size (typically
+ # 1MB compressed).
+ #
+ # - Memcached has no space remaining in the relevant slab
+ # class. This is unlikely with recent versions of
+ # memcached.
+ #
+ # Either way, if there is a local cache, nothing bad will
+ # happen. If there is no local cache, disabling the message
+ # cache for all requests avoids incurring a loadFromDB()
+ # overhead on every request, and thus saves the wiki from
+ # complete downtime under moderate traffic conditions.
+ if ( !$wgUseLocalMessageCache ) {
+ $this->mMemc->set( $statusKey, 'error', 60 * 5 );
+ $where[] = 'could not save cache, disabled globally for 5 minutes';
+ } else {
+ $where[] = "could not save global cache";
+ }
+ }
+
+ # Load from DB complete, no need to retry
+ break;
+ } elseif ( $staleCache ) {
+ # Use the stale cache while some other thread constructs the new one
+ $where[] = 'using stale cache';
+ $this->mCache[$code] = $staleCache;
+ $success = true;
+ break;
+ } elseif ( $failedAttempts > 0 ) {
+ # Already retried once, still failed, so don't do another lock/unlock cycle
+ # This case will typically be hit if memcached is down, or if
+ # loadFromDB() takes longer than MSG_WAIT_TIMEOUT
+ $where[] = "could not acquire status key.";
+ break;
} else {
- $this->mMemc->set( $statusKey, 'error', 60 * 5 );
- wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
+ $status = $this->mMemc->get( $statusKey );
+ if ( $status === 'error' ) {
+ # Disable cache
+ break;
+ } else {
+ # Wait for the other thread to finish, then retry
+ $where[] = 'waited for other thread to complete';
+ $this->lock( $cacheKey );
+ $this->unlock( $cacheKey );
+ }
}
}
- $this->unlock($cacheKey);
}
if ( !$success ) {
- # Bad luck... this should not happen
$where[] = 'loading FAILED - cache is disabled';
- $info = implode( ', ', $where );
- wfDebug( __METHOD__ . ": Loading $code... $info\n" );
$this->mDisable = true;
$this->mCache = false;
+ # This used to throw an exception, but that led to nasty side effects like
+ # the whole wiki being instantly down if the memcached server died
} else {
# All good, just record the success
- $info = implode( ', ', $where );
- wfDebug( __METHOD__ . ": Loading $code... $info\n" );
$this->mLoadedLanguages[$code] = true;
}
+ $info = implode( ', ', $where );
+ wfDebug( __METHOD__ . ": Loading $code... $info\n" );
wfProfileOut( __METHOD__ );
return $success;
}
@@ -388,8 +423,8 @@ class MessageCache {
* $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
* on-demand from the database later.
*
- * @param $code String: language code.
- * @return Array: loaded messages for storing in caches.
+ * @param string $code Language code.
+ * @return array Loaded messages for storing in caches.
*/
function loadFromDB( $code ) {
wfProfileIn( __METHOD__ );
@@ -404,19 +439,20 @@ class MessageCache {
);
$mostused = array();
- if ( $wgAdaptiveMessageCache ) {
- $mostused = $this->getMostUsedMessages();
- if ( $code !== $wgLanguageCode ) {
- foreach ( $mostused as $key => $value ) {
- $mostused[$key] = "$value/$code";
- }
+ if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
+ if ( !isset( $this->mCache[$wgLanguageCode] ) ) {
+ $this->load( $wgLanguageCode );
+ }
+ $mostused = array_keys( $this->mCache[$wgLanguageCode] );
+ foreach ( $mostused as $key => $value ) {
+ $mostused[$key] = "$value/$code";
}
}
if ( count( $mostused ) ) {
$conds['page_title'] = $mostused;
} elseif ( $code !== $wgLanguageCode ) {
- $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), "/$code" );
+ $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code );
} else {
# Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
# other than language code.
@@ -448,24 +484,23 @@ class MessageCache {
foreach ( $res as $row ) {
$text = Revision::getRevisionText( $row );
- if( $text === false ) {
+ if ( $text === false ) {
// Failed to fetch data; possible ES errors?
// Store a marker to fetch on-demand as a workaround...
$entry = '!TOO BIG';
- wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$row->page_title} ($code)" );
+ wfDebugLog(
+ 'MessageCache',
+ __METHOD__
+ . ": failed to load message page text for {$row->page_title} ($code)"
+ );
} else {
$entry = ' ' . $text;
}
$cache[$row->page_title] = $entry;
}
- foreach ( $mostused as $key ) {
- if ( !isset( $cache[$key] ) ) {
- $cache[$key] = '!NONEXISTENT';
- }
- }
-
$cache['VERSION'] = MSG_CACHE_VERSION;
+ $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
wfProfileOut( __METHOD__ );
return $cache;
}
@@ -473,8 +508,8 @@ class MessageCache {
/**
* Updates cache as necessary when message page is changed
*
- * @param $title String: name of the page changed.
- * @param $text Mixed: new contents of the page.
+ * @param string $title Name of the page changed.
+ * @param mixed $text New contents of the page.
*/
public function replace( $title, $text ) {
global $wgMaxMsgCacheEntrySize;
@@ -507,12 +542,12 @@ class MessageCache {
}
# Update caches
- $this->saveToCaches( $this->mCache[$code], true, $code );
+ $this->saveToCaches( $this->mCache[$code], 'all', $code );
$this->unlock( $cacheKey );
// Also delete cached sidebar... just in case it is affected
$codes = array( $code );
- if ( $code === 'en' ) {
+ if ( $code === 'en' ) {
// Delete all sidebars, like for example on action=purge on the
// sidebar messages
$codes = array_keys( Language::fetchLanguageNames() );
@@ -534,21 +569,41 @@ class MessageCache {
}
/**
+ * Is the given cache array expired due to time passing or a version change?
+ *
+ * @param $cache
+ * @return bool
+ */
+ protected function isCacheExpired( $cache ) {
+ if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) {
+ return true;
+ }
+ if ( $cache['VERSION'] != MSG_CACHE_VERSION ) {
+ return true;
+ }
+ if ( wfTimestampNow() >= $cache['EXPIRY'] ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Shortcut to update caches.
*
- * @param $cache Array: cached messages with a version.
- * @param $memc Bool: Wether to update or not memcache.
- * @param $code String: Language code.
- * @return bool on somekind of error.
+ * @param array $cache Cached messages with a version.
+ * @param string $dest Either "local-only" to save to local caches only
+ * or "all" to save to all caches.
+ * @param string|bool $code Language code (default: false)
+ * @return bool
*/
- protected function saveToCaches( $cache, $memc = true, $code = false ) {
+ protected function saveToCaches( $cache, $dest, $code = false ) {
wfProfileIn( __METHOD__ );
- global $wgUseLocalMessageCache, $wgLocalMessageCacheSerialized;
+ global $wgUseLocalMessageCache;
$cacheKey = wfMemcKey( 'messages', $code );
- if ( $memc ) {
- $success = $this->mMemc->set( $cacheKey, $cache, $this->mExpiry );
+ if ( $dest === 'all' ) {
+ $success = $this->mMemc->set( $cacheKey, $cache );
} else {
$success = true;
}
@@ -557,12 +612,8 @@ class MessageCache {
if ( $wgUseLocalMessageCache ) {
$serialized = serialize( $cache );
$hash = md5( $serialized );
- $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry );
- if ($wgLocalMessageCacheSerialized) {
- $this->saveToLocal( $serialized, $hash, $code );
- } else {
- $this->saveToScript( $cache, $hash, $code );
- }
+ $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash );
+ $this->saveToLocal( $serialized, $hash, $code );
}
wfProfileOut( __METHOD__ );
@@ -570,19 +621,35 @@ class MessageCache {
}
/**
- * Represents a write lock on the messages key
+ * Represents a write lock on the messages key.
*
- * @param $key string
+ * Will retry MessageCache::MSG_WAIT_TIMEOUT times, each operations having
+ * a timeout of MessageCache::MSG_LOCK_TIMEOUT.
*
+ * @param string $key
* @return Boolean: success
*/
function lock( $key ) {
$lockKey = $key . ':lock';
- for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) {
+ $acquired = false;
+ $testDone = false;
+ for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$acquired; $i++ ) {
+ $acquired = $this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT );
+ if ( $acquired ) {
+ break;
+ }
+
+ # Fail fast if memcached is totally down
+ if ( !$testDone ) {
+ $testDone = true;
+ if ( !$this->mMemc->set( wfMemcKey( 'test' ), 'test', 1 ) ) {
+ break;
+ }
+ }
sleep( 1 );
}
- return $i >= MSG_WAIT_TIMEOUT;
+ return $acquired;
}
function unlock( $key ) {
@@ -593,50 +660,62 @@ class MessageCache {
/**
* Get a message from either the content language or the user language.
*
- * @param $key String: the message cache key
- * @param $useDB Boolean: get the message from the DB, false to use only
- * the localisation
- * @param $langcode String: code of the language to get the message for, if
- * it is a valid code create a language for that language,
- * if it is a string but not a valid code then make a basic
- * language object, if it is a false boolean then use the
- * current users language (as a fallback for the old
- * parameter functionality), or if it is a true boolean
- * then use the wikis content language (also as a
- * fallback).
- * @param $isFullKey Boolean: specifies whether $key is a two part key
+ * First, assemble a list of languages to attempt getting the message from. This
+ * chain begins with the requested language and its fallbacks and then continues with
+ * the content language and its fallbacks. For each language in the chain, the following
+ * process will occur (in this order):
+ * 1. If a language-specific override, i.e., [[MW:msg/lang]], is available, use that.
+ * Note: for the content language, there is no /lang subpage.
+ * 2. Fetch from the static CDB cache.
+ * 3. If available, check the database for fallback language overrides.
+ *
+ * This process provides a number of guarantees. When changing this code, make sure all
+ * of these guarantees are preserved.
+ * * If the requested language is *not* the content language, then the CDB cache for that
+ * specific language will take precedence over the root database page ([[MW:msg]]).
+ * * Fallbacks will be just that: fallbacks. A fallback language will never be reached if
+ * the message is available *anywhere* in the language for which it is a fallback.
+ *
+ * @param string $key the message key
+ * @param bool $useDB If true, look for the message in the DB, false
+ * to use only the compiled l10n cache.
+ * @param bool|string|object $langcode Code of the language to get the message for.
+ * - If string and a valid code, will create a standard language object
+ * - If string but not a valid code, will create a basic language object
+ * - If boolean and false, create object from the current users language
+ * - If boolean and true, create object from the wikis content language
+ * - If language object, use it as given
+ * @param bool $isFullKey specifies whether $key is a two part key
* "msg/lang".
*
- * @return string|bool
+ * @throws MWException when given an invalid key
+ * @return string|bool False if the message doesn't exist, otherwise the message (which can be empty)
*/
function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
- global $wgLanguageCode, $wgContLang;
+ global $wgContLang;
- if ( is_int( $key ) ) {
- // "Non-string key given" exception sometimes happens for numerical strings that become ints somewhere on their way here
- $key = strval( $key );
- }
+ $section = new ProfileSection( __METHOD__ );
- if ( !is_string( $key ) ) {
+ if ( is_int( $key ) ) {
+ // Fix numerical strings that somehow become ints
+ // on their way here
+ $key = (string)$key;
+ } elseif ( !is_string( $key ) ) {
throw new MWException( 'Non-string key given' );
- }
-
- if ( strval( $key ) === '' ) {
- # Shortcut: the empty key is always missing
+ } elseif ( $key === '' ) {
+ // Shortcut: the empty key is always missing
return false;
}
- $lang = wfGetLangObj( $langcode );
- if ( !$lang ) {
- throw new MWException( "Bad lang code $langcode given" );
+ // For full keys, get the language code from the key
+ $pos = strrpos( $key, '/' );
+ if ( $isFullKey && $pos !== false ) {
+ $langcode = substr( $key, $pos + 1 );
+ $key = substr( $key, 0, $pos );
}
- $langcode = $lang->getCode();
-
- $message = false;
-
- # Normalise title-case input (with some inlining)
- $lckey = str_replace( ' ', '_', $key );
+ // Normalise title-case input (with some inlining)
+ $lckey = strtr( $key, ' ', '_' );
if ( ord( $key ) < 128 ) {
$lckey[0] = strtolower( $lckey[0] );
$uckey = ucfirst( $lckey );
@@ -645,89 +724,160 @@ class MessageCache {
$uckey = $wgContLang->ucfirst( $lckey );
}
- /**
- * Record each message request, but only once per request.
- * This information is not used unless $wgAdaptiveMessageCache
- * is enabled.
- */
- $this->mRequestedMessages[$uckey] = true;
+ // Loop through each language in the fallback list until we find something useful
+ $lang = wfGetLangObj( $langcode );
+ $message = $this->getMessageFromFallbackChain( $lang, $lckey, $uckey, !$this->mDisable && $useDB );
- # Try the MediaWiki namespace
- if( !$this->mDisable && $useDB ) {
- $title = $uckey;
- if( !$isFullKey && ( $langcode != $wgLanguageCode ) ) {
- $title .= '/' . $langcode;
+ // If we still have no message, maybe the key was in fact a full key so try that
+ if ( $message === false ) {
+ $parts = explode( '/', $lckey );
+ // We may get calls for things that are http-urls from sidebar
+ // Let's not load nonexistent languages for those
+ // They usually have more than one slash.
+ if ( count( $parts ) == 2 && $parts[1] !== '' ) {
+ $message = Language::getMessageFor( $parts[0], $parts[1] );
+ if ( $message === null ) {
+ $message = false;
+ }
}
- $message = $this->getMsgFromNamespace( $title, $langcode );
}
- # Try the array in the language object
- if ( $message === false ) {
- $message = $lang->getMessage( $lckey );
- if ( is_null( $message ) ) {
- $message = false;
+ // Post-processing if the message exists
+ if ( $message !== false ) {
+ // Fix whitespace
+ $message = str_replace(
+ array(
+ # Fix for trailing whitespace, removed by textarea
+ '&#32;',
+ # Fix for NBSP, converted to space by firefox
+ '&nbsp;',
+ '&#160;',
+ ),
+ array(
+ ' ',
+ "\xc2\xa0",
+ "\xc2\xa0"
+ ),
+ $message
+ );
+ }
+
+ return $message;
+ }
+
+ /**
+ * Given a language, try and fetch a message from that language, then the
+ * fallbacks of that language, then the site language, then the fallbacks for the
+ * site language.
+ *
+ * @param Language $lang Requested language
+ * @param string $lckey Lowercase key for the message
+ * @param string $uckey Uppercase key for the message
+ * @param bool $useDB Whether to use the database
+ *
+ * @see MessageCache::get
+ * @return string|bool The message, or false if not found
+ */
+ protected function getMessageFromFallbackChain( $lang, $lckey, $uckey, $useDB ) {
+ global $wgLanguageCode, $wgContLang;
+
+ $langcode = $lang->getCode();
+ $message = false;
+
+ // First try the requested language.
+ if ( $useDB ) {
+ if ( $langcode === $wgLanguageCode ) {
+ // Messages created in the content language will not have the /lang extension
+ $message = $this->getMsgFromNamespace( $uckey, $langcode );
+ } else {
+ $message = $this->getMsgFromNamespace( "$uckey/$langcode", $langcode );
}
}
- # Try the array of another language
- if( $message === false ) {
- $parts = explode( '/', $lckey );
- # We may get calls for things that are http-urls from sidebar
- # Let's not load nonexistent languages for those
- # They usually have more than one slash.
- if ( count( $parts ) == 2 && $parts[1] !== '' ) {
- $message = Language::getMessageFor( $parts[0], $parts[1] );
- if ( is_null( $message ) ) {
- $message = false;
+ if ( $message !== false ) {
+ return $message;
+ }
+
+ // Check the CDB cache
+ $message = $lang->getMessage( $lckey );
+ if ( $message !== null ) {
+ return $message;
+ }
+
+ list( $fallbackChain, $siteFallbackChain ) = Language::getFallbacksIncludingSiteLanguage( $langcode );
+
+ // Next try checking the database for all of the fallback languages of the requested language.
+ if ( $useDB ) {
+ foreach ( $fallbackChain as $code ) {
+ if ( $code === $wgLanguageCode ) {
+ // Messages created in the content language will not have the /lang extension
+ $message = $this->getMsgFromNamespace( $uckey, $code );
+ } else {
+ $message = $this->getMsgFromNamespace( "$uckey/$code", $code );
+ }
+
+ if ( $message !== false ) {
+ // Found the message.
+ return $message;
}
}
}
- # Is this a custom message? Try the default language in the db...
- if( ( $message === false || $message === '-' ) &&
- !$this->mDisable && $useDB &&
- !$isFullKey && ( $langcode != $wgLanguageCode ) ) {
+ // Now try checking the site language.
+ if ( $useDB ) {
$message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode );
+ if ( $message !== false ) {
+ return $message;
+ }
}
- # Final fallback
- if( $message === false ) {
- return false;
+ $message = $wgContLang->getMessage( $lckey );
+ if ( $message !== null ) {
+ return $message;
}
- # Fix whitespace
- $message = strtr( $message,
- array(
- # Fix for trailing whitespace, removed by textarea
- '&#32;' => ' ',
- # Fix for NBSP, converted to space by firefox
- '&nbsp;' => "\xc2\xa0",
- '&#160;' => "\xc2\xa0",
- ) );
+ // Finally try the DB for the site language's fallbacks.
+ if ( $useDB ) {
+ foreach ( $siteFallbackChain as $code ) {
+ $message = $this->getMsgFromNamespace( "$uckey/$code", $code );
+ if ( $message === false && $code === $wgLanguageCode ) {
+ // Messages created in the content language will not have the /lang extension
+ $message = $this->getMsgFromNamespace( $uckey, $code );
+ }
- return $message;
+ if ( $message !== false ) {
+ // Found the message.
+ return $message;
+ }
+ }
+ }
+
+ return false;
}
/**
* Get a message from the MediaWiki namespace, with caching. The key must
* first be converted to two-part lang/msg form if necessary.
*
- * @param $title String: Message cache key with initial uppercase letter.
- * @param $code String: code denoting the language to try.
+ * Unlike self::get(), this function doesn't resolve fallback chains, and
+ * some callers require this behavior. LanguageConverter::parseCachedTable()
+ * and self::get() are some examples in core.
*
- * @return string|bool False on failure
+ * @param string $title Message cache key with initial uppercase letter.
+ * @param string $code Code denoting the language to try.
+ * @return string|bool The message, or false if it does not exist or on error
*/
function getMsgFromNamespace( $title, $code ) {
- global $wgAdaptiveMessageCache;
-
$this->load( $code );
if ( isset( $this->mCache[$code][$title] ) ) {
$entry = $this->mCache[$code][$title];
if ( substr( $entry, 0, 1 ) === ' ' ) {
- return substr( $entry, 1 );
+ // The message exists, so make sure a string
+ // is returned.
+ return (string)substr( $entry, 1 );
} elseif ( $entry === '!NONEXISTENT' ) {
return false;
- } elseif( $entry === '!TOO BIG' ) {
+ } elseif ( $entry === '!TOO BIG' ) {
// Fall through and try invididual message cache below
}
} else {
@@ -738,15 +888,7 @@ class MessageCache {
return $message;
}
- /**
- * If message cache is in normal mode, it is guaranteed
- * (except bugs) that there is always entry (or placeholder)
- * in the cache if message exists. Thus we can do minor
- * performance improvement and return false early.
- */
- if ( !$wgAdaptiveMessageCache ) {
- return false;
- }
+ return false;
}
# Try the individual message cache
@@ -755,7 +897,9 @@ class MessageCache {
if ( $entry ) {
if ( substr( $entry, 0, 1 ) === ' ' ) {
$this->mCache[$code][$title] = $entry;
- return substr( $entry, 1 );
+ // The message exists, so make sure a string
+ // is returned.
+ return (string)substr( $entry, 1 );
} elseif ( $entry === '!NONEXISTENT' ) {
$this->mCache[$code][$title] = '!NONEXISTENT';
return false;
@@ -770,16 +914,39 @@ class MessageCache {
Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST
);
if ( $revision ) {
- $message = $revision->getText();
- if ($message === false) {
+ $content = $revision->getContent();
+ if ( !$content ) {
// A possibly temporary loading failure.
- wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title} ($code)" );
+ wfDebugLog(
+ 'MessageCache',
+ __METHOD__ . ": failed to load message page text for {$title} ($code)"
+ );
+ $message = null; // no negative caching
} else {
- $this->mCache[$code][$title] = ' ' . $message;
- $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
+ // XXX: Is this the right way to turn a Content object into a message?
+ // NOTE: $content is typically either WikitextContent, JavaScriptContent or
+ // CssContent. MessageContent is *not* used for storing messages, it's
+ // only used for wrapping them when needed.
+ $message = $content->getWikitextForTransclusion();
+
+ if ( $message === false || $message === null ) {
+ wfDebugLog(
+ 'MessageCache',
+ __METHOD__ . ": message content doesn't provide wikitext "
+ . "(content model: " . $content->getContentHandler() . ")"
+ );
+
+ $message = false; // negative caching
+ } else {
+ $this->mCache[$code][$title] = ' ' . $message;
+ $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
+ }
}
} else {
- $message = false;
+ $message = false; // negative caching
+ }
+
+ if ( $message === false ) { // negative caching
$this->mCache[$code][$title] = '!NONEXISTENT';
$this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
}
@@ -788,15 +955,15 @@ class MessageCache {
}
/**
- * @param $message string
- * @param $interface bool
- * @param $language
- * @param $title Title
+ * @param string $message
+ * @param bool $interface
+ * @param string $language Language code
+ * @param Title $title
* @return string
*/
function transform( $message, $interface = false, $language = null, $title = null ) {
// Avoid creating parser if nothing to transform
- if( strpos( $message, '{{' ) === false ) {
+ if ( strpos( $message, '{{' ) === false ) {
return $message;
}
@@ -840,14 +1007,16 @@ class MessageCache {
}
/**
- * @param $text string
- * @param $title Title
- * @param $linestart bool
- * @param $interface bool
- * @param $language
- * @return ParserOutput
+ * @param string $text
+ * @param Title $title
+ * @param bool $linestart Whether or not this is at the start of a line
+ * @param bool $interface Whether this is an interface message
+ * @param string $language Language code
+ * @return ParserOutput|string
*/
- public function parse( $text, $title = null, $linestart = true, $interface = false, $language = null ) {
+ public function parse( $text, $title = null, $linestart = true,
+ $interface = false, $language = null
+ ) {
if ( $this->mInParser ) {
return htmlspecialchars( $text );
}
@@ -890,7 +1059,7 @@ class MessageCache {
*/
function clear() {
$langs = Language::fetchLanguageNames( null, 'mw' );
- foreach ( array_keys($langs) as $code ) {
+ foreach ( array_keys( $langs ) as $code ) {
# Global cache
$this->mMemc->delete( wfMemcKey( 'messages', $code ) );
# Invalidate all local caches
@@ -906,12 +1075,12 @@ class MessageCache {
public function figureMessage( $key ) {
global $wgLanguageCode;
$pieces = explode( '/', $key );
- if( count( $pieces ) < 2 ) {
+ if ( count( $pieces ) < 2 ) {
return array( $key, $wgLanguageCode );
}
$lang = array_pop( $pieces );
- if( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
+ if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
return array( $key, $wgLanguageCode );
}
@@ -919,88 +1088,12 @@ class MessageCache {
return array( $message, $lang );
}
- public static function logMessages() {
- wfProfileIn( __METHOD__ );
- global $wgAdaptiveMessageCache;
- if ( !$wgAdaptiveMessageCache || !self::$instance instanceof MessageCache ) {
- wfProfileOut( __METHOD__ );
- return;
- }
-
- $cachekey = wfMemckey( 'message-profiling' );
- $cache = wfGetCache( CACHE_DB );
- $data = $cache->get( $cachekey );
-
- if ( !$data ) {
- $data = array();
- }
-
- $age = self::$mAdaptiveDataAge;
- $filterDate = substr( wfTimestamp( TS_MW, time() - $age ), 0, 8 );
- foreach ( array_keys( $data ) as $key ) {
- if ( $key < $filterDate ) {
- unset( $data[$key] );
- }
- }
-
- $index = substr( wfTimestampNow(), 0, 8 );
- if ( !isset( $data[$index] ) ) {
- $data[$index] = array();
- }
-
- foreach ( self::$instance->mRequestedMessages as $message => $_ ) {
- if ( !isset( $data[$index][$message] ) ) {
- $data[$index][$message] = 0;
- }
- $data[$index][$message]++;
- }
-
- $cache->set( $cachekey, $data );
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * @return array
- */
- public function getMostUsedMessages() {
- wfProfileIn( __METHOD__ );
- $cachekey = wfMemcKey( 'message-profiling' );
- $cache = wfGetCache( CACHE_DB );
- $data = $cache->get( $cachekey );
- if ( !$data ) {
- wfProfileOut( __METHOD__ );
- return array();
- }
-
- $list = array();
-
- foreach( $data as $messages ) {
- foreach( $messages as $message => $count ) {
- $key = $message;
- if ( !isset( $list[$key] ) ) {
- $list[$key] = 0;
- }
- $list[$key] += $count;
- }
- }
-
- $max = max( $list );
- foreach ( $list as $message => $count ) {
- if ( $count < intval( $max * self::$mAdaptiveInclusionThreshold ) ) {
- unset( $list[$message] );
- }
- }
-
- wfProfileOut( __METHOD__ );
- return array_keys( $list );
- }
-
/**
* Get all message keys stored in the message cache for a given language.
* If $code is the content language code, this will return all message keys
* for which MediaWiki:msgkey exists. If $code is another language code, this
* will ONLY return message keys for which MediaWiki:msgkey/$code exists.
- * @param $code string
+ * @param string $code Language code
* @return array of message keys (strings)
*/
public function getAllMessageKeys( $code ) {
@@ -1010,9 +1103,12 @@ class MessageCache {
// Apparently load() failed
return null;
}
- $cache = $this->mCache[$code]; // Copy the cache
- unset( $cache['VERSION'] ); // Remove the VERSION key
- $cache = array_diff( $cache, array( '!NONEXISTENT' ) ); // Remove any !NONEXISTENT keys
+ // Remove administrative keys
+ $cache = $this->mCache[$code];
+ unset( $cache['VERSION'] );
+ unset( $cache['EXPIRY'] );
+ // Remove any !NONEXISTENT keys
+ $cache = array_diff( $cache, array( '!NONEXISTENT' ) );
// Keys may appear with a capital first letter. lcfirst them.
return array_map( array( $wgContLang, 'lcfirst' ), array_keys( $cache ) );
}
diff --git a/includes/cache/ProcessCacheLRU.php b/includes/cache/ProcessCacheLRU.php
index f215ebd8..76c76f37 100644
--- a/includes/cache/ProcessCacheLRU.php
+++ b/includes/cache/ProcessCacheLRU.php
@@ -28,6 +28,8 @@
class ProcessCacheLRU {
/** @var Array */
protected $cache = array(); // (key => prop => value)
+ /** @var Array */
+ protected $cacheTimes = array(); // (key => prop => UNIX timestamp)
protected $maxCacheKeys; // integer; max entries
@@ -44,7 +46,7 @@ class ProcessCacheLRU {
/**
* Set a property field for a cache entry.
- * This will prune the cache if it gets too large.
+ * This will prune the cache if it gets too large based on LRU.
* If the item is already set, it will be pushed to the top of the cache.
*
* @param $key string
@@ -57,9 +59,12 @@ class ProcessCacheLRU {
$this->ping( $key ); // push to top
} elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
reset( $this->cache );
- unset( $this->cache[key( $this->cache )] );
+ $evictKey = key( $this->cache );
+ unset( $this->cache[$evictKey] );
+ unset( $this->cacheTimes[$evictKey] );
}
$this->cache[$key][$prop] = $value;
+ $this->cacheTimes[$key][$prop] = time();
}
/**
@@ -67,10 +72,14 @@ class ProcessCacheLRU {
*
* @param $key string
* @param $prop string
+ * @param $maxAge integer Ignore items older than this many seconds (since 1.21)
* @return bool
*/
- public function has( $key, $prop ) {
- return isset( $this->cache[$key][$prop] );
+ public function has( $key, $prop, $maxAge = 0 ) {
+ if ( isset( $this->cache[$key][$prop] ) ) {
+ return ( $maxAge <= 0 || ( time() - $this->cacheTimes[$key][$prop] ) <= $maxAge );
+ }
+ return false;
}
/**
@@ -100,9 +109,11 @@ class ProcessCacheLRU {
public function clear( $keys = null ) {
if ( $keys === null ) {
$this->cache = array();
+ $this->cacheTimes = array();
} else {
foreach ( (array)$keys as $key ) {
unset( $this->cache[$key] );
+ unset( $this->cacheTimes[$key] );
}
}
}
diff --git a/includes/cache/ResourceFileCache.php b/includes/cache/ResourceFileCache.php
index 61f1e8c3..2ad7b853 100644
--- a/includes/cache/ResourceFileCache.php
+++ b/includes/cache/ResourceFileCache.php
@@ -29,7 +29,7 @@
class ResourceFileCache extends FileCacheBase {
protected $mCacheWorthy;
- /* @TODO: configurable? */
+ /* @todo configurable? */
const MISS_THRESHOLD = 360; // 6/min * 60 min
/**
diff --git a/includes/cache/SquidUpdate.php b/includes/cache/SquidUpdate.php
index 423e3884..71afeba9 100644
--- a/includes/cache/SquidUpdate.php
+++ b/includes/cache/SquidUpdate.php
@@ -26,32 +26,42 @@
* @ingroup Cache
*/
class SquidUpdate {
- var $urlArr, $mMaxTitles;
/**
- * @param $urlArr array
- * @param $maxTitles bool|int
+ * Collection of URLs to purge.
+ * @var array
*/
- function __construct( $urlArr = array(), $maxTitles = false ) {
+ protected $urlArr;
+
+ /**
+ * @param array $urlArr Collection of URLs to purge
+ * @param bool|int $maxTitles Maximum number of unique URLs to purge
+ */
+ public function __construct( $urlArr = array(), $maxTitles = false ) {
global $wgMaxSquidPurgeTitles;
if ( $maxTitles === false ) {
- $this->mMaxTitles = $wgMaxSquidPurgeTitles;
- } else {
- $this->mMaxTitles = $maxTitles;
+ $maxTitles = $wgMaxSquidPurgeTitles;
}
- $urlArr = array_unique( $urlArr ); // Remove duplicates
- if ( count( $urlArr ) > $this->mMaxTitles ) {
- $urlArr = array_slice( $urlArr, 0, $this->mMaxTitles );
+
+ // Remove duplicate URLs from list
+ $urlArr = array_unique( $urlArr );
+ if ( count( $urlArr ) > $maxTitles ) {
+ // Truncate to desired maximum URL count
+ $urlArr = array_slice( $urlArr, 0, $maxTitles );
}
$this->urlArr = $urlArr;
}
/**
- * @param $title Title
+ * Create a SquidUpdate from the given Title object.
*
+ * The resulting SquidUpdate will purge the given Title's URLs as well as
+ * the pages that link to it. Capped at $wgMaxSquidPurgeTitles total URLs.
+ *
+ * @param Title $title
* @return SquidUpdate
*/
- static function newFromLinksTo( &$title ) {
+ public static function newFromLinksTo( Title $title ) {
global $wgMaxSquidPurgeTitles;
wfProfileIn( __METHOD__ );
@@ -61,13 +71,13 @@ class SquidUpdate {
array( 'page_namespace', 'page_title' ),
array(
'pl_namespace' => $title->getNamespace(),
- 'pl_title' => $title->getDBkey(),
+ 'pl_title' => $title->getDBkey(),
'pl_from=page_id' ),
__METHOD__ );
$blurlArr = $title->getSquidURLs();
- if ( $dbr->numRows( $res ) <= $wgMaxSquidPurgeTitles ) {
+ if ( $res->numRows() <= $wgMaxSquidPurgeTitles ) {
foreach ( $res as $BL ) {
- $tobj = Title::makeTitle( $BL->page_namespace, $BL->page_title ) ;
+ $tobj = Title::makeTitle( $BL->page_namespace, $BL->page_title );
$blurlArr[] = $tobj->getInternalURL();
}
}
@@ -79,12 +89,11 @@ class SquidUpdate {
/**
* Create a SquidUpdate from an array of Title objects, or a TitleArray object
*
- * @param $titles array
- * @param $urlArr array
- *
+ * @param array $titles
+ * @param array $urlArr
* @return SquidUpdate
*/
- static function newFromTitles( $titles, $urlArr = array() ) {
+ public static function newFromTitles( $titles, $urlArr = array() ) {
global $wgMaxSquidPurgeTitles;
$i = 0;
foreach ( $titles as $title ) {
@@ -97,20 +106,19 @@ class SquidUpdate {
}
/**
- * @param $title Title
- *
+ * @param Title $title
* @return SquidUpdate
*/
- static function newSimplePurge( &$title ) {
+ public static function newSimplePurge( Title $title ) {
$urlArr = $title->getSquidURLs();
return new SquidUpdate( $urlArr );
}
/**
- * Purges the list of URLs passed to the constructor
+ * Purges the list of URLs passed to the constructor.
*/
- function doUpdate() {
- SquidUpdate::purge( $this->urlArr );
+ public function doUpdate() {
+ self::purge( $this->urlArr );
}
/**
@@ -119,25 +127,30 @@ class SquidUpdate {
* (example: $urlArr[] = 'http://my.host/something')
* XXX report broken Squids per mail or log
*
- * @param $urlArr array
- * @return void
+ * @param array $urlArr List of full URLs to purge
*/
- static function purge( $urlArr ) {
- global $wgSquidServers, $wgHTCPMulticastRouting;
+ public static function purge( $urlArr ) {
+ global $wgSquidServers, $wgHTCPRouting;
- if( !$urlArr ) {
+ if ( !$urlArr ) {
return;
}
- if ( $wgHTCPMulticastRouting ) {
- SquidUpdate::HTCPPurge( $urlArr );
+ wfDebugLog( 'squid', __METHOD__ . ': ' . implode( ' ', $urlArr ) . "\n" );
+
+ if ( $wgHTCPRouting ) {
+ self::HTCPPurge( $urlArr );
}
wfProfileIn( __METHOD__ );
- $urlArr = array_unique( $urlArr ); // Remove duplicates
- $maxSocketsPerSquid = 8; // socket cap per Squid
- $urlsPerSocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while
+ // Remove duplicate URLs
+ $urlArr = array_unique( $urlArr );
+ // Maximum number of parallel connections per squid
+ $maxSocketsPerSquid = 8;
+ // Number of requests to send per socket
+ // 400 seems to be a good tradeoff, opening a socket takes a while
+ $urlsPerSocket = 400;
$socketsPerSquid = ceil( count( $urlArr ) / $urlsPerSocket );
if ( $socketsPerSquid > $maxSocketsPerSquid ) {
$socketsPerSquid = $maxSocketsPerSquid;
@@ -160,17 +173,20 @@ class SquidUpdate {
}
/**
+ * Send Hyper Text Caching Protocol (HTCP) CLR requests.
+ *
* @throws MWException
- * @param $urlArr array
+ * @param array $urlArr Collection of URLs to purge
*/
- static function HTCPPurge( $urlArr ) {
- global $wgHTCPMulticastRouting, $wgHTCPMulticastTTL;
+ public static function HTCPPurge( $urlArr ) {
+ global $wgHTCPRouting, $wgHTCPMulticastTTL;
wfProfileIn( __METHOD__ );
- $htcpOpCLR = 4; // HTCP CLR
+ // HTCP CLR operation
+ $htcpOpCLR = 4;
// @todo FIXME: PHP doesn't support these socket constants (include/linux/in.h)
- if( !defined( "IPPROTO_IP" ) ) {
+ if ( !defined( "IPPROTO_IP" ) ) {
define( "IPPROTO_IP", 0 );
define( "IP_MULTICAST_LOOP", 34 );
define( "IP_MULTICAST_TTL", 33 );
@@ -178,55 +194,73 @@ class SquidUpdate {
// pfsockopen doesn't work because we need set_sock_opt
$conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
- if ( $conn ) {
- // Set socket options
- socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_LOOP, 0 );
- if ( $wgHTCPMulticastTTL != 1 )
- socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_TTL,
- $wgHTCPMulticastTTL );
-
- $urlArr = array_unique( $urlArr ); // Remove duplicates
- foreach ( $urlArr as $url ) {
- if( !is_string( $url ) ) {
- throw new MWException( 'Bad purge URL' );
- }
- $url = SquidUpdate::expand( $url );
- $conf = self::getRuleForURL( $url, $wgHTCPMulticastRouting );
- if ( !$conf ) {
- wfDebug( "No HTCP rule configured for URL $url , skipping\n" );
- continue;
- }
- if ( !isset( $conf['host'] ) || !isset( $conf['port'] ) ) {
+ if ( ! $conn ) {
+ $errstr = socket_strerror( socket_last_error() );
+ wfDebugLog( 'squid', __METHOD__ .
+ ": Error opening UDP socket: $errstr\n" );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ // Set socket options
+ socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_LOOP, 0 );
+ if ( $wgHTCPMulticastTTL != 1 ) {
+ // Set multicast time to live (hop count) option on socket
+ socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_TTL,
+ $wgHTCPMulticastTTL );
+ }
+
+ // Remove duplicate URLs from collection
+ $urlArr = array_unique( $urlArr );
+ foreach ( $urlArr as $url ) {
+ if ( !is_string( $url ) ) {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( 'Bad purge URL' );
+ }
+ $url = self::expand( $url );
+ $conf = self::getRuleForURL( $url, $wgHTCPRouting );
+ if ( !$conf ) {
+ wfDebugLog( 'squid', __METHOD__ .
+ "No HTCP rule configured for URL {$url} , skipping\n" );
+ continue;
+ }
+
+ if ( isset( $conf['host'] ) && isset( $conf['port'] ) ) {
+ // Normalize single entries
+ $conf = array( $conf );
+ }
+ foreach ( $conf as $subconf ) {
+ if ( !isset( $subconf['host'] ) || !isset( $subconf['port'] ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Invalid HTCP rule for URL $url\n" );
}
+ }
- // Construct a minimal HTCP request diagram
- // as per RFC 2756
- // Opcode 'CLR', no response desired, no auth
- $htcpTransID = rand();
+ // Construct a minimal HTCP request diagram
+ // as per RFC 2756
+ // Opcode 'CLR', no response desired, no auth
+ $htcpTransID = rand();
- $htcpSpecifier = pack( 'na4na*na8n',
- 4, 'HEAD', strlen( $url ), $url,
- 8, 'HTTP/1.0', 0 );
+ $htcpSpecifier = pack( 'na4na*na8n',
+ 4, 'HEAD', strlen( $url ), $url,
+ 8, 'HTTP/1.0', 0 );
- $htcpDataLen = 8 + 2 + strlen( $htcpSpecifier );
- $htcpLen = 4 + $htcpDataLen + 2;
+ $htcpDataLen = 8 + 2 + strlen( $htcpSpecifier );
+ $htcpLen = 4 + $htcpDataLen + 2;
- // Note! Squid gets the bit order of the first
- // word wrong, wrt the RFC. Apparently no other
- // implementation exists, so adapt to Squid
- $htcpPacket = pack( 'nxxnCxNxxa*n',
- $htcpLen, $htcpDataLen, $htcpOpCLR,
- $htcpTransID, $htcpSpecifier, 2);
+ // Note! Squid gets the bit order of the first
+ // word wrong, wrt the RFC. Apparently no other
+ // implementation exists, so adapt to Squid
+ $htcpPacket = pack( 'nxxnCxNxxa*n',
+ $htcpLen, $htcpDataLen, $htcpOpCLR,
+ $htcpTransID, $htcpSpecifier, 2 );
- // Send out
- wfDebug( "Purging URL $url via HTCP\n" );
+ wfDebugLog( 'squid', __METHOD__ .
+ "Purging URL $url via HTCP\n" );
+ foreach ( $conf as $subconf ) {
socket_sendto( $conn, $htcpPacket, $htcpLen, 0,
- $conf['host'], $conf['port'] );
+ $subconf['host'], $subconf['port'] );
}
- } else {
- $errstr = socket_strerror( socket_last_error() );
- wfDebug( __METHOD__ . "(): Error opening UDP socket: $errstr\n" );
}
wfProfileOut( __METHOD__ );
}
@@ -242,21 +276,20 @@ class SquidUpdate {
*
* Client functions should not need to call this.
*
- * @param $url string
- *
+ * @param string $url
* @return string
*/
- static function expand( $url ) {
+ public static function expand( $url ) {
return wfExpandUrl( $url, PROTO_INTERNAL );
}
-
+
/**
* Find the HTCP routing rule to use for a given URL.
- * @param $url string URL to match
- * @param $rules array Array of rules, see $wgHTCPMulticastRouting for format and behavior
+ * @param string $url URL to match
+ * @param array $rules Array of rules, see $wgHTCPRouting for format and behavior
* @return mixed Element of $rules that matched, or false if nothing matched
*/
- static function getRuleForURL( $url, $rules ) {
+ private static function getRuleForURL( $url, $rules ) {
foreach ( $rules as $regex => $routing ) {
if ( $regex === '' || preg_match( $regex, $url ) ) {
return $routing;
@@ -264,5 +297,4 @@ class SquidUpdate {
}
return false;
}
-
}
diff --git a/includes/cache/UserCache.php b/includes/cache/UserCache.php
index 6ec23669..6085f586 100644
--- a/includes/cache/UserCache.php
+++ b/includes/cache/UserCache.php
@@ -45,7 +45,7 @@ class UserCache {
* Get a property of a user based on their user ID
*
* @param $userId integer User ID
- * @param $prop string User property
+ * @param string $prop User property
* @return mixed The property or false if the user does not exist
*/
public function getProp( $userId, $prop ) {
@@ -59,10 +59,21 @@ class UserCache {
}
/**
+ * Get the name of a user or return $ip if the user ID is 0
+ *
+ * @param integer $userId
+ * @param string $ip
+ * @since 1.22
+ */
+ public function getUserName( $userId, $ip ) {
+ return $userId > 0 ? $this->getProp( $userId, 'name' ) : $ip;
+ }
+
+ /**
* Preloads user names for given list of users.
- * @param $userIds Array List of user IDs
- * @param $options Array Option flags; include 'userpage' and 'usertalk'
- * @param $caller String: the calling method
+ * @param array $userIds List of user IDs
+ * @param array $options Option flags; include 'userpage' and 'usertalk'
+ * @param string $caller the calling method
*/
public function doQuery( array $userIds, $options = array(), $caller = '' ) {
wfProfileIn( __METHOD__ );
@@ -70,6 +81,8 @@ class UserCache {
$usersToCheck = array();
$usersToQuery = array();
+ $userIds = array_unique( $userIds );
+
foreach ( $userIds as $userId ) {
$userId = (int)$userId;
if ( $userId <= 0 ) {
@@ -124,8 +137,8 @@ class UserCache {
* Check if a cache type is in $options and was not loaded for this user
*
* @param $uid integer user ID
- * @param $type string Cache type
- * @param $options Array Requested cache types
+ * @param string $type Cache type
+ * @param array $options Requested cache types
* @return bool
*/
protected function queryNeeded( $uid, $type, array $options ) {
diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php
new file mode 100644
index 00000000..bf800c46
--- /dev/null
+++ b/includes/changes/ChangesList.php
@@ -0,0 +1,552 @@
+<?php
+/**
+ * Base class for all changes lists.
+ *
+ * The class is used for formatting recent changes, related changes and watchlist.
+ *
+ * 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
+ */
+
+class ChangesList extends ContextSource {
+
+ /**
+ * @var Skin
+ */
+ public $skin;
+
+ protected $watchlist = false;
+
+ protected $message;
+
+ /**
+ * Changeslist constructor
+ *
+ * @param $obj Skin or IContextSource
+ */
+ public function __construct( $obj ) {
+ if ( $obj instanceof IContextSource ) {
+ $this->setContext( $obj );
+ $this->skin = $obj->getSkin();
+ } else {
+ $this->setContext( $obj->getContext() );
+ $this->skin = $obj;
+ }
+ $this->preCacheMessages();
+ }
+
+ /**
+ * Fetch an appropriate changes list class for the main context
+ * This first argument used to be an User object.
+ *
+ * @deprecated in 1.18; use newFromContext() instead
+ * @param string|User $unused Unused
+ * @return ChangesList|EnhancedChangesList|OldChangesList derivative
+ */
+ public static function newFromUser( $unused ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return self::newFromContext( RequestContext::getMain() );
+ }
+
+ /**
+ * Fetch an appropriate changes list class for the specified context
+ * Some users might want to use an enhanced list format, for instance
+ *
+ * @param $context IContextSource to use
+ * @return ChangesList|EnhancedChangesList|OldChangesList derivative
+ */
+ public static function newFromContext( IContextSource $context ) {
+ $user = $context->getUser();
+ $sk = $context->getSkin();
+ $list = null;
+ if ( wfRunHooks( 'FetchChangesList', array( $user, &$sk, &$list ) ) ) {
+ $new = $context->getRequest()->getBool( 'enhanced', $user->getOption( 'usenewrc' ) );
+ return $new ? new EnhancedChangesList( $context ) : new OldChangesList( $context );
+ } else {
+ 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
+ * they are called often, we call them once and save them in $this->message
+ */
+ private function preCacheMessages() {
+ if ( !isset( $this->message ) ) {
+ foreach ( array(
+ 'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
+ 'semicolon-separator', 'pipe-separator' ) as $msg
+ ) {
+ $this->message[$msg] = $this->msg( $msg )->escaped();
+ }
+ }
+ }
+
+ /**
+ * Returns the appropriate flags for new page, minor change and patrolling
+ * @param array $flags Associative array of 'flag' => Bool
+ * @param string $nothing to use for empty space
+ * @return String
+ */
+ public function recentChangesFlags( $flags, $nothing = '&#160;' ) {
+ global $wgRecentChangesFlags;
+ $f = '';
+ foreach ( array_keys( $wgRecentChangesFlags ) as $flag ) {
+ $f .= isset( $flags[$flag] ) && $flags[$flag]
+ ? self::flag( $flag )
+ : $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 string $flag One key of $wgRecentChangesFlags
+ * @return String: Raw HTML
+ */
+ public static function flag( $flag ) {
+ static $flagInfos = null;
+ if ( is_null( $flagInfos ) ) {
+ global $wgRecentChangesFlags;
+ $flagInfos = array();
+ foreach ( $wgRecentChangesFlags as $key => $value ) {
+ $flagInfos[$key]['letter'] = wfMessage( $value['letter'] )->escaped();
+ $flagInfos[$key]['title'] = wfMessage( $value['title'] )->escaped();
+ // Allow customized class name, fall back to flag name
+ $flagInfos[$key]['class'] = Sanitizer::escapeClass(
+ isset( $value['class'] ) ? $value['class'] : $key );
+ }
+ }
+
+ // Inconsistent naming, bleh, kepted for b/c
+ $map = array(
+ 'minoredit' => 'minor',
+ 'botedit' => 'bot',
+ );
+ if ( isset( $map[$flag] ) ) {
+ $flag = $map[$flag];
+ }
+
+ return "<abbr class='" . $flagInfos[$flag]['class'] . "' title='" . $flagInfos[$flag]['title'] . "'>" .
+ $flagInfos[$flag]['letter'] .
+ '</abbr>';
+ }
+
+ /**
+ * Returns text for the start of the tabular part of RC
+ * @return String
+ */
+ public function beginRecentChangesList() {
+ $this->rc_cache = array();
+ $this->rcMoveIndex = 0;
+ $this->rcCacheIndex = 0;
+ $this->lastdate = '';
+ $this->rclistOpen = false;
+ $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' );
+ return '';
+ }
+
+ /**
+ * Show formatted char difference
+ * @param $old Integer: bytes
+ * @param $new Integer: bytes
+ * @param $context IContextSource context to use
+ * @return String
+ */
+ public static function showCharacterDifference( $old, $new, IContextSource $context = null ) {
+ global $wgRCChangedSizeThreshold, $wgMiserMode;
+
+ if ( !$context ) {
+ $context = RequestContext::getMain();
+ }
+
+ $new = (int)$new;
+ $old = (int)$old;
+ $szdiff = $new - $old;
+
+ $lang = $context->getLanguage();
+ $code = $lang->getCode();
+ static $fastCharDiff = array();
+ if ( !isset( $fastCharDiff[$code] ) ) {
+ $fastCharDiff[$code] = $wgMiserMode || $context->msg( 'rc-change-size' )->plain() === '$1';
+ }
+
+ $formattedSize = $lang->formatNum( $szdiff );
+
+ if ( !$fastCharDiff[$code] ) {
+ $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
+ }
+
+ if ( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) {
+ $tag = 'strong';
+ } else {
+ $tag = 'span';
+ }
+
+ if ( $szdiff === 0 ) {
+ $formattedSizeClass = 'mw-plusminus-null';
+ }
+ if ( $szdiff > 0 ) {
+ $formattedSize = '+' . $formattedSize;
+ $formattedSizeClass = 'mw-plusminus-pos';
+ }
+ if ( $szdiff < 0 ) {
+ $formattedSizeClass = 'mw-plusminus-neg';
+ }
+
+ $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
+
+ return Html::element( $tag,
+ array( 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ),
+ $context->msg( 'parentheses', $formattedSize )->plain() ) . $lang->getDirMark();
+ }
+
+ /**
+ * Format the character difference of one or several changes.
+ *
+ * @param $old RecentChange
+ * @param $new RecentChange last change to use, if not provided, $old will be used
+ * @return string HTML fragment
+ */
+ public function formatCharacterDifference( RecentChange $old, RecentChange $new = null ) {
+ $oldlen = $old->mAttribs['rc_old_len'];
+
+ if ( $new ) {
+ $newlen = $new->mAttribs['rc_new_len'];
+ } else {
+ $newlen = $old->mAttribs['rc_new_len'];
+ }
+
+ if ( $oldlen === null || $newlen === null ) {
+ return '';
+ }
+
+ return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() );
+ }
+
+ /**
+ * Returns text for the end of RC
+ * @return String
+ */
+ public function endRecentChangesList() {
+ if ( $this->rclistOpen ) {
+ return "</ul>\n";
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * @param string $s HTML to update
+ * @param $rc_timestamp mixed
+ */
+ public function insertDateHeader( &$s, $rc_timestamp ) {
+ # Make date header if necessary
+ $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
+ if ( $date != $this->lastdate ) {
+ if ( $this->lastdate != '' ) {
+ $s .= "</ul>\n";
+ }
+ $s .= Xml::element( 'h4', null, $date ) . "\n<ul class=\"special\">";
+ $this->lastdate = $date;
+ $this->rclistOpen = true;
+ }
+ }
+
+ /**
+ * @param string $s HTML to update
+ * @param $title Title
+ * @param $logtype string
+ */
+ public function insertLog( &$s, $title, $logtype ) {
+ $page = new LogPage( $logtype );
+ $logname = $page->getName()->escaped();
+ $s .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $title, $logname ) )->escaped();
+ }
+
+ /**
+ * @param string $s HTML to update
+ * @param $rc RecentChange
+ * @param $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'];
+ } elseif ( !self::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $diffLink = $this->message['diff'];
+ } else {
+ $query = array(
+ 'curid' => $rc->mAttribs['rc_cur_id'],
+ 'diff' => $rc->mAttribs['rc_this_oldid'],
+ 'oldid' => $rc->mAttribs['rc_last_oldid']
+ );
+
+ $diffLink = Linker::linkKnown(
+ $rc->getTitle(),
+ $this->message['diff'],
+ array( 'tabindex' => $rc->counter ),
+ $query
+ );
+ }
+ $diffhist = $diffLink . $this->message['pipe-separator'];
+ # History link
+ $diffhist .= Linker::linkKnown(
+ $rc->getTitle(),
+ $this->message['hist'],
+ array(),
+ array(
+ 'curid' => $rc->mAttribs['rc_cur_id'],
+ 'action' => 'history'
+ )
+ );
+ $s .= $this->msg( 'parentheses' )->rawParams( $diffhist )->escaped() . ' <span class="mw-changeslist-separator">. .</span> ';
+ }
+
+ /**
+ * @param string $s HTML to update
+ * @param $rc RecentChange
+ * @param $unpatrolled
+ * @param $watched
+ */
+ public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) {
+ $params = array();
+
+ $articlelink = Linker::linkKnown(
+ $rc->getTitle(),
+ null,
+ array( 'class' => 'mw-changeslist-title' ),
+ $params
+ );
+ if ( $this->isDeleted( $rc, Revision::DELETED_TEXT ) ) {
+ $articlelink = '<span class="history-deleted">' . $articlelink . '</span>';
+ }
+ # To allow for boldening pages watched by this user
+ $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
+ # RTL/LTR marker
+ $articlelink .= $this->getLanguage()->getDirMark();
+
+ wfRunHooks( 'ChangesListInsertArticleLink',
+ array( &$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched ) );
+
+ $s .= " $articlelink";
+ }
+
+ /**
+ * Get the timestamp from $rc formatted with current user's settings
+ * and a separator
+ *
+ * @param $rc RecentChange
+ * @return string HTML fragment
+ */
+ public function getTimestamp( $rc ) {
+ return $this->message['semicolon-separator'] . '<span class="mw-changeslist-date">' .
+ $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() ) . '</span> <span class="mw-changeslist-separator">. .</span> ';
+ }
+
+ /**
+ * Insert time timestamp string from $rc into $s
+ *
+ * @param string $s HTML to update
+ * @param $rc RecentChange
+ */
+ public function insertTimestamp( &$s, $rc ) {
+ $s .= $this->getTimestamp( $rc );
+ }
+
+ /**
+ * Insert links to user page, user talk page and eventually a blocking link
+ *
+ * @param &$s String HTML to update
+ * @param &$rc RecentChange
+ */
+ public function insertUserRelatedLinks( &$s, &$rc ) {
+ if ( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
+ $s .= ' <span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
+ } else {
+ $s .= $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs['rc_user'],
+ $rc->mAttribs['rc_user_text'] );
+ $s .= Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+ }
+ }
+
+ /**
+ * Insert a formatted action
+ *
+ * @param $rc RecentChange
+ * @return string
+ */
+ public function insertLogEntry( $rc ) {
+ $formatter = LogFormatter::newFromRow( $rc->mAttribs );
+ $formatter->setContext( $this->getContext() );
+ $formatter->setShowUserToolLinks( true );
+ $mark = $this->getLanguage()->getDirMark();
+ return $formatter->getActionText() . " $mark" . $formatter->getComment();
+ }
+
+ /**
+ * Insert a formatted comment
+ * @param $rc RecentChange
+ * @return string
+ */
+ public function insertComment( $rc ) {
+ if ( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
+ if ( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) {
+ return ' <span class="history-deleted">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
+ } else {
+ return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Check whether to enable recent changes patrol features
+ *
+ * @deprecated since 1.22
+ * @return Boolean
+ */
+ public static function usePatrol() {
+ global $wgUser;
+
+ wfDeprecated( __METHOD__, '1.22' );
+
+ return $wgUser->useRCPatrol();
+ }
+
+ /**
+ * Returns the string which indicates the number of watching users
+ * @return string
+ */
+ protected function numberofWatchingusers( $count ) {
+ static $cache = array();
+ if ( $count > 0 ) {
+ if ( !isset( $cache[$count] ) ) {
+ $cache[$count] = $this->msg( 'number_of_watching_users_RCview' )->numParams( $count )->escaped();
+ }
+ return $cache[$count];
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Determine if said field of a revision is hidden
+ * @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;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted.
+ * @param $rc RCCacheEntry
+ * @param $field Integer
+ * @param $user User object to check, or null to use $wgUser
+ * @return Boolean
+ */
+ public static function userCan( $rc, $field, User $user = null ) {
+ if ( $rc->mAttribs['rc_type'] == RC_LOG ) {
+ return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $user );
+ } else {
+ return Revision::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $user );
+ }
+ }
+
+ /**
+ * @param $link string
+ * @param $watched bool
+ * @return string
+ */
+ protected function maybeWatchedLink( $link, $watched = false ) {
+ if ( $watched ) {
+ return '<strong class="mw-watched">' . $link . '</strong>';
+ } else {
+ return '<span class="mw-rc-unwatched">' . $link . '</span>';
+ }
+ }
+
+ /** Inserts a rollback link
+ *
+ * @param $s string
+ * @param $rc RecentChange
+ */
+ public function insertRollback( &$s, &$rc ) {
+ if ( $rc->mAttribs['rc_type'] == RC_EDIT && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) {
+ $page = $rc->getTitle();
+ /** Check for rollback and edit permissions, disallow special pages, and only
+ * show a link on the top-most revision */
+ if ( $this->getUser()->isAllowed( 'rollback' ) && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] )
+ {
+ $rev = new Revision( array(
+ 'title' => $page,
+ 'id' => $rc->mAttribs['rc_this_oldid'],
+ 'user' => $rc->mAttribs['rc_user'],
+ 'user_text' => $rc->mAttribs['rc_user_text'],
+ 'deleted' => $rc->mAttribs['rc_deleted']
+ ) );
+ $s .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
+ }
+ }
+ }
+
+ /**
+ * @param $s string
+ * @param $rc RecentChange
+ * @param $classes
+ */
+ public function insertTags( &$s, &$rc, &$classes ) {
+ if ( empty( $rc->mAttribs['ts_tags'] ) ) {
+ return;
+ }
+
+ list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' );
+ $classes = array_merge( $classes, $newClasses );
+ $s .= ' ' . $tagSummary;
+ }
+
+ public function insertExtra( &$s, &$rc, &$classes ) {
+ // Empty, used for subclasses to add anything special.
+ }
+
+ protected function showAsUnpatrolled( RecentChange $rc ) {
+ $unpatrolled = false;
+ if ( !$rc->mAttribs['rc_patrolled'] ) {
+ if ( $this->getUser()->useRCPatrol() ) {
+ $unpatrolled = true;
+ } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_type'] == RC_NEW ) {
+ $unpatrolled = true;
+ }
+ }
+ return $unpatrolled;
+ }
+}
diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php
new file mode 100644
index 00000000..4c8aa451
--- /dev/null
+++ b/includes/changes/EnhancedChangesList.php
@@ -0,0 +1,661 @@
+<?php
+/**
+ * Generates a list of changes using an Enhanced system (uses javascript).
+ *
+ * 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
+ */
+
+class EnhancedChangesList extends ChangesList {
+
+ protected $rc_cache;
+
+ /**
+ * Add the JavaScript file for enhanced changeslist
+ * @return String
+ */
+ public function beginRecentChangesList() {
+ $this->rc_cache = array();
+ $this->rcMoveIndex = 0;
+ $this->rcCacheIndex = 0;
+ $this->lastdate = '';
+ $this->rclistOpen = false;
+ $this->getOutput()->addModuleStyles( array(
+ 'mediawiki.special.changeslist',
+ 'mediawiki.special.changeslist.enhanced',
+ ) );
+ $this->getOutput()->addModules( array(
+ 'jquery.makeCollapsible',
+ 'mediawiki.icon',
+ ) );
+ return '';
+ }
+ /**
+ * Format a line for enhanced recentchange (aka with javascript and block of lines).
+ *
+ * @param $baseRC RecentChange
+ * @param $watched bool
+ *
+ * @return string
+ */
+ public function recentChangesLine( &$baseRC, $watched = false ) {
+ wfProfileIn( __METHOD__ );
+
+ # Create a specialised object
+ $rc = RCCacheEntry::newFromParent( $baseRC );
+
+ $curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] );
+
+ # If it's a new day, add the headline and flush the cache
+ $date = $this->getLanguage()->userDate( $rc->mAttribs['rc_timestamp'], $this->getUser() );
+ $ret = '';
+ if ( $date != $this->lastdate ) {
+ # Process current cache
+ $ret = $this->recentChangesBlock();
+ $this->rc_cache = array();
+ $ret .= Xml::element( 'h4', null, $date ) . "\n";
+ $this->lastdate = $date;
+ }
+
+ # Should patrol-related stuff be shown?
+ $rc->unpatrolled = $this->showAsUnpatrolled( $rc );
+
+ $showdifflinks = true;
+ # Make article link
+ $type = $rc->mAttribs['rc_type'];
+ $logType = $rc->mAttribs['rc_log_type'];
+ // Page moves, very old style, not supported anymore
+ if ( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
+ // New unpatrolled pages
+ } elseif ( $rc->unpatrolled && $type == RC_NEW ) {
+ $clink = Linker::linkKnown( $rc->getTitle() );
+ // Log entries
+ } elseif ( $type == RC_LOG ) {
+ if ( $logType ) {
+ $logtitle = SpecialPage::getTitleFor( 'Log', $logType );
+ $logpage = new LogPage( $logType );
+ $logname = $logpage->getName()->escaped();
+ $clink = $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logtitle, $logname ) )->escaped();
+ } else {
+ $clink = Linker::link( $rc->getTitle() );
+ }
+ $watched = false;
+ // Log entries (old format) and special pages
+ } elseif ( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) {
+ wfDebug( "Unexpected special page in recentchanges\n" );
+ $clink = '';
+ // Edits
+ } else {
+ $clink = Linker::linkKnown( $rc->getTitle() );
+ }
+
+ # Don't show unusable diff links
+ if ( !ChangesList::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $showdifflinks = false;
+ }
+
+ $time = $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() );
+ $rc->watched = $watched;
+ $rc->link = $clink;
+ $rc->timestamp = $time;
+ $rc->numberofWatchingusers = $baseRC->numberofWatchingusers;
+
+ # Make "cur" and "diff" links. Do not use link(), it is too slow if
+ # called too many times (50% of CPU time on RecentChanges!).
+ $thisOldid = $rc->mAttribs['rc_this_oldid'];
+ $lastOldid = $rc->mAttribs['rc_last_oldid'];
+
+ $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $thisOldid );
+ $querydiff = $curIdEq + array( 'diff' => $thisOldid, 'oldid' => $lastOldid );
+
+ if ( !$showdifflinks ) {
+ $curLink = $this->message['cur'];
+ $diffLink = $this->message['diff'];
+ } elseif ( in_array( $type, array( RC_NEW, RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) {
+ if ( $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 {
+ $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 || !$lastOldid ) {
+ $lastLink = $this->message['last'];
+ } elseif ( in_array( $type, array( RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) {
+ $lastLink = $this->message['last'];
+ } else {
+ $lastLink = Linker::linkKnown( $rc->getTitle(), $this->message['last'],
+ array(), $curIdEq + array( 'diff' => $thisOldid, 'oldid' => $lastOldid ) );
+ }
+
+ # Make user links
+ if ( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
+ $rc->userlink = ' <span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
+ } else {
+ $rc->userlink = Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+ $rc->usertalklink = Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+ }
+
+ $rc->lastlink = $lastLink;
+ $rc->curlink = $curLink;
+ $rc->difflink = $diffLink;
+
+ # Put accumulated information into the cache, for later display
+ # Page moves go on their own line
+ $title = $rc->getTitle();
+ $secureName = $title->getPrefixedDBkey();
+ if ( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
+ # Use an @ character to prevent collision with page names
+ $this->rc_cache['@@' . ( $this->rcMoveIndex++ )] = array( $rc );
+ } else {
+ # Logs are grouped by type
+ if ( $type == RC_LOG ) {
+ $secureName = SpecialPage::getTitleFor( 'Log', $logType )->getPrefixedDBkey();
+ }
+ if ( !isset( $this->rc_cache[$secureName] ) ) {
+ $this->rc_cache[$secureName] = array();
+ }
+
+ array_push( $this->rc_cache[$secureName], $rc );
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return $ret;
+ }
+
+ /**
+ * Enhanced RC group
+ * @return string
+ */
+ protected function recentChangesBlockGroup( $block ) {
+ global $wgRCShowChangedSize;
+
+ wfProfileIn( __METHOD__ );
+
+ # Add the namespace and title of the block as part of the class
+ $classes = array( 'mw-collapsible', 'mw-collapsed', 'mw-enhanced-rc' );
+ if ( $block[0]->mAttribs['rc_log_type'] ) {
+ # Log entry
+ $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-'
+ . $block[0]->mAttribs['rc_log_type'] );
+ } else {
+ $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns'
+ . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] );
+ }
+ $classes[] = $block[0]->watched && $block[0]->mAttribs['rc_timestamp'] >= $block[0]->watched
+ ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched';
+ $r = Html::openElement( 'table', array( 'class' => $classes ) ) .
+ Html::openElement( 'tr' );
+
+ # Collate list of users
+ $userlinks = array();
+ # Other properties
+ $unpatrolled = false;
+ $isnew = false;
+ $allBots = true;
+ $allMinors = true;
+ $curId = $currentRevision = 0;
+ # Some catalyst variables...
+ $namehidden = true;
+ $allLogs = true;
+ foreach ( $block as $rcObj ) {
+ $oldid = $rcObj->mAttribs['rc_last_oldid'];
+ if ( $rcObj->mAttribs['rc_type'] == RC_NEW ) {
+ $isnew = true;
+ }
+ // If all log actions to this page were hidden, then don't
+ // give the name of the affected page for this block!
+ if ( !$this->isDeleted( $rcObj, LogPage::DELETED_ACTION ) ) {
+ $namehidden = false;
+ }
+ $u = $rcObj->userlink;
+ if ( !isset( $userlinks[$u] ) ) {
+ $userlinks[$u] = 0;
+ }
+ if ( $rcObj->unpatrolled ) {
+ $unpatrolled = true;
+ }
+ if ( $rcObj->mAttribs['rc_type'] != RC_LOG ) {
+ $allLogs = false;
+ }
+ # Get the latest entry with a page_id and oldid
+ # since logs may not have these.
+ if ( !$curId && $rcObj->mAttribs['rc_cur_id'] ) {
+ $curId = $rcObj->mAttribs['rc_cur_id'];
+ }
+ if ( !$currentRevision && $rcObj->mAttribs['rc_this_oldid'] ) {
+ $currentRevision = $rcObj->mAttribs['rc_this_oldid'];
+ }
+
+ if ( !$rcObj->mAttribs['rc_bot'] ) {
+ $allBots = false;
+ }
+ if ( !$rcObj->mAttribs['rc_minor'] ) {
+ $allMinors = false;
+ }
+
+ $userlinks[$u]++;
+ }
+
+ # Sort the list and convert to text
+ krsort( $userlinks );
+ asort( $userlinks );
+ $users = array();
+ foreach ( $userlinks as $userlink => $count ) {
+ $text = $userlink;
+ $text .= $this->getLanguage()->getDirMark();
+ if ( $count > 1 ) {
+ $text .= ' ' . $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->formatNum( $count ) . '×' )->escaped();
+ }
+ array_push( $users, $text );
+ }
+
+ $users = ' <span class="changedby">'
+ . $this->msg( 'brackets' )->rawParams(
+ implode( $this->message['semicolon-separator'], $users )
+ )->escaped() . '</span>';
+
+ $tl = '<span class="mw-collapsible-toggle mw-collapsible-arrow mw-enhancedchanges-arrow mw-enhancedchanges-arrow-space"></span>';
+ $r .= "<td>$tl</td>";
+
+ # Main line
+ $r .= '<td class="mw-enhanced-rc">' . $this->recentChangesFlags( array(
+ 'newpage' => $isnew, # show, when one have this flag
+ 'minor' => $allMinors, # show only, when all have this flag
+ 'unpatrolled' => $unpatrolled, # show, when one have this flag
+ 'bot' => $allBots, # show only, when all have this flag
+ ) );
+
+ # Timestamp
+ $r .= '&#160;' . $block[0]->timestamp . '&#160;</td><td>';
+
+ # Article link
+ if ( $namehidden ) {
+ $r .= ' <span class="history-deleted">' . $this->msg( 'rev-deleted-event' )->escaped() . '</span>';
+ } elseif ( $allLogs ) {
+ $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
+ } else {
+ $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched );
+ }
+
+ $r .= $this->getLanguage()->getDirMark();
+
+ $queryParams['curid'] = $curId;
+
+ # Changes message
+ static $nchanges = array();
+ static $sinceLastVisitMsg = array();
+
+ $n = count( $block );
+ if ( !isset( $nchanges[$n] ) ) {
+ $nchanges[$n] = $this->msg( 'nchanges' )->numParams( $n )->escaped();
+ }
+
+ $sinceLast = 0;
+ $unvisitedOldid = null;
+ foreach ( $block as $rcObj ) {
+ // Same logic as below inside main foreach
+ if ( $rcObj->watched && $rcObj->mAttribs['rc_timestamp'] >= $rcObj->watched ) {
+ $sinceLast++;
+ $unvisitedOldid = $rcObj->mAttribs['rc_last_oldid'];
+ }
+ }
+ if ( !isset( $sinceLastVisitMsg[$sinceLast] ) ) {
+ $sinceLastVisitMsg[$sinceLast] =
+ $this->msg( 'enhancedrc-since-last-visit' )->numParams( $sinceLast )->escaped();
+ }
+
+ # Total change link
+ $r .= ' ';
+ $logtext = '';
+ if ( !$allLogs ) {
+ if ( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $logtext .= $nchanges[$n];
+ } elseif ( $isnew ) {
+ $logtext .= $nchanges[$n];
+ } else {
+ $logtext .= Linker::link(
+ $block[0]->getTitle(),
+ $nchanges[$n],
+ array(),
+ $queryParams + array(
+ 'diff' => $currentRevision,
+ 'oldid' => $oldid,
+ ),
+ array( 'known', 'noclasses' )
+ );
+ if ( $sinceLast > 0 && $sinceLast < $n ) {
+ $logtext .= $this->message['pipe-separator'] . Linker::link(
+ $block[0]->getTitle(),
+ $sinceLastVisitMsg[$sinceLast],
+ array(),
+ $queryParams + array(
+ 'diff' => $currentRevision,
+ 'oldid' => $unvisitedOldid,
+ ),
+ array( 'known', 'noclasses' )
+ );
+ }
+ }
+ }
+
+ # History
+ if ( $allLogs ) {
+ // don't show history link for logs
+ } elseif ( $namehidden || !$block[0]->getTitle()->exists() ) {
+ $logtext .= $this->message['pipe-separator'] . $this->message['enhancedrc-history'];
+ } else {
+ $params = $queryParams;
+ $params['action'] = 'history';
+
+ $logtext .= $this->message['pipe-separator'] .
+ Linker::linkKnown(
+ $block[0]->getTitle(),
+ $this->message['enhancedrc-history'],
+ array(),
+ $params
+ );
+ }
+
+ if ( $logtext !== '' ) {
+ $r .= $this->msg( 'parentheses' )->rawParams( $logtext )->escaped();
+ }
+
+ $r .= ' <span class="mw-changeslist-separator">. .</span> ';
+
+ # Character difference (does not apply if only log items)
+ if ( $wgRCShowChangedSize && !$allLogs ) {
+ $last = 0;
+ $first = count( $block ) - 1;
+ # Some events (like logs) have an "empty" size, so we need to skip those...
+ while ( $last < $first && $block[$last]->mAttribs['rc_new_len'] === null ) {
+ $last++;
+ }
+ while ( $first > $last && $block[$first]->mAttribs['rc_old_len'] === null ) {
+ $first--;
+ }
+ # Get net change
+ $chardiff = $this->formatCharacterDifference( $block[$first], $block[$last] );
+
+ if ( $chardiff == '' ) {
+ $r .= ' ';
+ } else {
+ $r .= ' ' . $chardiff . ' <span class="mw-changeslist-separator">. .</span> ';
+ }
+ }
+
+ $r .= $users;
+ $r .= $this->numberofWatchingusers( $block[0]->numberofWatchingusers );
+
+ # Sub-entries
+ foreach ( $block as $rcObj ) {
+ # Classes to apply -- TODO implement
+ $classes = array();
+ $type = $rcObj->mAttribs['rc_type'];
+
+ $trClass = $rcObj->watched && $rcObj->mAttribs['rc_timestamp'] >= $rcObj->watched
+ ? ' class="mw-enhanced-watched"' : '';
+
+ $r .= '<tr' . $trClass . '><td></td><td class="mw-enhanced-rc">';
+ $r .= $this->recentChangesFlags( array(
+ 'newpage' => $type == RC_NEW,
+ 'minor' => $rcObj->mAttribs['rc_minor'],
+ 'unpatrolled' => $rcObj->unpatrolled,
+ 'bot' => $rcObj->mAttribs['rc_bot'],
+ ) );
+ $r .= '&#160;</td><td class="mw-enhanced-rc-nested"><span class="mw-enhanced-rc-time">';
+
+ $params = $queryParams;
+
+ if ( $rcObj->mAttribs['rc_this_oldid'] != 0 ) {
+ $params['oldid'] = $rcObj->mAttribs['rc_this_oldid'];
+ }
+
+ # Log timestamp
+ if ( $type == RC_LOG ) {
+ $link = $rcObj->timestamp;
+ # Revision link
+ } elseif ( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $link = '<span class="history-deleted">' . $rcObj->timestamp . '</span> ';
+ } else {
+
+ $link = Linker::linkKnown(
+ $rcObj->getTitle(),
+ $rcObj->timestamp,
+ array(),
+ $params
+ );
+ if ( $this->isDeleted( $rcObj, Revision::DELETED_TEXT ) ) {
+ $link = '<span class="history-deleted">' . $link . '</span> ';
+ }
+ }
+ $r .= $link . '</span>';
+
+ if ( !$type == RC_LOG || $type == RC_NEW ) {
+ $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->curlink . $this->message['pipe-separator'] . $rcObj->lastlink )->escaped();
+ }
+ $r .= ' <span class="mw-changeslist-separator">. .</span> ';
+
+ # Character diff
+ if ( $wgRCShowChangedSize ) {
+ $cd = $this->formatCharacterDifference( $rcObj );
+ if ( $cd !== '' ) {
+ $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
+ }
+ }
+
+ if ( $rcObj->mAttribs['rc_type'] == RC_LOG ) {
+ $r .= $this->insertLogEntry( $rcObj );
+ } else {
+ # User links
+ $r .= $rcObj->userlink;
+ $r .= $rcObj->usertalklink;
+ $r .= $this->insertComment( $rcObj );
+ }
+
+ # Rollback
+ $this->insertRollback( $r, $rcObj );
+ # Tags
+ $this->insertTags( $r, $rcObj, $classes );
+
+ $r .= "</td></tr>\n";
+ }
+ $r .= "</table>\n";
+
+ $this->rcCacheIndex++;
+
+ wfProfileOut( __METHOD__ );
+
+ return $r;
+ }
+
+ /**
+ * Generate HTML for an arrow or placeholder graphic
+ * @param string $dir one of '', 'd', 'l', 'r'
+ * @param string $alt text
+ * @param string $title text
+ * @return String: HTML "<img>" tag
+ */
+ protected function arrow( $dir, $alt = '', $title = '' ) {
+ global $wgStylePath;
+ $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' );
+ $encAlt = htmlspecialchars( $alt );
+ $encTitle = htmlspecialchars( $title );
+ return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" title=\"$encTitle\" />";
+ }
+
+ /**
+ * Generate HTML for a right- or left-facing arrow,
+ * depending on language direction.
+ * @return String: HTML "<img>" tag
+ */
+ protected function sideArrow() {
+ $dir = $this->getLanguage()->isRTL() ? 'l' : 'r';
+ return $this->arrow( $dir, '+', $this->msg( 'rc-enhanced-expand' )->text() );
+ }
+
+ /**
+ * Generate HTML for a down-facing arrow
+ * depending on language direction.
+ * @return String: HTML "<img>" tag
+ */
+ protected function downArrow() {
+ return $this->arrow( 'd', '-', $this->msg( 'rc-enhanced-hide' )->text() );
+ }
+
+ /**
+ * Generate HTML for a spacer image
+ * @return String: HTML "<img>" tag
+ */
+ protected function spacerArrow() {
+ return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space
+ }
+
+ /**
+ * Enhanced RC ungrouped line.
+ *
+ * @param $rcObj RecentChange
+ * @return String: a HTML formatted line (generated using $r)
+ */
+ protected function recentChangesBlockLine( $rcObj ) {
+ global $wgRCShowChangedSize;
+
+ wfProfileIn( __METHOD__ );
+ $query['curid'] = $rcObj->mAttribs['rc_cur_id'];
+
+ $type = $rcObj->mAttribs['rc_type'];
+ $logType = $rcObj->mAttribs['rc_log_type'];
+ $classes = array( 'mw-enhanced-rc' );
+ if ( $logType ) {
+ # Log entry
++ $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType );
+ } else {
+ $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' .
+ $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] );
+ }
+ $classes[] = $rcObj->watched && $rcObj->mAttribs['rc_timestamp'] >= $rcObj->watched
+ ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched';
+ $r = Html::openElement( 'table', array( 'class' => $classes ) ) .
+ Html::openElement( 'tr' );
+
+ $r .= '<td class="mw-enhanced-rc"><span class="mw-enhancedchanges-arrow-space"></span>';
+ # Flag and Timestamp
+ if ( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
+ $r .= $this->recentChangesFlags( array() ); // no flags, but need the placeholders
+ } else {
+ $r .= $this->recentChangesFlags( array(
+ 'newpage' => $type == RC_NEW,
+ 'minor' => $rcObj->mAttribs['rc_minor'],
+ 'unpatrolled' => $rcObj->unpatrolled,
+ 'bot' => $rcObj->mAttribs['rc_bot'],
+ ) );
+ }
+ $r .= '&#160;' . $rcObj->timestamp . '&#160;</td><td>';
+ # Article or log link
+ if ( $logType ) {
+ $logPage = new LogPage( $logType );
+ $logTitle = SpecialPage::getTitleFor( 'Log', $logType );
+ $logName = $logPage->getName()->escaped();
+ $r .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logTitle, $logName ) )->escaped();
+ } else {
+ $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched );
+ }
+ # Diff and hist links
+ if ( $type != RC_LOG ) {
+ $query['action'] = 'history';
+ $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->difflink . $this->message['pipe-separator'] . Linker::linkKnown(
+ $rcObj->getTitle(),
+ $this->message['hist'],
+ array(),
+ $query
+ ) )->escaped();
+ }
+ $r .= ' <span class="mw-changeslist-separator">. .</span> ';
+ # Character diff
+ if ( $wgRCShowChangedSize ) {
+ $cd = $this->formatCharacterDifference( $rcObj );
+ if ( $cd !== '' ) {
+ $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
+ }
+ }
+
+ if ( $type == RC_LOG ) {
+ $r .= $this->insertLogEntry( $rcObj );
+ } else {
+ $r .= ' ' . $rcObj->userlink . $rcObj->usertalklink;
+ $r .= $this->insertComment( $rcObj );
+ $this->insertRollback( $r, $rcObj );
+ }
+
+ # Tags
+ $this->insertTags( $r, $rcObj, $classes );
+ # Show how many people are watching this if enabled
+ $r .= $this->numberofWatchingusers( $rcObj->numberofWatchingusers );
+
+ $r .= "</td></tr></table>\n";
+
+ wfProfileOut( __METHOD__ );
+
+ return $r;
+ }
+
+ /**
+ * If enhanced RC is in use, this function takes the previously cached
+ * RC lines, arranges them, and outputs the HTML
+ *
+ * @return string
+ */
+ protected function recentChangesBlock() {
+ if ( count ( $this->rc_cache ) == 0 ) {
+ return '';
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ $blockOut = '';
+ foreach ( $this->rc_cache as $block ) {
+ if ( count( $block ) < 2 ) {
+ $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) );
+ } else {
+ $blockOut .= $this->recentChangesBlockGroup( $block );
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return '<div>' . $blockOut . '</div>';
+ }
+
+ /**
+ * Returns text for the end of RC
+ * If enhanced RC is in use, returns pretty much all the text
+ * @return string
+ */
+ public function endRecentChangesList() {
+ return $this->recentChangesBlock() . parent::endRecentChangesList();
+ }
+
+}
diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php
new file mode 100644
index 00000000..a7fe9342
--- /dev/null
+++ b/includes/changes/OldChangesList.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Generate a list of changes using the good old system (no javascript).
+ *
+ * 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
+ */
+class OldChangesList extends ChangesList {
+
+ /**
+ * Format a line using the old system (aka without any javascript).
+ *
+ * @param $rc RecentChange, passed by reference
+ * @param bool $watched (default false)
+ * @param int $linenumber (default null)
+ *
+ * @return string|bool
+ */
+ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
+ global $wgRCShowChangedSize;
+ wfProfileIn( __METHOD__ );
+
+ # Should patrol-related stuff be shown?
+ $unpatrolled = $this->showAsUnpatrolled( $rc );
+
+ $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience.
+ $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] );
+
+ $s = '';
+ $classes = array();
+ // use mw-line-even/mw-line-odd class only if linenumber is given (feature from bug 14468)
+ if ( $linenumber ) {
+ if ( $linenumber & 1 ) {
+ $classes[] = 'mw-line-odd';
+ } else {
+ $classes[] = 'mw-line-even';
+ }
+ }
+
+ // Indicate watched status on the line to allow for more
+ // comprehensive styling.
+ $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
+ ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched';
+
+ // Moved pages (very very old, not supported anymore)
+ if ( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) {
+ // Log entries
+ } elseif ( $rc->mAttribs['rc_log_type'] ) {
+ $logtitle = SpecialPage::getTitleFor( 'Log', $rc->mAttribs['rc_log_type'] );
+ $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] );
+ // Log entries (old format) or log targets, and special pages
+ } elseif ( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) {
+ list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] );
+ if ( $name == 'Log' ) {
+ $this->insertLog( $s, $rc->getTitle(), $subpage );
+ }
+ // Regular entries
+ } else {
+ $this->insertDiffHist( $s, $rc, $unpatrolled );
+ # M, N, b and ! (minor, new, bot and unpatrolled)
+ $s .= $this->recentChangesFlags(
+ array(
+ 'newpage' => $rc->mAttribs['rc_type'] == RC_NEW,
+ 'minor' => $rc->mAttribs['rc_minor'],
+ 'unpatrolled' => $unpatrolled,
+ 'bot' => $rc->mAttribs['rc_bot']
+ ),
+ ''
+ );
+ $this->insertArticleLink( $s, $rc, $unpatrolled, $watched );
+ }
+ # Edit/log timestamp
+ $this->insertTimestamp( $s, $rc );
+ # Bytes added or removed
+ if ( $wgRCShowChangedSize ) {
+ $cd = $this->formatCharacterDifference( $rc );
+ if ( $cd !== '' ) {
+ $s .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
+ }
+ }
+
+ if ( $rc->mAttribs['rc_type'] == RC_LOG ) {
+ $s .= $this->insertLogEntry( $rc );
+ } else {
+ # User tool links
+ $this->insertUserRelatedLinks( $s, $rc );
+ # LTR/RTL direction mark
+ $s .= $this->getLanguage()->getDirMark();
+ $s .= $this->insertComment( $rc );
+ }
+
+ # Tags
+ $this->insertTags( $s, $rc, $classes );
+ # Rollback
+ $this->insertRollback( $s, $rc );
+ # For subclasses
+ $this->insertExtra( $s, $rc, $classes );
+
+ # How many users watch this page
+ if ( $rc->numberofWatchingusers > 0 ) {
+ $s .= ' ' . $this->numberofWatchingusers( $rc->numberofWatchingusers );
+ }
+
+ if ( $this->watchlist ) {
+ $classes[] = Sanitizer::escapeClass( 'watchlist-' . $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
+ }
+
+ if ( !wfRunHooks( 'OldChangesListRecentChangesLine', array( &$this, &$s, $rc, &$classes ) ) ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfProfileOut( __METHOD__ );
+ return "$dateheader<li class=\"" . implode( ' ', $classes ) . "\">" . $s . "</li>\n";
+ }
+}
diff --git a/includes/changes/RCCacheEntry.php b/includes/changes/RCCacheEntry.php
new file mode 100644
index 00000000..9aef3d30
--- /dev/null
+++ b/includes/changes/RCCacheEntry.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+class RCCacheEntry extends RecentChange {
+ var $secureName, $link;
+ var $curlink, $difflink, $lastlink, $usertalklink, $versionlink;
+ var $userlink, $timestamp, $watched;
+
+ /**
+ * @param $rc RecentChange
+ * @return RCCacheEntry
+ */
+ static function newFromParent( $rc ) {
+ $rc2 = new RCCacheEntry;
+ $rc2->mAttribs = $rc->mAttribs;
+ $rc2->mExtra = $rc->mExtra;
+ return $rc2;
+ }
+}
diff --git a/includes/RecentChange.php b/includes/changes/RecentChange.php
index 332d0390..980bd0a0 100644
--- a/includes/RecentChange.php
+++ b/includes/changes/RecentChange.php
@@ -55,6 +55,7 @@
* lang the interwiki prefix, automatically set in save()
* oldSize text size before the change
* newSize text size after the change
+ * pageStatus status of the page: created, deleted, moved, restored, changed
*
* temporary: not stored in the database
* notificationtimestamp
@@ -79,7 +80,7 @@ class RecentChange {
* @var Title
*/
var $mMovedToTitle = false;
- var $numberofWatchingusers = 0 ; # Dummy to prevent error message in SpecialRecentchangeslinked
+ var $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentchangeslinked
var $notificationtimestamp;
# Factory methods
@@ -95,10 +96,12 @@ class RecentChange {
}
/**
+ * @deprecated in 1.22
* @param $row
* @return RecentChange
*/
public static function newFromCurRow( $row ) {
+ wfDeprecated( __METHOD__, '1.22' );
$rc = new RecentChange;
$rc->loadFromCurRow( $row );
$rc->notificationtimestamp = false;
@@ -109,7 +112,7 @@ class RecentChange {
/**
* Obtain the recent change with a given rc_id value
*
- * @param $rcid Int rc_id value to retrieve
+ * @param int $rcid rc_id value to retrieve
* @return RecentChange
*/
public static function newFromId( $rcid ) {
@@ -119,13 +122,14 @@ class RecentChange {
/**
* Find the first recent change matching some specific conditions
*
- * @param $conds Array of conditions
+ * @param array $conds of conditions
* @param $fname Mixed: override the method name in profiling/logs
+ * @param $options Array Query options
* @return RecentChange
*/
- public static function newFromConds( $conds, $fname = __METHOD__ ) {
+ public static function newFromConds( $conds, $fname = __METHOD__, $options = array() ) {
$dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow( 'recentchanges', '*', $conds, $fname );
+ $row = $dbr->selectRow( 'recentchanges', self::selectFields(), $conds, $fname, $options );
if ( $row !== false ) {
return self::newFromRow( $row );
} else {
@@ -133,6 +137,40 @@ class RecentChange {
}
}
+ /**
+ * Return the list of recentchanges fields that should be selected to create
+ * a new recentchanges object.
+ * @return array
+ */
+ public static function selectFields() {
+ return array(
+ 'rc_id',
+ 'rc_timestamp',
+ 'rc_cur_time',
+ 'rc_user',
+ 'rc_user_text',
+ 'rc_namespace',
+ 'rc_title',
+ 'rc_comment',
+ 'rc_minor',
+ 'rc_bot',
+ 'rc_new',
+ 'rc_cur_id',
+ 'rc_this_oldid',
+ 'rc_last_oldid',
+ 'rc_type',
+ 'rc_patrolled',
+ 'rc_ip',
+ 'rc_old_len',
+ 'rc_new_len',
+ 'rc_deleted',
+ 'rc_logid',
+ 'rc_log_type',
+ 'rc_log_action',
+ 'rc_params',
+ );
+ }
+
# Accessors
/**
@@ -154,26 +192,13 @@ class RecentChange {
* @return Title
*/
public function &getTitle() {
- if( $this->mTitle === false ) {
+ if ( $this->mTitle === false ) {
$this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
- # Make sure the correct page ID is process cached
- $this->mTitle->resetArticleID( $this->mAttribs['rc_cur_id'] );
}
return $this->mTitle;
}
/**
- * @return bool|Title
- */
- public function getMovedToTitle() {
- if( $this->mMovedToTitle === false ) {
- $this->mMovedToTitle = Title::makeTitle( $this->mAttribs['rc_moved_to_ns'],
- $this->mAttribs['rc_moved_to_title'] );
- }
- return $this->mMovedToTitle;
- }
-
- /**
* Get the User object of the person who performed this change.
*
* @return User
@@ -197,30 +222,33 @@ class RecentChange {
global $wgLocalInterwiki, $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang;
$dbw = wfGetDB( DB_MASTER );
- if( !is_array($this->mExtra) ) {
+ if ( !is_array( $this->mExtra ) ) {
$this->mExtra = array();
}
$this->mExtra['lang'] = $wgLocalInterwiki;
- if( !$wgPutIPinRC ) {
+ if ( !$wgPutIPinRC ) {
$this->mAttribs['rc_ip'] = '';
}
# If our database is strict about IP addresses, use NULL instead of an empty string
- if( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) {
+ if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) {
unset( $this->mAttribs['rc_ip'] );
}
+ # Trim spaces on user supplied text
+ $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
+
# Make sure summary is truncated (whole multibyte characters)
$this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 );
# Fixup database timestamps
- $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']);
- $this->mAttribs['rc_cur_time'] = $dbw->timestamp($this->mAttribs['rc_cur_time']);
+ $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
+ $this->mAttribs['rc_cur_time'] = $dbw->timestamp( $this->mAttribs['rc_cur_time'] );
$this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
- if( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) {
+ if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id'] == 0 ) {
unset( $this->mAttribs['rc_cur_id'] );
}
@@ -235,72 +263,117 @@ class RecentChange {
# Notify external application via UDP
if ( !$noudp ) {
- $this->notifyRC2UDP();
+ $this->notifyRCFeeds();
}
# E-mail notifications
- if( $wgUseEnotif || $wgShowUpdatedMarker ) {
+ if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
$editor = $this->getPerformer();
$title = $this->getTitle();
- if ( wfRunHooks( 'AbortEmailNotification', array($editor, $title) ) ) {
+ if ( wfRunHooks( 'AbortEmailNotification', array( $editor, $title ) ) ) {
# @todo FIXME: This would be better as an extension hook
$enotif = new EmailNotification();
$enotif->notifyOnPageChange( $editor, $title,
$this->mAttribs['rc_timestamp'],
$this->mAttribs['rc_comment'],
$this->mAttribs['rc_minor'],
- $this->mAttribs['rc_last_oldid'] );
+ $this->mAttribs['rc_last_oldid'],
+ $this->mExtra['pageStatus'] );
}
}
}
+ /**
+ * @deprecated since 1.22, use notifyRCFeeds instead.
+ */
public function notifyRC2UDP() {
- global $wgRC2UDPAddress, $wgRC2UDPOmitBots;
- # Notify external application via UDP
- if( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) {
- self::sendToUDP( $this->getIRCLine() );
- }
+ wfDeprecated( __METHOD__, '1.22' );
+ $this->notifyRCFeeds();
}
/**
* Send some text to UDP.
- * @see RecentChange::cleanupForIRC
- * @param $line String: text to send
- * @param $address String: defaults to $wgRC2UDPAddress.
- * @param $prefix String: defaults to $wgRC2UDPPrefix.
- * @param $port Int: defaults to $wgRC2UDPPort. (Since 1.17)
- * @return Boolean: success
+ * @deprecated since 1.22
*/
public static function sendToUDP( $line, $address = '', $prefix = '', $port = '' ) {
- global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort;
+ global $wgRC2UDPAddress, $wgRC2UDPInterwikiPrefix, $wgRC2UDPPort, $wgRC2UDPPrefix;
+
+ wfDeprecated( __METHOD__, '1.22' );
+
# Assume default for standard RC case
$address = $address ? $address : $wgRC2UDPAddress;
$prefix = $prefix ? $prefix : $wgRC2UDPPrefix;
$port = $port ? $port : $wgRC2UDPPort;
- # Notify external application via UDP
- if( $address ) {
- $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
- if( $conn ) {
- $line = $prefix . $line;
- wfDebug( __METHOD__ . ": sending UDP line: $line\n" );
- socket_sendto( $conn, $line, strlen($line), 0, $address, $port );
- socket_close( $conn );
- return true;
+
+ $engine = new UDPRCFeedEngine();
+ $feed = array(
+ 'uri' => "udp://$address:$port/$prefix",
+ 'formatter' => 'IRCColourfulRCFeedFormatter',
+ 'add_interwiki_prefix' => $wgRC2UDPInterwikiPrefix,
+ );
+
+ return $engine->send( $feed, $line );
+ }
+
+ /**
+ * Notify all the feeds about the change.
+ */
+ public function notifyRCFeeds() {
+ global $wgRCFeeds;
+
+ foreach ( $wgRCFeeds as $feed ) {
+ $engine = self::getEngine( $feed['uri'] );
+
+ if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
+ $actionComment = $this->mExtra['actionCommentIRC'];
} else {
- wfDebug( __METHOD__ . ": failed to create UDP socket\n" );
+ $actionComment = null;
}
+
+ $omitBots = isset( $feed['omit_bots'] ) ? $feed['omit_bots'] : false;
+
+ if (
+ ( $omitBots && $this->mAttribs['rc_bot'] ) ||
+ $this->mAttribs['rc_type'] == RC_EXTERNAL
+ ) {
+ continue;
+ }
+
+ $formatter = new $feed['formatter']();
+ $line = $formatter->getLine( $feed, $this, $actionComment );
+
+ $engine->send( $feed, $line );
+ }
+ }
+
+ /**
+ * Gets the stream engine object for a given URI from $wgRCEngines
+ *
+ * @param $uri string URI to get the engine object for
+ * @return object The engine object
+ */
+ private static function getEngine( $uri ) {
+ global $wgRCEngines;
+
+ $scheme = parse_url( $uri, PHP_URL_SCHEME );
+ if ( !$scheme ) {
+ throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
}
- return false;
+
+ if ( !isset( $wgRCEngines[$scheme] ) ) {
+ throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
+ }
+
+ return new $wgRCEngines[$scheme];
}
/**
- * Remove newlines, carriage returns and decode html entites
- * @param $text String
- * @return String
+ * @deprecated since 1.22, moved to IRCColourfulRCFeedFormatter
*/
public static function cleanupForIRC( $text ) {
- return Sanitizer::decodeCharReferences( str_replace( array( "\n", "\r" ), array( "", "" ), $text ) );
+ wfDeprecated( __METHOD__, '1.22' );
+ return IRCColourfulRCFeedFormatter::cleanupForIRC( $text );
}
/**
@@ -315,9 +388,9 @@ class RecentChange {
$change = $change instanceof RecentChange
? $change
- : RecentChange::newFromId($change);
+ : RecentChange::newFromId( $change );
- if( !$change instanceof RecentChange ) {
+ if ( !$change instanceof RecentChange ) {
return null;
}
return $change->doMarkPatrolled( $wgUser, $auto );
@@ -336,32 +409,32 @@ class RecentChange {
$errors = array();
// If recentchanges patrol is disabled, only new pages
// can be patrolled
- if( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute('rc_type') != RC_NEW ) ) {
- $errors[] = array('rcpatroldisabled');
+ if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) ) {
+ $errors[] = array( 'rcpatroldisabled' );
}
// Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
$right = $auto ? 'autopatrol' : 'patrol';
$errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
- if( !wfRunHooks('MarkPatrolled', array($this->getAttribute('rc_id'), &$user, false)) ) {
- $errors[] = array('hookaborted');
+ if ( !wfRunHooks( 'MarkPatrolled', array( $this->getAttribute( 'rc_id' ), &$user, false ) ) ) {
+ $errors[] = array( 'hookaborted' );
}
// Users without the 'autopatrol' right can't patrol their
// own revisions
- if( $user->getName() == $this->getAttribute('rc_user_text') && !$user->isAllowed('autopatrol') ) {
- $errors[] = array('markedaspatrollederror-noautopatrol');
+ if ( $user->getName() == $this->getAttribute( 'rc_user_text' ) && !$user->isAllowed( 'autopatrol' ) ) {
+ $errors[] = array( 'markedaspatrollederror-noautopatrol' );
}
- if( $errors ) {
+ if ( $errors ) {
return $errors;
}
// If the change was patrolled already, do nothing
- if( $this->getAttribute('rc_patrolled') ) {
+ if ( $this->getAttribute( 'rc_patrolled' ) ) {
return array();
}
// Actually set the 'patrolled' flag in RC
$this->reallyMarkPatrolled();
// Log this patrol event
PatrolLog::record( $this, $auto, $user );
- wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('rc_id'), &$user, false) );
+ wfRunHooks( 'MarkPatrolledComplete', array( $this->getAttribute( 'rc_id' ), &$user, false ) );
return array();
}
@@ -377,10 +450,13 @@ class RecentChange {
'rc_patrolled' => 1
),
array(
- 'rc_id' => $this->getAttribute('rc_id')
+ 'rc_id' => $this->getAttribute( 'rc_id' )
),
__METHOD__
);
+ // Invalidate the page cache after the page has been patrolled
+ // to make sure that the Patrol link isn't visible any longer!
+ $this->getTitle()->invalidateCache();
return $dbw->affectedRows();
}
@@ -403,7 +479,7 @@ class RecentChange {
* @return RecentChange
*/
public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId,
- $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 ) {
+ $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0 ) {
$rc = new RecentChange;
$rc->mTitle = $title;
$rc->mPerformer = $user;
@@ -421,10 +497,8 @@ class RecentChange {
'rc_this_oldid' => $newId,
'rc_last_oldid' => $oldId,
'rc_bot' => $bot ? 1 : 0,
- 'rc_moved_to_ns' => 0,
- 'rc_moved_to_title' => '',
'rc_ip' => self::checkIPAddress( $ip ),
- 'rc_patrolled' => intval($patrol),
+ 'rc_patrolled' => intval( $patrol ),
'rc_new' => 0, # obsolete
'rc_old_len' => $oldSize,
'rc_new_len' => $newSize,
@@ -435,11 +509,12 @@ class RecentChange {
'rc_params' => ''
);
- $rc->mExtra = array(
+ $rc->mExtra = array(
'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => $lastTimestamp,
'oldSize' => $oldSize,
'newSize' => $newSize,
+ 'pageStatus' => 'changed'
);
$rc->save();
return $rc;
@@ -463,7 +538,7 @@ class RecentChange {
* @return RecentChange
*/
public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
- $ip='', $size=0, $newId=0, $patrol=0 ) {
+ $ip = '', $size = 0, $newId = 0, $patrol = 0 ) {
$rc = new RecentChange;
$rc->mTitle = $title;
$rc->mPerformer = $user;
@@ -481,10 +556,8 @@ class RecentChange {
'rc_this_oldid' => $newId,
'rc_last_oldid' => 0,
'rc_bot' => $bot ? 1 : 0,
- 'rc_moved_to_ns' => 0,
- 'rc_moved_to_title' => '',
'rc_ip' => self::checkIPAddress( $ip ),
- 'rc_patrolled' => intval($patrol),
+ 'rc_patrolled' => intval( $patrol ),
'rc_new' => 1, # obsolete
'rc_old_len' => 0,
'rc_new_len' => $size,
@@ -495,11 +568,12 @@ class RecentChange {
'rc_params' => ''
);
- $rc->mExtra = array(
+ $rc->mExtra = array(
'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => 0,
'oldSize' => 0,
- 'newSize' => $size
+ 'newSize' => $size,
+ 'pageStatus' => 'created'
);
$rc->save();
return $rc;
@@ -521,11 +595,11 @@ class RecentChange {
* @return bool
*/
public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
- $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='' )
+ $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '' )
{
global $wgLogRestrictions;
# Don't add private logs to RC!
- if( isset($wgLogRestrictions[$type]) && $wgLogRestrictions[$type] != '*' ) {
+ if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
return false;
}
$rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
@@ -550,9 +624,30 @@ class RecentChange {
* @return RecentChange
*/
public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
- $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='' ) {
+ $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '' ) {
global $wgRequest;
+ ## Get pageStatus for email notification
+ switch ( $type . '-' . $action ) {
+ case 'delete-delete':
+ $pageStatus = 'deleted';
+ break;
+ case 'move-move':
+ case 'move-move_redir':
+ $pageStatus = 'moved';
+ break;
+ case 'delete-restore':
+ $pageStatus = 'restored';
+ break;
+ case 'upload-upload':
+ $pageStatus = 'created';
+ break;
+ case 'upload-overwrite':
+ default:
+ $pageStatus = 'changed';
+ break;
+ }
+
$rc = new RecentChange;
$rc->mTitle = $target;
$rc->mPerformer = $user;
@@ -570,8 +665,6 @@ class RecentChange {
'rc_this_oldid' => 0,
'rc_last_oldid' => 0,
'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
- 'rc_moved_to_ns' => 0,
- 'rc_moved_to_title' => '',
'rc_ip' => self::checkIPAddress( $ip ),
'rc_patrolled' => 1,
'rc_new' => 0, # obsolete
@@ -584,10 +677,11 @@ class RecentChange {
'rc_params' => $params
);
- $rc->mExtra = array(
+ $rc->mExtra = array(
'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => 0,
'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
+ 'pageStatus' => $pageStatus,
'actionCommentIRC' => $actionCommentIRC
);
return $rc;
@@ -600,18 +694,20 @@ class RecentChange {
*/
public function loadFromRow( $row ) {
$this->mAttribs = get_object_vars( $row );
- $this->mAttribs['rc_timestamp'] = wfTimestamp(TS_MW, $this->mAttribs['rc_timestamp']);
+ $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
$this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
}
/**
* Makes a pseudo-RC entry from a cur row
*
+ * @deprected in 1.22
* @param $row
*/
public function loadFromCurRow( $row ) {
+ wfDeprecated( __METHOD__, '1.22' );
$this->mAttribs = array(
- 'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp),
+ 'rc_timestamp' => wfTimestamp( TS_MW, $row->rev_timestamp ),
'rc_cur_time' => $row->rev_timestamp,
'rc_user' => $row->rev_user,
'rc_user_text' => $row->rev_user_text,
@@ -621,21 +717,19 @@ class RecentChange {
'rc_minor' => $row->rev_minor_edit ? 1 : 0,
'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
'rc_cur_id' => $row->page_id,
- 'rc_this_oldid' => $row->rev_id,
- 'rc_last_oldid' => isset($row->rc_last_oldid) ? $row->rc_last_oldid : 0,
- 'rc_bot' => 0,
- 'rc_moved_to_ns' => 0,
- 'rc_moved_to_title' => '',
+ 'rc_this_oldid' => $row->rev_id,
+ 'rc_last_oldid' => isset( $row->rc_last_oldid ) ? $row->rc_last_oldid : 0,
+ 'rc_bot' => 0,
'rc_ip' => '',
'rc_id' => $row->rc_id,
'rc_patrolled' => $row->rc_patrolled,
'rc_new' => $row->page_is_new, # obsolete
'rc_old_len' => $row->rc_old_len,
'rc_new_len' => $row->rc_new_len,
- 'rc_params' => isset($row->rc_params) ? $row->rc_params : '',
- 'rc_log_type' => isset($row->rc_log_type) ? $row->rc_log_type : null,
- 'rc_log_action' => isset($row->rc_log_action) ? $row->rc_log_action : null,
- 'rc_log_id' => isset($row->rc_log_id) ? $row->rc_log_id: 0,
+ 'rc_params' => isset( $row->rc_params ) ? $row->rc_params : '',
+ 'rc_log_type' => isset( $row->rc_log_type ) ? $row->rc_log_type : null,
+ 'rc_log_action' => isset( $row->rc_log_action ) ? $row->rc_log_action : null,
+ 'rc_logid' => isset( $row->rc_logid ) ? $row->rc_logid : 0,
'rc_deleted' => $row->rc_deleted // MUST be set
);
}
@@ -643,7 +737,7 @@ class RecentChange {
/**
* Get an attribute value
*
- * @param $name String Attribute name
+ * @param string $name Attribute name
* @return mixed
*/
public function getAttribute( $name ) {
@@ -664,13 +758,13 @@ class RecentChange {
* @return string
*/
public function diffLinkTrail( $forceCur ) {
- if( $this->mAttribs['rc_type'] == RC_EDIT ) {
- $trail = "curid=" . (int)($this->mAttribs['rc_cur_id']) .
- "&oldid=" . (int)($this->mAttribs['rc_last_oldid']);
- if( $forceCur ) {
- $trail .= '&diff=0' ;
+ if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
+ $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
+ "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
+ if ( $forceCur ) {
+ $trail .= '&diff=0';
} else {
- $trail .= '&diff=' . (int)($this->mAttribs['rc_this_oldid']);
+ $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
}
} else {
$trail = '';
@@ -679,89 +773,6 @@ class RecentChange {
}
/**
- * @return string
- */
- public function getIRCLine() {
- global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki,
- $wgCanonicalServer, $wgScript;
-
- if( $this->mAttribs['rc_type'] == RC_LOG ) {
- // Don't use SpecialPage::getTitleFor, backwards compatibility with
- // IRC API which expects "Log".
- $titleObj = Title::newFromText( 'Log/' . $this->mAttribs['rc_log_type'], NS_SPECIAL );
- } else {
- $titleObj =& $this->getTitle();
- }
- $title = $titleObj->getPrefixedText();
- $title = self::cleanupForIRC( $title );
-
- if( $this->mAttribs['rc_type'] == RC_LOG ) {
- $url = '';
- } else {
- $url = $wgCanonicalServer . $wgScript;
- if( $this->mAttribs['rc_type'] == RC_NEW ) {
- $query = '?oldid=' . $this->mAttribs['rc_this_oldid'];
- } else {
- $query = '?diff=' . $this->mAttribs['rc_this_oldid'] . '&oldid=' . $this->mAttribs['rc_last_oldid'];
- }
- if ( $wgUseRCPatrol || ( $this->mAttribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
- $query .= '&rcid=' . $this->mAttribs['rc_id'];
- }
- // HACK: We need this hook for WMF's secure server setup
- wfRunHooks( 'IRCLineURL', array( &$url, &$query ) );
- $url .= $query;
- }
-
- if( $this->mAttribs['rc_old_len'] !== null && $this->mAttribs['rc_new_len'] !== null ) {
- $szdiff = $this->mAttribs['rc_new_len'] - $this->mAttribs['rc_old_len'];
- if($szdiff < -500) {
- $szdiff = "\002$szdiff\002";
- } elseif($szdiff >= 0) {
- $szdiff = '+' . $szdiff ;
- }
- // @todo i18n with parentheses in content language?
- $szdiff = '(' . $szdiff . ')' ;
- } else {
- $szdiff = '';
- }
-
- $user = self::cleanupForIRC( $this->mAttribs['rc_user_text'] );
-
- if ( $this->mAttribs['rc_type'] == RC_LOG ) {
- $targetText = $this->getTitle()->getPrefixedText();
- $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $this->mExtra['actionCommentIRC'] ) );
- $flag = $this->mAttribs['rc_log_action'];
- } else {
- $comment = self::cleanupForIRC( $this->mAttribs['rc_comment'] );
- $flag = '';
- if ( !$this->mAttribs['rc_patrolled'] && ( $wgUseRCPatrol || $this->mAttribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
- $flag .= '!';
- }
- $flag .= ( $this->mAttribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $this->mAttribs['rc_minor'] ? "M" : "" ) . ( $this->mAttribs['rc_bot'] ? "B" : "" );
- }
-
- if ( $wgRC2UDPInterwikiPrefix === true && $wgLocalInterwiki !== false ) {
- $prefix = $wgLocalInterwiki;
- } elseif ( $wgRC2UDPInterwikiPrefix ) {
- $prefix = $wgRC2UDPInterwikiPrefix;
- } else {
- $prefix = false;
- }
- if ( $prefix !== false ) {
- $titleString = "\00314[[\00303$prefix:\00307$title\00314]]";
- } else {
- $titleString = "\00314[[\00307$title\00314]]";
- }
-
- # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
- # no colour (\003) switches back to the term default
- $fullString = "$titleString\0034 $flag\00310 " .
- "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
-
- return $fullString;
- }
-
- /**
* Returns the change size (HTML).
* The lengths can be given optionally.
* @param $old int
@@ -769,18 +780,41 @@ class RecentChange {
* @return string
*/
public function getCharacterDifference( $old = 0, $new = 0 ) {
- if( $old === 0 ) {
+ if ( $old === 0 ) {
$old = $this->mAttribs['rc_old_len'];
}
- if( $new === 0 ) {
+ if ( $new === 0 ) {
$new = $this->mAttribs['rc_new_len'];
}
- if( $old === null || $new === null ) {
+ if ( $old === null || $new === null ) {
return '';
}
return ChangesList::showCharacterDifference( $old, $new );
}
+ /**
+ * Purge expired changes from the recentchanges table
+ * @since 1.22
+ */
+ public static function purgeExpiredChanges() {
+ if ( wfReadOnly() ) {
+ return;
+ }
+
+ $method = __METHOD__;
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->onTransactionIdle( function() use ( $dbw, $method ) {
+ global $wgRCMaxAge;
+
+ $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
+ $dbw->delete(
+ 'recentchanges',
+ array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ),
+ $method
+ );
+ } );
+ }
+
private static function checkIPAddress( $ip ) {
global $wgRequest;
if ( $ip ) {
@@ -789,9 +823,24 @@ class RecentChange {
}
} else {
$ip = $wgRequest->getIP();
- if( !$ip )
+ if ( !$ip ) {
$ip = '';
+ }
}
return $ip;
}
+
+ /**
+ * Check whether the given timestamp is new enough to have a RC row with a given tolerance
+ * as the recentchanges table might not be cleared out regularly (so older entries might exist)
+ * or rows which will be deleted soon shouldn't be included.
+ *
+ * @param $timestamp mixed MWTimestamp compatible timestamp
+ * @param $tolerance integer Tolerance in seconds
+ * @return bool
+ */
+ public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
+ global $wgRCMaxAge;
+ return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
+ }
}
diff --git a/includes/clientpool/RedisConnectionPool.php b/includes/clientpool/RedisConnectionPool.php
new file mode 100644
index 00000000..ef71b182
--- /dev/null
+++ b/includes/clientpool/RedisConnectionPool.php
@@ -0,0 +1,356 @@
+<?php
+/**
+ * Redis client connection pooling manager.
+ *
+ * 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
+ * @defgroup Redis Redis
+ * @author Aaron Schulz
+ */
+
+/**
+ * Helper class to manage Redis connections.
+ *
+ * This can be used to get handle wrappers that free the handle when the wrapper
+ * leaves scope. The maximum number of free handles (connections) is configurable.
+ * This provides an easy way to cache connection handles that may also have state,
+ * such as a handle does between multi() and exec(), and without hoarding connections.
+ * The wrappers use PHP magic methods so that calling functions on them calls the
+ * function of the actual Redis object handle.
+ *
+ * @ingroup Redis
+ * @since 1.21
+ */
+class RedisConnectionPool {
+ /**
+ * @name Pool settings.
+ * Settings there are shared for any connection made in this pool.
+ * See the singleton() method documentation for more details.
+ * @{
+ */
+ /** @var string Connection timeout in seconds */
+ protected $connectTimeout;
+ /** @var string Plaintext auth password */
+ protected $password;
+ /** @var bool Whether connections persist */
+ protected $persistent;
+ /** @var integer Serializer to use (Redis::SERIALIZER_*) */
+ protected $serializer;
+ /** @} */
+
+ /** @var integer Current idle pool size */
+ protected $idlePoolSize = 0;
+
+ /** @var Array (server name => ((connection info array),...) */
+ protected $connections = array();
+ /** @var Array (server name => UNIX timestamp) */
+ protected $downServers = array();
+
+ /** @var Array (pool ID => RedisConnectionPool) */
+ protected static $instances = array();
+
+ /** integer; seconds to cache servers as "down". */
+ const SERVER_DOWN_TTL = 30;
+
+ /**
+ * @param array $options
+ */
+ protected function __construct( array $options ) {
+ if ( !class_exists( 'Redis' ) ) {
+ throw new MWException( __CLASS__ . ' requires a Redis client library. ' .
+ 'See https://www.mediawiki.org/wiki/Redis#Setup' );
+ }
+ $this->connectTimeout = $options['connectTimeout'];
+ $this->persistent = $options['persistent'];
+ $this->password = $options['password'];
+ if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
+ $this->serializer = Redis::SERIALIZER_PHP;
+ } elseif ( $options['serializer'] === 'igbinary' ) {
+ $this->serializer = Redis::SERIALIZER_IGBINARY;
+ } elseif ( $options['serializer'] === 'none' ) {
+ $this->serializer = Redis::SERIALIZER_NONE;
+ } else {
+ throw new MWException( "Invalid serializer specified." );
+ }
+ }
+
+ /**
+ * @param $options Array
+ * @return Array
+ */
+ protected static function applyDefaultConfig( array $options ) {
+ if ( !isset( $options['connectTimeout'] ) ) {
+ $options['connectTimeout'] = 1;
+ }
+ if ( !isset( $options['persistent'] ) ) {
+ $options['persistent'] = false;
+ }
+ if ( !isset( $options['password'] ) ) {
+ $options['password'] = null;
+ }
+ return $options;
+ }
+
+ /**
+ * @param $options Array
+ * $options include:
+ * - connectTimeout : The timeout for new connections, in seconds.
+ * Optional, default is 1 second.
+ * - persistent : Set this to true to allow connections to persist across
+ * multiple web requests. False by default.
+ * - password : The authentication password, will be sent to Redis in clear text.
+ * Optional, if it is unspecified, no AUTH command will be sent.
+ * - serializer : Set to "php", "igbinary", or "none". Default is "php".
+ * @return RedisConnectionPool
+ */
+ public static function singleton( array $options ) {
+ $options = self::applyDefaultConfig( $options );
+ // Map the options to a unique hash...
+ ksort( $options ); // normalize to avoid pool fragmentation
+ $id = sha1( serialize( $options ) );
+ // Initialize the object at the hash as needed...
+ if ( !isset( self::$instances[$id] ) ) {
+ self::$instances[$id] = new self( $options );
+ wfDebug( "Creating a new " . __CLASS__ . " instance with id $id." );
+ }
+ return self::$instances[$id];
+ }
+
+ /**
+ * Get a connection to a redis server. Based on code in RedisBagOStuff.php.
+ *
+ * @param string $server A hostname/port combination or the absolute path of a UNIX socket.
+ * If a hostname is specified but no port, port 6379 will be used.
+ * @return RedisConnRef|bool Returns false on failure
+ * @throws MWException
+ */
+ public function getConnection( $server ) {
+ // Check the listing "dead" servers which have had a connection errors.
+ // Servers are marked dead for a limited period of time, to
+ // avoid excessive overhead from repeated connection timeouts.
+ if ( isset( $this->downServers[$server] ) ) {
+ $now = time();
+ if ( $now > $this->downServers[$server] ) {
+ // Dead time expired
+ unset( $this->downServers[$server] );
+ } else {
+ // Server is dead
+ wfDebug( "server $server is marked down for another " .
+ ( $this->downServers[$server] - $now ) . " seconds, can't get connection" );
+ return false;
+ }
+ }
+
+ // Check if a connection is already free for use
+ if ( isset( $this->connections[$server] ) ) {
+ foreach ( $this->connections[$server] as &$connection ) {
+ if ( $connection['free'] ) {
+ $connection['free'] = false;
+ --$this->idlePoolSize;
+ return new RedisConnRef( $this, $server, $connection['conn'] );
+ }
+ }
+ }
+
+ if ( substr( $server, 0, 1 ) === '/' ) {
+ // UNIX domain socket
+ // These are required by the redis extension to start with a slash, but
+ // we still need to set the port to a special value to make it work.
+ $host = $server;
+ $port = 0;
+ } else {
+ // TCP connection
+ $hostPort = IP::splitHostAndPort( $server );
+ if ( !$hostPort ) {
+ throw new MWException( __CLASS__ . ": invalid configured server \"$server\"" );
+ }
+ list( $host, $port ) = $hostPort;
+ if ( $port === false ) {
+ $port = 6379;
+ }
+ }
+
+ $conn = new Redis();
+ try {
+ if ( $this->persistent ) {
+ $result = $conn->pconnect( $host, $port, $this->connectTimeout );
+ } else {
+ $result = $conn->connect( $host, $port, $this->connectTimeout );
+ }
+ if ( !$result ) {
+ wfDebugLog( 'redis', "Could not connect to server $server" );
+ // Mark server down for some time to avoid further timeouts
+ $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
+ return false;
+ }
+ if ( $this->password !== null ) {
+ if ( !$conn->auth( $this->password ) ) {
+ wfDebugLog( 'redis', "Authentication error connecting to $server" );
+ }
+ }
+ } catch ( RedisException $e ) {
+ $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
+ wfDebugLog( 'redis', "Redis exception: " . $e->getMessage() . "\n" );
+ return false;
+ }
+
+ if ( $conn ) {
+ $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
+ $this->connections[$server][] = array( 'conn' => $conn, 'free' => false );
+ return new RedisConnRef( $this, $server, $conn );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Mark a connection to a server as free to return to the pool
+ *
+ * @param $server string
+ * @param $conn Redis
+ * @return boolean
+ */
+ public function freeConnection( $server, Redis $conn ) {
+ $found = false;
+
+ foreach ( $this->connections[$server] as &$connection ) {
+ if ( $connection['conn'] === $conn && !$connection['free'] ) {
+ $connection['free'] = true;
+ ++$this->idlePoolSize;
+ break;
+ }
+ }
+
+ $this->closeExcessIdleConections();
+
+ return $found;
+ }
+
+ /**
+ * Close any extra idle connections if there are more than the limit
+ *
+ * @return void
+ */
+ protected function closeExcessIdleConections() {
+ if ( $this->idlePoolSize <= count( $this->connections ) ) {
+ return; // nothing to do (no more connections than servers)
+ }
+
+ foreach ( $this->connections as $server => &$serverConnections ) {
+ foreach ( $serverConnections as $key => &$connection ) {
+ if ( $connection['free'] ) {
+ unset( $serverConnections[$key] );
+ if ( --$this->idlePoolSize <= count( $this->connections ) ) {
+ return; // done (no more connections than servers)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * The redis extension throws an exception in response to various read, write
+ * and protocol errors. Sometimes it also closes the connection, sometimes
+ * not. The safest response for us is to explicitly destroy the connection
+ * object and let it be reopened during the next request.
+ *
+ * @param $server string
+ * @param $cref RedisConnRef
+ * @param $e RedisException
+ * @return void
+ */
+ public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
+ wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
+ foreach ( $this->connections[$server] as $key => $connection ) {
+ if ( $cref->isConnIdentical( $connection['conn'] ) ) {
+ $this->idlePoolSize -= $connection['free'] ? 1 : 0;
+ unset( $this->connections[$server][$key] );
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
+ *
+ * @ingroup Redis
+ * @since 1.21
+ */
+class RedisConnRef {
+ /** @var RedisConnectionPool */
+ protected $pool;
+ /** @var Redis */
+ protected $conn;
+
+ protected $server; // string
+
+ /**
+ * @param $pool RedisConnectionPool
+ * @param $server string
+ * @param $conn Redis
+ */
+ public function __construct( RedisConnectionPool $pool, $server, Redis $conn ) {
+ $this->pool = $pool;
+ $this->server = $server;
+ $this->conn = $conn;
+ }
+
+ public function __call( $name, $arguments ) {
+ return call_user_func_array( array( $this->conn, $name ), $arguments );
+ }
+
+ /**
+ * @param string $script
+ * @param array $params
+ * @param integer $numKeys
+ * @return mixed
+ * @throws RedisException
+ */
+ public function luaEval( $script, array $params, $numKeys ) {
+ $sha1 = sha1( $script ); // 40 char hex
+ $conn = $this->conn; // convenience
+
+ // Try to run the server-side cached copy of the script
+ $conn->clearLastError();
+ $res = $conn->evalSha( $sha1, $params, $numKeys );
+ // If the script is not in cache, use eval() to retry and cache it
+ if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
+ $conn->clearLastError();
+ $res = $conn->eval( $script, $params, $numKeys );
+ wfDebugLog( 'redis', "Used eval() for Lua script $sha1." );
+ }
+
+ if ( $conn->getLastError() ) { // script bug?
+ wfDebugLog( 'redis', "Lua script error: " . $conn->getLastError() );
+ }
+
+ return $res;
+ }
+
+ /**
+ * @param RedisConnRef $conn
+ * @return bool
+ */
+ public function isConnIdentical( Redis $conn ) {
+ return $this->conn === $conn;
+ }
+
+ function __destruct() {
+ $this->pool->freeConnection( $this->server, $this->conn );
+ }
+}
diff --git a/includes/content/AbstractContent.php b/includes/content/AbstractContent.php
new file mode 100644
index 00000000..137efb8a
--- /dev/null
+++ b/includes/content/AbstractContent.php
@@ -0,0 +1,444 @@
+<?php
+/**
+ * A content object represents page content, e.g. the text to show on a page.
+ * Content objects have no knowledge about how they relate to Wiki pages.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Base implementation for content objects.
+ *
+ * @ingroup Content
+ */
+abstract class AbstractContent implements Content {
+
+ /**
+ * Name of the content model this Content object represents.
+ * Use with CONTENT_MODEL_XXX constants
+ *
+ * @since 1.21
+ *
+ * @var string $model_id
+ */
+ protected $model_id;
+
+ /**
+ * @param string|null $modelId
+ *
+ * @since 1.21
+ */
+ public function __construct( $modelId = null ) {
+ $this->model_id = $modelId;
+ }
+
+ /**
+ * @see Content::getModel
+ *
+ * @since 1.21
+ */
+ public function getModel() {
+ return $this->model_id;
+ }
+
+ /**
+ * Throws an MWException if $model_id is not the id of the content model
+ * supported by this Content object.
+ *
+ * @since 1.21
+ *
+ * @param string $modelId The model to check
+ *
+ * @throws MWException
+ */
+ protected function checkModelID( $modelId ) {
+ if ( $modelId !== $this->model_id ) {
+ throw new MWException(
+ "Bad content model: " .
+ "expected {$this->model_id} " .
+ "but got $modelId."
+ );
+ }
+ }
+
+ /**
+ * @see Content::getContentHandler
+ *
+ * @since 1.21
+ */
+ public function getContentHandler() {
+ return ContentHandler::getForContent( $this );
+ }
+
+ /**
+ * @see Content::getDefaultFormat
+ *
+ * @since 1.21
+ */
+ public function getDefaultFormat() {
+ return $this->getContentHandler()->getDefaultFormat();
+ }
+
+ /**
+ * @see Content::getSupportedFormats
+ *
+ * @since 1.21
+ */
+ public function getSupportedFormats() {
+ return $this->getContentHandler()->getSupportedFormats();
+ }
+
+ /**
+ * @see Content::isSupportedFormat
+ *
+ * @param string $format
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function isSupportedFormat( $format ) {
+ if ( !$format ) {
+ return true; // this means "use the default"
+ }
+
+ return $this->getContentHandler()->isSupportedFormat( $format );
+ }
+
+ /**
+ * Throws an MWException if $this->isSupportedFormat( $format ) does not
+ * return true.
+ *
+ * @since 1.21
+ *
+ * @param string $format
+ * @throws MWException
+ */
+ protected function checkFormat( $format ) {
+ if ( !$this->isSupportedFormat( $format ) ) {
+ throw new MWException(
+ "Format $format is not supported for content model " .
+ $this->getModel()
+ );
+ }
+ }
+
+ /**
+ * @see Content::serialize
+ *
+ * @param string|null $format
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function serialize( $format = null ) {
+ return $this->getContentHandler()->serializeContent( $this, $format );
+ }
+
+ /**
+ * @see Content::isEmpty
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function isEmpty() {
+ return $this->getSize() === 0;
+ }
+
+ /**
+ * @see Content::isValid
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function isValid() {
+ return true;
+ }
+
+ /**
+ * @see Content::equals
+ *
+ * @since 1.21
+ *
+ * @param Content|null $that
+ *
+ * @return boolean
+ */
+ public function equals( Content $that = null ) {
+ if ( is_null( $that ) ) {
+ return false;
+ }
+
+ if ( $that === $this ) {
+ return true;
+ }
+
+ if ( $that->getModel() !== $this->getModel() ) {
+ return false;
+ }
+
+ return $this->getNativeData() === $that->getNativeData();
+ }
+
+ /**
+ * Returns a list of DataUpdate objects for recording information about this
+ * Content in some secondary data store.
+ *
+ * This default implementation calls
+ * $this->getParserOutput( $content, $title, null, null, false ),
+ * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
+ * resulting ParserOutput object.
+ *
+ * Subclasses may override this to determine the secondary data updates more
+ * efficiently, preferably without the need to generate a parser output object.
+ *
+ * @see Content::getSecondaryDataUpdates()
+ *
+ * @param $title Title The context for determining the necessary updates
+ * @param $old Content|null An optional Content object representing the
+ * previous content, i.e. the content being replaced by this Content
+ * object.
+ * @param $recursive boolean Whether to include recursive updates (default:
+ * false).
+ * @param $parserOutput ParserOutput|null Optional ParserOutput object.
+ * Provide if you have one handy, to avoid re-parsing of the content.
+ *
+ * @return Array. A list of DataUpdate objects for putting information
+ * about this content object somewhere.
+ *
+ * @since 1.21
+ */
+ public function getSecondaryDataUpdates( Title $title,
+ Content $old = null,
+ $recursive = true, ParserOutput $parserOutput = null
+ ) {
+ if ( $parserOutput === null ) {
+ $parserOutput = $this->getParserOutput( $title, null, null, false );
+ }
+
+ return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
+ }
+
+ /**
+ * @see Content::getRedirectChain
+ *
+ * @since 1.21
+ */
+ public function getRedirectChain() {
+ global $wgMaxRedirects;
+ $title = $this->getRedirectTarget();
+ if ( is_null( $title ) ) {
+ return null;
+ }
+ // recursive check to follow double redirects
+ $recurse = $wgMaxRedirects;
+ $titles = array( $title );
+ while ( --$recurse > 0 ) {
+ if ( $title->isRedirect() ) {
+ $page = WikiPage::factory( $title );
+ $newtitle = $page->getRedirectTarget();
+ } else {
+ break;
+ }
+ // Redirects to some special pages are not permitted
+ if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
+ // The new title passes the checks, so make that our current
+ // title so that further recursion can be checked
+ $title = $newtitle;
+ $titles[] = $newtitle;
+ } else {
+ break;
+ }
+ }
+ return $titles;
+ }
+
+ /**
+ * @see Content::getRedirectTarget
+ *
+ * @since 1.21
+ */
+ public function getRedirectTarget() {
+ return null;
+ }
+
+ /**
+ * @see Content::getUltimateRedirectTarget
+ * @note: migrated here from Title::newFromRedirectRecurse
+ *
+ * @since 1.21
+ */
+ public function getUltimateRedirectTarget() {
+ $titles = $this->getRedirectChain();
+ return $titles ? array_pop( $titles ) : null;
+ }
+
+ /**
+ * @see Content::isRedirect
+ *
+ * @since 1.21
+ *
+ * @return bool
+ */
+ public function isRedirect() {
+ return $this->getRedirectTarget() !== null;
+ }
+
+ /**
+ * @see Content::updateRedirect
+ *
+ * This default implementation always returns $this.
+ *
+ * @param Title $target
+ *
+ * @since 1.21
+ *
+ * @return Content $this
+ */
+ public function updateRedirect( Title $target ) {
+ return $this;
+ }
+
+ /**
+ * @see Content::getSection
+ *
+ * @since 1.21
+ */
+ public function getSection( $sectionId ) {
+ return null;
+ }
+
+ /**
+ * @see Content::replaceSection
+ *
+ * @since 1.21
+ */
+ public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+ return null;
+ }
+
+ /**
+ * @see Content::preSaveTransform
+ *
+ * @since 1.21
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ return $this;
+ }
+
+ /**
+ * @see Content::addSectionHeader
+ *
+ * @since 1.21
+ */
+ public function addSectionHeader( $header ) {
+ return $this;
+ }
+
+ /**
+ * @see Content::preloadTransform
+ *
+ * @since 1.21
+ */
+ public function preloadTransform( Title $title, ParserOptions $popts ) {
+ return $this;
+ }
+
+ /**
+ * @see Content::prepareSave
+ *
+ * @since 1.21
+ */
+ public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
+ if ( $this->isValid() ) {
+ return Status::newGood();
+ } else {
+ return Status::newFatal( "invalid-content-data" );
+ }
+ }
+
+ /**
+ * @see Content::getDeletionUpdates
+ *
+ * @since 1.21
+ *
+ * @param $page WikiPage the deleted page
+ * @param $parserOutput null|ParserOutput optional parser output object
+ * for efficient access to meta-information about the content object.
+ * Provide if you have one handy.
+ *
+ * @return array A list of DataUpdate instances that will clean up the
+ * database after deletion.
+ */
+ public function getDeletionUpdates( WikiPage $page,
+ ParserOutput $parserOutput = null )
+ {
+ return array(
+ new LinksDeletionUpdate( $page ),
+ );
+ }
+
+ /**
+ * This default implementation always returns false. Subclasses may override this to supply matching logic.
+ *
+ * @see Content::matchMagicWord
+ *
+ * @since 1.21
+ *
+ * @param MagicWord $word
+ *
+ * @return bool
+ */
+ public function matchMagicWord( MagicWord $word ) {
+ return false;
+ }
+
+ /**
+ * @see Content::convert()
+ *
+ * This base implementation calls the hook ConvertContent to enable custom conversions.
+ * Subclasses may override this to implement conversion for "their" content model.
+ *
+ * @param string $toModel the desired content model, use the CONTENT_MODEL_XXX flags.
+ * @param string $lossy flag, set to "lossy" to allow lossy conversion. If lossy conversion is
+ * not allowed, full round-trip conversion is expected to work without losing information.
+ *
+ * @return Content|bool A content object with the content model $toModel, or false if
+ * that conversion is not supported.
+ */
+ public function convert( $toModel, $lossy = '' ) {
+ if ( $this->getModel() === $toModel ) {
+ //nothing to do, shorten out.
+ return $this;
+ }
+
+ $lossy = ( $lossy === 'lossy' ); // string flag, convert to boolean for convenience
+ $result = false;
+
+ wfRunHooks( 'ConvertContent', array( $this, $toModel, $lossy, &$result ) );
+ return $result;
+ }
+}
diff --git a/includes/content/Content.php b/includes/content/Content.php
new file mode 100644
index 00000000..5a90e092
--- /dev/null
+++ b/includes/content/Content.php
@@ -0,0 +1,508 @@
+<?php
+/**
+ * A content object represents page content, e.g. the text to show on a page.
+ * Content objects have no knowledge about how they relate to wiki pages.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Base interface for content objects.
+ *
+ * @ingroup Content
+ */
+interface Content {
+
+ /**
+ * @since 1.21
+ *
+ * @return string A string representing the content in a way useful for
+ * building a full text search index. If no useful representation exists,
+ * this method returns an empty string.
+ *
+ * @todo Test that this actually works
+ * @todo Make sure this also works with LuceneSearch / WikiSearch
+ */
+ public function getTextForSearchIndex();
+
+ /**
+ * @since 1.21
+ *
+ * @return string|false The wikitext to include when another page includes this
+ * content, or false if the content is not includable in a wikitext page.
+ *
+ * @todo Allow native handling, bypassing wikitext representation, like
+ * for includable special pages.
+ * @todo Allow transclusion into other content models than Wikitext!
+ * @todo Used in WikiPage and MessageCache to get message text. Not so
+ * nice. What should we use instead?!
+ */
+ public function getWikitextForTransclusion();
+
+ /**
+ * Returns a textual representation of the content suitable for use in edit
+ * summaries and log messages.
+ *
+ * @since 1.21
+ *
+ * @param int $maxLength Maximum length of the summary text
+ * @return string The summary text
+ */
+ public function getTextForSummary( $maxLength = 250 );
+
+ /**
+ * Returns native representation of the data. Interpretation depends on
+ * the data model used, as given by getDataModel().
+ *
+ * @since 1.21
+ *
+ * @return mixed The native representation of the content. Could be a
+ * string, a nested array structure, an object, a binary blob...
+ * anything, really.
+ *
+ * @note Caller must be aware of content model!
+ */
+ public function getNativeData();
+
+ /**
+ * Returns the content's nominal size in bogo-bytes.
+ *
+ * @return int
+ */
+ public function getSize();
+
+ /**
+ * Returns the ID of the content model used by this Content object.
+ * Corresponds to the CONTENT_MODEL_XXX constants.
+ *
+ * @since 1.21
+ *
+ * @return String The model id
+ */
+ public function getModel();
+
+ /**
+ * Convenience method that returns the ContentHandler singleton for handling
+ * the content model that this Content object uses.
+ *
+ * Shorthand for ContentHandler::getForContent( $this )
+ *
+ * @since 1.21
+ *
+ * @return ContentHandler
+ */
+ public function getContentHandler();
+
+ /**
+ * Convenience method that returns the default serialization format for the
+ * content model that this Content object uses.
+ *
+ * Shorthand for $this->getContentHandler()->getDefaultFormat()
+ *
+ * @since 1.21
+ *
+ * @return String
+ */
+ public function getDefaultFormat();
+
+ /**
+ * Convenience method that returns the list of serialization formats
+ * supported for the content model that this Content object uses.
+ *
+ * Shorthand for $this->getContentHandler()->getSupportedFormats()
+ *
+ * @since 1.21
+ *
+ * @return Array of supported serialization formats
+ */
+ public function getSupportedFormats();
+
+ /**
+ * Returns true if $format is a supported serialization format for this
+ * Content object, false if it isn't.
+ *
+ * Note that this should always return true if $format is null, because null
+ * stands for the default serialization.
+ *
+ * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
+ *
+ * @since 1.21
+ *
+ * @param string $format The format to check
+ * @return bool Whether the format is supported
+ */
+ public function isSupportedFormat( $format );
+
+ /**
+ * Convenience method for serializing this Content object.
+ *
+ * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
+ *
+ * @since 1.21
+ *
+ * @param $format null|string The desired serialization format (or null for
+ * the default format).
+ * @return string Serialized form of this Content object
+ */
+ public function serialize( $format = null );
+
+ /**
+ * Returns true if this Content object represents empty content.
+ *
+ * @since 1.21
+ *
+ * @return bool Whether this Content object is empty
+ */
+ public function isEmpty();
+
+ /**
+ * Returns whether the content is valid. This is intended for local validity
+ * checks, not considering global consistency.
+ *
+ * Content needs to be valid before it can be saved.
+ *
+ * This default implementation always returns true.
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function isValid();
+
+ /**
+ * Returns true if this Content objects is conceptually equivalent to the
+ * given Content object.
+ *
+ * Contract:
+ *
+ * - Will return false if $that is null.
+ * - Will return true if $that === $this.
+ * - Will return false if $that->getModel() != $this->getModel().
+ * - Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
+ * where the meaning of "equal" depends on the actual data model.
+ *
+ * Implementations should be careful to make equals() transitive and reflexive:
+ *
+ * - $a->equals( $b ) <=> $b->equals( $a )
+ * - $a->equals( $b ) && $b->equals( $c ) ==> $a->equals( $c )
+ *
+ * @since 1.21
+ *
+ * @param $that Content The Content object to compare to
+ * @return bool True if this Content object is equal to $that, false otherwise.
+ */
+ public function equals( Content $that = null );
+
+ /**
+ * Return a copy of this Content object. The following must be true for the
+ * object returned:
+ *
+ * if $copy = $original->copy()
+ *
+ * - get_class($original) === get_class($copy)
+ * - $original->getModel() === $copy->getModel()
+ * - $original->equals( $copy )
+ *
+ * If and only if the Content object is immutable, the copy() method can and
+ * should return $this. That is, $copy === $original may be true, but only
+ * for immutable content objects.
+ *
+ * @since 1.21
+ *
+ * @return Content. A copy of this object
+ */
+ public function copy();
+
+ /**
+ * Returns true if this content is countable as a "real" wiki page, provided
+ * that it's also in a countable location (e.g. a current revision in the
+ * main namespace).
+ *
+ * @since 1.21
+ *
+ * @param bool $hasLinks If it is known whether this content contains
+ * links, provide this information here, to avoid redundant parsing to
+ * find out.
+ * @return boolean
+ */
+ public function isCountable( $hasLinks = null );
+
+ /**
+ * Parse the Content object and generate a ParserOutput from the result.
+ * $result->getText() can be used to obtain the generated HTML. If no HTML
+ * is needed, $generateHtml can be set to false; in that case,
+ * $result->getText() may return null.
+ *
+ * @param $title Title The page title to use as a context for rendering
+ * @param $revId null|int The revision being rendered (optional)
+ * @param $options null|ParserOptions Any parser options
+ * @param $generateHtml Boolean Whether to generate HTML (default: true). If false,
+ * the result of calling getText() on the ParserOutput object returned by
+ * this method is undefined.
+ *
+ * @since 1.21
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput( Title $title,
+ $revId = null,
+ ParserOptions $options = null, $generateHtml = true );
+ // TODO: make RenderOutput and RenderOptions base classes
+
+ /**
+ * Returns a list of DataUpdate objects for recording information about this
+ * Content in some secondary data store. If the optional second argument,
+ * $old, is given, the updates may model only the changes that need to be
+ * made to replace information about the old content with information about
+ * the new content.
+ *
+ * This default implementation calls
+ * $this->getParserOutput( $content, $title, null, null, false ),
+ * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
+ * resulting ParserOutput object.
+ *
+ * Subclasses may implement this to determine the necessary updates more
+ * efficiently, or make use of information about the old content.
+ *
+ * @param $title Title The context for determining the necessary updates
+ * @param $old Content|null An optional Content object representing the
+ * previous content, i.e. the content being replaced by this Content
+ * object.
+ * @param $recursive boolean Whether to include recursive updates (default:
+ * false).
+ * @param $parserOutput ParserOutput|null Optional ParserOutput object.
+ * Provide if you have one handy, to avoid re-parsing of the content.
+ *
+ * @return Array. A list of DataUpdate objects for putting information
+ * about this content object somewhere.
+ *
+ * @since 1.21
+ */
+ public function getSecondaryDataUpdates( Title $title,
+ Content $old = null,
+ $recursive = true, ParserOutput $parserOutput = null
+ );
+
+ /**
+ * Construct the redirect destination from this content and return an
+ * array of Titles, or null if this content doesn't represent a redirect.
+ * The last element in the array is the final destination after all redirects
+ * have been resolved (up to $wgMaxRedirects times).
+ *
+ * @since 1.21
+ *
+ * @return Array of Titles, with the destination last
+ */
+ public function getRedirectChain();
+
+ /**
+ * Construct the redirect destination from this content and return a Title,
+ * or null if this content doesn't represent a redirect.
+ * This will only return the immediate redirect target, useful for
+ * the redirect table and other checks that don't need full recursion.
+ *
+ * @since 1.21
+ *
+ * @return Title: The corresponding Title
+ */
+ public function getRedirectTarget();
+
+ /**
+ * Construct the redirect destination from this content and return the
+ * Title, or null if this content doesn't represent a redirect.
+ *
+ * This will recurse down $wgMaxRedirects times or until a non-redirect
+ * target is hit in order to provide (hopefully) the Title of the final
+ * destination instead of another redirect.
+ *
+ * There is usually no need to override the default behavior, subclasses that
+ * want to implement redirects should override getRedirectTarget().
+ *
+ * @since 1.21
+ *
+ * @return Title
+ */
+ public function getUltimateRedirectTarget();
+
+ /**
+ * Returns whether this Content represents a redirect.
+ * Shorthand for getRedirectTarget() !== null.
+ *
+ * @since 1.21
+ *
+ * @return bool
+ */
+ public function isRedirect();
+
+ /**
+ * If this Content object is a redirect, this method updates the redirect target.
+ * Otherwise, it does nothing.
+ *
+ * @since 1.21
+ *
+ * @param Title $target the new redirect target
+ *
+ * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
+ */
+ public function updateRedirect( Title $target );
+
+ /**
+ * Returns the section with the given ID.
+ *
+ * @since 1.21
+ *
+ * @param string $sectionId The section's ID, given as a numeric string.
+ * The ID "0" retrieves the section before the first heading, "1" the
+ * text between the first heading (included) and the second heading
+ * (excluded), etc.
+ * @return Content|Boolean|null The section, or false if no such section
+ * exist, or null if sections are not supported.
+ */
+ public function getSection( $sectionId );
+
+ /**
+ * Replaces a section of the content and returns a Content object with the
+ * section replaced.
+ *
+ * @since 1.21
+ *
+ * @param $section null/false or a section number (0, 1, 2, T1, T2...), or "new"
+ * @param $with Content: new content of the section
+ * @param string $sectionTitle new section's subject, only if $section is 'new'
+ * @return string Complete article text, or null if error
+ */
+ public function replaceSection( $section, Content $with, $sectionTitle = '' );
+
+ /**
+ * Returns a Content object with pre-save transformations applied (or this
+ * object if no transformations apply).
+ *
+ * @since 1.21
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $parserOptions null|ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $parserOptions );
+
+ /**
+ * Returns a new WikitextContent object with the given section heading
+ * prepended, if supported. The default implementation just returns this
+ * Content object unmodified, ignoring the section header.
+ *
+ * @since 1.21
+ *
+ * @param $header string
+ * @return Content
+ */
+ public function addSectionHeader( $header );
+
+ /**
+ * Returns a Content object with preload transformations applied (or this
+ * object if no transformations apply).
+ *
+ * @since 1.21
+ *
+ * @param $title Title
+ * @param $parserOptions null|ParserOptions
+ * @return Content
+ */
+ public function preloadTransform( Title $title, ParserOptions $parserOptions );
+
+ /**
+ * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent() and in
+ * similar places.
+ *
+ * This may be used to check the content's consistency with global state. This function should
+ * NOT write any information to the database.
+ *
+ * Note that this method will usually be called inside the same transaction bracket that will be used
+ * to save the new revision.
+ *
+ * Note that this method is called before any update to the page table is performed. This means that
+ * $page may not yet know a page ID.
+ *
+ * @since 1.21
+ *
+ * @param WikiPage $page The page to be saved.
+ * @param int $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent()
+ * @param int $baseRevId the ID of the current revision
+ * @param User $user
+ *
+ * @return Status A status object indicating whether the content was successfully prepared for saving.
+ * If the returned status indicates an error, a rollback will be performed and the
+ * transaction aborted.
+ *
+ * @see see WikiPage::doEditContent()
+ */
+ public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user );
+
+ /**
+ * Returns a list of updates to perform when this content is deleted.
+ * The necessary updates may be taken from the Content object, or depend on
+ * the current state of the database.
+ *
+ * @since 1.21
+ *
+ * @param $page WikiPage the deleted page
+ * @param $parserOutput null|ParserOutput optional parser output object
+ * for efficient access to meta-information about the content object.
+ * Provide if you have one handy.
+ *
+ * @return array A list of DataUpdate instances that will clean up the
+ * database after deletion.
+ */
+ public function getDeletionUpdates( WikiPage $page,
+ ParserOutput $parserOutput = null );
+
+ /**
+ * Returns true if this Content object matches the given magic word.
+ *
+ * @since 1.21
+ *
+ * @param MagicWord $word the magic word to match
+ *
+ * @return bool whether this Content object matches the given magic word.
+ */
+ public function matchMagicWord( MagicWord $word );
+
+ /**
+ * Converts this content object into another content object with the given content model,
+ * if that is possible.
+ *
+ * @param string $toModel the desired content model, use the CONTENT_MODEL_XXX flags.
+ * @param string $lossy flag, set to "lossy" to allow lossy conversion. If lossy conversion is
+ * not allowed, full round-trip conversion is expected to work without losing information.
+ *
+ * @return Content|bool A content object with the content model $toModel, or false if
+ * that conversion is not supported.
+ */
+ public function convert( $toModel, $lossy = '' );
+
+ // TODO: ImagePage and CategoryPage interfere with per-content action handlers
+ // TODO: nice&sane integration of GeSHi syntax highlighting
+ // [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a
+ // config to set the class which handles syntax highlighting
+ // [12:00] <vvv> And default it to a DummyHighlighter
+}
diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php
new file mode 100644
index 00000000..2a92e233
--- /dev/null
+++ b/includes/content/ContentHandler.php
@@ -0,0 +1,1115 @@
+<?php
+/**
+ * Base class for content handling.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Exception representing a failure to serialize or unserialize a content object.
+ *
+ * @ingroup Content
+ */
+class MWContentSerializationException extends MWException {
+
+}
+
+/**
+ * A content handler knows how do deal with a specific type of content on a wiki
+ * page. Content is stored in the database in a serialized form (using a
+ * serialization format a.k.a. MIME type) and is unserialized into its native
+ * PHP representation (the content model), which is wrapped in an instance of
+ * the appropriate subclass of Content.
+ *
+ * ContentHandler instances are stateless singletons that serve, among other
+ * things, as a factory for Content objects. Generally, there is one subclass
+ * of ContentHandler and one subclass of Content for every type of content model.
+ *
+ * Some content types have a flat model, that is, their native representation
+ * is the same as their serialized form. Examples would be JavaScript and CSS
+ * code. As of now, this also applies to wikitext (MediaWiki's default content
+ * type), but wikitext content may be represented by a DOM or AST structure in
+ * the future.
+ *
+ * @ingroup Content
+ */
+abstract class ContentHandler {
+
+ /**
+ * Switch for enabling deprecation warnings. Used by ContentHandler::deprecated()
+ * and ContentHandler::runLegacyHooks().
+ *
+ * Once the ContentHandler code has settled in a bit, this should be set to true to
+ * make extensions etc. show warnings when using deprecated functions and hooks.
+ */
+ protected static $enableDeprecationWarnings = false;
+
+ /**
+ * Convenience function for getting flat text from a Content object. This
+ * should only be used in the context of backwards compatibility with code
+ * that is not yet able to handle Content objects!
+ *
+ * If $content is null, this method returns the empty string.
+ *
+ * If $content is an instance of TextContent, this method returns the flat
+ * text as returned by $content->getNativeData().
+ *
+ * If $content is not a TextContent object, the behavior of this method
+ * depends on the global $wgContentHandlerTextFallback:
+ * - If $wgContentHandlerTextFallback is 'fail' and $content is not a
+ * TextContent object, an MWException is thrown.
+ * - If $wgContentHandlerTextFallback is 'serialize' and $content is not a
+ * TextContent object, $content->serialize() is called to get a string
+ * form of the content.
+ * - If $wgContentHandlerTextFallback is 'ignore' and $content is not a
+ * TextContent object, this method returns null.
+ * - otherwise, the behavior is undefined.
+ *
+ * @since 1.21
+ *
+ * @param $content Content|null
+ * @return null|string the textual form of $content, if available
+ * @throws MWException if $content is not an instance of TextContent and
+ * $wgContentHandlerTextFallback was set to 'fail'.
+ */
+ public static function getContentText( Content $content = null ) {
+ global $wgContentHandlerTextFallback;
+
+ if ( is_null( $content ) ) {
+ return '';
+ }
+
+ if ( $content instanceof TextContent ) {
+ return $content->getNativeData();
+ }
+
+ wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' );
+
+ if ( $wgContentHandlerTextFallback == 'fail' ) {
+ throw new MWException(
+ "Attempt to get text from Content with model " .
+ $content->getModel()
+ );
+ }
+
+ if ( $wgContentHandlerTextFallback == 'serialize' ) {
+ return $content->serialize();
+ }
+
+ return null;
+ }
+
+ /**
+ * Convenience function for creating a Content object from a given textual
+ * representation.
+ *
+ * $text will be deserialized into a Content object of the model specified
+ * by $modelId (or, if that is not given, $title->getContentModel()) using
+ * the given format.
+ *
+ * @since 1.21
+ *
+ * @param string $text the textual representation, will be
+ * unserialized to create the Content object
+ * @param $title null|Title the title of the page this text belongs to.
+ * Required if $modelId is not provided.
+ * @param $modelId null|string the model to deserialize to. If not provided,
+ * $title->getContentModel() is used.
+ * @param $format null|string the format to use for deserialization. If not
+ * given, the model's default format is used.
+ *
+ * @throws MWException
+ * @return Content a Content object representing $text
+ *
+ * @throws MWException if $model or $format is not supported or if $text can
+ * not be unserialized using $format.
+ */
+ public static function makeContent( $text, Title $title = null,
+ $modelId = null, $format = null )
+ {
+ if ( is_null( $modelId ) ) {
+ if ( is_null( $title ) ) {
+ throw new MWException( "Must provide a Title object or a content model ID." );
+ }
+
+ $modelId = $title->getContentModel();
+ }
+
+ $handler = ContentHandler::getForModelID( $modelId );
+ return $handler->unserializeContent( $text, $format );
+ }
+
+ /**
+ * Returns the name of the default content model to be used for the page
+ * with the given title.
+ *
+ * Note: There should rarely be need to call this method directly.
+ * To determine the actual content model for a given page, use
+ * Title::getContentModel().
+ *
+ * Which model is to be used by default for the page is determined based
+ * on several factors:
+ * - The global setting $wgNamespaceContentModels specifies a content model
+ * per namespace.
+ * - The hook ContentHandlerDefaultModelFor may be used to override the page's default
+ * model.
+ * - Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript
+ * model if they end in .js or .css, respectively.
+ * - Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
+ * - The hook TitleIsCssOrJsPage may be used to force a page to use the CSS
+ * or JavaScript model. This is a compatibility feature. The ContentHandlerDefaultModelFor
+ * hook should be used instead if possible.
+ * - The hook TitleIsWikitextPage may be used to force a page to use the
+ * wikitext model. This is a compatibility feature. The ContentHandlerDefaultModelFor
+ * hook should be used instead if possible.
+ *
+ * If none of the above applies, the wikitext model is used.
+ *
+ * Note: this is used by, and may thus not use, Title::getContentModel()
+ *
+ * @since 1.21
+ *
+ * @param $title Title
+ * @return null|string default model name for the page given by $title
+ */
+ public static function getDefaultModelFor( Title $title ) {
+ // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
+ // because it is used to initialize the mContentModel member.
+
+ $ns = $title->getNamespace();
+
+ $ext = false;
+ $m = null;
+ $model = MWNamespace::getNamespaceContentModel( $ns );
+
+ // Hook can determine default model
+ if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
+ if ( !is_null( $model ) ) {
+ return $model;
+ }
+ }
+
+ // Could this page contain custom CSS or JavaScript, based on the title?
+ $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
+ if ( $isCssOrJsPage ) {
+ $ext = $m[1];
+ }
+
+ // Hook can force JS/CSS
+ wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );
+
+ // Is this a .css subpage of a user page?
+ $isJsCssSubpage = NS_USER == $ns
+ && !$isCssOrJsPage
+ && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
+ if ( $isJsCssSubpage ) {
+ $ext = $m[1];
+ }
+
+ // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
+ $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
+ $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;
+
+ // Hook can override $isWikitext
+ wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
+
+ if ( !$isWikitext ) {
+ switch ( $ext ) {
+ case 'js':
+ return CONTENT_MODEL_JAVASCRIPT;
+ case 'css':
+ return CONTENT_MODEL_CSS;
+ default:
+ return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
+ }
+ }
+
+ // We established that it must be wikitext
+
+ return CONTENT_MODEL_WIKITEXT;
+ }
+
+ /**
+ * Returns the appropriate ContentHandler singleton for the given title.
+ *
+ * @since 1.21
+ *
+ * @param $title Title
+ * @return ContentHandler
+ */
+ public static function getForTitle( Title $title ) {
+ $modelId = $title->getContentModel();
+ return ContentHandler::getForModelID( $modelId );
+ }
+
+ /**
+ * Returns the appropriate ContentHandler singleton for the given Content
+ * object.
+ *
+ * @since 1.21
+ *
+ * @param $content Content
+ * @return ContentHandler
+ */
+ public static function getForContent( Content $content ) {
+ $modelId = $content->getModel();
+ return ContentHandler::getForModelID( $modelId );
+ }
+
+ /**
+ * @var Array A Cache of ContentHandler instances by model id
+ */
+ static $handlers;
+
+ /**
+ * Returns the ContentHandler singleton for the given model ID. Use the
+ * CONTENT_MODEL_XXX constants to identify the desired content model.
+ *
+ * ContentHandler singletons are taken from the global $wgContentHandlers
+ * array. Keys in that array are model names, the values are either
+ * ContentHandler singleton objects, or strings specifying the appropriate
+ * subclass of ContentHandler.
+ *
+ * If a class name is encountered when looking up the singleton for a given
+ * model name, the class is instantiated and the class name is replaced by
+ * the resulting singleton in $wgContentHandlers.
+ *
+ * If no ContentHandler is defined for the desired $modelId, the
+ * ContentHandler may be provided by the ContentHandlerForModelID hook.
+ * If no ContentHandler can be determined, an MWException is raised.
+ *
+ * @since 1.21
+ *
+ * @param string $modelId The ID of the content model for which to get a
+ * handler. Use CONTENT_MODEL_XXX constants.
+ * @return ContentHandler The ContentHandler singleton for handling the
+ * model given by $modelId
+ * @throws MWException if no handler is known for $modelId.
+ */
+ public static function getForModelID( $modelId ) {
+ global $wgContentHandlers;
+
+ if ( isset( ContentHandler::$handlers[$modelId] ) ) {
+ return ContentHandler::$handlers[$modelId];
+ }
+
+ if ( empty( $wgContentHandlers[$modelId] ) ) {
+ $handler = null;
+
+ wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
+
+ if ( $handler === null ) {
+ throw new MWException( "No handler for model '$modelId' registered in \$wgContentHandlers" );
+ }
+
+ if ( !( $handler instanceof ContentHandler ) ) {
+ throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
+ }
+ } else {
+ $class = $wgContentHandlers[$modelId];
+ $handler = new $class( $modelId );
+
+ if ( !( $handler instanceof ContentHandler ) ) {
+ throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" );
+ }
+ }
+
+ wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId
+ . ': ' . get_class( $handler ) );
+
+ ContentHandler::$handlers[$modelId] = $handler;
+ return ContentHandler::$handlers[$modelId];
+ }
+
+ /**
+ * Returns the localized name for a given content model.
+ *
+ * Model names are localized using system messages. Message keys
+ * have the form content-model-$name, where $name is getContentModelName( $id ).
+ *
+ * @param string $name The content model ID, as given by a CONTENT_MODEL_XXX
+ * constant or returned by Revision::getContentModel().
+ *
+ * @return string The content format's localized name.
+ * @throws MWException if the model id isn't known.
+ */
+ public static function getLocalizedName( $name ) {
+ $key = "content-model-$name";
+
+ $msg = wfMessage( $key );
+
+ return $msg->exists() ? $msg->plain() : $name;
+ }
+
+ public static function getContentModels() {
+ global $wgContentHandlers;
+
+ return array_keys( $wgContentHandlers );
+ }
+
+ public static function getAllContentFormats() {
+ global $wgContentHandlers;
+
+ $formats = array();
+
+ foreach ( $wgContentHandlers as $model => $class ) {
+ $handler = ContentHandler::getForModelID( $model );
+ $formats = array_merge( $formats, $handler->getSupportedFormats() );
+ }
+
+ $formats = array_unique( $formats );
+ return $formats;
+ }
+
+ // ------------------------------------------------------------------------
+
+ protected $mModelID;
+ protected $mSupportedFormats;
+
+ /**
+ * Constructor, initializing the ContentHandler instance with its model ID
+ * and a list of supported formats. Values for the parameters are typically
+ * provided as literals by subclass's constructors.
+ *
+ * @param string $modelId (use CONTENT_MODEL_XXX constants).
+ * @param array $formats List for supported serialization formats
+ * (typically as MIME types)
+ */
+ public function __construct( $modelId, $formats ) {
+ $this->mModelID = $modelId;
+ $this->mSupportedFormats = $formats;
+
+ $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
+ $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
+ $this->mModelName = strtolower( $this->mModelName );
+ }
+
+ /**
+ * Serializes a Content object of the type supported by this ContentHandler.
+ *
+ * @since 1.21
+ *
+ * @param $content Content The Content object to serialize
+ * @param $format null|String The desired serialization format
+ * @return string Serialized form of the content
+ */
+ abstract public function serializeContent( Content $content, $format = null );
+
+ /**
+ * Unserializes a Content object of the type supported by this ContentHandler.
+ *
+ * @since 1.21
+ *
+ * @param string $blob serialized form of the content
+ * @param $format null|String the format used for serialization
+ * @return Content the Content object created by deserializing $blob
+ */
+ abstract public function unserializeContent( $blob, $format = null );
+
+ /**
+ * Creates an empty Content object of the type supported by this
+ * ContentHandler.
+ *
+ * @since 1.21
+ *
+ * @return Content
+ */
+ abstract public function makeEmptyContent();
+
+ /**
+ * Creates a new Content object that acts as a redirect to the given page,
+ * or null of redirects are not supported by this content model.
+ *
+ * This default implementation always returns null. Subclasses supporting redirects
+ * must override this method.
+ *
+ * Note that subclasses that override this method to return a Content object
+ * should also override supportsRedirects() to return true.
+ *
+ * @since 1.21
+ *
+ * @param Title $destination the page to redirect to.
+ * @param string $text text to include in the redirect, if possible.
+ *
+ * @return Content
+ */
+ public function makeRedirectContent( Title $destination, $text = '' ) {
+ return null;
+ }
+
+ /**
+ * Returns the model id that identifies the content model this
+ * ContentHandler can handle. Use with the CONTENT_MODEL_XXX constants.
+ *
+ * @since 1.21
+ *
+ * @return String The model ID
+ */
+ public function getModelID() {
+ return $this->mModelID;
+ }
+
+ /**
+ * Throws an MWException if $model_id is not the ID of the content model
+ * supported by this ContentHandler.
+ *
+ * @since 1.21
+ *
+ * @param string $model_id The model to check
+ *
+ * @throws MWException
+ */
+ protected function checkModelID( $model_id ) {
+ if ( $model_id !== $this->mModelID ) {
+ throw new MWException( "Bad content model: " .
+ "expected {$this->mModelID} " .
+ "but got $model_id." );
+ }
+ }
+
+ /**
+ * Returns a list of serialization formats supported by the
+ * serializeContent() and unserializeContent() methods of this
+ * ContentHandler.
+ *
+ * @since 1.21
+ *
+ * @return array of serialization formats as MIME type like strings
+ */
+ public function getSupportedFormats() {
+ return $this->mSupportedFormats;
+ }
+
+ /**
+ * The format used for serialization/deserialization by default by this
+ * ContentHandler.
+ *
+ * This default implementation will return the first element of the array
+ * of formats that was passed to the constructor.
+ *
+ * @since 1.21
+ *
+ * @return string the name of the default serialization format as a MIME type
+ */
+ public function getDefaultFormat() {
+ return $this->mSupportedFormats[0];
+ }
+
+ /**
+ * Returns true if $format is a serialization format supported by this
+ * ContentHandler, and false otherwise.
+ *
+ * Note that if $format is null, this method always returns true, because
+ * null means "use the default format".
+ *
+ * @since 1.21
+ *
+ * @param string $format the serialization format to check
+ * @return bool
+ */
+ public function isSupportedFormat( $format ) {
+
+ if ( !$format ) {
+ return true; // this means "use the default"
+ }
+
+ return in_array( $format, $this->mSupportedFormats );
+ }
+
+ /**
+ * Throws an MWException if isSupportedFormat( $format ) is not true.
+ * Convenient for checking whether a format provided as a parameter is
+ * actually supported.
+ *
+ * @param string $format the serialization format to check
+ *
+ * @throws MWException
+ */
+ protected function checkFormat( $format ) {
+ if ( !$this->isSupportedFormat( $format ) ) {
+ throw new MWException(
+ "Format $format is not supported for content model "
+ . $this->getModelID()
+ );
+ }
+ }
+
+ /**
+ * Returns overrides for action handlers.
+ * Classes listed here will be used instead of the default one when
+ * (and only when) $wgActions[$action] === true. This allows subclasses
+ * to override the default action handlers.
+ *
+ * @since 1.21
+ *
+ * @return Array
+ */
+ public function getActionOverrides() {
+ return array();
+ }
+
+ /**
+ * Factory for creating an appropriate DifferenceEngine for this content model.
+ *
+ * @since 1.21
+ *
+ * @param $context IContextSource context to use, anything else will be
+ * ignored
+ * @param $old Integer Old ID we want to show and diff with.
+ * @param int|string $new String either 'prev' or 'next'.
+ * @param $rcid Integer ??? FIXME (default 0)
+ * @param $refreshCache boolean If set, refreshes the diff cache
+ * @param $unhide boolean If set, allow viewing deleted revs
+ *
+ * @return DifferenceEngine
+ */
+ public function createDifferenceEngine( IContextSource $context,
+ $old = 0, $new = 0,
+ $rcid = 0, # FIXME: use everywhere!
+ $refreshCache = false, $unhide = false
+ ) {
+ $diffEngineClass = $this->getDiffEngineClass();
+
+ return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
+ }
+
+ /**
+ * Get the language in which the content of the given page is written.
+ *
+ * This default implementation just returns $wgContLang (except for pages in the MediaWiki namespace)
+ *
+ * Note that the pages language is not cacheable, since it may in some cases depend on user settings.
+ *
+ * Also note that the page language may or may not depend on the actual content of the page,
+ * that is, this method may load the content in order to determine the language.
+ *
+ * @since 1.21
+ *
+ * @param Title $title the page to determine the language for.
+ * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+ *
+ * @return Language the page's language
+ */
+ public function getPageLanguage( Title $title, Content $content = null ) {
+ global $wgContLang, $wgLang;
+ $pageLang = $wgContLang;
+
+ if ( $title->getNamespace() == NS_MEDIAWIKI ) {
+ // Parse mediawiki messages with correct target language
+ list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
+ $pageLang = wfGetLangObj( $lang );
+ }
+
+ wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) );
+ return wfGetLangObj( $pageLang );
+ }
+
+ /**
+ * Get the language in which the content of this page is written when
+ * viewed by user. Defaults to $this->getPageLanguage(), but if the user
+ * specified a preferred variant, the variant will be used.
+ *
+ * This default implementation just returns $this->getPageLanguage( $title, $content ) unless
+ * the user specified a preferred variant.
+ *
+ * Note that the pages view language is not cacheable, since it depends on user settings.
+ *
+ * Also note that the page language may or may not depend on the actual content of the page,
+ * that is, this method may load the content in order to determine the language.
+ *
+ * @since 1.21
+ *
+ * @param Title $title the page to determine the language for.
+ * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+ *
+ * @return Language the page's language for viewing
+ */
+ public function getPageViewLanguage( Title $title, Content $content = null ) {
+ $pageLang = $this->getPageLanguage( $title, $content );
+
+ if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
+ // If the user chooses a variant, the content is actually
+ // in a language whose code is the variant code.
+ $variant = $pageLang->getPreferredVariant();
+ if ( $pageLang->getCode() !== $variant ) {
+ $pageLang = Language::factory( $variant );
+ }
+ }
+
+ return $pageLang;
+ }
+
+ /**
+ * Determines whether the content type handled by this ContentHandler
+ * can be used on the given page.
+ *
+ * This default implementation always returns true.
+ * Subclasses may override this to restrict the use of this content model to specific locations,
+ * typically based on the namespace or some other aspect of the title, such as a special suffix
+ * (e.g. ".svg" for SVG content).
+ *
+ * @param Title $title the page's title.
+ *
+ * @return bool true if content of this kind can be used on the given page, false otherwise.
+ */
+ public function canBeUsedOn( Title $title ) {
+ return true;
+ }
+
+ /**
+ * Returns the name of the diff engine to use.
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ protected function getDiffEngineClass() {
+ return 'DifferenceEngine';
+ }
+
+ /**
+ * Attempts to merge differences between three versions.
+ * Returns a new Content object for a clean merge and false for failure or
+ * a conflict.
+ *
+ * This default implementation always returns false.
+ *
+ * @since 1.21
+ *
+ * @param $oldContent Content|string String
+ * @param $myContent Content|string String
+ * @param $yourContent Content|string String
+ *
+ * @return Content|Bool
+ */
+ public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+ return false;
+ }
+
+ /**
+ * Return an applicable auto-summary if one exists for the given edit.
+ *
+ * @since 1.21
+ *
+ * @param $oldContent Content|null: the previous text of the page.
+ * @param $newContent Content|null: The submitted text of the page.
+ * @param int $flags Bit mask: a bit mask of flags submitted for the edit.
+ *
+ * @return string An appropriate auto-summary, or an empty string.
+ */
+ public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
+ // Decide what kind of auto-summary is needed.
+
+ // Redirect auto-summaries
+
+ /**
+ * @var $ot Title
+ * @var $rt Title
+ */
+
+ $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
+ $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
+
+ if ( is_object( $rt ) ) {
+ if ( !is_object( $ot )
+ || !$rt->equals( $ot )
+ || $ot->getFragment() != $rt->getFragment() )
+ {
+ $truncatedtext = $newContent->getTextForSummary(
+ 250
+ - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
+ - strlen( $rt->getFullText() ) );
+
+ return wfMessage( 'autoredircomment', $rt->getFullText() )
+ ->rawParams( $truncatedtext )->inContentLanguage()->text();
+ }
+ }
+
+ // New page auto-summaries
+ if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
+ // If they're making a new article, give its text, truncated, in
+ // the summary.
+
+ $truncatedtext = $newContent->getTextForSummary(
+ 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
+
+ return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
+ }
+
+ // Blanking auto-summaries
+ if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
+ return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
+ } elseif ( !empty( $oldContent )
+ && $oldContent->getSize() > 10 * $newContent->getSize()
+ && $newContent->getSize() < 500 )
+ {
+ // Removing more than 90% of the article
+
+ $truncatedtext = $newContent->getTextForSummary(
+ 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
+
+ return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
+ }
+
+ // If we reach this point, there's no applicable auto-summary for our
+ // case, so our auto-summary is empty.
+ return '';
+ }
+
+ /**
+ * Auto-generates a deletion reason
+ *
+ * @since 1.21
+ *
+ * @param $title Title: the page's title
+ * @param &$hasHistory Boolean: whether the page has a history
+ * @return mixed String containing deletion reason or empty string, or
+ * boolean false if no revision occurred
+ *
+ * @XXX &$hasHistory is extremely ugly, it's here because
+ * WikiPage::getAutoDeleteReason() and Article::generateReason()
+ * have it / want it.
+ */
+ public function getAutoDeleteReason( Title $title, &$hasHistory ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ // Get the last revision
+ $rev = Revision::newFromTitle( $title );
+
+ if ( is_null( $rev ) ) {
+ return false;
+ }
+
+ // Get the article's contents
+ $content = $rev->getContent();
+ $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 ( !$content || $content->isEmpty() ) {
+ $prev = $rev->getPrevious();
+
+ if ( $prev ) {
+ $rev = $prev;
+ $content = $rev->getContent();
+ $blank = true;
+ }
+ }
+
+ $this->checkModelID( $rev->getContentModel() );
+
+ // Find out if there was only one contributor
+ // Only scan the last 20 revisions
+ $res = $dbw->select( 'revision', 'rev_user_text',
+ array(
+ 'rev_page' => $title->getArticleID(),
+ $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
+ ),
+ __METHOD__,
+ array( 'LIMIT' => 20 )
+ );
+
+ if ( $res === false ) {
+ // This page has no revisions, which is very weird
+ return false;
+ }
+
+ $hasHistory = ( $res->numRows() > 1 );
+ $row = $dbw->fetchObject( $res );
+
+ if ( $row ) { // $row is false if the only contributor is hidden
+ $onlyAuthor = $row->rev_user_text;
+ // Try to find a second contributor
+ foreach ( $res as $row ) {
+ if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
+ $onlyAuthor = false;
+ break;
+ }
+ }
+ } else {
+ $onlyAuthor = false;
+ }
+
+ // Generate the summary with a '$1' placeholder
+ if ( $blank ) {
+ // The current revision is blank and the one before is also
+ // blank. It's just not our lucky day
+ $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
+ } else {
+ if ( $onlyAuthor ) {
+ $reason = wfMessage(
+ 'excontentauthor',
+ '$1',
+ $onlyAuthor
+ )->inContentLanguage()->text();
+ } else {
+ $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
+ }
+ }
+
+ if ( $reason == '-' ) {
+ // Allow these UI messages to be blanked out cleanly
+ return '';
+ }
+
+ // Max content length = max comment length - length of the comment (excl. $1)
+ $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
+
+ // Now replace the '$1' placeholder
+ $reason = str_replace( '$1', $text, $reason );
+
+ return $reason;
+ }
+
+ /**
+ * Get the Content object 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.
+ *
+ * @since 1.21
+ *
+ * @param $current Revision The current text
+ * @param $undo Revision The revision to undo
+ * @param $undoafter Revision Must be an earlier revision than $undo
+ *
+ * @return mixed String on success, false on failure
+ */
+ public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
+ $cur_content = $current->getContent();
+
+ if ( empty( $cur_content ) ) {
+ return false; // no page
+ }
+
+ $undo_content = $undo->getContent();
+ $undoafter_content = $undoafter->getContent();
+
+ $this->checkModelID( $cur_content->getModel() );
+ $this->checkModelID( $undo_content->getModel() );
+ $this->checkModelID( $undoafter_content->getModel() );
+
+ if ( $cur_content->equals( $undo_content ) ) {
+ // No use doing a merge if it's just a straight revert.
+ return $undoafter_content;
+ }
+
+ $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
+
+ return $undone_content;
+ }
+
+ /**
+ * Get parser options suitable for rendering the primary article wikitext
+ *
+ * @param IContextSource|User|string $context One of the following:
+ * - IContextSource: Use the User and the Language of the provided
+ * context
+ * - User: Use the provided User object and $wgLang for the language,
+ * so use an IContextSource object if possible.
+ * - 'canonical': Canonical options (anonymous user with default
+ * preferences and content language).
+ *
+ * @param IContextSource|User|string $context
+ *
+ * @throws MWException
+ * @return ParserOptions
+ */
+ public function makeParserOptions( $context ) {
+ global $wgContLang, $wgEnableParserLimitReporting;
+
+ if ( $context instanceof IContextSource ) {
+ $options = ParserOptions::newFromContext( $context );
+ } elseif ( $context instanceof User ) { // settings per user (even anons)
+ $options = ParserOptions::newFromUser( $context );
+ } elseif ( $context === 'canonical' ) { // canonical settings
+ $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+ } else {
+ throw new MWException( "Bad context for parser options: $context" );
+ }
+
+ $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports
+ $options->setTidy( true ); // fix bad HTML
+
+ return $options;
+ }
+
+ /**
+ * Returns true for content models that support caching using the
+ * ParserCache mechanism. See WikiPage::isParserCacheUsed().
+ *
+ * @since 1.21
+ *
+ * @return bool
+ */
+ public function isParserCacheSupported() {
+ return false;
+ }
+
+ /**
+ * Returns true if this content model supports sections.
+ * This default implementation returns false.
+ *
+ * Content models that return true here should also implement
+ * Content::getSection, Content::replaceSection, etc. to handle sections..
+ *
+ * @return boolean whether sections are supported.
+ */
+ public function supportsSections() {
+ return false;
+ }
+
+ /**
+ * Returns true if this content model supports redirects.
+ * This default implementation returns false.
+ *
+ * Content models that return true here should also implement
+ * ContentHandler::makeRedirectContent to return a Content object.
+ *
+ * @return boolean whether redirects are supported.
+ */
+ public function supportsRedirects() {
+ return false;
+ }
+
+ /**
+ * Logs a deprecation warning, visible if $wgDevelopmentWarnings, but only if
+ * self::$enableDeprecationWarnings is set to true.
+ *
+ * @param string $func The name of the deprecated function
+ * @param string $version The version since the method is deprecated. Usually 1.21
+ * for ContentHandler related stuff.
+ * @param string|bool $component: Component to which the function belongs.
+ * If false, it is assumed the function is in MediaWiki core.
+ *
+ * @see ContentHandler::$enableDeprecationWarnings
+ * @see wfDeprecated
+ */
+ public static function deprecated( $func, $version, $component = false ) {
+ if ( self::$enableDeprecationWarnings ) {
+ wfDeprecated( $func, $version, $component, 3 );
+ }
+ }
+
+ /**
+ * Call a legacy hook that uses text instead of Content objects.
+ * Will log a warning when a matching hook function is registered.
+ * If the textual representation of the content is changed by the
+ * hook function, a new Content object is constructed from the new
+ * text.
+ *
+ * @param string $event event name
+ * @param array $args parameters passed to hook functions
+ * @param bool $warn whether to log a warning.
+ * Default to self::$enableDeprecationWarnings.
+ * May be set to false for testing.
+ *
+ * @return Boolean True if no handler aborted the hook
+ *
+ * @see ContentHandler::$enableDeprecationWarnings
+ */
+ public static function runLegacyHooks( $event, $args = array(),
+ $warn = null ) {
+
+ if ( $warn === null ) {
+ $warn = self::$enableDeprecationWarnings;
+ }
+
+ if ( !Hooks::isRegistered( $event ) ) {
+ return true; // nothing to do here
+ }
+
+ if ( $warn ) {
+ // Log information about which handlers are registered for the legacy hook,
+ // so we can find and fix them.
+
+ $handlers = Hooks::getHandlers( $event );
+ $handlerInfo = array();
+
+ wfSuppressWarnings();
+
+ foreach ( $handlers as $handler ) {
+ if ( is_array( $handler ) ) {
+ if ( is_object( $handler[0] ) ) {
+ $info = get_class( $handler[0] );
+ } else {
+ $info = $handler[0];
+ }
+
+ if ( isset( $handler[1] ) ) {
+ $info .= '::' . $handler[1];
+ }
+ } elseif ( is_object( $handler ) ) {
+ $info = get_class( $handler[0] );
+ $info .= '::on' . $event;
+ } else {
+ $info = $handler;
+ }
+
+ $handlerInfo[] = $info;
+ }
+
+ wfRestoreWarnings();
+
+ wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . implode( ', ', $handlerInfo ), 2 );
+ }
+
+ // convert Content objects to text
+ $contentObjects = array();
+ $contentTexts = array();
+
+ foreach ( $args as $k => $v ) {
+ if ( $v instanceof Content ) {
+ /* @var Content $v */
+
+ $contentObjects[$k] = $v;
+
+ $v = $v->serialize();
+ $contentTexts[$k] = $v;
+ $args[$k] = $v;
+ }
+ }
+
+ // call the hook functions
+ $ok = wfRunHooks( $event, $args );
+
+ // see if the hook changed the text
+ foreach ( $contentTexts as $k => $orig ) {
+ /* @var Content $content */
+
+ $modified = $args[$k];
+ $content = $contentObjects[$k];
+
+ if ( $modified !== $orig ) {
+ // text was changed, create updated Content object
+ $content = $content->getContentHandler()->unserializeContent( $modified );
+ }
+
+ $args[$k] = $content;
+ }
+
+ return $ok;
+ }
+}
diff --git a/includes/content/CssContent.php b/includes/content/CssContent.php
new file mode 100644
index 00000000..03cc2d00
--- /dev/null
+++ b/includes/content/CssContent.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Content object for CSS pages.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Content object for CSS pages.
+ *
+ * @ingroup Content
+ */
+class CssContent extends TextContent {
+ public function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_CSS );
+ }
+
+ /**
+ * Returns a Content object with pre-save transformations applied using
+ * Parser::preSaveTransform().
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ global $wgParser;
+ // @todo Make pre-save transformation optional for script pages
+
+ $text = $this->getNativeData();
+ $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+ return new CssContent( $pst );
+ }
+
+ protected function getHtml() {
+ $html = "";
+ $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+ $html .= $this->getHighlightHtml();
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+}
diff --git a/includes/content/CssContentHandler.php b/includes/content/CssContentHandler.php
new file mode 100644
index 00000000..cb5a349d
--- /dev/null
+++ b/includes/content/CssContentHandler.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Content handler for CSS pages.
+ *
+ * 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 Content
+ */
+
+/**
+ * Content handler for CSS pages.
+ *
+ * @since 1.21
+ * @ingroup Content
+ */
+class CssContentHandler extends TextContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_CSS ) {
+ parent::__construct( $modelId, array( CONTENT_FORMAT_CSS ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new CssContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new CssContent( '' );
+ }
+
+ /**
+ * Returns the english language, because CSS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageLanguage()
+ */
+ public function getPageLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+
+ /**
+ * Returns the english language, because CSS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageViewLanguage()
+ */
+ public function getPageViewLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+}
diff --git a/includes/content/JavaScriptContent.php b/includes/content/JavaScriptContent.php
new file mode 100644
index 00000000..2ae572be
--- /dev/null
+++ b/includes/content/JavaScriptContent.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Content for JavaScript pages.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Content for JavaScript pages.
+ *
+ * @ingroup Content
+ */
+class JavaScriptContent extends TextContent {
+ public function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_JAVASCRIPT );
+ }
+
+ /**
+ * Returns a Content object with pre-save transformations applied using
+ * Parser::preSaveTransform().
+ *
+ * @param Title $title
+ * @param User $user
+ * @param ParserOptions $popts
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ global $wgParser;
+ // @todo Make pre-save transformation optional for script pages
+ // See bug #32858
+
+ $text = $this->getNativeData();
+ $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+ return new JavaScriptContent( $pst );
+ }
+
+ protected function getHtml() {
+ $html = "";
+ $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+ $html .= $this->getHighlightHtml();
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+}
diff --git a/includes/content/JavaScriptContentHandler.php b/includes/content/JavaScriptContentHandler.php
new file mode 100644
index 00000000..33fa9172
--- /dev/null
+++ b/includes/content/JavaScriptContentHandler.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Content handler for JavaScript pages.
+ *
+ * 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
+ */
+
+/**
+ * Content handler for JavaScript pages.
+ *
+ * @since 1.21
+ * @ingroup Content
+ * @todo make ScriptContentHandler base class, do highlighting stuff there?
+ */
+class JavaScriptContentHandler extends TextContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_JAVASCRIPT ) {
+ parent::__construct( $modelId, array( CONTENT_FORMAT_JAVASCRIPT ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new JavaScriptContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new JavaScriptContent( '' );
+ }
+
+ /**
+ * Returns the english language, because JS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageLanguage()
+ */
+ public function getPageLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+
+ /**
+ * Returns the english language, because JS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageViewLanguage()
+ */
+ public function getPageViewLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+}
diff --git a/includes/content/MessageContent.php b/includes/content/MessageContent.php
new file mode 100644
index 00000000..b36b670c
--- /dev/null
+++ b/includes/content/MessageContent.php
@@ -0,0 +1,158 @@
+<?php
+/**
+ * Wrapper content object allowing to handle a system message as a Content object.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Wrapper allowing us to handle a system message as a Content object.
+ * Note that this is generally *not* used to represent content from the
+ * MediaWiki namespace, and that there is no MessageContentHandler.
+ * MessageContent is just intended as glue for wrapping a message programatically.
+ *
+ * @ingroup Content
+ */
+class MessageContent extends AbstractContent {
+
+ /**
+ * @var Message
+ */
+ protected $mMessage;
+
+ /**
+ * @param Message|String $msg A Message object, or a message key
+ * @param array|null $params An optional array of message parameters
+ */
+ public function __construct( $msg, $params = null ) {
+ # XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
+ parent::__construct( CONTENT_MODEL_WIKITEXT );
+
+ if ( is_string( $msg ) ) {
+ $this->mMessage = wfMessage( $msg );
+ } else {
+ $this->mMessage = clone $msg;
+ }
+
+ if ( $params ) {
+ $this->mMessage = $this->mMessage->params( $params );
+ }
+ }
+
+ /**
+ * Returns the message as rendered HTML
+ *
+ * @return string The message text, parsed into html
+ */
+ public function getHtml() {
+ return $this->mMessage->parse();
+ }
+
+ /**
+ * Returns the message as rendered HTML
+ *
+ * @return string The message text, parsed into html
+ */
+ public function getWikitext() {
+ return $this->mMessage->text();
+ }
+
+ /**
+ * Returns the message object, with any parameters already substituted.
+ *
+ * @return Message The message object.
+ */
+ public function getNativeData() {
+ //NOTE: Message objects are mutable. Cloning here makes MessageContent immutable.
+ return clone $this->mMessage;
+ }
+
+ /**
+ * @see Content::getTextForSearchIndex
+ */
+ public function getTextForSearchIndex() {
+ return $this->mMessage->plain();
+ }
+
+ /**
+ * @see Content::getWikitextForTransclusion
+ */
+ public function getWikitextForTransclusion() {
+ return $this->getWikitext();
+ }
+
+ /**
+ * @see Content::getTextForSummary
+ */
+ public function getTextForSummary( $maxlength = 250 ) {
+ return substr( $this->mMessage->plain(), 0, $maxlength );
+ }
+
+ /**
+ * @see Content::getSize
+ *
+ * @return int
+ */
+ public function getSize() {
+ return strlen( $this->mMessage->plain() );
+ }
+
+ /**
+ * @see Content::copy
+ *
+ * @return Content. A copy of this object
+ */
+ public function copy() {
+ // MessageContent is immutable (because getNativeData() returns a clone of the Message object)
+ return $this;
+ }
+
+ /**
+ * @see Content::isCountable
+ *
+ * @return bool false
+ */
+ public function isCountable( $hasLinks = null ) {
+ return false;
+ }
+
+ /**
+ * @see Content::getParserOutput
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput(
+ Title $title, $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+
+ if ( $generateHtml ) {
+ $html = $this->getHtml();
+ } else {
+ $html = '';
+ }
+
+ $po = new ParserOutput( $html );
+ return $po;
+ }
+}
diff --git a/includes/content/TextContent.php b/includes/content/TextContent.php
new file mode 100644
index 00000000..f66dacd7
--- /dev/null
+++ b/includes/content/TextContent.php
@@ -0,0 +1,286 @@
+<?php
+/**
+ * Content object implementation for representing flat text.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Content object implementation for representing flat text.
+ *
+ * TextContent instances are immutable
+ *
+ * @ingroup Content
+ */
+class TextContent extends AbstractContent {
+
+ public function __construct( $text, $model_id = CONTENT_MODEL_TEXT ) {
+ parent::__construct( $model_id );
+
+ if ( $text === null || $text === false ) {
+ wfWarn( "TextContent constructed with \$text = " . var_export( $text, true ) . "! "
+ . "This may indicate an error in the caller's scope." );
+
+ $text = '';
+ }
+
+ if ( !is_string( $text ) ) {
+ throw new MWException( "TextContent expects a string in the constructor." );
+ }
+
+ $this->mText = $text;
+ }
+
+ public function copy() {
+ return $this; # NOTE: this is ok since TextContent are immutable.
+ }
+
+ public function getTextForSummary( $maxlength = 250 ) {
+ global $wgContLang;
+
+ $text = $this->getNativeData();
+
+ $truncatedtext = $wgContLang->truncate(
+ preg_replace( "/[\n\r]/", ' ', $text ),
+ max( 0, $maxlength ) );
+
+ return $truncatedtext;
+ }
+
+ /**
+ * returns the text's size in bytes.
+ *
+ * @return int The size
+ */
+ public function getSize() {
+ $text = $this->getNativeData();
+ return strlen( $text );
+ }
+
+ /**
+ * Returns true if this content is not a redirect, and $wgArticleCountMethod
+ * is "any".
+ *
+ * @param bool $hasLinks if it is known whether this content contains links,
+ * provide this information here, to avoid redundant parsing to find out.
+ *
+ * @return bool True if the content is countable
+ */
+ public function isCountable( $hasLinks = null ) {
+ global $wgArticleCountMethod;
+
+ if ( $this->isRedirect() ) {
+ return false;
+ }
+
+ if ( $wgArticleCountMethod === 'any' ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the text represented by this Content object, as a string.
+ *
+ * @return string: the raw text
+ */
+ public function getNativeData() {
+ $text = $this->mText;
+ return $text;
+ }
+
+ /**
+ * Returns the text represented by this Content object, as a string.
+ *
+ * @return string: the raw text
+ */
+ public function getTextForSearchIndex() {
+ return $this->getNativeData();
+ }
+
+ /**
+ * Returns attempts to convert this content object to wikitext,
+ * and then returns the text string. The conversion may be lossy.
+ *
+ * @note: this allows any text-based content to be transcluded as if it was wikitext.
+ *
+ * @return string|false: the raw text, or null if the conversion failed
+ */
+ public function getWikitextForTransclusion() {
+ $wikitext = $this->convert( CONTENT_MODEL_WIKITEXT, 'lossy' );
+
+ if ( $wikitext ) {
+ return $wikitext->getNativeData();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a Content object with pre-save transformations applied.
+ * This implementation just trims trailing whitespace.
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ $text = $this->getNativeData();
+ $pst = rtrim( $text );
+
+ return ( $text === $pst ) ? $this : new WikitextContent( $pst );
+ }
+
+ /**
+ * Diff this content object with another content object.
+ *
+ * @since 1.21diff
+ *
+ * @param $that Content: The other content object to compare this content
+ * object to.
+ * @param $lang Language: The language object to use for text segmentation.
+ * If not given, $wgContentLang is used.
+ *
+ * @return DiffResult: A diff representing the changes that would have to be
+ * made to this content object to make it equal to $that.
+ */
+ public function diff( Content $that, Language $lang = null ) {
+ global $wgContLang;
+
+ $this->checkModelID( $that->getModel() );
+
+ // @todo could implement this in DifferenceEngine and just delegate here?
+
+ if ( !$lang ) {
+ $lang = $wgContLang;
+ }
+
+ $otext = $this->getNativeData();
+ $ntext = $this->getNativeData();
+
+ # Note: Use native PHP diff, external engines don't give us abstract output
+ $ota = explode( "\n", $lang->segmentForDiff( $otext ) );
+ $nta = explode( "\n", $lang->segmentForDiff( $ntext ) );
+
+ $diff = new Diff( $ota, $nta );
+ return $diff;
+ }
+
+ /**
+ * Returns a generic ParserOutput object, wrapping the HTML returned by
+ * getHtml().
+ *
+ * @param $title Title Context title for parsing
+ * @param int|null $revId Revision ID (for {{REVISIONID}})
+ * @param $options ParserOptions|null Parser options
+ * @param bool $generateHtml Whether or not to generate HTML
+ *
+ * @return ParserOutput representing the HTML form of the text
+ */
+ public function getParserOutput( Title $title,
+ $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+ global $wgParser, $wgTextModelsToParse;
+
+ if ( !$options ) {
+ //NOTE: use canonical options per default to produce cacheable output
+ $options = $this->getContentHandler()->makeParserOptions( 'canonical' );
+ }
+
+ if ( in_array( $this->getModel(), $wgTextModelsToParse ) ) {
+ // parse just to get links etc into the database
+ $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
+ } else {
+ $po = new ParserOutput();
+ }
+
+ if ( $generateHtml ) {
+ $html = $this->getHtml();
+ } else {
+ $html = '';
+ }
+
+ $po->setText( $html );
+ return $po;
+ }
+
+ /**
+ * Generates an HTML version of the content, for display. Used by
+ * getParserOutput() to construct a ParserOutput object.
+ *
+ * This default implementation just calls getHighlightHtml(). Content
+ * models that have another mapping to HTML (as is the case for markup
+ * languages like wikitext) should override this method to generate the
+ * appropriate HTML.
+ *
+ * @return string An HTML representation of the content
+ */
+ protected function getHtml() {
+ return $this->getHighlightHtml();
+ }
+
+ /**
+ * Generates a syntax-highlighted version of the content, as HTML.
+ * Used by the default implementation of getHtml().
+ *
+ * @return string an HTML representation of the content's markup
+ */
+ protected function getHighlightHtml() {
+ # TODO: make Highlighter interface, use highlighter here, if available
+ return htmlspecialchars( $this->getNativeData() );
+ }
+
+ /**
+ * @see Content::convert()
+ *
+ * This implementation provides lossless conversion between content models based
+ * on TextContent.
+ *
+ * @param string $toModel the desired content model, use the CONTENT_MODEL_XXX flags.
+ * @param string $lossy flag, set to "lossy" to allow lossy conversion. If lossy conversion is
+ * not allowed, full round-trip conversion is expected to work without losing information.
+ *
+ * @return Content|bool A content object with the content model $toModel, or false if
+ * that conversion is not supported.
+ */
+ public function convert( $toModel, $lossy = '' ) {
+ $converted = parent::convert( $toModel, $lossy );
+
+ if ( $converted !== false ) {
+ return $converted;
+ }
+
+ $toHandler = ContentHandler::getForModelID( $toModel );
+
+ if ( $toHandler instanceof TextContentHandler ) {
+ //NOTE: ignore content serialization format - it's just text anyway.
+ $text = $this->getNativeData();
+ $converted = $toHandler->unserializeContent( $text );
+ }
+
+ return $converted;
+ }
+}
diff --git a/includes/content/TextContentHandler.php b/includes/content/TextContentHandler.php
new file mode 100644
index 00000000..e7f41e18
--- /dev/null
+++ b/includes/content/TextContentHandler.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Base content handler class for flat text contents.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ */
+
+/**
+ * Base content handler implementation for flat text contents.
+ *
+ * @ingroup Content
+ */
+class TextContentHandler extends ContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_TEXT, $formats = array( CONTENT_FORMAT_TEXT ) ) {
+ parent::__construct( $modelId, $formats );
+ }
+
+ /**
+ * Returns the content's text as-is.
+ *
+ * @param $content Content
+ * @param $format string|null
+ * @return mixed
+ */
+ public function serializeContent( Content $content, $format = null ) {
+ $this->checkFormat( $format );
+ return $content->getNativeData();
+ }
+
+ /**
+ * Attempts to merge differences between three versions. Returns a new
+ * Content object for a clean merge and false for failure or a conflict.
+ *
+ * All three Content objects passed as parameters must have the same
+ * content model.
+ *
+ * This text-based implementation uses wfMerge().
+ *
+ * @param $oldContent Content|string String
+ * @param $myContent Content|string String
+ * @param $yourContent Content|string String
+ *
+ * @return Content|Bool
+ */
+ public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+ $this->checkModelID( $oldContent->getModel() );
+ $this->checkModelID( $myContent->getModel() );
+ $this->checkModelID( $yourContent->getModel() );
+
+ $format = $this->getDefaultFormat();
+
+ $old = $this->serializeContent( $oldContent, $format );
+ $mine = $this->serializeContent( $myContent, $format );
+ $yours = $this->serializeContent( $yourContent, $format );
+
+ $ok = wfMerge( $old, $mine, $yours, $result );
+
+ if ( !$ok ) {
+ return false;
+ }
+
+ if ( !$result ) {
+ return $this->makeEmptyContent();
+ }
+
+ $mergedContent = $this->unserializeContent( $result, $format );
+ return $mergedContent;
+ }
+
+ /**
+ * Unserializes a Content object of the type supported by this ContentHandler.
+ *
+ * @since 1.21
+ *
+ * @param $text string serialized form of the content
+ * @param $format null|String the format used for serialization
+ *
+ * @return Content the TextContent object wrapping $text
+ */
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new TextContent( $text );
+ }
+
+ /**
+ * Creates an empty TextContent object.
+ *
+ * @since 1.21
+ *
+ * @return Content
+ */
+ public function makeEmptyContent() {
+ return new TextContent( '' );
+ }
+}
diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php
new file mode 100644
index 00000000..26337db9
--- /dev/null
+++ b/includes/content/WikitextContent.php
@@ -0,0 +1,323 @@
+<?php
+/**
+ * Content object for wiki text pages.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Content object for wiki text pages.
+ *
+ * @ingroup Content
+ */
+class WikitextContent extends TextContent {
+
+ public function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
+ }
+
+ /**
+ * @see Content::getSection()
+ */
+ public function getSection( $section ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+ $sect = $wgParser->getSection( $text, $section, false );
+
+ if ( $sect === false ) {
+ return false;
+ } else {
+ return new WikitextContent( $sect );
+ }
+ }
+
+ /**
+ * @see Content::replaceSection()
+ */
+ public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ $myModelId = $this->getModel();
+ $sectionModelId = $with->getModel();
+
+ if ( $sectionModelId != $myModelId ) {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Incompatible content model for section: " .
+ "document uses $myModelId but " .
+ "section uses $sectionModelId." );
+ }
+
+ $oldtext = $this->getNativeData();
+ $text = $with->getNativeData();
+
+ if ( $section === '' ) {
+ wfProfileOut( __METHOD__ );
+ return $with; # XXX: copy first?
+ } if ( $section == 'new' ) {
+ # Inserting a new section
+ $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
+ ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
+ if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
+ $text = strlen( trim( $oldtext ) ) > 0
+ ? "{$oldtext}\n\n{$subject}{$text}"
+ : "{$subject}{$text}";
+ }
+ } else {
+ # Replacing an existing section; roll out the big guns
+ global $wgParser;
+
+ $text = $wgParser->replaceSection( $oldtext, $section, $text );
+ }
+
+ $newContent = new WikitextContent( $text );
+
+ wfProfileOut( __METHOD__ );
+ return $newContent;
+ }
+
+ /**
+ * Returns a new WikitextContent object with the given section heading
+ * prepended.
+ *
+ * @param $header string
+ * @return Content
+ */
+ public function addSectionHeader( $header ) {
+ $text = wfMessage( 'newsectionheaderdefaultlevel' )
+ ->rawParams( $header )->inContentLanguage()->text();
+ $text .= "\n\n";
+ $text .= $this->getNativeData();
+
+ return new WikitextContent( $text );
+ }
+
+ /**
+ * Returns a Content object with pre-save transformations applied using
+ * Parser::preSaveTransform().
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+ $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+ rtrim( $pst );
+
+ return ( $text === $pst ) ? $this : new WikitextContent( $pst );
+ }
+
+ /**
+ * Returns a Content object with preload transformations applied (or this
+ * object if no transformations apply).
+ *
+ * @param $title Title
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preloadTransform( Title $title, ParserOptions $popts ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+ $plt = $wgParser->getPreloadText( $text, $title, $popts );
+
+ return new WikitextContent( $plt );
+ }
+
+ /**
+ * Implement redirect extraction for wikitext.
+ *
+ * @return null|Title
+ *
+ * @note: migrated here from Title::newFromRedirectInternal()
+ *
+ * @see Content::getRedirectTarget
+ * @see AbstractContent::getRedirectTarget
+ */
+ public function getRedirectTarget() {
+ global $wgMaxRedirects;
+ if ( $wgMaxRedirects < 1 ) {
+ // redirects are disabled, so quit early
+ return null;
+ }
+ $redir = MagicWord::get( 'redirect' );
+ $text = trim( $this->getNativeData() );
+ if ( $redir->matchStartAndRemove( $text ) ) {
+ // Extract the first link and see if it's usable
+ // Ensure that it really does come directly after #REDIRECT
+ // Some older redirects included a colon, so don't freak about that!
+ $m = array();
+ if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
+ // Strip preceding colon used to "escape" categories, etc.
+ // and URL-decode links
+ if ( strpos( $m[1], '%' ) !== false ) {
+ // Match behavior of inline link parsing here;
+ $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
+ }
+ $title = Title::newFromText( $m[1] );
+ // If the title is a redirect to bad special pages or is invalid, return null
+ if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
+ return null;
+ }
+ return $title;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @see Content::updateRedirect()
+ *
+ * This implementation replaces the first link on the page with the given new target
+ * if this Content object is a redirect. Otherwise, this method returns $this.
+ *
+ * @since 1.21
+ *
+ * @param Title $target
+ *
+ * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
+ */
+ public function updateRedirect( Title $target ) {
+ if ( !$this->isRedirect() ) {
+ return $this;
+ }
+
+ # Fix the text
+ # Remember that redirect pages can have categories, templates, etc.,
+ # so the regex has to be fairly general
+ $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x',
+ '[[' . $target->getFullText() . ']]',
+ $this->getNativeData(), 1 );
+
+ return new WikitextContent( $newText );
+ }
+
+ /**
+ * Returns true if this content is not a redirect, and this content's text
+ * is countable according to the criteria defined by $wgArticleCountMethod.
+ *
+ * @param bool $hasLinks if it is known whether this content contains
+ * links, provide this information here, to avoid redundant parsing to
+ * find out (default: null).
+ * @param $title Title: (default: null)
+ *
+ * @internal param \IContextSource $context context for parsing if necessary
+ *
+ * @return bool True if the content is countable
+ */
+ public function isCountable( $hasLinks = null, Title $title = null ) {
+ global $wgArticleCountMethod;
+
+ if ( $this->isRedirect() ) {
+ return false;
+ }
+
+ $text = $this->getNativeData();
+
+ switch ( $wgArticleCountMethod ) {
+ case 'any':
+ return true;
+ case 'comma':
+ return strpos( $text, ',' ) !== false;
+ case 'link':
+ if ( $hasLinks === null ) { # not known, find out
+ if ( !$title ) {
+ $context = RequestContext::getMain();
+ $title = $context->getTitle();
+ }
+
+ $po = $this->getParserOutput( $title, null, null, false );
+ $links = $po->getLinks();
+ $hasLinks = !empty( $links );
+ }
+
+ return $hasLinks;
+ }
+
+ return false;
+ }
+
+ public function getTextForSummary( $maxlength = 250 ) {
+ $truncatedtext = parent::getTextForSummary( $maxlength );
+
+ # clean up unfinished links
+ # XXX: make this optional? wasn't there in autosummary, but required for
+ # deletion summary.
+ $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
+
+ return $truncatedtext;
+ }
+
+ /**
+ * Returns a ParserOutput object resulting from parsing the content's text
+ * using $wgParser.
+ *
+ * @since 1.21
+ *
+ * @param $title Title
+ * @param int $revId Revision to pass to the parser (default: null)
+ * @param $options ParserOptions (default: null)
+ * @param bool $generateHtml (default: false)
+ *
+ * @internal param \IContextSource|null $context
+ * @return ParserOutput representing the HTML form of the text
+ */
+ public function getParserOutput( Title $title,
+ $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+ global $wgParser;
+
+ if ( !$options ) {
+ //NOTE: use canonical options per default to produce cacheable output
+ $options = $this->getContentHandler()->makeParserOptions( 'canonical' );
+ }
+
+ $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
+ return $po;
+ }
+
+ protected function getHtml() {
+ throw new MWException(
+ "getHtml() not implemented for wikitext. "
+ . "Use getParserOutput()->getText()."
+ );
+ }
+
+ /**
+ * @see Content::matchMagicWord()
+ *
+ * This implementation calls $word->match() on the this TextContent object's text.
+ *
+ * @param MagicWord $word
+ *
+ * @return bool whether this Content object matches the given magic word.
+ */
+ public function matchMagicWord( MagicWord $word ) {
+ return $word->match( $this->getNativeData() );
+ }
+}
diff --git a/includes/content/WikitextContentHandler.php b/includes/content/WikitextContentHandler.php
new file mode 100644
index 00000000..b1b461fb
--- /dev/null
+++ b/includes/content/WikitextContentHandler.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Content handler for wiki text pages.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Content
+ */
+
+/**
+ * Content handler for wiki text pages.
+ *
+ * @ingroup Content
+ */
+class WikitextContentHandler extends TextContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_WIKITEXT ) {
+ parent::__construct( $modelId, array( CONTENT_FORMAT_WIKITEXT ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new WikitextContent( $text );
+ }
+
+ /**
+ * @see ContentHandler::makeEmptyContent
+ *
+ * @return Content
+ */
+ public function makeEmptyContent() {
+ return new WikitextContent( '' );
+ }
+
+ /**
+ * Returns a WikitextContent object representing a redirect to the given destination page.
+ *
+ * @see ContentHandler::makeRedirectContent
+ *
+ * @param Title $destination the page to redirect to.
+ * @param string $text text to include in the redirect, if possible.
+ *
+ * @return Content
+ */
+ public function makeRedirectContent( Title $destination, $text = '' ) {
+ $optionalColon = '';
+
+ if ( $destination->getNamespace() == NS_CATEGORY ) {
+ $optionalColon = ':';
+ } else {
+ $iw = $destination->getInterwiki();
+ if ( $iw && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
+ $optionalColon = ':';
+ }
+ }
+
+ $mwRedir = MagicWord::get( 'redirect' );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $optionalColon . $destination->getFullText() . ']]';
+ if ( $text != '' ) {
+ $redirectText .= "\n" . $text;
+ }
+
+ return new WikitextContent( $redirectText );
+ }
+
+ /**
+ * Returns true because wikitext supports redirects.
+ *
+ * @see ContentHandler::supportsRedirects
+ *
+ * @return boolean whether redirects are supported.
+ */
+ public function supportsRedirects() {
+ return true;
+ }
+
+ /**
+ * Returns true because wikitext supports sections.
+ *
+ * @return boolean whether sections are supported.
+ */
+ public function supportsSections() {
+ return true;
+ }
+
+ /**
+ * Returns true, because wikitext supports caching using the
+ * ParserCache mechanism.
+ *
+ * @since 1.21
+ * @return bool
+ */
+ public function isParserCacheSupported() {
+ return true;
+ }
+}
diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php
index 45bd6fff..e13cfa88 100644
--- a/includes/context/ContextSource.php
+++ b/includes/context/ContextSource.php
@@ -28,7 +28,6 @@
* member variable and provide accessors to it.
*/
abstract class ContextSource implements IContextSource {
-
/**
* @var IContextSource
*/
@@ -42,7 +41,7 @@ abstract class ContextSource implements IContextSource {
public function getContext() {
if ( $this->context === null ) {
$class = get_class( $this );
- wfDebug( __METHOD__ . " ($class): called and \$context is null. Using RequestContext::getMain() for sanity\n" );
+ wfDebug( __METHOD__ . " ($class): called and \$context is null. Using RequestContext::getMain() for sanity\n" );
$this->context = RequestContext::getMain();
}
return $this->context;
@@ -52,7 +51,7 @@ abstract class ContextSource implements IContextSource {
* Set the IContextSource object
*
* @since 1.18
- * @param $context IContextSource
+ * @param IContextSource $context
*/
public function setContext( IContextSource $context ) {
$this->context = $context;
@@ -107,7 +106,7 @@ abstract class ContextSource implements IContextSource {
* Get the OutputPage object
*
* @since 1.18
- * @return OutputPage object
+ * @return OutputPage
*/
public function getOutput() {
return $this->getContext()->getOutput();
@@ -126,7 +125,7 @@ abstract class ContextSource implements IContextSource {
/**
* Get the Language object
*
- * @deprecated 1.19 Use getLanguage instead
+ * @deprecated since 1.19 Use getLanguage instead
* @return Language
*/
public function getLang() {
@@ -159,12 +158,21 @@ abstract class ContextSource implements IContextSource {
* Parameters are the same as wfMessage()
*
* @since 1.18
- * @return Message object
+ * @return Message
*/
public function msg( /* $args */ ) {
$args = func_get_args();
return call_user_func_array( array( $this->getContext(), 'msg' ), $args );
}
-
-}
+ /**
+ * Export the resolved user IP, HTTP headers, user ID, and session ID.
+ * The result will be reasonably sized to allow for serialization.
+ *
+ * @return Array
+ * @since 1.21
+ */
+ public function exportSession() {
+ return $this->getContext()->exportSession();
+ }
+}
diff --git a/includes/context/DerivativeContext.php b/includes/context/DerivativeContext.php
index 5adf3621..fd9bf963 100644
--- a/includes/context/DerivativeContext.php
+++ b/includes/context/DerivativeContext.php
@@ -30,7 +30,6 @@
* a different Title instance set on it.
*/
class DerivativeContext extends ContextSource {
-
/**
* @var WebRequest
*/
@@ -68,7 +67,7 @@ class DerivativeContext extends ContextSource {
/**
* Constructor
- * @param $context IContextSource Context to inherit from
+ * @param IContextSource $context Context to inherit from
*/
public function __construct( IContextSource $context ) {
$this->setContext( $context );
@@ -77,7 +76,7 @@ class DerivativeContext extends ContextSource {
/**
* Set the WebRequest object
*
- * @param $r WebRequest object
+ * @param WebRequest $r
*/
public function setRequest( WebRequest $r ) {
$this->request = $r;
@@ -99,9 +98,12 @@ class DerivativeContext extends ContextSource {
/**
* Set the Title object
*
- * @param $t Title object
+ * @param Title $t
*/
- public function setTitle( Title $t ) {
+ public function setTitle( $t ) {
+ if ( $t !== null && !$t instanceof Title ) {
+ throw new MWException( __METHOD__ . " expects an instance of Title" );
+ }
$this->title = $t;
}
@@ -140,7 +142,7 @@ class DerivativeContext extends ContextSource {
* Set the WikiPage object
*
* @since 1.19
- * @param $p WikiPage object
+ * @param WikiPage $p
*/
public function setWikiPage( WikiPage $p ) {
$this->wikipage = $p;
@@ -166,7 +168,7 @@ class DerivativeContext extends ContextSource {
/**
* Set the OutputPage object
*
- * @param $o OutputPage
+ * @param OutputPage $o
*/
public function setOutput( OutputPage $o ) {
$this->output = $o;
@@ -175,7 +177,7 @@ class DerivativeContext extends ContextSource {
/**
* Get the OutputPage object
*
- * @return OutputPage object
+ * @return OutputPage
*/
public function getOutput() {
if ( !is_null( $this->output ) ) {
@@ -188,7 +190,7 @@ class DerivativeContext extends ContextSource {
/**
* Set the User object
*
- * @param $u User
+ * @param User $u
*/
public function setUser( User $u ) {
$this->user = $u;
@@ -210,8 +212,8 @@ class DerivativeContext extends ContextSource {
/**
* Set the Language object
*
- * @deprecated 1.19 Use setLanguage instead
- * @param $l Mixed Language instance or language code
+ * @deprecated since 1.19 Use setLanguage instead
+ * @param Language|string $l Language instance or language code
*/
public function setLang( $l ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -221,7 +223,8 @@ class DerivativeContext extends ContextSource {
/**
* Set the Language object
*
- * @param $l Mixed Language instance or language code
+ * @param Language|string $l Language instance or language code
+ * @throws MWException
* @since 1.19
*/
public function setLanguage( $l ) {
@@ -237,7 +240,7 @@ class DerivativeContext extends ContextSource {
}
/**
- * @deprecated 1.19 Use getLanguage instead
+ * @deprecated since 1.19 Use getLanguage instead
* @return Language
*/
public function getLang() {
@@ -262,7 +265,7 @@ class DerivativeContext extends ContextSource {
/**
* Set the Skin object
*
- * @param $s Skin
+ * @param Skin $s
*/
public function setSkin( Skin $s ) {
$this->skin = clone $s;
@@ -282,5 +285,19 @@ class DerivativeContext extends ContextSource {
}
}
+ /**
+ * Get a message using the current context.
+ *
+ * This can't just inherit from ContextSource, since then
+ * it would set only the original context, and not take
+ * into account any changes.
+ *
+ * @param String Message name
+ * @param Variable number of message arguments
+ * @return Message
+ */
+ public function msg() {
+ $args = func_get_args();
+ return call_user_func_array( 'wfMessage', $args )->setContext( $this );
+ }
}
-
diff --git a/includes/context/IContextSource.php b/includes/context/IContextSource.php
index 476035b5..35d5aed6 100644
--- a/includes/context/IContextSource.php
+++ b/includes/context/IContextSource.php
@@ -27,7 +27,6 @@
* Interface for objects which can provide a context on request.
*/
interface IContextSource {
-
/**
* Get the WebRequest object
*
@@ -66,7 +65,7 @@ interface IContextSource {
/**
* Get the OutputPage object
*
- * @return OutputPage object
+ * @return OutputPage
*/
public function getOutput();
@@ -80,7 +79,7 @@ interface IContextSource {
/**
* Get the Language object
*
- * @deprecated 1.19 Use getLanguage instead
+ * @deprecated since 1.19 Use getLanguage instead
* @return Language
*/
public function getLang();
@@ -103,8 +102,16 @@ interface IContextSource {
/**
* Get a Message object with context set
*
- * @return Message object
+ * @return Message
*/
public function msg();
-}
+ /**
+ * Export the resolved user IP, HTTP headers, user ID, and session ID.
+ * The result will be reasonably sized to allow for serialization.
+ *
+ * @return Array
+ * @since 1.21
+ */
+ public function exportSession();
+}
diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php
index 9e7837d9..01ec57c0 100644
--- a/includes/context/RequestContext.php
+++ b/includes/context/RequestContext.php
@@ -28,7 +28,6 @@
* Group all the pieces relevant to the context of a request into one instance
*/
class RequestContext implements IContextSource {
-
/**
* @var WebRequest
*/
@@ -67,7 +66,7 @@ class RequestContext implements IContextSource {
/**
* Set the WebRequest object
*
- * @param $r WebRequest object
+ * @param WebRequest $r
*/
public function setRequest( WebRequest $r ) {
$this->request = $r;
@@ -89,10 +88,15 @@ class RequestContext implements IContextSource {
/**
* Set the Title object
*
- * @param $t Title object
+ * @param Title $t
*/
- public function setTitle( Title $t ) {
+ public function setTitle( $t ) {
+ if ( $t !== null && !$t instanceof Title ) {
+ throw new MWException( __METHOD__ . " expects an instance of Title" );
+ }
$this->title = $t;
+ // Erase the WikiPage so a new one with the new title gets created.
+ $this->wikipage = null;
}
/**
@@ -135,9 +139,15 @@ class RequestContext implements IContextSource {
* Set the WikiPage object
*
* @since 1.19
- * @param $p WikiPage object
+ * @param WikiPage $p
*/
public function setWikiPage( WikiPage $p ) {
+ $contextTitle = $this->getTitle();
+ $pageTitle = $p->getTitle();
+ if ( !$contextTitle || !$pageTitle->equals( $contextTitle ) ) {
+ $this->setTitle( $pageTitle );
+ }
+ // Defer this to the end since setTitle sets it to null.
$this->wikipage = $p;
}
@@ -148,6 +158,7 @@ class RequestContext implements IContextSource {
* canUseWikiPage() to check whether this method can be called safely.
*
* @since 1.19
+ * @throws MWException
* @return WikiPage
*/
public function getWikiPage() {
@@ -171,7 +182,7 @@ class RequestContext implements IContextSource {
/**
* Get the OutputPage object
*
- * @return OutputPage object
+ * @return OutputPage
*/
public function getOutput() {
if ( $this->output === null ) {
@@ -183,7 +194,7 @@ class RequestContext implements IContextSource {
/**
* Set the User object
*
- * @param $u User
+ * @param User $u
*/
public function setUser( User $u ) {
$this->user = $u;
@@ -204,7 +215,7 @@ class RequestContext implements IContextSource {
/**
* Accepts a language code and ensures it's sane. Outputs a cleaned up language
* code and replaces with $wgLanguageCode if not sane.
- * @param $code string
+ * @param string $code Language code
* @return string
*/
public static function sanitizeLangCode( $code ) {
@@ -214,7 +225,7 @@ class RequestContext implements IContextSource {
$code = strtolower( $code );
# Validate $code
- if( empty( $code ) || !Language::isValidCode( $code ) || ( $code === 'qqq' ) ) {
+ if ( empty( $code ) || !Language::isValidCode( $code ) || ( $code === 'qqq' ) ) {
wfDebug( "Invalid user language code\n" );
$code = $wgLanguageCode;
}
@@ -225,8 +236,8 @@ class RequestContext implements IContextSource {
/**
* Set the Language object
*
- * @deprecated 1.19 Use setLanguage instead
- * @param $l Mixed Language instance or language code
+ * @deprecated since 1.19 Use setLanguage instead
+ * @param Language|string $l Language instance or language code
*/
public function setLang( $l ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -236,7 +247,8 @@ class RequestContext implements IContextSource {
/**
* Set the Language object
*
- * @param $l Mixed Language instance or language code
+ * @param Language|string $l Language instance or language code
+ * @throws MWException
* @since 1.19
*/
public function setLanguage( $l ) {
@@ -252,7 +264,7 @@ class RequestContext implements IContextSource {
}
/**
- * @deprecated 1.19 Use getLanguage instead
+ * @deprecated since 1.19 Use getLanguage instead
* @return Language
*/
public function getLang() {
@@ -289,7 +301,7 @@ class RequestContext implements IContextSource {
wfRunHooks( 'UserGetLanguageObject', array( $user, &$code, $this ) );
- if( $code === $wgLanguageCode ) {
+ if ( $code === $wgLanguageCode ) {
$this->lang = $wgContLang;
} else {
$obj = Language::factory( $code );
@@ -305,7 +317,7 @@ class RequestContext implements IContextSource {
/**
* Set the Skin object
*
- * @param $s Skin
+ * @param Skin $s
*/
public function setSkin( Skin $s ) {
$this->skin = clone $s;
@@ -320,14 +332,14 @@ class RequestContext implements IContextSource {
public function getSkin() {
if ( $this->skin === null ) {
wfProfileIn( __METHOD__ . '-createskin' );
-
+
$skin = null;
wfRunHooks( 'RequestContextCreateSkin', array( $this, &$skin ) );
// If the hook worked try to set a skin from it
if ( $skin instanceof Skin ) {
$this->skin = $skin;
- } elseif ( is_string($skin) ) {
+ } elseif ( is_string( $skin ) ) {
$this->skin = Skin::newFromKey( $skin );
}
@@ -335,7 +347,7 @@ class RequestContext implements IContextSource {
// then go through the normal processing to load a skin
if ( $this->skin === null ) {
global $wgHiddenPrefs;
- if( !in_array( 'skin', $wgHiddenPrefs ) ) {
+ if ( !in_array( 'skin', $wgHiddenPrefs ) ) {
# get the user skin
$userSkin = $this->getUser()->getOption( 'skin' );
$userSkin = $this->getRequest()->getVal( 'useskin', $userSkin );
@@ -361,7 +373,7 @@ class RequestContext implements IContextSource {
* Get a Message object with context set
* Parameters are the same as wfMessage()
*
- * @return Message object
+ * @return Message
*/
public function msg() {
$args = func_get_args();
@@ -373,7 +385,7 @@ class RequestContext implements IContextSource {
/**
* Get the RequestContext object associated with the main request
*
- * @return RequestContext object
+ * @return RequestContext
*/
public static function getMain() {
static $instance = null;
@@ -384,6 +396,98 @@ class RequestContext implements IContextSource {
}
/**
+ * Export the resolved user IP, HTTP headers, user ID, and session ID.
+ * The result will be reasonably sized to allow for serialization.
+ *
+ * @return Array
+ * @since 1.21
+ */
+ public function exportSession() {
+ return array(
+ 'ip' => $this->getRequest()->getIP(),
+ 'headers' => $this->getRequest()->getAllHeaders(),
+ 'sessionId' => session_id(),
+ 'userId' => $this->getUser()->getId()
+ );
+ }
+
+ /**
+ * Import the resolved user IP, HTTP headers, user ID, and session ID.
+ * This sets the current session and sets $wgUser and $wgRequest.
+ * Once the return value falls out of scope, the old context is restored.
+ * This function can only be called within CLI mode scripts.
+ *
+ * This will setup the session from the given ID. This is useful when
+ * background scripts inherit context when acting on behalf of a user.
+ *
+ * @note suhosin.session.encrypt may interfere with this method.
+ *
+ * @param array $params Result of RequestContext::exportSession()
+ * @return ScopedCallback
+ * @throws MWException
+ * @since 1.21
+ */
+ public static function importScopedSession( array $params ) {
+ if ( PHP_SAPI !== 'cli' ) {
+ // Don't send random private cookies or turn $wgRequest into FauxRequest
+ throw new MWException( "Sessions can only be imported in cli mode." );
+ } elseif ( !strlen( $params['sessionId'] ) ) {
+ throw new MWException( "No session ID was specified." );
+ }
+
+ if ( $params['userId'] ) { // logged-in user
+ $user = User::newFromId( $params['userId'] );
+ if ( !$user ) {
+ throw new MWException( "No user with ID '{$params['userId']}'." );
+ }
+ } elseif ( !IP::isValid( $params['ip'] ) ) {
+ throw new MWException( "Could not load user '{$params['ip']}'." );
+ } else { // anon user
+ $user = User::newFromName( $params['ip'], false );
+ }
+
+ $importSessionFunction = function( User $user, array $params ) {
+ global $wgRequest, $wgUser;
+
+ $context = RequestContext::getMain();
+ // Commit and close any current session
+ session_write_close(); // persist
+ session_id( '' ); // detach
+ $_SESSION = array(); // clear in-memory array
+ // Remove any user IP or agent information
+ $context->setRequest( new FauxRequest() );
+ $wgRequest = $context->getRequest(); // b/c
+ // Now that all private information is detached from the user, it should
+ // be safe to load the new user. If errors occur or an exception is thrown
+ // and caught (leaving the main context in a mixed state), there is no risk
+ // of the User object being attached to the wrong IP, headers, or session.
+ $context->setUser( $user );
+ $wgUser = $context->getUser(); // b/c
+ if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
+ wfSetupSession( $params['sessionId'] ); // sets $_SESSION
+ }
+ $request = new FauxRequest( array(), false, $_SESSION );
+ $request->setIP( $params['ip'] );
+ foreach ( $params['headers'] as $name => $value ) {
+ $request->setHeader( $name, $value );
+ }
+ // Set the current context to use the new WebRequest
+ $context->setRequest( $request );
+ $wgRequest = $context->getRequest(); // b/c
+ };
+
+ // Stash the old session and load in the new one
+ $oUser = self::getMain()->getUser();
+ $oParams = self::getMain()->exportSession();
+ $importSessionFunction( $user, $params );
+
+ // Set callback to save and close the new session and reload the old one
+ return new ScopedCallback( function() use ( $importSessionFunction, $oUser, $oParams ) {
+ $importSessionFunction( $oUser, $oParams );
+ } );
+ }
+
+ /**
* Create a new extraneous context. The context is filled with information
* external to the current session.
* - Title is specified by argument
@@ -397,7 +501,7 @@ class RequestContext implements IContextSource {
* @param WebRequest|array $request A WebRequest or data to use for a FauxRequest
* @return RequestContext
*/
- public static function newExtraneousContext( Title $title, $request=array() ) {
+ public static function newExtraneousContext( Title $title, $request = array() ) {
$context = new self;
$context->setTitle( $title );
if ( $request instanceof WebRequest ) {
@@ -408,6 +512,4 @@ class RequestContext implements IContextSource {
$context->user = User::newFromName( '127.0.0.1', false );
return $context;
}
-
}
-
diff --git a/includes/dao/DBAccessBase.php b/includes/dao/DBAccessBase.php
new file mode 100644
index 00000000..6c009dee
--- /dev/null
+++ b/includes/dao/DBAccessBase.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * Base class for objects that allow access to other wiki's databases using
+ * the foreign database access mechanism implemented by LBFactory_multi.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Database
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+abstract class DBAccessBase implements IDBAccessObject {
+
+ /**
+ * @var String|bool $wiki The target wiki's name. This must be an ID
+ * that LBFactory can understand.
+ */
+ protected $wiki = false;
+
+ /**
+ * @param string|bool $wiki The target wiki's name. This must be an ID
+ * that LBFactory can understand.
+ */
+ public function __construct( $wiki = false ) {
+ $this->wiki = $wiki;
+ }
+
+ /**
+ * Returns a database connection.
+ *
+ * @see wfGetDB()
+ * @see LoadBalancer::getConnection()
+ *
+ * @since 1.21
+ *
+ * @param int $id Which connection to use
+ * @param array $groups Query groups
+ *
+ * @return DatabaseBase
+ */
+ protected function getConnection( $id, $groups = array() ) {
+ $loadBalancer = wfGetLB( $this->wiki );
+ return $loadBalancer->getConnection( $id, $groups, $this->wiki );
+ }
+
+ /**
+ * Releases a database connection and makes it available for recycling.
+ *
+ * @see LoadBalancer::reuseConnection()
+ *
+ * @since 1.21
+ *
+ * @param DatabaseBase $db the database connection to release.
+ */
+ protected function releaseConnection( DatabaseBase $db ) {
+ if ( $this->wiki !== false ) {
+ $loadBalancer = $this->getLoadBalancer();
+ $loadBalancer->reuseConnection( $db );
+ }
+ }
+
+ /**
+ * Get the database type used for read operations.
+ *
+ * @see wfGetLB
+ *
+ * @since 1.21
+ *
+ * @return LoadBalancer The database load balancer object
+ */
+ public function getLoadBalancer() {
+ return wfGetLB( $this->wiki );
+ }
+}
diff --git a/includes/dao/IDBAccessObject.php b/includes/dao/IDBAccessObject.php
index e30522a5..4eb6ff3e 100644
--- a/includes/dao/IDBAccessObject.php
+++ b/includes/dao/IDBAccessObject.php
@@ -41,10 +41,12 @@
* - b) Determine the new row (expensive, so we don't want to hold locks now)
* - c) Re-read the current row with READ_LOCKING; if it changed then bail out
* - d) otherwise, do the updates
+ *
+ * @since 1.20
*/
interface IDBAccessObject {
// Constants for object loading bitfield flags (higher => higher QoS)
- const READ_LATEST = 1; // read from the master
+ const READ_LATEST = 1; // read from the master
const READ_LOCKING = 3; // READ_LATEST and "FOR UPDATE"
// Convenience constant for callers to explicitly request slave data
diff --git a/includes/db/ChronologyProtector.php b/includes/db/ChronologyProtector.php
new file mode 100644
index 00000000..de5e72c3
--- /dev/null
+++ b/includes/db/ChronologyProtector.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Generator of database load balancing objects.
+ *
+ * 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 Database
+ */
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector {
+ /** @var Array (DB master name => position) */
+ protected $startupPositions = array();
+ /** @var Array (DB master name => position) */
+ protected $shutdownPositions = array();
+
+ protected $initialized = false; // bool; whether the session data was loaded
+
+ /**
+ * Initialise a LoadBalancer to give it appropriate chronology protection.
+ *
+ * If the session has a previous master position recorded, this will try to
+ * make sure that the next query to a slave of that master will see changes up
+ * to that position by delaying execution. The delay may timeout and allow stale
+ * data if no non-lagged slaves are available.
+ *
+ * @param $lb LoadBalancer
+ * @return void
+ */
+ public function initLB( LoadBalancer $lb ) {
+ if ( $lb->getServerCount() <= 1 ) {
+ return; // non-replicated setup
+ }
+ if ( !$this->initialized ) {
+ $this->initialized = true;
+ if ( isset( $_SESSION[__CLASS__] ) && is_array( $_SESSION[__CLASS__] ) ) {
+ $this->startupPositions = $_SESSION[__CLASS__];
+ }
+ }
+ $masterName = $lb->getServerName( 0 );
+ if ( !empty( $this->startupPositions[$masterName] ) ) {
+ $info = $lb->parentInfo();
+ $pos = $this->startupPositions[$masterName];
+ wfDebug( __METHOD__ . ": LB " . $info['id'] . " waiting for master pos $pos\n" );
+ $lb->waitFor( $pos );
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LoadBalancer is about to shut
+ * down. Saves replication positions.
+ *
+ * @param $lb LoadBalancer
+ * @return void
+ */
+ public function shutdownLB( LoadBalancer $lb ) {
+ if ( session_id() == '' || $lb->getServerCount() <= 1 ) {
+ return; // don't start a session; don't bother with non-replicated setups
+ }
+ $masterName = $lb->getServerName( 0 );
+ if ( isset( $this->shutdownPositions[$masterName] ) ) {
+ return; // already done
+ }
+ // Only save the position if writes have been done on the connection
+ $db = $lb->getAnyOpenConnection( 0 );
+ $info = $lb->parentInfo();
+ if ( !$db || !$db->doneWrites() ) {
+ wfDebug( __METHOD__ . ": LB {$info['id']}, no writes done\n" );
+ return;
+ }
+ $pos = $db->getMasterPos();
+ wfDebug( __METHOD__ . ": LB {$info['id']} has master pos $pos\n" );
+ $this->shutdownPositions[$masterName] = $pos;
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+ * May commit chronology data to persistent storage.
+ *
+ * @return void
+ */
+ public function shutdown() {
+ if ( session_id() != '' && count( $this->shutdownPositions ) ) {
+ wfDebug( __METHOD__ . ": saving master pos for " .
+ count( $this->shutdownPositions ) . " master(s)\n" );
+ $_SESSION[__CLASS__] = $this->shutdownPositions;
+ }
+ }
+}
diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php
index 4e43642f..819925cb 100644
--- a/includes/db/CloneDatabase.php
+++ b/includes/db/CloneDatabase.php
@@ -60,9 +60,9 @@ class CloneDatabase {
* Constructor
*
* @param $db DatabaseBase A database subclass
- * @param $tablesToClone Array An array of tables to clone, unprefixed
- * @param $newTablePrefix String Prefix to assign to the tables
- * @param $oldTablePrefix String Prefix on current tables, if not $wgDBprefix
+ * @param array $tablesToClone An array of tables to clone, unprefixed
+ * @param string $newTablePrefix Prefix to assign to the tables
+ * @param string $oldTablePrefix Prefix on current tables, if not $wgDBprefix
* @param $dropCurrentTables bool
*/
public function __construct( DatabaseBase $db, array $tablesToClone,
@@ -77,7 +77,7 @@ class CloneDatabase {
/**
* Set whether to use temporary tables or not
- * @param $u Bool Use temporary tables when cloning the structure
+ * @param bool $u Use temporary tables when cloning the structure
*/
public function useTemporaryTables( $u = true ) {
$this->useTemporaryTables = $u;
@@ -87,40 +87,37 @@ class CloneDatabase {
* Clone the table structure
*/
public function cloneTableStructure() {
-
- foreach( $this->tablesToClone as $tbl ) {
+ foreach ( $this->tablesToClone as $tbl ) {
# Clean up from previous aborted run. So that table escaping
# works correctly across DB engines, we need to change the pre-
# fix back and forth so tableName() works right.
-
+
self::changePrefix( $this->oldTablePrefix );
$oldTableName = $this->db->tableName( $tbl, 'raw' );
-
+
self::changePrefix( $this->newTablePrefix );
$newTableName = $this->db->tableName( $tbl, 'raw' );
-
- if( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) {
+
+ if ( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) {
$this->db->dropTable( $tbl, __METHOD__ );
- wfDebug( __METHOD__." dropping {$newTableName}\n", true);
+ wfDebug( __METHOD__ . " dropping {$newTableName}\n", true );
//Dropping the oldTable because the prefix was changed
}
# Create new table
- wfDebug( __METHOD__." duplicating $oldTableName to $newTableName\n", true );
+ wfDebug( __METHOD__ . " duplicating $oldTableName to $newTableName\n", true );
$this->db->duplicateTableStructure( $oldTableName, $newTableName, $this->useTemporaryTables );
-
}
-
}
/**
* Change the prefix back to the original.
- * @param $dropTables bool Optionally drop the tables we created
+ * @param bool $dropTables Optionally drop the tables we created
*/
public function destroy( $dropTables = false ) {
- if( $dropTables ) {
+ if ( $dropTables ) {
self::changePrefix( $this->newTablePrefix );
- foreach( $this->tablesToClone as $tbl ) {
+ foreach ( $this->tablesToClone as $tbl ) {
$this->db->dropTable( $tbl );
}
}
@@ -130,7 +127,7 @@ class CloneDatabase {
/**
* Change the table prefix on all open DB connections/
*
- * @param $prefix
+ * @param $prefix
* @return void
*/
public static function changePrefix( $prefix ) {
@@ -140,8 +137,8 @@ class CloneDatabase {
}
/**
- * @param $lb LoadBalancer
- * @param $prefix
+ * @param $lb LoadBalancer
+ * @param $prefix
* @return void
*/
public static function changeLBPrefix( $lb, $prefix ) {
@@ -149,8 +146,8 @@ class CloneDatabase {
}
/**
- * @param $db DatabaseBase
- * @param $prefix
+ * @param $db DatabaseBase
+ * @param $prefix
* @return void
*/
public static function changeDBPrefix( $db, $prefix ) {
diff --git a/includes/db/Database.php b/includes/db/Database.php
index 5f10b97d..10645608 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -24,13 +24,6 @@
* @ingroup Database
*/
-/** Number of times to re-try an operation in case of deadlock */
-define( 'DEADLOCK_TRIES', 4 );
-/** Minimum time to wait before retry, in microseconds */
-define( 'DEADLOCK_DELAY_MIN', 500000 );
-/** Maximum time to wait before retry */
-define( 'DEADLOCK_DELAY_MAX', 1500000 );
-
/**
* Base interface for all DBMS-specific code. At a bare minimum, all of the
* following must be implemented to support MediaWiki
@@ -49,10 +42,10 @@ interface DatabaseType {
/**
* Open a connection to the database. Usually aborts on failure
*
- * @param $server String: database server host
- * @param $user String: database user name
- * @param $password String: database user password
- * @param $dbName String: database name
+ * @param string $server database server host
+ * @param string $user database user name
+ * @param string $password database user password
+ * @param string $dbName database name
* @return bool
* @throws DBConnectionError
*/
@@ -62,9 +55,10 @@ interface DatabaseType {
* Fetch the next row from the given result object, in object form.
* Fields can be retrieved with $row->fieldname, with fields acting like
* member variables.
+ * If no more rows are available, false is returned.
*
* @param $res ResultWrapper|object as returned from DatabaseBase::query(), etc.
- * @return Row object
+ * @return object|bool
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject( $res );
@@ -72,9 +66,10 @@ interface DatabaseType {
/**
* Fetch the next row from the given result object, in associative array
* form. Fields are retrieved with $row['fieldname'].
+ * If no more rows are available, false is returned.
*
* @param $res ResultWrapper result object as returned from DatabaseBase::query(), etc.
- * @return Row object
+ * @return array|bool
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow( $res );
@@ -112,8 +107,8 @@ interface DatabaseType {
* The value inserted should be fetched from nextSequenceValue()
*
* Example:
- * $id = $dbw->nextSequenceValue('page_page_id_seq');
- * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->nextSequenceValue( 'page_page_id_seq' );
+ * $dbw->insert( 'page', array( 'page_id' => $id ) );
* $id = $dbw->insertId();
*
* @return int
@@ -149,8 +144,8 @@ interface DatabaseType {
* mysql_fetch_field() wrapper
* Returns false if the field doesn't exist
*
- * @param $table string: table name
- * @param $field string: field name
+ * @param string $table table name
+ * @param string $field field name
*
* @return Field
*/
@@ -158,12 +153,12 @@ interface DatabaseType {
/**
* Get information about an index into an object
- * @param $table string: Table name
- * @param $index string: Index name
- * @param $fname string: Calling function name
+ * @param string $table Table name
+ * @param string $index Index name
+ * @param string $fname Calling function name
* @return Mixed: Database-specific index description class or false if the index does not exist
*/
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' );
+ function indexInfo( $table, $index, $fname = __METHOD__ );
/**
* Get the number of rows affected by the last write query
@@ -176,7 +171,7 @@ interface DatabaseType {
/**
* Wrapper for addslashes()
*
- * @param $s string: to be slashed.
+ * @param string $s to be slashed.
* @return string: slashed string.
*/
function strencode( $s );
@@ -189,7 +184,7 @@ interface DatabaseType {
*
* @return string: wikitext of a link to the server software's web site
*/
- static function getSoftwareLink();
+ function getSoftwareLink();
/**
* A string describing the current software version, like from
@@ -210,10 +205,22 @@ interface DatabaseType {
}
/**
+ * Interface for classes that implement or wrap DatabaseBase
+ * @ingroup Database
+ */
+interface IDatabase {}
+
+/**
* Database abstraction object
* @ingroup Database
*/
-abstract class DatabaseBase implements DatabaseType {
+abstract class DatabaseBase implements IDatabase, DatabaseType {
+ /** Number of times to re-try an operation in case of deadlock */
+ const DEADLOCK_TRIES = 4;
+ /** Minimum time to wait before retry, in microseconds */
+ const DEADLOCK_DELAY_MIN = 500000;
+ /** Maximum time to wait before retry */
+ const DEADLOCK_DELAY_MAX = 1500000;
# ------------------------------------------------------------------------------
# Variables
@@ -228,14 +235,14 @@ abstract class DatabaseBase implements DatabaseType {
protected $mConn = null;
protected $mOpened = false;
- /**
- * @since 1.20
- * @var array of Closure
- */
+ /** @var callable[] */
protected $mTrxIdleCallbacks = array();
+ /** @var callable[] */
+ protected $mTrxPreCommitCallbacks = array();
protected $mTablePrefix;
protected $mFlags;
+ protected $mForeign;
protected $mTrxLevel = 0;
protected $mErrorCount = 0;
protected $mLBInfo = array();
@@ -249,6 +256,43 @@ abstract class DatabaseBase implements DatabaseType {
protected $delimiter = ';';
+ /**
+ * Remembers the function name given for starting the most recent transaction via begin().
+ * Used to provide additional context for error reporting.
+ *
+ * @var String
+ * @see DatabaseBase::mTrxLevel
+ */
+ private $mTrxFname = null;
+
+ /**
+ * Record if possible write queries were done in the last transaction started
+ *
+ * @var Bool
+ * @see DatabaseBase::mTrxLevel
+ */
+ private $mTrxDoneWrites = false;
+
+ /**
+ * Record if the current transaction was started implicitly due to DBO_TRX being set.
+ *
+ * @var Bool
+ * @see DatabaseBase::mTrxLevel
+ */
+ private $mTrxAutomatic = false;
+
+ /**
+ * @since 1.21
+ * @var file handle for upgrade
+ */
+ protected $fileHandle = null;
+
+ /**
+ * @since 1.22
+ * @var Process cache of VIEWs names in the database
+ */
+ protected $allViews = null;
+
# ------------------------------------------------------------------------------
# Accessors
# ------------------------------------------------------------------------------
@@ -266,6 +310,13 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
+ * @return string: command delimiter used by this database engine
+ */
+ public function getDelimiter() {
+ return $this->delimiter;
+ }
+
+ /**
* Boolean, controls output of large amounts of debug information.
* @param $debug bool|null
* - true to enable debugging
@@ -315,6 +366,8 @@ abstract class DatabaseBase implements DatabaseType {
* code should use lastErrno() and lastError() to handle the
* situation as appropriate.
*
+ * Do not use this function outside of the Database classes.
+ *
* @param $ignoreErrors bool|null
*
* @return bool The previous value of the flag.
@@ -329,7 +382,7 @@ abstract class DatabaseBase implements DatabaseType {
* Historically, transactions were allowed to be "nested". This is no
* longer supported, so this function really only returns a boolean.
*
- * @param $level int An integer (0 or 1), or omitted to leave it unchanged.
+ * @param int $level An integer (0 or 1), or omitted to leave it unchanged.
* @return int The previous value
*/
public function trxLevel( $level = null ) {
@@ -338,7 +391,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Get/set the number of errors logged. Only useful when errors are ignored
- * @param $count int The count to set, or omitted to leave it unchanged.
+ * @param int $count The count to set, or omitted to leave it unchanged.
* @return int The error count
*/
public function errorCount( $count = null ) {
@@ -347,7 +400,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Get/set the table prefix.
- * @param $prefix string The table prefix to set, or omitted to leave it unchanged.
+ * @param string $prefix The table prefix to set, or omitted to leave it unchanged.
* @return string The previous table prefix.
*/
public function tablePrefix( $prefix = null ) {
@@ -355,10 +408,19 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
+ * Set the filehandle to copy write statements to.
+ *
+ * @param $fh filehandle
+ */
+ public function setFileHandle( $fh ) {
+ $this->fileHandle = $fh;
+ }
+
+ /**
* Get properties passed down from the server info array of the load
* balancer.
*
- * @param $name string The entry of the info array to get, or null to get the
+ * @param string $name The entry of the info array to get, or null to get the
* whole array
*
* @return LoadBalancer|null
@@ -441,7 +503,7 @@ abstract class DatabaseBase implements DatabaseType {
* Returns true if this database uses timestamps rather than integers
*
* @return bool
- */
+ */
public function realTimestamps() {
return false;
}
@@ -504,12 +566,14 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Returns true if there is a transaction open with possible write
- * queries or transaction idle callbacks waiting on it to finish.
+ * queries or transaction pre-commit/idle callbacks waiting on it to finish.
*
* @return bool
*/
public function writesOrCallbacksPending() {
- return $this->mTrxLevel && ( $this->mDoneWrites || $this->mTrxIdleCallbacks );
+ return $this->mTrxLevel && (
+ $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
+ );
}
/**
@@ -526,7 +590,6 @@ abstract class DatabaseBase implements DatabaseType {
* @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
@@ -535,8 +598,8 @@ abstract class DatabaseBase implements DatabaseType {
public function setFlag( $flag ) {
global $wgDebugDBTransactions;
$this->mFlags |= $flag;
- if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) {
- wfDebug("Implicit transactions are now disabled.\n");
+ if ( ( $flag & DBO_TRX ) & $wgDebugDBTransactions ) {
+ wfDebug( "Implicit transactions are now disabled.\n" );
}
}
@@ -549,7 +612,7 @@ abstract class DatabaseBase implements DatabaseType {
global $wgDebugDBTransactions;
$this->mFlags &= ~$flag;
if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
- wfDebug("Implicit transactions are now disabled.\n");
+ wfDebug( "Implicit transactions are now disabled.\n" );
}
}
@@ -605,15 +668,28 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Constructor.
- * @param $server String: database server host
- * @param $user String: database user name
- * @param $password String: database user password
- * @param $dbName String: database name
+ *
+ * FIXME: It is possible to construct a Database object with no associated
+ * connection object, by specifying no parameters to __construct(). This
+ * feature is deprecated and should be removed.
+ *
+ * FIXME: The long list of formal parameters here is not really appropriate
+ * for MySQL, and not at all appropriate for any other DBMS. It should be
+ * replaced by named parameters as in DatabaseBase::factory().
+ *
+ * DatabaseBase subclasses should not be constructed directly in external
+ * code. DatabaseBase::factory() should be used instead.
+ *
+ * @param string $server database server host
+ * @param string $user database user name
+ * @param string $password database user password
+ * @param string $dbName database name
* @param $flags
- * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
+ * @param string $tablePrefix database table prefixes. By default use the prefix gave in LocalSettings.php
+ * @param bool $foreign disable some operations specific to local databases
*/
function __construct( $server = false, $user = false, $password = false, $dbName = false,
- $flags = 0, $tablePrefix = 'get from global'
+ $flags = 0, $tablePrefix = 'get from global', $foreign = false
) {
global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions;
@@ -623,12 +699,12 @@ abstract class DatabaseBase implements DatabaseType {
if ( $wgCommandLineMode ) {
$this->mFlags &= ~DBO_TRX;
if ( $wgDebugDBTransactions ) {
- wfDebug("Implicit transaction open disabled.\n");
+ wfDebug( "Implicit transaction open disabled.\n" );
}
} else {
$this->mFlags |= DBO_TRX;
if ( $wgDebugDBTransactions ) {
- wfDebug("Implicit transaction open enabled.\n");
+ wfDebug( "Implicit transaction open enabled.\n" );
}
}
}
@@ -640,6 +716,8 @@ abstract class DatabaseBase implements DatabaseType {
$this->mTablePrefix = $tablePrefix;
}
+ $this->mForeign = $foreign;
+
if ( $user ) {
$this->open( $server, $user, $password, $dbName );
}
@@ -657,7 +735,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Given a DB type, construct the name of the appropriate child class of
* DatabaseBase. This is designed to replace all of the manual stuff like:
- * $class = 'Database' . ucfirst( strtolower( $type ) );
+ * $class = 'Database' . ucfirst( strtolower( $dbType ) );
* as well as validate against the canonical list of DB types we have
*
* This factory function is mostly useful for when you need to connect to a
@@ -665,32 +743,62 @@ abstract class DatabaseBase implements DatabaseType {
* an extension, et cetera). Do not use this to connect to the MediaWiki
* database. Example uses in core:
* @see LoadBalancer::reallyOpenConnection()
- * @see ExternalUser_MediaWiki::initFromCond()
* @see ForeignDBRepo::getMasterDB()
* @see WebInstaller_DBConnect::execute()
*
* @since 1.18
*
- * @param $dbType String A possible DB type
- * @param $p Array An array of options to pass to the constructor.
- * Valid options are: host, user, password, dbname, flags, tablePrefix
+ * @param string $dbType A possible DB type
+ * @param array $p An array of options to pass to the constructor.
+ * Valid options are: host, user, password, dbname, flags, tablePrefix, driver
* @return DatabaseBase subclass or null
*/
- public final static function factory( $dbType, $p = array() ) {
+ final public static function factory( $dbType, $p = array() ) {
$canonicalDBTypes = array(
- 'mysql', 'postgres', 'sqlite', 'oracle', 'mssql', 'ibm_db2'
+ 'mysql' => array( 'mysqli', 'mysql' ),
+ 'postgres' => array(),
+ 'sqlite' => array(),
+ 'oracle' => array(),
+ 'mssql' => array(),
);
+
+ $driver = false;
$dbType = strtolower( $dbType );
- $class = 'Database' . ucfirst( $dbType );
+ if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
+ $possibleDrivers = $canonicalDBTypes[$dbType];
+ if ( !empty( $p['driver'] ) ) {
+ if ( in_array( $p['driver'], $possibleDrivers ) ) {
+ $driver = $p['driver'];
+ } else {
+ throw new MWException( __METHOD__ .
+ " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
+ }
+ } else {
+ foreach ( $possibleDrivers as $posDriver ) {
+ if ( extension_loaded( $posDriver ) ) {
+ $driver = $posDriver;
+ break;
+ }
+ }
+ }
+ } else {
+ $driver = $dbType;
+ }
+ if ( $driver === false ) {
+ throw new MWException( __METHOD__ .
+ " no viable database extension found for type '$dbType'" );
+ }
- if( in_array( $dbType, $canonicalDBTypes ) || ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) ) {
+ $class = 'Database' . ucfirst( $driver );
+ if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
return new $class(
isset( $p['host'] ) ? $p['host'] : false,
isset( $p['user'] ) ? $p['user'] : false,
isset( $p['password'] ) ? $p['password'] : false,
isset( $p['dbname'] ) ? $p['dbname'] : false,
isset( $p['flags'] ) ? $p['flags'] : 0,
- isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global'
+ isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global',
+ isset( $p['foreign'] ) ? $p['foreign'] : false
);
} else {
return null;
@@ -723,8 +831,9 @@ abstract class DatabaseBase implements DatabaseType {
/**
* @param $errno
* @param $errstr
+ * @access private
*/
- protected function connectionErrorHandler( $errno, $errstr ) {
+ public function connectionErrorHandler( $errno, $errstr ) {
$this->mPHPError = $errstr;
}
@@ -732,6 +841,7 @@ abstract class DatabaseBase implements DatabaseType {
* Closes a database connection.
* if it is open : commits any open transactions
*
+ * @throws MWException
* @return Bool operation success. true if already closed.
*/
public function close() {
@@ -741,8 +851,14 @@ abstract class DatabaseBase implements DatabaseType {
$this->mOpened = false;
if ( $this->mConn ) {
if ( $this->trxLevel() ) {
- $this->commit( __METHOD__ );
+ if ( !$this->mTrxAutomatic ) {
+ wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
+ " performing implicit commit before closing connection!" );
+ }
+
+ $this->commit( __METHOD__, 'flush' );
}
+
$ret = $this->closeConnection();
$this->mConn = false;
return $ret;
@@ -756,10 +872,11 @@ abstract class DatabaseBase implements DatabaseType {
* @since 1.20
* @return bool: Whether connection was closed successfully
*/
- protected abstract function closeConnection();
+ abstract protected function closeConnection();
/**
- * @param $error String: fallback error message, used if none is given by DB
+ * @param string $error fallback error message, used if none is given by DB
+ * @throws DBConnectionError
*/
function reportConnectionError( $error = 'Unknown error' ) {
$myError = $this->lastError();
@@ -777,7 +894,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $sql String: SQL query.
* @return ResultWrapper Result object to feed to fetchObject, fetchRow, ...; or false on failure
*/
- protected abstract function doQuery( $sql );
+ abstract protected function doQuery( $sql );
/**
* Determine whether a query writes to the DB.
@@ -809,27 +926,12 @@ abstract class DatabaseBase implements DatabaseType {
* comment (you can use __METHOD__ or add some extra info)
* @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors...
* maybe best to catch the exception instead?
+ * @throws MWException
* @return boolean|ResultWrapper. true for a successful write query, ResultWrapper object
* for a successful read query, or false on failure if $tempIgnore set
- * @throws DBQueryError Thrown when the database returns an error of any kind
*/
- public function query( $sql, $fname = '', $tempIgnore = false ) {
- $isMaster = !is_null( $this->getLBInfo( 'master' ) );
- if ( !Profiler::instance()->isStub() ) {
- # generalizeSQL will probably cut down the query to reasonable
- # logging size most of the time. The substr is really just a sanity check.
-
- if ( $isMaster ) {
- $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query-master';
- } else {
- $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query';
- }
-
- wfProfileIn( $totalProf );
- wfProfileIn( $queryProf );
- }
+ public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+ global $wgUser, $wgDebugDBTransactions;
$this->mLastQuery = $sql;
if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) {
@@ -839,7 +941,6 @@ abstract class DatabaseBase implements DatabaseType {
}
# Add a comment for easy SHOW PROCESSLIST interpretation
- global $wgUser;
if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
$userName = $wgUser->getName();
if ( mb_strlen( $userName ) > 15 ) {
@@ -849,24 +950,49 @@ abstract class DatabaseBase implements DatabaseType {
} else {
$userName = '';
}
- $commentedSql = preg_replace( '/\s/', " /* $fname $userName */ ", $sql, 1 );
+
+ // Add trace comment to the begin of the sql string, right after the operator.
+ // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
+ $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
# If DBO_TRX is set, start a transaction
- if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
- $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' ) {
- # avoid establishing transactions for SHOW and SET statements too -
+ if ( ( $this->mFlags & DBO_TRX ) && !$this->mTrxLevel &&
+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' )
+ {
+ # Avoid establishing transactions for SHOW and SET statements too -
# that would delay transaction initializations to once connection
# is really used by application
$sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm)
if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) {
- global $wgDebugDBTransactions;
if ( $wgDebugDBTransactions ) {
- wfDebug("Implicit transaction start.\n");
+ wfDebug( "Implicit transaction start.\n" );
}
$this->begin( __METHOD__ . " ($fname)" );
+ $this->mTrxAutomatic = true;
}
}
+ # Keep track of whether the transaction has write queries pending
+ if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $this->isWriteQuery( $sql ) ) {
+ $this->mTrxDoneWrites = true;
+ Profiler::instance()->transactionWritingIn( $this->mServer, $this->mDBname );
+ }
+
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ if ( !Profiler::instance()->isStub() ) {
+ # generalizeSQL will probably cut down the query to reasonable
+ # logging size most of the time. The substr is really just a sanity check.
+ if ( $isMaster ) {
+ $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'DatabaseBase::query-master';
+ } else {
+ $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'DatabaseBase::query';
+ }
+ wfProfileIn( $totalProf );
+ wfProfileIn( $queryProf );
+ }
+
if ( $this->debug() ) {
static $cnt = 0;
@@ -878,10 +1004,6 @@ abstract class DatabaseBase implements DatabaseType {
wfDebug( "Query {$this->mDBname} ($cnt) ($master): $sqlx\n" );
}
- if ( istainted( $sql ) & TC_MYSQL ) {
- throw new MWException( 'Tainted query found' );
- }
-
$queryId = MWDebug::query( $sql, $fname, $isMaster );
# Do the query and handle errors
@@ -894,6 +1016,7 @@ abstract class DatabaseBase implements DatabaseType {
# Transaction is gone, like it or not
$this->mTrxLevel = 0;
$this->mTrxIdleCallbacks = array(); // cancel
+ $this->mTrxPreCommitCallbacks = array(); // cancel
wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->ping() ) {
@@ -933,6 +1056,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $sql String
* @param $fname String
* @param $tempIgnore Boolean
+ * @throws DBQueryError
*/
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
# Ignore errors during error handling to avoid infinite recursion
@@ -981,7 +1105,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Execute a prepared query with the various arguments
- * @param $prepared String: the prepared sql
+ * @param string $prepared the prepared sql
* @param $args Mixed: Either an array here, or put scalars as varargs
*
* @return ResultWrapper
@@ -1001,8 +1125,8 @@ abstract class DatabaseBase implements DatabaseType {
/**
* 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
+ * @param string $preparedQuery a 'preparable' SQL statement
+ * @param array $args of arguments to fill it with
* @return string executable SQL
*/
public function fillPrepared( $preparedQuery, $args ) {
@@ -1019,20 +1143,26 @@ abstract class DatabaseBase implements DatabaseType {
* while we're doing this.
*
* @param $matches Array
+ * @throws DBUnexpectedError
* @return String
*/
protected function fillPreparedArg( $matches ) {
- switch( $matches[1] ) {
- case '\\?': return '?';
- case '\\!': return '!';
- case '\\&': return '&';
- }
-
- list( /* $n */ , $arg ) = each( $this->preparedArgs );
-
- switch( $matches[1] ) {
- case '?': return $this->addQuotes( $arg );
- case '!': return $arg;
+ switch ( $matches[1] ) {
+ case '\\?':
+ return '?';
+ case '\\!':
+ return '!';
+ case '\\&':
+ return '&';
+ }
+
+ list( /* $n */, $arg ) = each( $this->preparedArgs );
+
+ switch ( $matches[1] ) {
+ case '?':
+ return $this->addQuotes( $arg );
+ case '!':
+ return $arg;
case '&':
# return $this->addQuotes( file_get_contents( $arg ) );
throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
@@ -1048,7 +1178,8 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $res Mixed: A SQL result
*/
- public function freeResult( $res ) {}
+ public function freeResult( $res ) {
+ }
/**
* A SELECT wrapper which returns a single field from a single result row.
@@ -1058,18 +1189,18 @@ abstract class DatabaseBase implements DatabaseType {
*
* If no result rows are returned from the query, false is returned.
*
- * @param $table string|array Table name. See DatabaseBase::select() for details.
- * @param $var string The field name to select. This must be a valid SQL
+ * @param string|array $table Table name. See DatabaseBase::select() for details.
+ * @param string $var The field name to select. This must be a valid SQL
* fragment: do not use unvalidated user input.
- * @param $cond string|array The condition array. See DatabaseBase::select() for details.
- * @param $fname string The function name of the caller.
- * @param $options string|array The query options. See DatabaseBase::select() for details.
+ * @param string|array $cond The condition array. See DatabaseBase::select() for details.
+ * @param string $fname The function name of the caller.
+ * @param string|array $options The query options. See DatabaseBase::select() for details.
*
* @return bool|mixed The value from the field, or false on failure.
*/
- public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
- $options = array() )
- {
+ public function selectField( $table, $var, $cond = '', $fname = __METHOD__,
+ $options = array()
+ ) {
if ( !is_array( $options ) ) {
$options = array( $options );
}
@@ -1095,7 +1226,7 @@ abstract class DatabaseBase implements DatabaseType {
* Returns an optional USE INDEX clause to go after the table, and a
* string to go at the end of the query.
*
- * @param $options Array: associative array of options to be turned into
+ * @param array $options associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return Array
* @see DatabaseBase::select()
@@ -1112,26 +1243,9 @@ abstract class DatabaseBase implements DatabaseType {
}
}
- if ( isset( $options['GROUP BY'] ) ) {
- $gb = is_array( $options['GROUP BY'] )
- ? implode( ',', $options['GROUP BY'] )
- : $options['GROUP BY'];
- $preLimitTail .= " GROUP BY {$gb}";
- }
+ $preLimitTail .= $this->makeGroupByWithHaving( $options );
- if ( isset( $options['HAVING'] ) ) {
- $having = is_array( $options['HAVING'] )
- ? $this->makeList( $options['HAVING'], LIST_AND )
- : $options['HAVING'];
- $preLimitTail .= " HAVING {$having}";
- }
-
- if ( isset( $options['ORDER BY'] ) ) {
- $ob = is_array( $options['ORDER BY'] )
- ? implode( ',', $options['ORDER BY'] )
- : $options['ORDER BY'];
- $preLimitTail .= " ORDER BY {$ob}";
- }
+ $preLimitTail .= $this->makeOrderBy( $options );
// if (isset($options['LIMIT'])) {
// $tailOpts .= $this->limitResult('', $options['LIMIT'],
@@ -1184,7 +1298,7 @@ abstract class DatabaseBase implements DatabaseType {
$startOpts .= ' SQL_NO_CACHE';
}
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
$useIndex = $this->useIndexClause( $options['USE INDEX'] );
} else {
$useIndex = '';
@@ -1194,14 +1308,57 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
+ * Returns an optional GROUP BY with an optional HAVING
+ *
+ * @param array $options associative array of options
+ * @return string
+ * @see DatabaseBase::select()
+ * @since 1.21
+ */
+ public function makeGroupByWithHaving( $options ) {
+ $sql = '';
+ if ( isset( $options['GROUP BY'] ) ) {
+ $gb = is_array( $options['GROUP BY'] )
+ ? implode( ',', $options['GROUP BY'] )
+ : $options['GROUP BY'];
+ $sql .= ' GROUP BY ' . $gb;
+ }
+ if ( isset( $options['HAVING'] ) ) {
+ $having = is_array( $options['HAVING'] )
+ ? $this->makeList( $options['HAVING'], LIST_AND )
+ : $options['HAVING'];
+ $sql .= ' HAVING ' . $having;
+ }
+ return $sql;
+ }
+
+ /**
+ * Returns an optional ORDER BY
+ *
+ * @param array $options associative array of options
+ * @return string
+ * @see DatabaseBase::select()
+ * @since 1.21
+ */
+ public function makeOrderBy( $options ) {
+ if ( isset( $options['ORDER BY'] ) ) {
+ $ob = is_array( $options['ORDER BY'] )
+ ? implode( ',', $options['ORDER BY'] )
+ : $options['ORDER BY'];
+ return ' ORDER BY ' . $ob;
+ }
+ return '';
+ }
+
+ /**
* Execute a SELECT query constructed using the various parameters provided.
* See below for full details of the parameters.
*
- * @param $table String|Array Table name
- * @param $vars String|Array Field names
- * @param $conds String|Array Conditions
- * @param $fname String Caller function name
- * @param $options Array Query options
+ * @param string|array $table Table name
+ * @param string|array $vars Field names
+ * @param string|array $conds Conditions
+ * @param string $fname Caller function name
+ * @param array $options Query options
* @param $join_conds Array Join conditions
*
* @param $table string|array
@@ -1325,14 +1482,14 @@ abstract class DatabaseBase implements DatabaseType {
* join, the second is an SQL fragment giving the join condition for that
* table. For example:
*
- * array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * array( 'page' => array( 'LEFT JOIN', 'page_latest=rev_id' ) )
*
* @return ResultWrapper. If the query returned no rows, a ResultWrapper
* with no rows in it will be returned. If there was a query error, a
* DBQueryError exception will be thrown, except if the "ignore errors"
* option was set, in which case false will be returned.
*/
- public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function select( $table, $vars, $conds = '', $fname = __METHOD__,
$options = array(), $join_conds = array() ) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
@@ -1345,17 +1502,17 @@ abstract class DatabaseBase implements DatabaseType {
* doing UNION queries, where the SQL text of each query is needed. In general,
* however, callers outside of Database classes should just use select().
*
- * @param $table string|array Table name
- * @param $vars string|array Field names
- * @param $conds string|array Conditions
- * @param $fname string Caller function name
- * @param $options string|array Query options
+ * @param string|array $table Table name
+ * @param string|array $vars Field names
+ * @param string|array $conds Conditions
+ * @param string $fname Caller function name
+ * @param string|array $options Query options
* @param $join_conds string|array Join conditions
*
* @return string SQL query string.
* @see DatabaseBase::select()
*/
- public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
$options = array(), $join_conds = array() )
{
if ( is_array( $vars ) ) {
@@ -1363,28 +1520,26 @@ abstract class DatabaseBase implements DatabaseType {
}
$options = (array)$options;
+ $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+ ? $options['USE INDEX']
+ : array();
if ( is_array( $table ) ) {
- $useIndex = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
- ? $options['USE INDEX']
- : array();
- if ( count( $join_conds ) || count( $useIndex ) ) {
- $from = ' FROM ' .
- $this->tableNamesWithUseIndexOrJOIN( $table, $useIndex, $join_conds );
- } else {
- $from = ' FROM ' . implode( ',', $this->tableNamesWithAlias( $table ) );
- }
+ $from = ' FROM ' .
+ $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
} elseif ( $table != '' ) {
if ( $table[0] == ' ' ) {
$from = ' FROM ' . $table;
} else {
- $from = ' FROM ' . $this->tableName( $table );
+ $from = ' FROM ' .
+ $this->tableNamesWithUseIndexOrJOIN( array( $table ), $useIndexes, array() );
}
} else {
$from = '';
}
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
+ $this->makeSelectOptions( $options );
if ( !empty( $conds ) ) {
if ( is_array( $conds ) ) {
@@ -1413,16 +1568,16 @@ abstract class DatabaseBase implements DatabaseType {
* that a single row object is returned. If the query returns no rows,
* false is returned.
*
- * @param $table string|array Table name
- * @param $vars string|array Field names
- * @param $conds array Conditions
- * @param $fname string Caller function name
- * @param $options string|array Query options
+ * @param string|array $table Table name
+ * @param string|array $vars Field names
+ * @param array $conds Conditions
+ * @param string $fname Caller function name
+ * @param string|array $options Query options
* @param $join_conds array|string Join conditions
*
* @return object|bool
*/
- public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
+ public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
$options = array(), $join_conds = array() )
{
$options = (array)$options;
@@ -1455,15 +1610,15 @@ abstract class DatabaseBase implements DatabaseType {
*
* Takes the same arguments as DatabaseBase::select().
*
- * @param $table String: table name
- * @param Array|string $vars : unused
- * @param Array|string $conds : filters on the table
- * @param $fname String: function name for profiling
- * @param $options Array: options for select
+ * @param string $table table name
+ * @param array|string $vars : unused
+ * @param array|string $conds : filters on the table
+ * @param string $fname function name for profiling
+ * @param array $options options for select
* @return Integer: row count
*/
public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = 'DatabaseBase::estimateRowCount', $options = array() )
+ $fname = __METHOD__, $options = array() )
{
$rows = 0;
$res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options );
@@ -1480,26 +1635,27 @@ abstract class DatabaseBase implements DatabaseType {
* Removes most variables from an SQL query and replaces them with X or N for numbers.
* It's only slightly flawed. Don't use for anything important.
*
- * @param $sql String A SQL Query
+ * @param string $sql A SQL Query
*
* @return string
*/
static function generalizeSQL( $sql ) {
# This does the same as the regexp below would do, but in such a way
# as to avoid crashing php on some large strings.
- # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
+ # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
- $sql = str_replace ( "\\\\", '', $sql );
- $sql = str_replace ( "\\'", '', $sql );
- $sql = str_replace ( "\\\"", '', $sql );
- $sql = preg_replace ( "/'.*'/s", "'X'", $sql );
- $sql = preg_replace ( '/".*"/s', "'X'", $sql );
+ $sql = str_replace( "\\\\", '', $sql );
+ $sql = str_replace( "\\'", '', $sql );
+ $sql = str_replace( "\\\"", '', $sql );
+ $sql = preg_replace( "/'.*'/s", "'X'", $sql );
+ $sql = preg_replace( '/".*"/s', "'X'", $sql );
# All newlines, tabs, etc replaced by single space
- $sql = preg_replace ( '/\s+/', ' ', $sql );
+ $sql = preg_replace( '/\s+/', ' ', $sql );
# All numbers => N
- $sql = preg_replace ( '/-?[0-9]+/s', 'N', $sql );
+ $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
+ $sql = preg_replace( '/-?\d+/s', 'N', $sql );
return $sql;
}
@@ -1507,12 +1663,12 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Determines whether a field exists in a table
*
- * @param $table String: table name
- * @param $field String: filed to check on that table
- * @param $fname String: calling function name (optional)
+ * @param string $table table name
+ * @param string $field filed to check on that table
+ * @param string $fname calling function name (optional)
* @return Boolean: whether $table has filed $field
*/
- public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
+ public function fieldExists( $table, $field, $fname = __METHOD__ ) {
$info = $this->fieldInfo( $table, $field );
return (bool)$info;
@@ -1529,7 +1685,11 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool|null
*/
- public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
+ public function indexExists( $table, $index, $fname = __METHOD__ ) {
+ if ( !$this->tableExists( $table ) ) {
+ return null;
+ }
+
$info = $this->indexInfo( $table, $index, $fname );
if ( is_null( $info ) ) {
return null;
@@ -1626,11 +1786,11 @@ abstract class DatabaseBase implements DatabaseType {
* DatabaseBase::tableName().
* @param $a Array of rows to insert
* @param $fname String Calling function name (use __METHOD__) for logs/profiling
- * @param $options Array of options
+ * @param array $options of options
*
* @return bool
*/
- public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
+ public function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $a ) ) {
return true;
@@ -1642,6 +1802,10 @@ abstract class DatabaseBase implements DatabaseType {
$options = array( $options );
}
+ $fh = null;
+ if ( isset( $options['fileHandle'] ) ) {
+ $fh = $options['fileHandle'];
+ }
$options = $this->makeInsertOptions( $options );
if ( isset( $a[0] ) && is_array( $a[0] ) ) {
@@ -1669,13 +1833,19 @@ abstract class DatabaseBase implements DatabaseType {
$sql .= '(' . $this->makeList( $a ) . ')';
}
+ if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
+ return false;
+ } elseif ( $fh !== null ) {
+ return true;
+ }
+
return (bool)$this->query( $sql, $fname );
}
/**
* Make UPDATE options for the DatabaseBase::update function
*
- * @param $options Array: The options passed to DatabaseBase::update
+ * @param array $options The options passed to DatabaseBase::update
* @return string
*/
protected function makeUpdateOptions( $options ) {
@@ -1702,7 +1872,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $table String name of the table to UPDATE. This will be passed through
* DatabaseBase::tableName().
*
- * @param $values Array: An array of values to SET. For each array element,
+ * @param array $values An array of values to SET. For each array element,
* the key gives the field name, and the value gives the data
* to set that field to. The data will be quoted by
* DatabaseBase::addQuotes().
@@ -1714,12 +1884,12 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fname String: The function name of the caller (from __METHOD__),
* for logging and profiling.
*
- * @param $options Array: An array of UPDATE options, can be:
+ * @param array $options An array of UPDATE options, can be:
* - IGNORE: Ignore unique key conflicts
* - LOW_PRIORITY: MySQL-specific, see MySQL manual.
* @return Boolean
*/
- function update( $table, $values, $conds, $fname = 'DatabaseBase::update', $options = array() ) {
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
$table = $this->tableName( $table );
$opts = $this->makeUpdateOptions( $options );
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
@@ -1733,8 +1903,8 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Makes an encoded list of strings from an array
- * @param $a Array containing the data
- * @param $mode int Constant
+ * @param array $a containing the data
+ * @param int $mode Constant
* - LIST_COMMA: comma separated, no field names
* - LIST_AND: ANDed WHERE clause (without the WHERE). See
* the documentation for $conds in DatabaseBase::select().
@@ -1742,6 +1912,7 @@ abstract class DatabaseBase implements DatabaseType {
* - LIST_SET: comma separated with field names, like a SET clause
* - LIST_NAMES: comma separated field names
*
+ * @throws MWException|DBUnexpectedError
* @return string
*/
public function makeList( $a, $mode = LIST_COMMA ) {
@@ -1771,7 +1942,7 @@ abstract class DatabaseBase implements DatabaseType {
$list .= "$value";
} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
if ( count( $value ) == 0 ) {
- throw new MWException( __METHOD__ . ': empty input' );
+ throw new MWException( __METHOD__ . ": empty input for field $field" );
} 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
@@ -1803,10 +1974,10 @@ abstract class DatabaseBase implements DatabaseType {
* Build a partial where clause from a 2-d array such as used for LinkBatch.
* The keys on each level may be either integers or strings.
*
- * @param $data Array: organized as 2-d
+ * @param array $data organized as 2-d
* array(baseKeyVal => array(subKeyVal => [ignored], ...), ...)
- * @param $baseKey String: field name to match the base-level keys to (eg 'pl_namespace')
- * @param $subKey String: field name to match the sub-level keys to (eg 'pl_title')
+ * @param string $baseKey field name to match the base-level keys to (eg 'pl_namespace')
+ * @param string $subKey field name to match the sub-level keys to (eg 'pl_title')
* @return Mixed: string SQL fragment, or false if no items in array.
*/
public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
@@ -1868,7 +2039,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Build a concatenation list to feed into a SQL query
- * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting
+ * @param array $stringList list of raw SQL expressions; caller is responsible for any quoting
* @return String
*/
public function buildConcat( $stringList ) {
@@ -1916,8 +2087,8 @@ abstract class DatabaseBase implements DatabaseType {
* themselves. Pass the canonical name to such functions. This is only needed
* when calling query() directly.
*
- * @param $name String: database table name
- * @param $format String One of:
+ * @param string $name database table name
+ * @param string $format One of:
* quoted - Automatically pass the table name through addIdentifierQuotes()
* so that it can be used in a query.
* raw - Do not add identifier quotes to the table name
@@ -1947,47 +2118,40 @@ abstract class DatabaseBase implements DatabaseType {
# Split database and table into proper variables.
# We reverse the explode so that database.table and table both output
# the correct table.
- $dbDetails = array_reverse( explode( '.', $name, 2 ) );
- if ( isset( $dbDetails[1] ) ) {
- list( $table, $database ) = $dbDetails;
+ $dbDetails = explode( '.', $name, 2 );
+ if ( count( $dbDetails ) == 2 ) {
+ list( $database, $table ) = $dbDetails;
+ # We don't want any prefix added in this case
+ $prefix = '';
} else {
list( $table ) = $dbDetails;
- }
- $prefix = $this->mTablePrefix; # Default prefix
-
- # A database name has been specified in input. We don't want any
- # prefixes added.
- if ( isset( $database ) ) {
- $prefix = '';
+ if ( $wgSharedDB !== null # We have a shared database
+ && $this->mForeign == false # We're not working on a foreign database
+ && !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`'
+ && in_array( $table, $wgSharedTables ) # A shared table is selected
+ ) {
+ $database = $wgSharedDB;
+ $prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
+ } else {
+ $database = null;
+ $prefix = $this->mTablePrefix; # Default prefix
+ }
}
- # Note that we use the long format because php will complain in in_array if
- # the input is not an array, and will complain in is_array if it is not set.
- if ( !isset( $database ) # Don't use shared database if pre selected.
- && isset( $wgSharedDB ) # We have a shared database
- && !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`'
- && isset( $wgSharedTables )
- && is_array( $wgSharedTables )
- && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
- $database = $wgSharedDB;
- $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix;
+ # Quote $table and apply the prefix if not quoted.
+ $tableName = "{$prefix}{$table}";
+ if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) ) {
+ $tableName = $this->addIdentifierQuotes( $tableName );
}
- # Quote the $database and $table and apply the prefix if not quoted.
- if ( isset( $database ) ) {
+ # Quote $database and merge it with the table name if needed
+ if ( $database !== null ) {
if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
$database = $this->addIdentifierQuotes( $database );
}
+ $tableName = $database . '.' . $tableName;
}
- $table = "{$prefix}{$table}";
- if ( $format == 'quoted' && !$this->isQuotedIdentifier( $table ) ) {
- $table = $this->addIdentifierQuotes( "{$table}" );
- }
-
- # Merge our database and table into our final table name.
- $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" );
-
return $tableName;
}
@@ -1996,7 +2160,7 @@ abstract class DatabaseBase implements DatabaseType {
* This is handy when you need to construct SQL for joins
*
* Example:
- * extract($dbr->tableNames('user','watchlist'));
+ * extract( $dbr->tableNames( 'user', 'watchlist' ) );
* $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
* WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*
@@ -2018,7 +2182,7 @@ abstract class DatabaseBase implements DatabaseType {
* This is handy when you need to construct SQL for joins
*
* Example:
- * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
+ * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
* $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
* WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*
@@ -2039,8 +2203,8 @@ abstract class DatabaseBase implements DatabaseType {
* Get an aliased table name
* e.g. tableName AS newTableName
*
- * @param $name string Table name, see tableName()
- * @param $alias string|bool Alias (optional)
+ * @param string $name Table name, see tableName()
+ * @param string|bool $alias Alias (optional)
* @return string SQL name for aliased table. Will not alias a table to its own name
*/
public function tableNameWithAlias( $name, $alias = false ) {
@@ -2072,8 +2236,8 @@ abstract class DatabaseBase implements DatabaseType {
* Get an aliased field name
* e.g. fieldName AS newFieldName
*
- * @param $name string Field name
- * @param $alias string|bool Alias (optional)
+ * @param string $name Field name
+ * @param string|bool $alias Alias (optional)
* @return string SQL name for aliased field. Will not alias a field to its own name
*/
public function fieldNameWithAlias( $name, $alias = false ) {
@@ -2105,7 +2269,7 @@ abstract class DatabaseBase implements DatabaseType {
* Get the aliased table name clause for a FROM clause
* which might have a JOIN and/or USE INDEX clause
*
- * @param $tables array ( [alias] => table )
+ * @param array $tables ( [alias] => table )
* @param $use_index array Same as for select()
* @param $join_conds array Same as for select()
* @return string
@@ -2155,11 +2319,11 @@ abstract class DatabaseBase implements DatabaseType {
}
// We can't separate explicit JOIN clauses with ',', use ' ' for those
- $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
- $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
+ $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
+ $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
// Compile our final table clause
- return implode( ' ', array( $straightJoins, $otherJoins ) );
+ return implode( ' ', array( $implicitJoins, $explicitJoins ) );
}
/**
@@ -2172,9 +2336,9 @@ abstract class DatabaseBase implements DatabaseType {
protected function indexName( $index ) {
// Backwards-compatibility hack
$renamed = array(
- 'ar_usertext_timestamp' => 'usertext_timestamp',
- 'un_user_id' => 'user_id',
- 'un_user_ip' => 'user_ip',
+ 'ar_usertext_timestamp' => 'usertext_timestamp',
+ 'un_user_id' => 'user_id',
+ 'un_user_ip' => 'user_ip',
);
if ( isset( $renamed[$index] ) ) {
@@ -2185,8 +2349,7 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * If it's a string, adds quotes and backslashes
- * Otherwise returns as-is
+ * Adds quotes and backslashes.
*
* @param $s string
*
@@ -2336,14 +2499,14 @@ abstract class DatabaseBase implements DatabaseType {
* to collide. However if you do this, you run the risk of encountering
* errors which wouldn't have occurred in MySQL.
*
- * @param $table String: The table to replace the row(s) in.
- * @param $rows array Can be either a single row to insert, or multiple rows,
+ * @param string $table The table to replace the row(s) in.
+ * @param array $rows Can be either a single row to insert, or multiple rows,
* in the same format as for DatabaseBase::insert()
- * @param $uniqueIndexes array is an array of indexes. Each element may be either
+ * @param array $uniqueIndexes is an array of indexes. Each element may be either
* a field name or an array of field names
- * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
*/
- public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
+ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
$quotedTable = $this->tableName( $table );
if ( count( $rows ) == 0 ) {
@@ -2355,7 +2518,7 @@ abstract class DatabaseBase implements DatabaseType {
$rows = array( $rows );
}
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
# Delete rows which collide
if ( $uniqueIndexes ) {
$sql = "DELETE FROM $quotedTable WHERE ";
@@ -2386,7 +2549,7 @@ abstract class DatabaseBase implements DatabaseType {
}
# Now insert the row
- $this->insert( $table, $row );
+ $this->insert( $table, $row, $fname );
}
}
@@ -2394,9 +2557,9 @@ abstract class DatabaseBase implements DatabaseType {
* REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
* statement.
*
- * @param $table string Table name
- * @param $rows array Rows to insert
- * @param $fname string Caller function name
+ * @param string $table Table name
+ * @param array $rows Rows to insert
+ * @param string $fname Caller function name
*
* @return ResultWrapper
*/
@@ -2425,6 +2588,92 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
+ * INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
+ *
+ * This updates any conflicting rows (according to the unique indexes) using
+ * the provided SET clause and inserts any remaining (non-conflicted) rows.
+ *
+ * $rows may be either:
+ * - A single associative array. The array keys are the field names, and
+ * the values are the values to insert. The values are treated as data
+ * and will be quoted appropriately. If NULL is inserted, this will be
+ * converted to a database NULL.
+ * - An array with numeric keys, holding a list of associative arrays.
+ * This causes a multi-row INSERT on DBMSs that support it. The keys in
+ * each subarray must be identical to each other, and in the same order.
+ *
+ * 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.
+ *
+ * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
+ * returns success.
+ *
+ * @param string $table Table name. This will be passed through DatabaseBase::tableName().
+ * @param array $rows A single row or list of rows to insert
+ * @param array $uniqueIndexes List of single field names or field name tuples
+ * @param array $set An array of values to SET. For each array element,
+ * the key gives the field name, and the value gives the data
+ * to set that field to. The data will be quoted by
+ * DatabaseBase::addQuotes().
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options of options
+ *
+ * @return bool
+ * @since 1.22
+ */
+ public function upsert(
+ $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+ ) {
+ if ( !count( $rows ) ) {
+ return true; // nothing to do
+ }
+ $rows = is_array( reset( $rows ) ) ? $rows : array( $rows );
+
+ if ( count( $uniqueIndexes ) ) {
+ $clauses = array(); // list WHERE clauses that each identify a single row
+ foreach ( $rows as $row ) {
+ foreach ( $uniqueIndexes as $index ) {
+ $index = is_array( $index ) ? $index : array( $index ); // columns
+ $rowKey = array(); // unique key to this row
+ foreach ( $index as $column ) {
+ $rowKey[$column] = $row[$column];
+ }
+ $clauses[] = $this->makeList( $rowKey, LIST_AND );
+ }
+ }
+ $where = array( $this->makeList( $clauses, LIST_OR ) );
+ } else {
+ $where = false;
+ }
+
+ $useTrx = !$this->mTrxLevel;
+ if ( $useTrx ) {
+ $this->begin( $fname );
+ }
+ try {
+ # Update any existing conflicting row(s)
+ if ( $where !== false ) {
+ $ok = $this->update( $table, $set, $where, $fname );
+ } else {
+ $ok = true;
+ }
+ # Now insert any non-conflicting row(s)
+ $ok = $this->insert( $table, $rows, $fname, array( 'IGNORE' ) ) && $ok;
+ } catch ( Exception $e ) {
+ if ( $useTrx ) {
+ $this->rollback( $fname );
+ }
+ throw $e;
+ }
+ if ( $useTrx ) {
+ $this->commit( $fname );
+ }
+
+ return $ok;
+ }
+
+ /**
* DELETE where the condition is a join.
*
* MySQL overrides this to use a multi-table DELETE syntax, in other databases
@@ -2443,9 +2692,10 @@ abstract class DatabaseBase implements DatabaseType {
* ANDed together in the WHERE clause
* @param $fname String: Calling function name (use __METHOD__) for
* logs/profiling
+ * @throws DBUnexpectedError
*/
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
- $fname = 'DatabaseBase::deleteJoin' )
+ $fname = __METHOD__ )
{
if ( !$conds ) {
throw new DBUnexpectedError( $this,
@@ -2503,14 +2753,15 @@ abstract class DatabaseBase implements DatabaseType {
/**
* DELETE query wrapper.
*
- * @param $table Array Table name
- * @param $conds String|Array of conditions. See $conds in DatabaseBase::select() for
+ * @param array $table Table name
+ * @param string|array $conds of conditions. See $conds in DatabaseBase::select() for
* the format. Use $conds == "*" to delete all rows
- * @param $fname String name of the calling function
+ * @param string $fname name of the calling function
*
- * @return bool
+ * @throws DBUnexpectedError
+ * @return bool|ResultWrapper
*/
- public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
+ public function delete( $table, $conds, $fname = __METHOD__ ) {
if ( !$conds ) {
throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
}
@@ -2519,7 +2770,10 @@ abstract class DatabaseBase implements DatabaseType {
$sql = "DELETE FROM $table";
if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ' WHERE ' . $conds;
}
return $this->query( $sql, $fname );
@@ -2529,30 +2783,30 @@ abstract class DatabaseBase implements DatabaseType {
* INSERT SELECT wrapper. Takes data from a SELECT query and inserts it
* into another table.
*
- * @param $destTable string The table name to insert into
- * @param $srcTable string|array May be either a table name, or an array of table names
+ * @param string $destTable The table name to insert into
+ * @param string|array $srcTable May be either a table name, or an array of table names
* to include in a join.
*
- * @param $varMap array must be an associative array of the form
+ * @param array $varMap must be an associative array of the form
* array( 'dest1' => 'source1', ...). Source items may be literals
* rather than field names, but strings should be quoted with
* DatabaseBase::addQuotes()
*
- * @param $conds array Condition array. See $conds in DatabaseBase::select() for
+ * @param array $conds Condition array. See $conds in DatabaseBase::select() for
* the details of the format of condition arrays. May be "*" to copy the
* whole table.
*
- * @param $fname string The function name of the caller, from __METHOD__
+ * @param string $fname The function name of the caller, from __METHOD__
*
- * @param $insertOptions array Options for the INSERT part of the query, see
+ * @param array $insertOptions Options for the INSERT part of the query, see
* DatabaseBase::insert() for details.
- * @param $selectOptions array Options for the SELECT part of the query, see
+ * @param array $selectOptions Options for the SELECT part of the query, see
* DatabaseBase::select() for details.
*
* @return ResultWrapper
*/
public function insertSelect( $destTable, $srcTable, $varMap, $conds,
- $fname = 'DatabaseBase::insertSelect',
+ $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
@@ -2568,7 +2822,7 @@ abstract class DatabaseBase implements DatabaseType {
list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
}
@@ -2602,10 +2856,11 @@ abstract class DatabaseBase implements DatabaseType {
* 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 string $sql SQL query we will append the limit too
* @param $limit Integer the SQL limit
* @param $offset Integer|bool the SQL offset (default false)
*
+ * @throws DBUnexpectedError
* @return string
*/
public function limitResult( $sql, $limit, $offset = false ) {
@@ -2630,7 +2885,7 @@ abstract class DatabaseBase implements DatabaseType {
* 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 array $sqls SQL statements to combine
* @param $all Boolean: use UNION ALL
* @return String: SQL fragment
*/
@@ -2643,9 +2898,9 @@ abstract class DatabaseBase implements DatabaseType {
* 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|array 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
+ * @param string|array $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 ) {
@@ -2659,9 +2914,9 @@ abstract class DatabaseBase implements DatabaseType {
* Returns a comand for str_replace function in SQL query.
* Uses REPLACE() in MySQL
*
- * @param $orig String: column to modify
- * @param $old String: column to seek
- * @param $new String: column to replace with
+ * @param string $orig column to modify
+ * @param string $old column to seek
+ * @param string $new column to replace with
*
* @return string
*/
@@ -2743,7 +2998,7 @@ abstract class DatabaseBase implements DatabaseType {
$args = func_get_args();
$function = array_shift( $args );
$oldIgnore = $this->ignoreErrors( true );
- $tries = DEADLOCK_TRIES;
+ $tries = self::DEADLOCK_TRIES;
if ( is_array( $function ) ) {
$fname = $function[0];
@@ -2760,7 +3015,7 @@ abstract class DatabaseBase implements DatabaseType {
if ( $errno ) {
if ( $this->wasDeadlock() ) {
# Retry
- usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+ usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
} else {
$this->reportQueryError( $error, $errno, $sql, $fname );
}
@@ -2850,23 +3105,47 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Run an anonymous function as soon as there is no transaction pending.
* If there is a transaction and it is rolled back, then the callback is cancelled.
+ * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls.
* Callbacks must commit any transactions that they begin.
*
- * This is useful for updates to different systems or separate transactions are needed.
+ * This is useful for updates to different systems or when separate transactions are needed.
+ * For example, one might want to enqueue jobs into a system outside the database, but only
+ * after the database is updated so that the jobs will see the data when they actually run.
+ * It can also be used for updates that easily cause deadlocks if locks are held too long.
*
- * @param Closure $callback
- * @return void
+ * @param callable $callback
+ * @since 1.20
*/
- final public function onTransactionIdle( Closure $callback ) {
+ final public function onTransactionIdle( $callback ) {
+ $this->mTrxIdleCallbacks[] = array( $callback, wfGetCaller() );
+ if ( !$this->mTrxLevel ) {
+ $this->runOnTransactionIdleCallbacks();
+ }
+ }
+
+ /**
+ * Run an anonymous function before the current transaction commits or now if there is none.
+ * If there is a transaction and it is rolled back, then the callback is cancelled.
+ * Callbacks must not start nor commit any transactions.
+ *
+ * This is useful for updates that easily cause deadlocks if locks are held too long
+ * but where atomicity is strongly desired for these updates and some related updates.
+ *
+ * @param callable $callback
+ * @since 1.22
+ */
+ final public function onTransactionPreCommitOrIdle( $callback ) {
if ( $this->mTrxLevel ) {
- $this->mTrxIdleCallbacks[] = $callback;
+ $this->mTrxPreCommitCallbacks[] = array( $callback, wfGetCaller() );
} else {
- $callback();
+ $this->onTransactionIdle( $callback ); // this will trigger immediately
}
}
/**
- * Actually run the "on transaction idle" callbacks
+ * Actually any "on transaction idle" callbacks.
+ *
+ * @since 1.20
*/
protected function runOnTransactionIdleCallbacks() {
$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
@@ -2877,8 +3156,9 @@ abstract class DatabaseBase implements DatabaseType {
$this->mTrxIdleCallbacks = array(); // recursion guard
foreach ( $callbacks as $callback ) {
try {
+ list( $phpCallback ) = $callback;
$this->clearFlag( DBO_TRX ); // make each query its own transaction
- $callback();
+ call_user_func( $phpCallback );
$this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin()
} catch ( Exception $e ) {}
}
@@ -2890,19 +3170,78 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Begin a transaction
+ * Actually any "on transaction pre-commit" callbacks.
+ *
+ * @since 1.22
+ */
+ protected function runOnTransactionPreCommitCallbacks() {
+ $e = null; // last exception
+ do { // callbacks may add callbacks :)
+ $callbacks = $this->mTrxPreCommitCallbacks;
+ $this->mTrxPreCommitCallbacks = array(); // recursion guard
+ foreach ( $callbacks as $callback ) {
+ try {
+ list( $phpCallback ) = $callback;
+ call_user_func( $phpCallback );
+ } catch ( Exception $e ) {}
+ }
+ } while ( count( $this->mTrxPreCommitCallbacks ) );
+
+ if ( $e instanceof Exception ) {
+ throw $e; // re-throw any last exception
+ }
+ }
+
+ /**
+ * Begin a transaction. If a transaction is already in progress, that transaction will be committed before the
+ * new transaction is started.
+ *
+ * Note that when the DBO_TRX flag is set (which is usually the case for web requests, but not for maintenance scripts),
+ * any previous database query will have started a transaction automatically.
+ *
+ * Nesting of transactions is not supported. Attempts to nest transactions will cause a warning, unless the current
+ * transaction was started automatically because of the DBO_TRX flag.
*
* @param $fname string
*/
- final public function begin( $fname = 'DatabaseBase::begin' ) {
+ final public function begin( $fname = __METHOD__ ) {
+ global $wgDebugDBTransactions;
+
if ( $this->mTrxLevel ) { // implicit commit
+ if ( !$this->mTrxAutomatic ) {
+ // We want to warn about inadvertently nested begin/commit pairs, but not about
+ // auto-committing implicit transactions that were started by query() via DBO_TRX
+ $msg = "$fname: Transaction already in progress (from {$this->mTrxFname}), " .
+ " performing implicit commit!";
+ wfWarn( $msg );
+ wfLogDBError( $msg );
+ } else {
+ // if the transaction was automatic and has done write operations,
+ // log it if $wgDebugDBTransactions is enabled.
+ if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) {
+ wfDebug( "$fname: Automatic transaction with writes in progress" .
+ " (from {$this->mTrxFname}), performing implicit commit!\n"
+ );
+ }
+ }
+
+ $this->runOnTransactionPreCommitCallbacks();
$this->doCommit( $fname );
+ if ( $this->mTrxDoneWrites ) {
+ Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
+ }
$this->runOnTransactionIdleCallbacks();
}
+
$this->doBegin( $fname );
+ $this->mTrxFname = $fname;
+ $this->mTrxDoneWrites = false;
+ $this->mTrxAutomatic = false;
}
/**
+ * Issues the BEGIN command to the database server.
+ *
* @see DatabaseBase::begin()
* @param type $fname
*/
@@ -2912,16 +3251,44 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * End a transaction
+ * Commits a transaction previously started using begin().
+ * If no transaction is in progress, a warning is issued.
+ *
+ * Nesting of transactions is not supported.
*
* @param $fname string
- */
- final public function commit( $fname = 'DatabaseBase::commit' ) {
+ * @param string $flush Flush flag, set to 'flush' to disable warnings about explicitly committing implicit
+ * transactions, or calling commit when no transaction is in progress.
+ * This will silently break any ongoing explicit transaction. Only set the flush flag if you are sure
+ * that it is safe to ignore these warnings in your context.
+ */
+ final public function commit( $fname = __METHOD__, $flush = '' ) {
+ if ( $flush != 'flush' ) {
+ if ( !$this->mTrxLevel ) {
+ wfWarn( "$fname: No transaction to commit, something got out of sync!" );
+ } elseif ( $this->mTrxAutomatic ) {
+ wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" );
+ }
+ } else {
+ if ( !$this->mTrxLevel ) {
+ return; // nothing to do
+ } elseif ( !$this->mTrxAutomatic ) {
+ wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" );
+ }
+ }
+
+ $this->runOnTransactionPreCommitCallbacks();
$this->doCommit( $fname );
+ if ( $this->mTrxDoneWrites ) {
+ Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
+ }
+ $this->mTrxDoneWrites = false;
$this->runOnTransactionIdleCallbacks();
}
/**
+ * Issues the COMMIT command to the database server.
+ *
* @see DatabaseBase::commit()
* @param type $fname
*/
@@ -2933,17 +3300,29 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Rollback a transaction.
+ * Rollback a transaction previously started using begin().
+ * If no transaction is in progress, a warning is issued.
+ *
* No-op on non-transactional databases.
*
* @param $fname string
*/
- final public function rollback( $fname = 'DatabaseBase::rollback' ) {
+ final public function rollback( $fname = __METHOD__ ) {
+ if ( !$this->mTrxLevel ) {
+ wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
+ }
$this->doRollback( $fname );
$this->mTrxIdleCallbacks = array(); // cancel
+ $this->mTrxPreCommitCallbacks = array(); // cancel
+ if ( $this->mTrxDoneWrites ) {
+ Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
+ }
+ $this->mTrxDoneWrites = false;
}
/**
+ * Issues the ROLLBACK command to the database server.
+ *
* @see DatabaseBase::rollback()
* @param type $fname
*/
@@ -2962,15 +3341,16 @@ abstract class DatabaseBase implements DatabaseType {
* The table names passed to this function shall not be quoted (this
* function calls addIdentifierQuotes when needed).
*
- * @param $oldName String: name of table whose structure should be copied
- * @param $newName String: name of table to be created
+ * @param string $oldName name of table whose structure should be copied
+ * @param string $newName name of table to be created
* @param $temporary Boolean: whether the new table should be temporary
- * @param $fname String: calling function name
+ * @param string $fname calling function name
+ * @throws MWException
* @return Boolean: true if operation was successful
*/
public function duplicateTableStructure( $oldName, $newName, $temporary = false,
- $fname = 'DatabaseBase::duplicateTableStructure' )
- {
+ $fname = __METHOD__
+ ) {
throw new MWException(
'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
}
@@ -2978,14 +3358,49 @@ abstract class DatabaseBase implements DatabaseType {
/**
* List all tables on the database
*
- * @param $prefix string Only show tables with this prefix, e.g. mw_
- * @param $fname String: calling function name
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname calling function name
+ * @throws MWException
*/
- function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
}
/**
+ * Reset the views process cache set by listViews()
+ * @since 1.22
+ */
+ final public function clearViewsCache() {
+ $this->allViews = null;
+ }
+
+ /**
+ * Lists all the VIEWs in the database
+ *
+ * For caching purposes the list of all views should be stored in
+ * $this->allViews. The process cache can be cleared with clearViewsCache()
+ *
+ * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
+ * @param string $fname Name of calling function
+ * @throws MWException
+ * @since 1.22
+ */
+ public function listViews( $prefix = null, $fname = __METHOD__ ) {
+ throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
+ }
+
+ /**
+ * Differentiates between a TABLE and a VIEW
+ *
+ * @param $name string: Name of the database-structure to test.
+ * @throws MWException
+ * @since 1.22
+ */
+ public function isView( $name ) {
+ throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
+ }
+
+ /**
* Convert a timestamp in one of the formats accepted by wfTimestamp()
* to the format used for inserting into timestamp fields in this DBMS.
*
@@ -3114,7 +3529,8 @@ abstract class DatabaseBase implements DatabaseType {
* @param $options Array
* @return void
*/
- public function setSessionOptions( array $options ) {}
+ public function setSessionOptions( array $options ) {
+ }
/**
* Read and execute SQL commands from a file.
@@ -3122,15 +3538,18 @@ abstract class DatabaseBase implements DatabaseType {
* Returns true on success, error string or exception on failure (depending
* on object's error ignore settings).
*
- * @param $filename String: File name to open
- * @param $lineCallback Callback: Optional function called before reading each line
- * @param $resultCallback Callback: Optional function called for each MySQL result
- * @param $fname String: Calling function name or false if name should be
+ * @param string $filename File name to open
+ * @param bool|callable $lineCallback Optional function called before reading each line
+ * @param bool|callable $resultCallback Optional function called for each MySQL result
+ * @param bool|string $fname Calling function name or false if name should be
* generated dynamically using $filename
+ * @param bool|callable $inputCallback Callback: Optional function called for each complete line sent
+ * @throws MWException
+ * @throws Exception|MWException
* @return bool|string
*/
public function sourceFile(
- $filename, $lineCallback = false, $resultCallback = false, $fname = false
+ $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
) {
wfSuppressWarnings();
$fp = fopen( $filename, 'r' );
@@ -3145,7 +3564,7 @@ abstract class DatabaseBase implements DatabaseType {
}
try {
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname );
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
}
catch ( MWException $e ) {
fclose( $fp );
@@ -3162,7 +3581,7 @@ abstract class DatabaseBase implements DatabaseType {
* 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
+ * @param string $patch The name of the patch, like patch-something.sql
* @return String Full path to patch file
*/
public function patchPath( $patch ) {
@@ -3181,7 +3600,7 @@ abstract class DatabaseBase implements DatabaseType {
* ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at
* all. If it's set to false, $GLOBALS will be used.
*
- * @param $vars bool|array mapping variable name to value.
+ * @param bool|array $vars mapping variable name to value.
*/
public function setSchemaVars( $vars ) {
$this->mSchemaVars = $vars;
@@ -3194,14 +3613,14 @@ abstract class DatabaseBase implements DatabaseType {
* on object's error ignore settings).
*
* @param $fp Resource: File handle
- * @param $lineCallback Callback: Optional function called before reading each line
+ * @param $lineCallback Callback: Optional function called before reading each query
* @param $resultCallback Callback: Optional function called for each MySQL result
- * @param $fname String: Calling function name
- * @param $inputCallback Callback: Optional function called for each complete line (ended with ;) sent
+ * @param string $fname Calling function name
+ * @param $inputCallback Callback: Optional function called for each complete query sent
* @return bool|string
*/
public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = 'DatabaseBase::sourceStream', $inputCallback = false )
+ $fname = __METHOD__, $inputCallback = false )
{
$cmd = '';
@@ -3230,20 +3649,19 @@ abstract class DatabaseBase implements DatabaseType {
if ( $done || feof( $fp ) ) {
$cmd = $this->replaceVars( $cmd );
- if ( $inputCallback ) {
- call_user_func( $inputCallback, $cmd );
- }
- $res = $this->query( $cmd, $fname );
- if ( $resultCallback ) {
- call_user_func( $resultCallback, $res, $this );
- }
+ if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
+ $res = $this->query( $cmd, $fname );
- if ( false === $res ) {
- $err = $this->lastError();
- return "Query \"{$cmd}\" failed with error code \"$err\".\n";
- }
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res, $this );
+ }
+ if ( false === $res ) {
+ $err = $this->lastError();
+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ }
+ }
$cmd = '';
}
}
@@ -3254,8 +3672,8 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Called by sourceStream() to check if we've reached a statement end
*
- * @param $sql String SQL assembled so far
- * @param $newLine String New line about to be added to $sql
+ * @param string $sql SQL assembled so far
+ * @param string $newLine New line about to be added to $sql
* @return Bool Whether $newLine contains end of the statement
*/
public function streamStatementEnd( &$sql, &$newLine ) {
@@ -3283,7 +3701,7 @@ abstract class DatabaseBase implements DatabaseType {
* - / *$var* / is just encoded, besides traditional table prefix and
* table options its use should be avoided.
*
- * @param $ins String: SQL statement to replace variables in
+ * @param string $ins SQL statement to replace variables in
* @return String The new SQL statement with variables replaced
*/
protected function replaceSchemaVars( $ins ) {
@@ -3294,7 +3712,7 @@ abstract class DatabaseBase implements DatabaseType {
// replace `{$var}`
$ins = str_replace( '`{$' . $var . '}`', $this->addIdentifierQuotes( $value ), $ins );
// replace /*$var*/
- $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ) , $ins );
+ $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ), $ins );
}
return $ins;
}
@@ -3371,8 +3789,8 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Check to see if a named lock is available. This is non-blocking.
*
- * @param $lockName String: name of lock to poll
- * @param $method String: name of method calling us
+ * @param string $lockName name of lock to poll
+ * @param string $method name of method calling us
* @return Boolean
* @since 1.20
*/
@@ -3386,8 +3804,8 @@ abstract class DatabaseBase implements DatabaseType {
* Abstracted from Filestore::lock() so child classes can implement for
* their own needs.
*
- * @param $lockName String: name of lock to aquire
- * @param $method String: name of method calling us
+ * @param string $lockName name of lock to aquire
+ * @param string $method name of method calling us
* @param $timeout Integer: timeout
* @return Boolean
*/
@@ -3398,8 +3816,8 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Release a lock.
*
- * @param $lockName String: Name of lock to release
- * @param $method String: Name of method calling us
+ * @param string $lockName Name of lock to release
+ * @param string $method Name of method calling us
*
* @return int 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
@@ -3412,10 +3830,10 @@ abstract class DatabaseBase implements DatabaseType {
/**
* 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
+ * @param array $read of tables to lock for read access
+ * @param array $write of tables to lock for write access
+ * @param string $method name of caller
+ * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
*
* @return bool
*/
@@ -3426,7 +3844,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Unlock specific tables
*
- * @param $method String the caller
+ * @param string $method the caller
*
* @return bool
*/
@@ -3441,12 +3859,12 @@ abstract class DatabaseBase implements DatabaseType {
* @return bool|ResultWrapper
* @since 1.18
*/
- public function dropTable( $tableName, $fName = 'DatabaseBase::dropTable' ) {
- if( !$this->tableExists( $tableName, $fName ) ) {
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ if ( !$this->tableExists( $tableName, $fName ) ) {
return false;
}
$sql = "DROP TABLE " . $this->tableName( $tableName );
- if( $this->cascadingDeletes() ) {
+ if ( $this->cascadingDeletes() ) {
$sql .= " CASCADE";
}
return $this->query( $sql, $fName );
@@ -3476,7 +3894,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Encode an expiry time into the DBMS dependent format
*
- * @param $expiry String: timestamp for expiry, or the 'infinity' string
+ * @param string $expiry timestamp for expiry, or the 'infinity' string
* @return String
*/
public function encodeExpiry( $expiry ) {
@@ -3488,7 +3906,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Decode an expiry time into a DBMS independent format
*
- * @param $expiry String: DB timestamp field value for expiry
+ * @param string $expiry DB timestamp field value for expiry
* @param $format integer: TS_* constant, defaults to TS_MW
* @return String
*/
@@ -3518,9 +3936,21 @@ abstract class DatabaseBase implements DatabaseType {
return (string)$this->mConn;
}
+ /**
+ * Run a few simple sanity checks
+ */
public function __destruct() {
- if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
- trigger_error( "Transaction idle callbacks still pending." );
+ if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
+ trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
+ }
+ if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
+ $callers = array();
+ foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) {
+ $callers[] = $callbackInfo[1];
+
+ }
+ $callers = implode( ', ', $callers );
+ trigger_error( "DB transaction callbacks still pending (from $callers)." );
}
}
}
diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php
index a53a6747..0875695f 100644
--- a/includes/db/DatabaseError.php
+++ b/includes/db/DatabaseError.php
@@ -35,32 +35,20 @@ class DBError extends MWException {
/**
* Construct a database error
* @param $db DatabaseBase object which threw the error
- * @param $error String A simple error message to be used for debugging
+ * @param string $error A simple error message to be used for debugging
*/
- function __construct( DatabaseBase &$db, $error ) {
+ function __construct( DatabaseBase $db = null, $error ) {
$this->db = $db;
parent::__construct( $error );
}
/**
- * @param $html string
- * @return string
- */
- protected function getContentMessage( $html ) {
- if ( $html ) {
- return nl2br( htmlspecialchars( $this->getMessage() ) );
- } else {
- return $this->getMessage();
- }
- }
-
- /**
* @return string
*/
function getText() {
global $wgShowDBErrorBacktrace;
- $s = $this->getContentMessage( false ) . "\n";
+ $s = $this->getTextContent() . "\n";
if ( $wgShowDBErrorBacktrace ) {
$s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
@@ -75,14 +63,29 @@ class DBError extends MWException {
function getHTML() {
global $wgShowDBErrorBacktrace;
- $s = $this->getContentMessage( true );
+ $s = $this->getHTMLContent();
if ( $wgShowDBErrorBacktrace ) {
- $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+ $s .= '<p>Backtrace:</p><p>' .
+ nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>';
}
return $s;
}
+
+ /**
+ * @return string
+ */
+ protected function getTextContent() {
+ return $this->getMessage();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getHTMLContent() {
+ return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) . '</p>';
+ }
}
/**
@@ -91,16 +94,17 @@ class DBError extends MWException {
class DBConnectionError extends DBError {
public $error;
- function __construct( DatabaseBase &$db, $error = 'unknown error' ) {
+ function __construct( DatabaseBase $db = null, $error = 'unknown error' ) {
$msg = 'DB connection error';
if ( trim( $error ) != '' ) {
$msg .= ": $error";
+ } elseif ( $db ) {
+ $error = $this->db->getServer();
}
- $this->error = $error;
-
parent::__construct( $db, $msg );
+ $this->error = $error;
}
/**
@@ -130,10 +134,10 @@ class DBConnectionError extends DBError {
}
/**
- * @return bool
+ * @return boolean
*/
- function getLogMessage() {
- # Don't send to the exception log
+ function isLoggable() {
+ // Don't send to the exception log, already in dberror log
return false;
}
@@ -141,42 +145,54 @@ class DBConnectionError extends DBError {
* @return string
*/
function getPageTitle() {
- global $wgSitename;
- return htmlspecialchars( $this->msg( 'dberr-header', "$wgSitename has a problem" ) );
+ return $this->msg( 'dberr-header', 'This wiki has a problem' );
}
/**
* @return string
*/
function getHTML() {
- global $wgShowDBErrorBacktrace;
+ global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
- $sorry = htmlspecialchars( $this->msg( 'dberr-problems', 'Sorry! This site is experiencing technical difficulties.' ) );
+ $sorry = htmlspecialchars( $this->msg( 'dberr-problems', "Sorry!\nThis site is experiencing technical difficulties." ) );
$again = htmlspecialchars( $this->msg( 'dberr-again', 'Try waiting a few minutes and reloading.' ) );
- $info = htmlspecialchars( $this->msg( 'dberr-info', '(Can\'t contact the database server: $1)' ) );
-
- # No database access
- MessageCache::singleton()->disable();
- if ( trim( $this->error ) == '' ) {
- $this->error = $this->db->getProperty( 'mServer' );
+ if ( $wgShowHostnames || $wgShowSQLErrors ) {
+ $info = str_replace(
+ '$1', Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ),
+ htmlspecialchars( $this->msg( 'dberr-info', '(Cannot contact the database server: $1)' ) )
+ );
+ } else {
+ $info = htmlspecialchars( $this->msg( 'dberr-info-hidden', '(Cannot contact the database server)' ) );
}
- $this->error = Html::element( 'span', array( 'dir' => 'ltr' ), $this->error );
+ # No database access
+ MessageCache::singleton()->disable();
- $noconnect = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
- $text = str_replace( '$1', $this->error, $noconnect );
+ $text = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
if ( $wgShowDBErrorBacktrace ) {
- $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+ $text .= '<p>Backtrace:</p><p>' .
+ nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>';
}
- $extra = $this->searchForm();
+ $text .= '<hr />';
+ $text .= $this->searchForm();
- return "$text<hr />$extra";
+ return $text;
+ }
+
+ protected function getTextContent() {
+ global $wgShowHostnames, $wgShowSQLErrors;
+
+ if ( $wgShowHostnames || $wgShowSQLErrors ) {
+ return $this->getMessage();
+ } else {
+ return 'DB connection error';
+ }
}
- public function reportHTML(){
+ public function reportHTML() {
global $wgUseFileCache;
# Check whether we can serve a file-cached copy of the page with the error underneath
@@ -188,7 +204,7 @@ class DBConnectionError extends DBError {
# Hack: extend the body for error messages
$cache = str_replace( array( '</html>', '</body>' ), '', $cache );
# Add cache notice...
- $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">'.
+ $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">' .
htmlspecialchars( $this->msg( 'dberr-cachederror',
'This is a cached copy of the requested page, and may not be up to date. ' ) ) .
'</div>';
@@ -288,11 +304,11 @@ class DBQueryError extends DBError {
* @param $sql string
* @param $fname string
*/
- function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) {
- $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
+ function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) {
+ $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
parent::__construct( $db, $message );
$this->error = $error;
@@ -302,55 +318,107 @@ class DBQueryError extends DBError {
}
/**
- * @param $html string
+ * @return boolean
+ */
+ function isLoggable() {
+ // Don't send to the exception log, already in dberror log
+ return false;
+ }
+
+ /**
* @return string
*/
- function getContentMessage( $html ) {
- if ( $this->useMessageCache() ) {
- if ( $html ) {
- $msg = 'dberrortext';
- $sql = htmlspecialchars( $this->getSQL() );
- $fname = htmlspecialchars( $this->fname );
- $error = htmlspecialchars( $this->error );
- } else {
- $msg = 'dberrortextcl';
- $sql = $this->getSQL();
- $fname = $this->fname;
- $error = $this->error;
+ function getPageTitle() {
+ return $this->msg( 'databaseerror', 'Database error' );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getHTMLContent() {
+ $key = 'databaseerror-text';
+ $s = Html::element( 'p', array(), $this->msg( $key, $this->getFallbackMessage( $key ) ) );
+
+ $details = $this->getTechnicalDetails();
+ if ( $details ) {
+ $s .= '<ul>';
+ foreach ( $details as $key => $detail ) {
+ $s .= str_replace(
+ '$1', call_user_func_array( 'Html::element', $detail ),
+ Html::element( 'li', array(),
+ $this->msg( $key, $this->getFallbackMessage( $key ) )
+ )
+ );
}
- return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text();
- } else {
- return parent::getContentMessage( $html );
+ $s .= '</ul>';
}
+
+ return $s;
}
/**
- * @return String
+ * @return string
*/
- function getSQL() {
- global $wgShowSQLErrors;
+ protected function getTextContent() {
+ $key = 'databaseerror-textcl';
+ $s = $this->msg( $key, $this->getFallbackMessage( $key ) ) . "\n";
- if ( !$wgShowSQLErrors ) {
- return $this->msg( 'sqlhidden', 'SQL hidden' );
- } else {
- return $this->sql;
+ foreach ( $this->getTechnicalDetails() as $key => $detail ) {
+ $s .= $this->msg( $key, $this->getFallbackMessage( $key ), $detail[2] ) . "\n";
}
+
+ return $s;
}
/**
- * @return bool
+ * Make a list of technical details that can be shown to the user. This information can
+ * aid in debugging yet may be useful to an attacker trying to exploit a security weakness
+ * in the software or server configuration.
+ *
+ * Thus no such details are shown by default, though if $wgShowHostnames is true, only the
+ * full SQL query is hidden; in fact, the error message often does contain a hostname, and
+ * sites using this option probably don't care much about "security by obscurity". Of course,
+ * if $wgShowSQLErrors is true, the SQL query *is* shown.
+ *
+ * @return array: Keys are message keys; values are arrays of arguments for Html::element().
+ * Array will be empty if users are not allowed to see any of these details at all.
*/
- function getLogMessage() {
- # Don't send to the exception log
- return false;
+ protected function getTechnicalDetails() {
+ global $wgShowHostnames, $wgShowSQLErrors;
+
+ $attribs = array( 'dir' => 'ltr' );
+ $details = array();
+
+ if ( $wgShowSQLErrors ) {
+ $details['databaseerror-query'] = array(
+ 'div', array( 'class' => 'mw-code' ) + $attribs, $this->sql );
+ }
+
+ if ( $wgShowHostnames || $wgShowSQLErrors ) {
+ $errorMessage = $this->errno . ' ' . $this->error;
+ $details['databaseerror-function'] = array( 'code', $attribs, $this->fname );
+ $details['databaseerror-error'] = array( 'samp', $attribs, $errorMessage );
+ }
+
+ return $details;
}
/**
- * @return String
+ * @param string $key Message key
+ * @return string: English message text
*/
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
+ private function getFallbackMessage( $key ) {
+ $messages = array(
+ 'databaseerror-text' => 'A database query error has occurred.
+This may indicate a bug in the software.',
+ 'databaseerror-textcl' => 'A database query error has occurred.',
+ 'databaseerror-query' => 'Query: $1',
+ 'databaseerror-function' => 'Function: $1',
+ 'databaseerror-error' => 'Error: $1',
+ );
+ return $messages[$key];
}
+
}
/**
diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php
deleted file mode 100644
index f1f6dfca..00000000
--- a/includes/db/DatabaseIbm_db2.php
+++ /dev/null
@@ -1,1721 +0,0 @@
-<?php
-/**
- * This is the IBM DB2 database abstraction layer.
- * See maintenance/ibm_db2/README for development notes
- * and other specific information.
- *
- * 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 Database
- * @author leo.petr+mediawiki@gmail.com
- */
-
-/**
- * This represents a column in a DB2 database
- * @ingroup Database
- */
-class IBM_DB2Field implements Field {
- private $name = '';
- private $tablename = '';
- private $type = '';
- private $nullable = false;
- private $max_length = 0;
-
- /**
- * Builder method for the class
- * @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 = <<<SQL
-SELECT
-lcase( coltype ) AS typname,
-nulls AS attnotnull, length AS attlen
-FROM sysibm.syscolumns
-WHERE tbcreator=%s AND tbname=%s AND name=%s;
-SQL;
- $res = $db->query(
- sprintf( $q,
- $db->addQuotes( $wgDBmwschema ),
- $db->addQuotes( $table ),
- $db->addQuotes( $field )
- )
- );
- $row = $db->fetchObject( $res );
- if ( !$row ) {
- return null;
- }
- $n = new IBM_DB2Field;
- $n->type = $row->typname;
- $n->nullable = ( $row->attnotnull == 'N' );
- $n->name = $field;
- $n->tablename = $table;
- $n->max_length = $row->attlen;
- return $n;
- }
- /**
- * Get column name
- * @return string column name
- */
- function name() { return $this->name; }
- /**
- * Get table name
- * @return string table name
- */
- function tableName() { return $this->tablename; }
- /**
- * Get column type
- * @return string column type
- */
- function type() { return $this->type; }
- /**
- * Can column be null?
- * @return bool true or false
- */
- function isNullable() { return $this->nullable; }
- /**
- * How much can you fit in the column per row?
- * @return int length
- */
- function maxLength() { return $this->max_length; }
-}
-
-/**
- * Wrapper around binary large objects
- * @ingroup Database
- */
-class IBM_DB2Blob {
- private $mData;
-
- public function __construct( $data ) {
- $this->mData = $data;
- }
-
- public function getData() {
- return $this->mData;
- }
-
- public function __toString() {
- return $this->mData;
- }
-}
-
-/**
- * Wrapper to address lack of certain operations in the DB2 driver
- * ( seek, num_rows )
- * @ingroup Database
- * @since 1.19
- */
-class IBM_DB2Result{
- private $db;
- private $result;
- private $num_rows;
- private $current_pos;
- private $columns = array();
- private $sql;
-
- private $resultSet = array();
- private $loadedLines = 0;
-
- /**
- * Construct and initialize a wrapper for DB2 query results
- * @param $db DatabaseBase
- * @param $result Object
- * @param $num_rows Integer
- * @param $sql String
- * @param $columns Array
- */
- public function __construct( $db, $result, $num_rows, $sql, $columns ){
- $this->db = $db;
-
- if( $result instanceof ResultWrapper ){
- $this->result = $result->result;
- }
- else{
- $this->result = $result;
- }
-
- $this->num_rows = $num_rows;
- $this->current_pos = 0;
- if ( $this->num_rows > 0 ) {
- // Make a lower-case list of the column names
- // By default, DB2 column names are capitalized
- // while MySQL column names are lowercase
-
- // Is there a reasonable maximum value for $i?
- // Setting to 2048 to prevent an infinite loop
- for( $i = 0; $i < 2048; $i++ ) {
- $name = db2_field_name( $this->result, $i );
- if ( $name != false ) {
- continue;
- }
- else {
- return false;
- }
-
- $this->columns[$i] = strtolower( $name );
- }
- }
-
- $this->sql = $sql;
- }
-
- /**
- * Unwrap the DB2 query results
- * @return mixed Object on success, false on failure
- */
- public function getResult() {
- if ( $this->result ) {
- return $this->result;
- }
- else return false;
- }
-
- /**
- * Get the number of rows in the result set
- * @return integer
- */
- public function getNum_rows() {
- return $this->num_rows;
- }
-
- /**
- * Return a row from the result set in object format
- * @return mixed Object on success, false on failure.
- */
- public function fetchObject() {
- if ( $this->result
- && $this->num_rows > 0
- && $this->current_pos >= 0
- && $this->current_pos < $this->num_rows )
- {
- $row = $this->fetchRow();
- $ret = new stdClass();
-
- foreach ( $row as $k => $v ) {
- $lc = $this->columns[$k];
- $ret->$lc = $v;
- }
- return $ret;
- }
- return false;
- }
-
- /**
- * Return a row form the result set in array format
- * @return mixed Array on success, false on failure
- * @throws DBUnexpectedError
- */
- public function fetchRow(){
- if ( $this->result
- && $this->num_rows > 0
- && $this->current_pos >= 0
- && $this->current_pos < $this->num_rows )
- {
- if ( $this->loadedLines <= $this->current_pos ) {
- $row = db2_fetch_array( $this->result );
- $this->resultSet[$this->loadedLines++] = $row;
- if ( $this->db->lastErrno() ) {
- throw new DBUnexpectedError( $this->db, 'Error in fetchRow(): '
- . htmlspecialchars( $this->db->lastError() ) );
- }
- }
-
- if ( $this->loadedLines > $this->current_pos ){
- return $this->resultSet[$this->current_pos++];
- }
-
- }
- return false;
- }
-
- /**
- * Free a DB2 result object
- * @throws DBUnexpectedError
- */
- public function freeResult(){
- unset( $this->resultSet );
- if ( !@db2_free_result( $this->result ) ) {
- throw new DBUnexpectedError( $this, "Unable to free DB2 result\n" );
- }
- }
-}
-
-/**
- * Primary database interface
- * @ingroup Database
- */
-class DatabaseIbm_db2 extends DatabaseBase {
- /*
- * Inherited members
- protected $mLastQuery = '';
- protected $mPHPError = false;
-
- protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
- protected $mOpened = false;
-
- protected $mTablePrefix;
- protected $mFlags;
- protected $mTrxLevel = 0;
- protected $mErrorCount = 0;
- protected $mLBInfo = array();
- protected $mFakeSlaveLag = null, $mFakeMaster = false;
- *
- */
-
- /** Database server port */
- protected $mPort = null;
- /** Schema for tables, stored procedures, triggers */
- protected $mSchema = null;
- /** Whether the schema has been applied in this session */
- protected $mSchemaSet = false;
- /** Result of last query */
- protected $mLastResult = null;
- /** Number of rows affected by last INSERT/UPDATE/DELETE */
- protected $mAffectedRows = null;
- /** Number of rows returned by last SELECT */
- protected $mNumRows = null;
- /** Current row number on the cursor of the last SELECT */
- protected $currentRow = 0;
-
- /** Connection config options - see constructor */
- public $mConnOptions = array();
- /** Statement config options -- see constructor */
- public $mStmtOptions = array();
-
- /** Default schema */
- const USE_GLOBAL = 'get from global';
-
- /** Option that applies to nothing */
- const NONE_OPTION = 0x00;
- /** Option that applies to connection objects */
- const CONN_OPTION = 0x01;
- /** Option that applies to statement objects */
- const STMT_OPTION = 0x02;
-
- /** Regular operation mode -- minimal debug messages */
- const REGULAR_MODE = 'regular';
- /** Installation mode -- lots of debug messages */
- const INSTALL_MODE = 'install';
-
- /** Controls the level of debug message output */
- protected $mMode = self::REGULAR_MODE;
-
- /** Last sequence value used for a primary key */
- protected $mInsertId = null;
-
- ######################################
- # Getters and Setters
- ######################################
-
- /**
- * Returns true if this database supports (and uses) cascading deletes
- * @return bool
- */
- function cascadingDeletes() {
- return true;
- }
-
- /**
- * Returns true if this database supports (and uses) triggers (e.g. on the
- * page table)
- * @return bool
- */
- function cleanupTriggers() {
- return true;
- }
-
- /**
- * Returns true if this database is strict about what can be put into an
- * IP field.
- * Specifically, it uses a NULL value instead of an empty string.
- * @return bool
- */
- function strictIPs() {
- return true;
- }
-
- /**
- * Returns true if this database uses timestamps rather than integers
- * @return bool
- */
- function realTimestamps() {
- return true;
- }
-
- /**
- * Returns true if this database does an implicit sort when doing GROUP BY
- * @return bool
- */
- function implicitGroupby() {
- return false;
- }
-
- /**
- * Returns true if this database does an implicit order by when the column
- * has an index
- * For example: SELECT page_title FROM page LIMIT 1
- * @return bool
- */
- function implicitOrderby() {
- return false;
- }
-
- /**
- * 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';
- * @return bool
- */
- function searchableIPs() {
- return true;
- }
-
- /**
- * Returns true if this database can use functional indexes
- * @return bool
- */
- function functionalIndexes() {
- return true;
- }
-
- /**
- * Returns a unique string representing the wiki on the server
- * @return string
- */
- public function getWikiID() {
- if( $this->mSchema ) {
- return "{$this->mDBname}-{$this->mSchema}";
- } else {
- return $this->mDBname;
- }
- }
-
- /**
- * Returns the database software identifieir
- * @return string
- */
- public function getType() {
- return 'ibm_db2';
- }
-
- /**
- * Returns the database connection object
- * @return Object
- */
- public function getDb(){
- return $this->mConn;
- }
-
- /**
- *
- * @param $server String: hostname of database server
- * @param $user String: username
- * @param $password String: password
- * @param $dbName String: database name on the server
- * @param $flags Integer: database behaviour flags (optional, unused)
- * @param $schema String
- */
- public function __construct( $server = false, $user = false,
- $password = false,
- $dbName = false, $flags = 0,
- $schema = self::USE_GLOBAL )
- {
- global $wgDBmwschema;
-
- if ( $schema == self::USE_GLOBAL ) {
- $this->mSchema = $wgDBmwschema;
- } else {
- $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 );
- parent::__construct( $server, $user, $password, $dbName, DBO_TRX | $flags );
- }
-
- /**
- * 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\n" );
- if ( $this->mMode == self::INSTALL_MODE ) {
- print "<li><pre>$string</pre></li>";
- flush();
- }
- }
-
- /**
- * Opens a database connection and returns it
- * Closes any existing connection
- *
- * @param $server String: hostname
- * @param $user String
- * @param $password String
- * @param $dbName String: database name
- * @return DatabaseBase a fresh connection
- */
- public function open( $server, $user, $password, $dbName ) {
- wfProfileIn( __METHOD__ );
-
- # Load IBM DB2 driver if missing
- wfDl( 'ibm_db2' );
-
- # Test for IBM DB2 support, to avoid suppressed fatal error
- if ( !function_exists( 'db2_connect' ) ) {
- throw new DBConnectionError( $this, "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?" );
- }
-
- global $wgDBport;
-
- // Close existing connection
- $this->close();
- // Cache conn info
- $this->mServer = $server;
- $this->mPort = $port = $wgDBport;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $this->openUncataloged( $dbName, $user, $password, $server, $port );
-
- if ( !$this->mConn ) {
- $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" );
- wfProfileOut( __METHOD__ );
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
- wfDebug( $this->lastError() . "\n" );
- throw new DBConnectionError( $this, $this->lastError() );
- }
-
- // Some MediaWiki code is still transaction-less (?).
- // The strategy is to keep AutoCommit on for that code
- // but switch it off whenever a transaction is begun.
- db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON );
-
- $this->mOpened = true;
- $this->applySchema();
-
- wfProfileOut( __METHOD__ );
- return $this->mConn;
- }
-
- /**
- * Opens a cataloged database connection, sets mConn
- */
- protected function openCataloged( $dbName, $user, $password ) {
- wfSuppressWarnings();
- $this->mConn = db2_pconnect( $dbName, $user, $password );
- wfRestoreWarnings();
- }
-
- /**
- * Opens an uncataloged database connection, sets mConn
- */
- protected function openUncataloged( $dbName, $user, $password, $server, $port )
- {
- $dsn = "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=$dbName;CHARSET=UTF-8;HOSTNAME=$server;PORT=$port;PROTOCOL=TCPIP;UID=$user;PWD=$password;";
- wfSuppressWarnings();
- $this->mConn = db2_pconnect( $dsn, "", "", array() );
- wfRestoreWarnings();
- }
-
- /**
- * Closes a database connection, if it is open
- * Returns success, true if already closed
- * @return bool
- */
- protected function closeConnection() {
- return db2_close( $this->mConn );
- }
-
- /**
- * Retrieves the most current database error
- * Forces a database rollback
- * @return bool|string
- */
- public function lastError() {
- $connerr = db2_conn_errormsg();
- if ( $connerr ) {
- //$this->rollback( __METHOD__ );
- return $connerr;
- }
- $stmterr = db2_stmt_errormsg();
- if ( $stmterr ) {
- //$this->rollback( __METHOD__ );
- return $stmterr;
- }
-
- return false;
- }
-
- /**
- * Get the last error number
- * Return 0 if no error
- * @return integer
- */
- public function lastErrno() {
- $connerr = db2_conn_error();
- if ( $connerr ) {
- return $connerr;
- }
- $stmterr = db2_stmt_error();
- if ( $stmterr ) {
- return $stmterr;
- }
- return 0;
- }
-
- /**
- * Is a database connection open?
- * @return
- */
- public function isOpen() { return $this->mOpened; }
-
- /**
- * The DBMS-dependent part of query()
- * @param $sql String: SQL query.
- * @return object Result object for fetch functions or false on failure
- */
- protected function doQuery( $sql ) {
- $this->applySchema();
-
- // Needed to handle any UTF-8 encoding issues in the raw sql
- // Note that we fully support prepared statements for DB2
- // prepare() and execute() should be used instead of doQuery() whenever possible
- $sql = utf8_decode( $sql );
-
- $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions );
- if( $ret == false ) {
- $error = db2_stmt_errormsg();
-
- $this->installPrint( "<pre>$sql</pre>" );
- $this->installPrint( $error );
- throw new DBUnexpectedError( $this, 'SQL error: '
- . htmlspecialchars( $error ) );
- }
- $this->mLastResult = $ret;
- $this->mAffectedRows = null; // Not calculated until asked for
- return $ret;
- }
-
- /**
- * @return string Version information from the database
- */
- public function getServerVersion() {
- $info = db2_server_info( $this->mConn );
- return $info->DBMS_VER;
- }
-
- /**
- * Queries whether a given table exists
- * @return boolean
- */
- public function tableExists( $table, $fname = __METHOD__ ) {
- $schema = $this->mSchema;
-
- $sql = "SELECT COUNT( * ) FROM SYSIBM.SYSTABLES ST WHERE ST.NAME = '" .
- strtoupper( $table ) .
- "' AND ST.CREATOR = '" .
- strtoupper( $schema ) . "'";
- $res = $this->query( $sql );
- if ( !$res ) {
- return false;
- }
-
- // If the table exists, there should be one of it
- $row = $this->fetchRow( $res );
- $count = $row[0];
- if ( $count == '1' || $count == 1 ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Fetch the next row from the given result object, in object form.
- * Fields can be retrieved with $row->fieldname, with fields acting like
- * member variables.
- *
- * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc.
- * @return DB2 row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- public function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $row = db2_fetch_object( $res );
- wfRestoreWarnings();
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchObject(): '
- . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
- *
- * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc.
- * @return ResultWrapper row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- public function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- if ( db2_num_rows( $res ) > 0) {
- wfSuppressWarnings();
- $row = db2_fetch_array( $res );
- wfRestoreWarnings();
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): '
- . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
- return false;
- }
-
- /**
- * Escapes strings
- * Doesn't escape numbers
- *
- * @param $s String: string to escape
- * @return string escaped string
- */
- public function addQuotes( $s ) {
- //$this->installPrint( "DB2::addQuotes( $s )\n" );
- if ( is_null( $s ) ) {
- return 'NULL';
- } elseif ( $s instanceof Blob ) {
- return "'" . $s->fetch( $s ) . "'";
- } elseif ( $s instanceof IBM_DB2Blob ) {
- return "'" . $this->decodeBlob( $s ) . "'";
- }
- $s = $this->strencode( $s );
- if ( is_numeric( $s ) ) {
- return $s;
- } else {
- return "'$s'";
- }
- }
-
- /**
- * Verifies that a DB2 column/field type is numeric
- *
- * @param $type String: DB2 column type
- * @return Boolean: true if numeric
- */
- public function is_numeric_type( $type ) {
- switch ( strtoupper( $type ) ) {
- case 'SMALLINT':
- case 'INTEGER':
- case 'INT':
- case 'BIGINT':
- case 'DECIMAL':
- case 'REAL':
- case 'DOUBLE':
- case 'DECFLOAT':
- return true;
- }
- return false;
- }
-
- /**
- * Alias for addQuotes()
- * @param $s String: string to escape
- * @return string escaped string
- */
- public function strencode( $s ) {
- // Bloody useless function
- // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a.
- // But also necessary
- $s = db2_escape_string( $s );
- // Wide characters are evil -- some of them look like '
- $s = utf8_encode( $s );
- // Fix its stupidity
- $from = array( "\\\\", "\\'", '\\n', '\\t', '\\"', '\\r' );
- $to = array( "\\", "''", "\n", "\t", '"', "\r" );
- $s = str_replace( $from, $to, $s ); // DB2 expects '', not \' escaping
- return $s;
- }
-
- /**
- * Switch into the database schema
- */
- protected function applySchema() {
- if ( !( $this->mSchemaSet ) ) {
- $this->mSchemaSet = true;
- $this->begin( __METHOD__ );
- $this->doQuery( "SET SCHEMA = $this->mSchema" );
- $this->commit( __METHOD__ );
- }
- }
-
- /**
- * Start a transaction (mandatory)
- */
- protected function doBegin( $fname = 'DatabaseIbm_db2::begin' ) {
- // BEGIN is implicit for DB2
- // However, it requires that AutoCommit be off.
-
- // Some MediaWiki code is still transaction-less (?).
- // The strategy is to keep AutoCommit on for that code
- // but switch it off whenever a transaction is begun.
- db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_OFF );
-
- $this->mTrxLevel = 1;
- }
-
- /**
- * End a transaction
- * Must have a preceding begin()
- */
- protected function doCommit( $fname = 'DatabaseIbm_db2::commit' ) {
- db2_commit( $this->mConn );
-
- // Some MediaWiki code is still transaction-less (?).
- // The strategy is to keep AutoCommit on for that code
- // but switch it off whenever a transaction is begun.
- db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON );
-
- $this->mTrxLevel = 0;
- }
-
- /**
- * Cancel a transaction
- */
- protected function doRollback( $fname = 'DatabaseIbm_db2::rollback' ) {
- db2_rollback( $this->mConn );
- // turn auto-commit back on
- // not sure if this is appropriate
- db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON );
- $this->mTrxLevel = 0;
- }
-
- /**
- * Makes an encoded list of strings from an array
- * $mode:
- * LIST_COMMA - comma separated, no field names
- * LIST_AND - ANDed WHERE clause (without the WHERE)
- * LIST_OR - ORed WHERE clause (without the WHERE)
- * LIST_SET - comma separated with field names, like a SET clause
- * LIST_NAMES - comma separated field names
- * LIST_SET_PREPARED - like LIST_SET, except with ? tokens as values
- * @return string
- */
- function makeList( $a, $mode = LIST_COMMA ) {
- if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this,
- 'DatabaseIbm_db2::makeList called with incorrect parameters' );
- }
-
- // if this is for a prepared UPDATE statement
- // (this should be promoted to the parent class
- // once other databases use prepared statements)
- if ( $mode == LIST_SET_PREPARED ) {
- $first = true;
- $list = '';
- foreach ( $a as $field => $value ) {
- if ( !$first ) {
- $list .= ", $field = ?";
- } else {
- $list .= "$field = ?";
- $first = false;
- }
- }
- $list .= '';
-
- return $list;
- }
-
- // otherwise, call the usual function
- return parent::makeList( $a, $mode );
- }
-
- /**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
- *
- * @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)
- * @return string
- */
- 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 ) {
- if ( stripos( $sql, 'where' ) === false ) {
- return "$sql AND ( ROWNUM BETWEEN $offset AND $offset+$limit )";
- } else {
- return "$sql WHERE ( ROWNUM BETWEEN $offset AND $offset+$limit )";
- }
- }
- return "$sql FETCH FIRST $limit ROWS ONLY ";
- }
-
- /**
- * Handle reserved keyword replacement in table names
- *
- * @param $name Object
- * @param $format String Ignored parameter Default 'quoted'Boolean
- * @return String
- */
- public function tableName( $name, $format = 'quoted' ) {
- // we want maximum compatibility with MySQL schema
- return $name;
- }
-
- /**
- * Generates a timestamp in an insertable format
- *
- * @param $ts string timestamp
- * @return String: timestamp value
- */
- public function timestamp( $ts = 0 ) {
- // TS_MW cannot be easily distinguished from an integer
- return wfTimestamp( TS_DB2, $ts );
- }
-
- /**
- * Return the next in a sequence, save the value for retrieval via insertId()
- * @param $seqName String: name of a defined sequence in the database
- * @return int next value in that sequence
- */
- public function nextSequenceValue( $seqName ) {
- // Not using sequences in the primary schema to allow for easier migration
- // from MySQL
- // 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];
- return $this->mInsertId;
- */
- return null;
- }
-
- /**
- * This must be called after nextSequenceVal
- * @return int Last sequence value used as a primary key
- */
- public function insertId() {
- 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
- * @param $stmt Resource: prepared statement resource
- * of the SELECT primary_key FROM FINAL TABLE ( INSERT ... ) form
- */
- private function calcInsertId( $table, $primaryKey, $stmt ) {
- if ( $primaryKey ) {
- $this->mInsertId = db2_last_insert_id( $this->mConn );
- }
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $args may be a single associative array, or an array of arrays
- * with numeric keys, for multi-row insert
- *
- * @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() )
- {
- if ( !count( $args ) ) {
- return true;
- }
- // get database-specific table name (not used)
- $table = $this->tableName( $table );
- // format options as an array
- $options = IBM_DB2Helper::makeArray( $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' : '';
-
- // assume success
- $res = true;
- // If we are not in a transaction, we need to be for savepoint trickery
- if ( !$this->mTrxLevel ) {
- $this->begin( __METHOD__ );
- }
-
- $sql = "INSERT INTO $table ( " . implode( ',', $keys ) . ' ) VALUES ';
- if ( $key_count == 1 ) {
- $sql .= '( ? )';
- } else {
- $sql .= '( ?' . str_repeat( ',?', $key_count-1 ) . ' )';
- }
- $this->installPrint( "Preparing the following SQL:" );
- $this->installPrint( "$sql" );
- $this->installPrint( print_r( $args, true ));
- $stmt = $this->prepare( $sql );
-
- // start a transaction/enter transaction mode
- $this->begin( __METHOD__ );
-
- if ( !$ignore ) {
- //$first = true;
- foreach ( $args as $row ) {
- //$this->installPrint( "Inserting " . print_r( $row, true ));
- // insert each row into the database
- $res = $res & $this->execute( $stmt, $row );
- if ( !$res ) {
- $this->installPrint( 'Last error:' );
- $this->installPrint( $this->lastError() );
- }
- // get the last inserted value into a generated column
- $this->calcInsertId( $table, $primaryKey, $stmt );
- }
- } 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;
-
- foreach ( $args as $row ) {
- $overhead = "SAVEPOINT $ignore ON ROLLBACK RETAIN CURSORS";
- db2_exec( $this->mConn, $overhead, $this->mStmtOptions );
-
- $res2 = $this->execute( $stmt, $row );
-
- if ( !$res2 ) {
- $this->installPrint( 'Last error:' );
- $this->installPrint( $this->lastError() );
- }
- // 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 );
- } else {
- db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore",
- $this->mStmtOptions );
- $numrowsinserted++;
- }
- }
-
- $olde = error_reporting( $olde );
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
- }
- // commit either way
- $this->commit( __METHOD__ );
- $this->freePrepared( $stmt );
-
- 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( filtered array of columns, array of primary keys )
- */
- private function removeNullPrimaryKeys( $table, $args ) {
- $schema = $this->mSchema;
-
- // find out the primary keys
- $keyres = $this->doQuery( "SELECT NAME FROM SYSIBM.SYSCOLUMNS WHERE TBNAME = '"
- . strtoupper( $table )
- . "' AND TBCREATOR = '"
- . strtoupper( $schema )
- . "' AND KEYSEQ > 0" );
-
- $keys = array();
- for (
- $row = $this->fetchRow( $keyres );
- $row != null;
- $row = $this->fetchRow( $keyres )
- )
- {
- $keys[] = strtolower( $row[0] );
- }
- // remove primary keys
- foreach ( $args as $ai => $row ) {
- foreach ( $keys as $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 $table String: The table to UPDATE
- * @param $values array An array of values to SET
- * @param $conds array An array of conditions ( WHERE ). Use '*' to update all rows.
- * @param $fname String: The Class::Function calling this function
- * ( for the log )
- * @param $options array An array of UPDATE options, can be one or
- * more of IGNORE, LOW_PRIORITY
- * @return Boolean
- */
- public function update( $table, $values, $conds, $fname = 'DatabaseIbm_db2::update',
- $options = array() )
- {
- $table = $this->tableName( $table );
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET "
- . $this->makeList( $values, LIST_SET_PREPARED );
- if ( $conds != '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
- }
- $stmt = $this->prepare( $sql );
- $this->installPrint( 'UPDATE: ' . print_r( $values, true ) );
- // assuming for now that an array with string keys will work
- // if not, convert to simple array first
- $result = $this->execute( $stmt, $values );
- $this->freePrepared( $stmt );
-
- return $result;
- }
-
- /**
- * DELETE query wrapper
- *
- * Use $conds == "*" to delete all rows
- * @return bool|\ResultWrapper
- */
- public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this,
- 'DatabaseIbm_db2::delete() called with no conditions' );
- }
- $table = $this->tableName( $table );
- $sql = "DELETE FROM $table";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- $result = $this->query( $sql, $fname );
-
- return $result;
- }
-
- /**
- * Returns the number of rows affected by the last query or 0
- * @return Integer: the number of rows affected by the last query
- */
- public function affectedRows() {
- if ( !is_null( $this->mAffectedRows ) ) {
- // Forced result for simulated queries
- return $this->mAffectedRows;
- }
- if( empty( $this->mLastResult ) ) {
- return 0;
- }
- return db2_num_rows( $this->mLastResult );
- }
-
- /**
- * Returns the number of rows in the result set
- * Has to be called right after the corresponding select query
- * @param $res Object result set
- * @return Integer: number of rows
- */
- public function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- if ( $this->mNumRows ) {
- return $this->mNumRows;
- } else {
- return 0;
- }
- }
-
- /**
- * Moves the row pointer of the result set
- * @param $res Object: result set
- * @param $row Integer: row number
- * @return bool success or failure
- */
- public function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- return $res = $res->result;
- }
- if ( $res instanceof IBM_DB2Result ) {
- return $res->dataSeek( $row );
- }
- wfDebug( "dataSeek operation in DB2 database\n" );
- return false;
- }
-
- ###
- # Fix notices in Block.php
- ###
-
- /**
- * Frees memory associated with a statement resource
- * @param $res Object: statement resource to free
- * @return Boolean success or failure
- */
- public function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $ok = db2_free_result( $res );
- wfRestoreWarnings();
- if ( !$ok ) {
- throw new DBUnexpectedError( $this, "Unable to free DB2 result\n" );
- }
- }
-
- /**
- * Returns the number of columns in a resource
- * @param $res Object: statement resource
- * @return Number of fields/columns in resource
- */
- public function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- if ( $res instanceof IBM_DB2Result ) {
- $res = $res->getResult();
- }
- return db2_num_fields( $res );
- }
-
- /**
- * Returns the nth column name
- * @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 ) {
- $res = $res->result;
- }
- if ( $res instanceof IBM_DB2Result ) {
- $res = $res->getResult();
- }
- return db2_field_name( $res, $n );
- }
-
- /**
- * SELECT wrapper
- *
- * @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 array Associative array of options
- * (e.g. array( 'GROUP BY' => 'page_title' )),
- * see Database::makeSelectOptions code for list of
- * supported stuff
- * @param $join_conds array Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN',
- * 'page_latest=rev_id') )
- * @return Mixed: database result resource for fetch functions or false
- * on failure
- */
- public function select( $table, $vars, $conds = '', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() )
- {
- $res = parent::select( $table, $vars, $conds, $fname, $options,
- $join_conds );
- $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
-
- // We must adjust for offset
- if ( isset( $options['LIMIT'] ) && isset ( $options['OFFSET'] ) ) {
- $limit = $options['LIMIT'];
- $offset = $options['OFFSET'];
- }
-
- // DB2 does not have a proper num_rows() function yet, so we must emulate
- // DB2 9.5.4 and the corresponding ibm_db2 driver will introduce
- // a working one
- // TODO: Yay!
-
- // we want the count
- $vars2 = array( 'count( * ) as num_rows' );
- // respecting just the limit option
- $options2 = array();
- if ( isset( $options['LIMIT'] ) ) {
- $options2['LIMIT'] = $options['LIMIT'];
- }
- // but don't try to emulate for GROUP BY
- if ( isset( $options['GROUP BY'] ) ) {
- return $res;
- }
-
- $res2 = parent::select( $table, $vars2, $conds, $fname, $options2,
- $join_conds );
-
- $obj = $this->fetchObject( $res2 );
- $this->mNumRows = $obj->num_rows;
-
- return new ResultWrapper( $this, new IBM_DB2Result( $this, $res, $obj->num_rows, $vars, $sql ) );
- }
-
- /**
- * Handles ordering, grouping, and having options ('GROUP BY' => colname)
- * Has limited support for per-column options (colnum => 'DISTINCT')
- *
- * @private
- *
- * @param $options array Associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return Array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- if ( isset( $options['GROUP BY'] ) ) {
- $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
- }
- if ( isset( $options['HAVING'] ) ) {
- $preLimitTail .= " HAVING {$options['HAVING']}";
- }
- if ( isset( $options['ORDER BY'] ) ) {
- $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
- }
-
- if ( isset( $noKeyOptions['DISTINCT'] )
- || isset( $noKeyOptions['DISTINCTROW'] ) )
- {
- $startOpts .= 'DISTINCT';
- }
-
- return array( $startOpts, '', $preLimitTail, $postLimitTail );
- }
-
- /**
- * Returns link to IBM DB2 free download
- * @return String: wikitext of a link to the server software's web site
- */
- public static function getSoftwareLink() {
- return '[http://www.ibm.com/db2/express/ IBM DB2]';
- }
-
- /**
- * Get search engine class. All subclasses of this
- * need to implement this if they wish to use searching.
- *
- * @return String
- */
- public function getSearchEngine() {
- return 'SearchIBM_DB2';
- }
-
- /**
- * Did the last database access fail because of deadlock?
- * @return Boolean
- */
- public function wasDeadlock() {
- // get SQLSTATE
- $err = $this->lastErrno();
- switch( $err ) {
- // This is literal port of the MySQL logic and may be wrong for DB2
- case '40001': // sql0911n, Deadlock or timeout, rollback
- case '57011': // sql0904n, Resource unavailable, no rollback
- case '57033': // sql0913n, Deadlock or timeout, no rollback
- $this->installPrint( "In a deadlock because of SQLSTATE $err" );
- return true;
- }
- return false;
- }
-
- /**
- * Ping the server and try to reconnect if it there is no connection
- * The connection may be closed and reopened while this happens
- * @return Boolean: whether the connection exists
- */
- public function ping() {
- // db2_ping() doesn't exist
- // Emulate
- $this->close();
- $this->openUncataloged( $this->mDBName, $this->mUser,
- $this->mPassword, $this->mServer, $this->mPort );
-
- return false;
- }
- ######################################
- # Unimplemented and not applicable
- ######################################
-
- /**
- * Only useful with fake prepare like in base Database class
- * @return string
- */
- public function fillPreparedArg( $matches ) {
- $this->installPrint( 'Not useful for DB2: fillPreparedArg()' );
- return '';
- }
-
- ######################################
- # Reflection
- ######################################
-
- /**
- * Returns information about an index
- * If errors are explicitly ignored, returns NULL on failure
- * @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 );
- $sql = <<<SQL
-SELECT name as indexname
-FROM sysibm.sysindexes si
-WHERE si.name='$index' AND si.tbname='$table'
-AND sc.tbcreator='$this->mSchema'
-SQL;
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
- $row = $this->fetchObject( $res );
- if ( $row != null ) {
- return $row;
- } else {
- return false;
- }
- }
-
- /**
- * Returns an information object on a table column
- * @param $table String: table name
- * @param $field String: column name
- * @return IBM_DB2Field
- */
- public function fieldInfo( $table, $field ) {
- return IBM_DB2Field::fromText( $this, $table, $field );
- }
-
- /**
- * db2_field_type() wrapper
- * @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 ) {
- $res = $res->result;
- }
- if ( $res instanceof IBM_DB2Result ) {
- $res = $res->getResult();
- }
- return db2_field_type( $res, $index );
- }
-
- /**
- * Verifies that an index was created as unique
- * @param $table String: table name
- * @param $index String: index name
- * @param $fname string function name for profiling
- * @return Bool
- */
- public function indexUnique ( $table, $index,
- $fname = 'DatabaseIbm_db2::indexUnique' )
- {
- $table = $this->tableName( $table );
- $sql = <<<SQL
-SELECT si.name as indexname
-FROM sysibm.sysindexes si
-WHERE si.name='$index' AND si.tbname='$table'
-AND sc.tbcreator='$this->mSchema'
-AND si.uniquerule IN ( 'U', 'P' )
-SQL;
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
- if ( $this->fetchObject( $res ) ) {
- return true;
- }
- return false;
-
- }
-
- /**
- * Returns the size of a text field, 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 );
- $sql = <<<SQL
-SELECT length as size
-FROM sysibm.syscolumns sc
-WHERE sc.name='$field' AND sc.tbname='$table'
-AND sc.tbcreator='$this->mSchema'
-SQL;
- $res = $this->query( $sql );
- $row = $this->fetchObject( $res );
- $size = $row->size;
- return $size;
- }
-
- /**
- * Description is left as an exercise for the reader
- * @param $b Mixed: data to be encoded
- * @return IBM_DB2Blob
- */
- public function encodeBlob( $b ) {
- return new IBM_DB2Blob( $b );
- }
-
- /**
- * Description is left as an exercise for the reader
- * @param $b IBM_DB2Blob: data to be decoded
- * @return mixed
- */
- public function decodeBlob( $b ) {
- return "$b";
- }
-
- /**
- * Convert into a list of string being concatenated
- * @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
- // Sample query: VALUES 'foo' CONCAT 'bar' CONCAT 'baz'
- return implode( ' || ', $stringList );
- }
-
- /**
- * Generates the SQL required to convert a DB2 timestamp into a Unix epoch
- * @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 );
- if ( !$res ) {
- $this->installPrint( db2_stmt_errormsg() );
- }
- return $res;
- }
-
- /**
- * 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
- * @return string
- */
- 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( $field )";
- }
-
- /**
- * 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 )";
- }
-}
-
-class IBM_DB2Helper {
- public static function makeArray( $maybeArray ) {
- if ( !is_array( $maybeArray ) ) {
- return array( $maybeArray );
- }
-
- return $maybeArray;
- }
-}
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
index 914ab408..37f5372e 100644
--- a/includes/db/DatabaseMssql.php
+++ b/includes/db/DatabaseMssql.php
@@ -28,39 +28,51 @@
* @ingroup Database
*/
class DatabaseMssql extends DatabaseBase {
- var $mInsertId = NULL;
- var $mLastResult = NULL;
- var $mAffectedRows = NULL;
+ var $mInsertId = null;
+ var $mLastResult = null;
+ var $mAffectedRows = null;
var $mPort;
function cascadingDeletes() {
return true;
}
+
function cleanupTriggers() {
return true;
}
+
function strictIPs() {
return true;
}
+
function realTimestamps() {
return true;
}
+
function implicitGroupby() {
return false;
}
+
function implicitOrderby() {
return false;
}
+
function functionalIndexes() {
return true;
}
+
function unionSupportsOrderAndLimit() {
return false;
}
/**
* Usually aborts on failure
+ * @param string $server
+ * @param string $user
+ * @param string $password
+ * @param string $dbName
+ * @throws DBConnectionError
* @return bool|DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
@@ -84,7 +96,7 @@ class DatabaseMssql extends DatabaseBase {
$connectionInfo = array();
- if( $dbName ) {
+ if ( $dbName ) {
$connectionInfo['Database'] = $dbName;
}
@@ -97,7 +109,7 @@ class DatabaseMssql extends DatabaseBase {
$ntAuthPassTest = strtolower( $password );
// Decide which auth scenerio to use
- if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ){
+ if ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) {
// Don't add credentials to $connectionInfo
} else {
$connectionInfo['UID'] = $user;
@@ -139,11 +151,11 @@ class DatabaseMssql extends DatabaseBase {
// $this->limitResult();
if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
// massage LIMIT -> TopN
- $sql = $this->LimitToTopN( $sql ) ;
+ $sql = $this->LimitToTopN( $sql );
}
// MSSQL doesn't have EXTRACT(epoch FROM XXX)
- if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
+ if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
// This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
$sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
}
@@ -151,7 +163,7 @@ class DatabaseMssql extends DatabaseBase {
// perform query
$stmt = sqlsrv_query( $this->mConn, $sql );
if ( $stmt == false ) {
- $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
+ $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
"Query: " . htmlentities( $sql ) . "\n" .
"Function: " . __METHOD__ . "\n";
// process each error (our driver will give us an array of errors unlike other providers)
@@ -197,9 +209,9 @@ class DatabaseMssql extends DatabaseBase {
$retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
if ( $retErrors != null ) {
foreach ( $retErrors as $arrError ) {
- $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
- $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
- $strRet .= "Message: " . $arrError[ 'message'] . "\n";
+ $strRet .= "SQLState: " . $arrError['SQLSTATE'] . "\n";
+ $strRet .= "Error Code: " . $arrError['code'] . "\n";
+ $strRet .= "Message: " . $arrError['message'] . "\n";
}
} else {
$strRet = "No errors found";
@@ -279,13 +291,13 @@ class DatabaseMssql extends DatabaseBase {
* @param $vars Mixed: array or string, field name(s) to be retrieved
* @param $conds Mixed: array or string, condition(s) for WHERE
* @param $fname String: calling function name (use __METHOD__) for logs/profiling
- * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * @param array $options associative array of options (e.g. array('GROUP BY' => 'page_title')),
* see Database::makeSelectOptions code for list of supported stuff
* @param $join_conds Array: Associative array of table join conditions (optional)
* (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
- function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
+ function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() )
{
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
if ( isset( $options['EXPLAIN'] ) ) {
@@ -304,17 +316,17 @@ class DatabaseMssql extends DatabaseBase {
* @param $vars Mixed: Array or string, field name(s) to be retrieved
* @param $conds Mixed: Array or string, condition(s) for WHERE
* @param $fname String: Calling function name (use __METHOD__) for logs/profiling
- * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
* see Database::makeSelectOptions code for list of supported stuff
* @param $join_conds Array: Associative array of table join conditions (optional)
* (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return string, the SQL text
*/
- function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
+ function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) {
if ( isset( $options['EXPLAIN'] ) ) {
unset( $options['EXPLAIN'] );
}
- return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+ return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
}
/**
@@ -325,14 +337,16 @@ class DatabaseMssql extends DatabaseBase {
* Takes same arguments as Database::select()
* @return int
*/
- function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
+ function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) {
$options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
$res = $this->select( $table, $vars, $conds, $fname, $options );
$rows = -1;
if ( $res ) {
$row = $this->fetchRow( $res );
- if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
+ if ( isset( $row['EstimateRows'] ) ) {
+ $rows = $row['EstimateRows'];
+ }
}
return $rows;
}
@@ -342,13 +356,13 @@ class DatabaseMssql extends DatabaseBase {
* If errors are explicitly ignored, returns NULL on failure
* @return array|bool|null
*/
- function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
# This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
# returned value except to check for the existance of indexes.
$sql = "sp_helpindex '" . $table . "'";
$res = $this->query( $sql, $fname );
if ( !$res ) {
- return NULL;
+ return null;
}
$result = array();
@@ -380,9 +394,14 @@ class DatabaseMssql extends DatabaseBase {
*
* Usually aborts on failure
* If errors are explicitly ignored, returns success
+ * @param string $table
+ * @param array $arrToInsert
+ * @param string $fname
+ * @param array $options
+ * @throws DBQueryError
* @return bool
*/
- function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
+ function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $arrToInsert ) ) {
return true;
@@ -404,7 +423,7 @@ class DatabaseMssql extends DatabaseBase {
$identity = null;
$tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
$res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
- if( $res && $res->numrows() ){
+ if ( $res && $res->numrows() ) {
// There is an identity for this table.
$identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
}
@@ -417,13 +436,13 @@ class DatabaseMssql extends DatabaseBase {
$identityClause = '';
// if we have an identity column
- if( $identity ) {
+ if ( $identity ) {
// iterate through
- foreach ($a as $k => $v ) {
+ foreach ( $a as $k => $v ) {
if ( $k == $identity ) {
- if( !is_null($v) ){
+ if ( !is_null( $v ) ) {
// there is a value being passed to us, we need to turn on and off inserted identity
- $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
+ $sqlPre = "SET IDENTITY_INSERT $table ON;";
$sqlPost = ";SET IDENTITY_INSERT $table OFF;";
} else {
@@ -474,7 +493,7 @@ class DatabaseMssql extends DatabaseBase {
} elseif ( is_array( $value ) || is_object( $value ) ) {
if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
$sql .= $this->addQuotes( $value );
- } else {
+ } else {
$sql .= $this->addQuotes( serialize( $value ) );
}
} else {
@@ -488,10 +507,10 @@ class DatabaseMssql extends DatabaseBase {
if ( $ret === false ) {
throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
- } elseif ( $ret != NULL ) {
+ } elseif ( $ret != null ) {
// remember number of rows affected
$this->mAffectedRows = sqlsrv_rows_affected( $ret );
- if ( !is_null($identity) ) {
+ if ( !is_null( $identity ) ) {
// then we want to get the identity column value we were assigned and save it off
$row = sqlsrv_fetch_object( $ret );
$this->mInsertId = $row->$identity;
@@ -510,20 +529,28 @@ class DatabaseMssql extends DatabaseBase {
* Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
* $conds may be "*" to copy the whole table
* srcTable may be an array of tables.
- * @return null|\ResultWrapper
+ * @param string $destTable
+ * @param array|string $srcTable
+ * @param array $varMap
+ * @param array $conds
+ * @param string $fname
+ * @param array $insertOptions
+ * @param array $selectOptions
+ * @throws DBQueryError
+ * @return null|ResultWrapper
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() ) {
$ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
if ( $ret === false ) {
throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname );
- } elseif ( $ret != NULL ) {
+ } elseif ( $ret != null ) {
// remember number of rows affected
$this->mAffectedRows = sqlsrv_rows_affected( $ret );
return $ret;
}
- return NULL;
+ return null;
}
/**
@@ -590,9 +617,9 @@ class DatabaseMssql extends DatabaseBase {
} else {
$sql = '
SELECT * FROM (
- SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
- SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
- ) as sub2
+ SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
+ SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
+ ) as sub2
) AS sub3
WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
return $sql;
@@ -627,8 +654,8 @@ class DatabaseMssql extends DatabaseBase {
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
- return "[http://www.microsoft.com/sql/ MS SQL Server]";
+ public function getSoftwareLink() {
+ return "[{{int:version-db-mssql-url}} MS SQL Server]";
}
/**
@@ -643,11 +670,11 @@ class DatabaseMssql extends DatabaseBase {
return $version;
}
- function tableExists ( $table, $fname = __METHOD__, $schema = false ) {
+ function tableExists( $table, $fname = __METHOD__, $schema = false ) {
$res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
WHERE table_type='BASE TABLE' AND table_name = '$table'" );
if ( $res === false ) {
- print( "Error in tableExists query: " . $this->getErrors() );
+ print "Error in tableExists query: " . $this->getErrors();
return false;
}
if ( sqlsrv_fetch( $res ) ) {
@@ -661,12 +688,12 @@ class DatabaseMssql extends DatabaseBase {
* Query whether a given column exists in the mediawiki schema
* @return bool
*/
- function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
+ function fieldExists( $table, $field, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
$res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
if ( $res === false ) {
- print( "Error in fieldExists query: " . $this->getErrors() );
+ print "Error in fieldExists query: " . $this->getErrors();
return false;
}
if ( sqlsrv_fetch( $res ) ) {
@@ -681,7 +708,7 @@ class DatabaseMssql extends DatabaseBase {
$res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
if ( $res === false ) {
- print( "Error in fieldInfo query: " . $this->getErrors() );
+ print "Error in fieldInfo query: " . $this->getErrors();
return false;
}
$meta = $this->fetchRow( $res );
@@ -694,7 +721,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Begin a transaction, committing any previously open transaction
*/
- protected function doBegin( $fname = 'DatabaseMssql::begin' ) {
+ protected function doBegin( $fname = __METHOD__ ) {
sqlsrv_begin_transaction( $this->mConn );
$this->mTrxLevel = 1;
}
@@ -702,7 +729,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* End a transaction
*/
- protected function doCommit( $fname = 'DatabaseMssql::commit' ) {
+ protected function doCommit( $fname = __METHOD__ ) {
sqlsrv_commit( $this->mConn );
$this->mTrxLevel = 0;
}
@@ -711,7 +738,7 @@ class DatabaseMssql extends DatabaseBase {
* Rollback a transaction.
* No-op on non-transactional databases.
*/
- protected function doRollback( $fname = 'DatabaseMssql::rollback' ) {
+ protected function doRollback( $fname = __METHOD__ ) {
sqlsrv_rollback( $this->mConn );
$this->mTrxLevel = 0;
}
@@ -720,6 +747,8 @@ class DatabaseMssql extends DatabaseBase {
* Escapes a identifier for use inm SQL.
* Throws an exception if it is invalid.
* Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
+ * @param $identifier
+ * @throws MWException
* @return string
*/
private function escapeIdentifier( $identifier ) {
@@ -750,17 +779,17 @@ class DatabaseMssql extends DatabaseBase {
$newUser = $this->escapeIdentifier( $newUser );
$loginPassword = $this->addQuotes( $loginPassword );
- $this->doQuery("CREATE DATABASE $dbName;");
- $this->doQuery("USE $dbName;");
- $this->doQuery("CREATE SCHEMA $dbName;");
- $this->doQuery("
+ $this->doQuery( "CREATE DATABASE $dbName;" );
+ $this->doQuery( "USE $dbName;" );
+ $this->doQuery( "CREATE SCHEMA $dbName;" );
+ $this->doQuery( "
CREATE
LOGIN $newUser
WITH
PASSWORD=$loginPassword
;
- ");
- $this->doQuery("
+ " );
+ $this->doQuery( "
CREATE
USER $newUser
FOR
@@ -768,8 +797,8 @@ class DatabaseMssql extends DatabaseBase {
WITH
DEFAULT_SCHEMA=$dbName
;
- ");
- $this->doQuery("
+ " );
+ $this->doQuery( "
GRANT
BACKUP DATABASE,
BACKUP LOG,
@@ -784,17 +813,15 @@ class DatabaseMssql extends DatabaseBase {
DATABASE::$dbName
TO $newUser
;
- ");
- $this->doQuery("
+ " );
+ $this->doQuery( "
GRANT
CONTROL
ON
SCHEMA::$dbName
TO $newUser
;
- ");
-
-
+ " );
}
function encodeBlob( $b ) {
@@ -873,7 +900,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* @private
*
- * @param $options Array: an associative array of options to be turned into
+ * @param array $options an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return Array
*/
@@ -888,29 +915,23 @@ class DatabaseMssql extends DatabaseBase {
}
}
- if ( isset( $options['GROUP BY'] ) ) {
- $tailOpts .= " GROUP BY {$options['GROUP BY']}";
- }
- if ( isset( $options['HAVING'] ) ) {
- $tailOpts .= " HAVING {$options['GROUP BY']}";
- }
- if ( isset( $options['ORDER BY'] ) ) {
- $tailOpts .= " ORDER BY {$options['ORDER BY']}";
- }
+ $tailOpts .= $this->makeGroupByWithHaving( $options );
+
+ $tailOpts .= $this->makeOrderBy( $options );
if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) {
$startOpts .= 'DISTINCT';
}
// we want this to be compatible with the output of parent::makeSelectOptions()
- return array( $startOpts, '' , $tailOpts, '' );
+ return array( $startOpts, '', $tailOpts, '' );
}
/**
* Get the type of the DBMS, as it appears in $wgDBtype.
* @return string
*/
- function getType(){
+ function getType() {
return 'mssql';
}
@@ -940,7 +961,7 @@ class DatabaseMssql extends DatabaseBase {
*/
class MssqlField implements Field {
private $name, $tablename, $default, $max_length, $nullable, $type;
- function __construct ( $info ) {
+ function __construct( $info ) {
$this->name = $info['COLUMN_NAME'];
$this->tablename = $info['TABLE_NAME'];
$this->default = $info['COLUMN_DEFAULT'];
@@ -990,7 +1011,7 @@ class MssqlResult {
$rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
if ( $row !== null ) {
foreach ( $row as $k => $v ) {
if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
@@ -1028,7 +1049,7 @@ class MssqlResult {
$arrNum[] = $value;
}
}
- switch( $mode ) {
+ switch ( $mode ) {
case SQLSRV_FETCH_ASSOC:
$ret = $this->mRows[$this->mCursor];
break;
@@ -1081,43 +1102,101 @@ class MssqlResult {
$i++;
}
// http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
- switch( $intType ) {
- case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break;
- case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break;
- case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break;
- case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break;
- case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break;
- case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
- case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break;
- case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break;
- case SQLSRV_SQLTYPE_INT: $strType = 'int'; break;
- case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break;
- case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
- case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
- case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
- // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
- case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break;
- case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break;
- case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break;
- case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break;
- case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break;
- case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break;
- case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break;
- case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break;
- case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
- case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break;
- case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
- // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
- case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
- // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
- case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break;
- default: $strType = $intType;
+ switch ( $intType ) {
+ case SQLSRV_SQLTYPE_BIGINT:
+ $strType = 'bigint';
+ break;
+ case SQLSRV_SQLTYPE_BINARY:
+ $strType = 'binary';
+ break;
+ case SQLSRV_SQLTYPE_BIT:
+ $strType = 'bit';
+ break;
+ case SQLSRV_SQLTYPE_CHAR:
+ $strType = 'char';
+ break;
+ case SQLSRV_SQLTYPE_DATETIME:
+ $strType = 'datetime';
+ break;
+ case SQLSRV_SQLTYPE_DECIMAL: // ($precision, $scale)
+ $strType = 'decimal';
+ break;
+ case SQLSRV_SQLTYPE_FLOAT:
+ $strType = 'float';
+ break;
+ case SQLSRV_SQLTYPE_IMAGE:
+ $strType = 'image';
+ break;
+ case SQLSRV_SQLTYPE_INT:
+ $strType = 'int';
+ break;
+ case SQLSRV_SQLTYPE_MONEY:
+ $strType = 'money';
+ break;
+ case SQLSRV_SQLTYPE_NCHAR: // ($charCount):
+ $strType = 'nchar';
+ break;
+ case SQLSRV_SQLTYPE_NUMERIC: // ($precision, $scale):
+ $strType = 'numeric';
+ break;
+ case SQLSRV_SQLTYPE_NVARCHAR: // ($charCount)
+ $strType = 'nvarchar';
+ break;
+ // case SQLSRV_SQLTYPE_NVARCHAR('max'):
+ // $strType = 'nvarchar(MAX)';
+ // break;
+ case SQLSRV_SQLTYPE_NTEXT:
+ $strType = 'ntext';
+ break;
+ case SQLSRV_SQLTYPE_REAL:
+ $strType = 'real';
+ break;
+ case SQLSRV_SQLTYPE_SMALLDATETIME:
+ $strType = 'smalldatetime';
+ break;
+ case SQLSRV_SQLTYPE_SMALLINT:
+ $strType = 'smallint';
+ break;
+ case SQLSRV_SQLTYPE_SMALLMONEY:
+ $strType = 'smallmoney';
+ break;
+ case SQLSRV_SQLTYPE_TEXT:
+ $strType = 'text';
+ break;
+ case SQLSRV_SQLTYPE_TIMESTAMP:
+ $strType = 'timestamp';
+ break;
+ case SQLSRV_SQLTYPE_TINYINT:
+ $strType = 'tinyint';
+ break;
+ case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER:
+ $strType = 'uniqueidentifier';
+ break;
+ case SQLSRV_SQLTYPE_UDT:
+ $strType = 'UDT';
+ break;
+ case SQLSRV_SQLTYPE_VARBINARY: // ($byteCount)
+ $strType = 'varbinary';
+ break;
+ // case SQLSRV_SQLTYPE_VARBINARY('max'):
+ // $strType = 'varbinary(MAX)';
+ // break;
+ case SQLSRV_SQLTYPE_VARCHAR: // ($charCount)
+ $strType = 'varchar';
+ break;
+ // case SQLSRV_SQLTYPE_VARCHAR('max'):
+ // $strType = 'varchar(MAX)';
+ // break;
+ case SQLSRV_SQLTYPE_XML:
+ $strType = 'xml';
+ break;
+ default:
+ $strType = $intType;
}
return $strType;
}
public function free() {
unset( $this->mRows );
- return;
}
}
diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php
index 7f389da9..956bb694 100644
--- a/includes/db/DatabaseMysql.php
+++ b/includes/db/DatabaseMysql.php
@@ -22,27 +22,19 @@
*/
/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
+ * Database abstraction object for PHP extension mysql.
*
* @ingroup Database
* @see Database
*/
-class DatabaseMysql extends DatabaseBase {
-
- /**
- * @return string
- */
- function getType() {
- return 'mysql';
- }
+class DatabaseMysql extends DatabaseMysqlBase {
/**
* @param $sql string
* @return resource
*/
protected function doQuery( $sql ) {
- if( $this->bufferResults() ) {
+ if ( $this->bufferResults() ) {
$ret = mysql_query( $sql, $this->mConn );
} else {
$ret = mysql_unbuffered_query( $sql, $this->mConn );
@@ -50,39 +42,13 @@ class DatabaseMysql extends DatabaseBase {
return $ret;
}
- /**
- * @param $server string
- * @param $user string
- * @param $password string
- * @param $dbName string
- * @return bool
- * @throws DBConnectionError
- */
- function open( $server, $user, $password, $dbName ) {
- global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode;
- wfProfileIn( __METHOD__ );
-
- # Load mysql.so if we don't have it
- wfDl( 'mysql' );
-
+ protected function mysqlConnect( $realServer ) {
# Fail now
# Otherwise we get a suppressed fatal error, which is very hard to track down
- if ( !function_exists( 'mysql_connect' ) ) {
+ if ( !extension_loaded( 'mysql' ) ) {
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;
-
$connFlags = 0;
if ( $this->mFlags & DBO_SSL ) {
$connFlags |= MYSQL_CLIENT_SSL;
@@ -91,81 +57,27 @@ class DatabaseMysql extends DatabaseBase {
$connFlags |= MYSQL_CLIENT_COMPRESS;
}
- 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++ ) {
+
+ $conn = false;
+
+ for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) {
if ( $i > 1 ) {
usleep( 1000 );
}
if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = mysql_pconnect( $realServer, $user, $password, $connFlags );
+ $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
} else {
# Create a new connection...
- $this->mConn = mysql_connect( $realServer, $user, $password, true, $connFlags );
- }
- #if ( $this->mConn === false ) {
- #$iplus = $i + 1;
- #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
- #}
- }
- $error = $this->restoreErrorHandler();
-
- wfProfileOut( "dbconnect-$server" );
-
- # Always log connection errors
- if ( !$this->mConn ) {
- if ( !$error ) {
- $error = $this->lastError();
- }
- wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
- wfDebug( "DB connection error\n" .
- "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
-
- wfProfileOut( __METHOD__ );
- $this->reportConnectionError( $error );
- }
-
- if ( $dbName != '' ) {
- wfSuppressWarnings();
- $success = mysql_select_db( $dbName, $this->mConn );
- wfRestoreWarnings();
- if ( !$success ) {
- wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" );
- wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
- "from client host " . wfHostname() . "\n" );
-
- wfProfileOut( __METHOD__ );
- $this->reportConnectionError( "Error selecting database $dbName" );
+ $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags );
}
}
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- if( $wgDBmysql5 ) {
- $this->query( 'SET NAMES utf8', __METHOD__ );
- } else {
- $this->query( 'SET NAMES binary', __METHOD__ );
- }
- // Set SQL mode, default is turning them all off, can be overridden or skipped with null
- if ( is_string( $wgSQLMode ) ) {
- $mode = $this->addQuotes( $wgSQLMode );
- $this->query( "SET sql_mode = $mode", __METHOD__ );
- }
-
- $this->mOpened = true;
- wfProfileOut( __METHOD__ );
- return true;
+ return $conn;
}
/**
@@ -176,111 +88,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $res ResultWrapper
- * @throws DBUnexpectedError
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $ok = mysql_free_result( $res );
- wfRestoreWarnings();
- if ( !$ok ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
- }
- }
-
- /**
- * @param $res ResultWrapper
- * @return object|stdClass
- * @throws DBUnexpectedError
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $row = mysql_fetch_object( $res );
- wfRestoreWarnings();
-
- $errno = $this->lastErrno();
- // Unfortunately, mysql_fetch_object does not reset the last errno.
- // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
- // these are the only errors mysql_fetch_object can cause.
- // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html.
- if( $errno == 2000 || $errno == 2013 ) {
- throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * @param $res ResultWrapper
- * @return array
- * @throws DBUnexpectedError
- */
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $row = mysql_fetch_array( $res );
- wfRestoreWarnings();
-
- $errno = $this->lastErrno();
- // Unfortunately, mysql_fetch_array does not reset the last errno.
- // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
- // these are the only errors mysql_fetch_object can cause.
- // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html.
- if( $errno == 2000 || $errno == 2013 ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * @throws DBUnexpectedError
- * @param $res ResultWrapper
- * @return int
- */
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $n = mysql_num_rows( $res );
- wfRestoreWarnings();
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $n;
- }
-
- /**
- * @param $res ResultWrapper
- * @return int
- */
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_num_fields( $res );
- }
-
- /**
- * @param $res ResultWrapper
- * @param $n string
- * @return string
- */
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_field_name( $res, $n );
- }
-
- /**
* @return int
*/
function insertId() {
@@ -288,18 +95,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $res ResultWrapper
- * @param $row
- * @return bool
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_data_seek( $res, $row );
- }
-
- /**
* @return int
*/
function lastErrno() {
@@ -311,27 +106,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @return string
- */
- 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;
- }
-
- /**
* @return int
*/
function affectedRows() {
@@ -339,100 +113,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $table string
- * @param $uniqueIndexes
- * @param $rows array
- * @param $fname string
- * @return ResultWrapper
- */
- function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMysql::replace' ) {
- return $this->nativeReplace( $table, $rows, $fname );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
- *
- * @param $table string|array
- * @param $vars string|array
- * @param $conds string|array
- * @param $fname string
- * @param $options string|array
- * @return int
- */
- public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabaseMysql::estimateRowCount', $options = array() ) {
- $options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- if ( $res === false ) {
- return false;
- }
- if ( !$this->numRows( $res ) ) {
- return 0;
- }
-
- $rows = 1;
- foreach ( $res as $plan ) {
- $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
- }
- return $rows;
- }
-
- /**
- * @param $table string
- * @param $field string
- * @return bool|MySQLField
- */
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
- if ( !$res ) {
- return false;
- }
- $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;
- }
-
- /**
- * Get information about an index into an object
- * Returns false if the index does not exist
- *
- * @param $table string
- * @param $index string
- * @param $fname string
- * @return bool|array|null False or null on failure
- */
- function indexInfo( $table, $index, $fname = 'DatabaseMysql::indexInfo' ) {
- # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
- # SHOW INDEX should work for 3.x and up:
- # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
- $table = $this->tableName( $table );
- $index = $this->indexName( $index );
- $sql = 'SHOW INDEX FROM ' . $table;
- $res = $this->query( $sql, $fname );
-
- if ( !$res ) {
- return null;
- }
-
- $result = array();
-
- foreach ( $res as $row ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
-
- return empty( $result ) ? false : $result;
- }
-
- /**
* @param $db
* @return bool
*/
@@ -442,599 +122,53 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $s string
- *
- * @return string
- */
- function strencode( $s ) {
- $sQuoted = mysql_real_escape_string( $s, $this->mConn );
-
- if($sQuoted === false) {
- $this->ping();
- $sQuoted = mysql_real_escape_string( $s, $this->mConn );
- }
- return $sQuoted;
- }
-
- /**
- * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
- *
- * @param $s string
- *
- * @return string
- */
- public function addIdentifierQuotes( $s ) {
- return "`" . $this->strencode( $s ) . "`";
- }
-
- /**
- * @param $name string
- * @return bool
- */
- public function isQuotedIdentifier( $name ) {
- return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
- }
-
- /**
- * @return bool
- */
- function ping() {
- $ping = mysql_ping( $this->mConn );
- if ( $ping ) {
- return true;
- }
-
- mysql_close( $this->mConn );
- $this->mOpened = false;
- $this->mConn = false;
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
- return true;
- }
-
- /**
- * Returns slave lag.
- *
- * This will do a SHOW SLAVE STATUS
- *
- * @return int
- */
- function getLag() {
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
- return $this->mFakeSlaveLag;
- }
-
- return $this->getLagFromSlaveStatus();
- }
-
- /**
- * @return bool|int
- */
- function getLagFromSlaveStatus() {
- $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
- if ( !$res ) {
- return false;
- }
- $row = $res->fetchObject();
- if ( !$row ) {
- return false;
- }
- if ( strval( $row->Seconds_Behind_Master ) === '' ) {
- return false;
- } else {
- return intval( $row->Seconds_Behind_Master );
- }
- }
-
- /**
- * @deprecated in 1.19, use getLagFromSlaveStatus
- *
- * @return bool|int
- */
- function getLagFromProcesslist() {
- wfDeprecated( __METHOD__, '1.19' );
- $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
- if( !$res ) {
- return false;
- }
- # Find slave SQL thread
- foreach( $res as $row ) {
- /* This should work for most situations - when default db
- * for thread is not specified, it had no events executed,
- * and therefore it doesn't know yet how lagged it is.
- *
- * Relay log I/O thread does not select databases.
- */
- if ( $row->User == 'system user' &&
- $row->State != 'Waiting for master to send event' &&
- $row->State != 'Connecting to master' &&
- $row->State != 'Queueing master event to the relay log' &&
- $row->State != 'Waiting for master update' &&
- $row->State != 'Requesting binlog dump' &&
- $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 ) {
- return false;
- } else {
- return $row->Time;
- }
- }
- }
- return false;
- }
-
- /**
- * Wait for the slave to catch up to a given master position.
- *
- * @param $pos DBMasterPos object
- * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
- * @return bool|string
- */
- function masterPosWait( DBMasterPos $pos, $timeout ) {
- $fname = 'DatabaseBase::masterPosWait';
- wfProfileIn( $fname );
-
- # Commit any open transactions
- if ( $this->mTrxLevel ) {
- $this->commit( __METHOD__ );
- }
-
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- $status = parent::masterPosWait( $pos, $timeout );
- wfProfileOut( $fname );
- return $status;
- }
-
- # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
- $encFile = $this->addQuotes( $pos->file );
- $encPos = intval( $pos->pos );
- $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
- $res = $this->doQuery( $sql );
-
- if ( $res && $row = $this->fetchRow( $res ) ) {
- wfProfileOut( $fname );
- return $row[0];
- } else {
- wfProfileOut( $fname );
- return false;
- }
- }
-
- /**
- * Get the position of the master from SHOW SLAVE STATUS
- *
- * @return MySQLMasterPos|bool
- */
- function getSlavePos() {
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- return parent::getSlavePos();
- }
-
- $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
- return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
- } else {
- return false;
- }
- }
-
- /**
- * Get the position of the master from SHOW MASTER STATUS
- *
- * @return MySQLMasterPos|bool
- */
- function getMasterPos() {
- if ( $this->mFakeMaster ) {
- return parent::getMasterPos();
- }
-
- $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- return new MySQLMasterPos( $row->File, $row->Position );
- } else {
- return false;
- }
- }
-
- /**
* @return string
*/
function getServerVersion() {
return mysql_get_server_info( $this->mConn );
}
- /**
- * @param $index
- * @return string
- */
- function useIndexClause( $index ) {
- return "FORCE INDEX (" . $this->indexName( $index ) . ")";
- }
-
- /**
- * @return string
- */
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
- }
-
- /**
- * @return string
- */
- public static function getSoftwareLink() {
- return '[http://www.mysql.com/ MySQL]';
- }
-
- /**
- * @param $options array
- */
- public function setSessionOptions( array $options ) {
- if ( isset( $options['connTimeout'] ) ) {
- $timeout = (int)$options['connTimeout'];
- $this->query( "SET net_read_timeout=$timeout" );
- $this->query( "SET net_write_timeout=$timeout" );
- }
- }
-
- public function streamStatementEnd( &$sql, &$newLine ) {
- if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
- preg_match( '/^DELIMITER\s+(\S+)/' , $newLine, $m );
- $this->delimiter = $m[1];
- $newLine = '';
- }
- return parent::streamStatementEnd( $sql, $newLine );
- }
-
- /**
- * Check to see if a named lock is available. This is non-blocking.
- *
- * @param $lockName String: name of lock to poll
- * @param $method String: name of method calling us
- * @return Boolean
- * @since 1.20
- */
- public function lockIsFree( $lockName, $method ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
- return ( $row->lockstatus == 1 );
- }
-
- /**
- * @param $lockName string
- * @param $method string
- * @param $timeout int
- * @return bool
- */
- 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 );
-
- if( $row->lockstatus == 1 ) {
- return true;
- } else {
- wfDebug( __METHOD__." failed to acquire lock\n" );
- return false;
- }
- }
-
- /**
- * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
- * @param $lockName string
- * @param $method string
- * @return bool
- */
- 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 == 1 );
- }
-
- /**
- * @param $read array
- * @param $write array
- * @param $method string
- * @param $lowPriority bool
- */
- 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 );
- }
-
- /**
- * @param $method string
- */
- public function unlockTables( $method ) {
- $this->query( "UNLOCK TABLES", $method );
- }
-
- /**
- * Get search engine class. All subclasses of this
- * need to implement this if they wish to use searching.
- *
- * @return String
- */
- public function getSearchEngine() {
- return 'SearchMySQL';
+ protected function mysqlFreeResult( $res ) {
+ return mysql_free_result( $res );
}
- /**
- * @param bool $value
- * @return mixed
- */
- 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__ );
+ protected function mysqlFetchObject( $res ) {
+ return mysql_fetch_object( $res );
}
- /**
- * DELETE where the condition is a join. MySql uses multi-table deletes.
- * @param $delTable string
- * @param $joinTable string
- * @param $delVar string
- * @param $joinVar string
- * @param $conds array|string
- * @param $fname bool
- * @return bool|ResultWrapper
- */
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
-
- if ( $conds != '*' ) {
- $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
- }
-
- return $this->query( $sql, $fname );
+ protected function mysqlFetchArray( $res ) {
+ return mysql_fetch_array( $res );
}
- /**
- * Determines how long the server has been up
- *
- * @return int
- */
- function getServerUptime() {
- $vars = $this->getMysqlStatus( 'Uptime' );
- return (int)$vars['Uptime'];
+ protected function mysqlNumRows( $res ) {
+ return mysql_num_rows( $res );
}
- /**
- * Determines if the last failure was due to a deadlock
- *
- * @return bool
- */
- function wasDeadlock() {
- return $this->lastErrno() == 1213;
- }
-
- /**
- * Determines if the last failure was due to a lock timeout
- *
- * @return bool
- */
- function wasLockTimeout() {
- return $this->lastErrno() == 1205;
- }
-
- /**
- * Determines if the last query error was something that should be dealt
- * with by pinging the connection and reissuing the query
- *
- * @return bool
- */
- function wasErrorReissuable() {
- return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
- }
-
- /**
- * Determines if the last failure was due to the database being read-only.
- *
- * @return bool
- */
- function wasReadOnlyError() {
- return $this->lastErrno() == 1223 ||
- ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
- }
-
- /**
- * @param $oldName
- * @param $newName
- * @param $temporary bool
- * @param $fname string
- */
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseMysql::duplicateTableStructure' ) {
- $tmp = $temporary ? 'TEMPORARY ' : '';
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
- $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
- $this->query( $query, $fname );
- }
-
- /**
- * List all tables on the database
- *
- * @param $prefix string Only show tables with this prefix, e.g. mw_
- * @param $fname String: calling function name
- * @return array
- */
- function listTables( $prefix = null, $fname = 'DatabaseMysql::listTables' ) {
- $result = $this->query( "SHOW TABLES", $fname);
-
- $endArray = array();
-
- foreach( $result as $table ) {
- $vars = get_object_vars($table);
- $table = array_pop( $vars );
-
- if( !$prefix || strpos( $table, $prefix ) === 0 ) {
- $endArray[] = $table;
- }
- }
-
- return $endArray;
- }
-
- /**
- * @param $tableName
- * @param $fName string
- * @return bool|ResultWrapper
- */
- public function dropTable( $tableName, $fName = 'DatabaseMysql::dropTable' ) {
- if( !$this->tableExists( $tableName, $fName ) ) {
- return false;
- }
- return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
- }
-
- /**
- * @return array
- */
- protected function getDefaultSchemaVars() {
- $vars = parent::getDefaultSchemaVars();
- $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
- $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['wgDBTableOptions'] );
- return $vars;
- }
-
- /**
- * Get status information from SHOW STATUS in an associative array
- *
- * @param $which string
- * @return array
- */
- function getMysqlStatus( $which = "%" ) {
- $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
- $status = array();
-
- foreach ( $res as $row ) {
- $status[$row->Variable_name] = $row->Value;
- }
-
- return $status;
- }
-
-}
-
-/**
- * Legacy support: Database == DatabaseMysql
- *
- * @deprecated in 1.16
- */
-class Database extends DatabaseMysql {}
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class MySQLField implements Field {
- private $name, $tablename, $default, $max_length, $nullable,
- $is_pk, $is_unique, $is_multiple, $is_key, $type;
-
- function __construct ( $info ) {
- $this->name = $info->name;
- $this->tablename = $info->table;
- $this->default = $info->def;
- $this->max_length = $info->max_length;
- $this->nullable = !$info->not_null;
- $this->is_pk = $info->primary_key;
- $this->is_unique = $info->unique_key;
- $this->is_multiple = $info->multiple_key;
- $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
- $this->type = $info->type;
- }
-
- /**
- * @return string
- */
- function name() {
- return $this->name;
- }
-
- /**
- * @return string
- */
- function tableName() {
- return $this->tableName;
- }
-
- /**
- * @return string
- */
- function type() {
- return $this->type;
+ protected function mysqlNumFields( $res ) {
+ return mysql_num_fields( $res );
}
- /**
- * @return bool
- */
- function isNullable() {
- return $this->nullable;
+ protected function mysqlFetchField( $res, $n ) {
+ return mysql_fetch_field( $res, $n );
}
- function defaultValue() {
- return $this->default;
+ protected function mysqlFieldName( $res, $n ) {
+ return mysql_field_name( $res, $n );
}
- /**
- * @return bool
- */
- function isKey() {
- return $this->is_key;
+ protected function mysqlDataSeek( $res, $row ) {
+ return mysql_data_seek( $res, $row );
}
- /**
- * @return bool
- */
- function isMultipleKey() {
- return $this->is_multiple;
+ protected function mysqlError( $conn = null ) {
+ return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning
}
-}
-
-class MySQLMasterPos implements DBMasterPos {
- var $file, $pos;
- function __construct( $file, $pos ) {
- $this->file = $file;
- $this->pos = $pos;
+ protected function mysqlRealEscapeString( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
}
- function __toString() {
- return "{$this->file}/{$this->pos}";
+ protected function mysqlPing() {
+ return mysql_ping( $this->mConn );
}
}
diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php
new file mode 100644
index 00000000..8f12b92d
--- /dev/null
+++ b/includes/db/DatabaseMysqlBase.php
@@ -0,0 +1,1161 @@
+<?php
+/**
+ * This is the MySQL database abstraction layer.
+ *
+ * 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 Database
+ */
+
+/**
+ * Database abstraction object for MySQL.
+ * Defines methods independent on used MySQL extension.
+ *
+ * @ingroup Database
+ * @since 1.22
+ * @see Database
+ */
+abstract class DatabaseMysqlBase extends DatabaseBase {
+ /** @var MysqlMasterPos */
+ protected $lastKnownSlavePos;
+
+ /**
+ * @return string
+ */
+ function getType() {
+ return 'mysql';
+ }
+
+ /**
+ * @param $server string
+ * @param $user string
+ * @param $password string
+ * @param $dbName string
+ * @return bool
+ * @throws DBConnectionError
+ */
+ function open( $server, $user, $password, $dbName ) {
+ global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode;
+ wfProfileIn( __METHOD__ );
+
+ # 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;
+
+ 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;
+ $this->installErrorHandler();
+ try {
+ $this->mConn = $this->mysqlConnect( $realServer );
+ } catch ( Exception $ex ) {
+ wfProfileOut( "dbconnect-$server" );
+ wfProfileOut( __METHOD__ );
+ throw $ex;
+ }
+ $error = $this->restoreErrorHandler();
+
+ wfProfileOut( "dbconnect-$server" );
+
+ # Always log connection errors
+ if ( !$this->mConn ) {
+ if ( !$error ) {
+ $error = $this->lastError();
+ }
+ wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
+ wfDebug( "DB connection error\n" .
+ "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
+
+ wfProfileOut( __METHOD__ );
+ return $this->reportConnectionError( $error );
+ }
+
+ if ( $dbName != '' ) {
+ wfSuppressWarnings();
+ $success = $this->selectDB( $dbName );
+ wfRestoreWarnings();
+ if ( !$success ) {
+ wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" );
+ wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host " . wfHostname() . "\n" );
+
+ wfProfileOut( __METHOD__ );
+ return $this->reportConnectionError( "Error selecting database $dbName" );
+ }
+ }
+
+ // Tell the server we're communicating with it in UTF-8.
+ // This may engage various charset conversions.
+ if ( $wgDBmysql5 ) {
+ $this->query( 'SET NAMES utf8', __METHOD__ );
+ } else {
+ $this->query( 'SET NAMES binary', __METHOD__ );
+ }
+ // Set SQL mode, default is turning them all off, can be overridden or skipped with null
+ if ( is_string( $wgSQLMode ) ) {
+ $mode = $this->addQuotes( $wgSQLMode );
+ $this->query( "SET sql_mode = $mode", __METHOD__ );
+ }
+
+ $this->mOpened = true;
+ wfProfileOut( __METHOD__ );
+ return true;
+ }
+
+ /**
+ * Open a connection to a MySQL server
+ *
+ * @param $realServer string
+ * @return mixed Raw connection
+ * @throws DBConnectionError
+ */
+ abstract protected function mysqlConnect( $realServer );
+
+ /**
+ * @param $res ResultWrapper
+ * @throws DBUnexpectedError
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $ok = $this->mysqlFreeResult( $res );
+ wfRestoreWarnings();
+ if ( !$ok ) {
+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+ }
+ }
+
+ /**
+ * Free result memory
+ *
+ * @param $res Raw result
+ * @return bool
+ */
+ abstract protected function mysqlFreeResult( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @return object|bool
+ * @throws DBUnexpectedError
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $row = $this->mysqlFetchObject( $res );
+ wfRestoreWarnings();
+
+ $errno = $this->lastErrno();
+ // Unfortunately, mysql_fetch_object does not reset the last errno.
+ // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
+ // these are the only errors mysql_fetch_object can cause.
+ // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+ if ( $errno == 2000 || $errno == 2013 ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch a result row as an object
+ *
+ * @param $res Raw result
+ * @return stdClass
+ */
+ abstract protected function mysqlFetchObject( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @return array|bool
+ * @throws DBUnexpectedError
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $row = $this->mysqlFetchArray( $res );
+ wfRestoreWarnings();
+
+ $errno = $this->lastErrno();
+ // Unfortunately, mysql_fetch_array does not reset the last errno.
+ // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
+ // these are the only errors mysql_fetch_array can cause.
+ // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+ if ( $errno == 2000 || $errno == 2013 ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch a result row as an associative and numeric array
+ *
+ * @param $res Raw result
+ * @return array
+ */
+ abstract protected function mysqlFetchArray( $res );
+
+ /**
+ * @throws DBUnexpectedError
+ * @param $res ResultWrapper
+ * @return int
+ */
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $n = $this->mysqlNumRows( $res );
+ wfRestoreWarnings();
+ // Unfortunately, mysql_num_rows does not reset the last errno.
+ // We are not checking for any errors here, since
+ // these are no errors mysql_num_rows can cause.
+ // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+ // See https://bugzilla.wikimedia.org/42430
+ return $n;
+ }
+
+ /**
+ * Get number of rows in result
+ *
+ * @param $res Raw result
+ * @return int
+ */
+ abstract protected function mysqlNumRows( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @return int
+ */
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return $this->mysqlNumFields( $res );
+ }
+
+ /**
+ * Get number of fields in result
+ *
+ * @param $res Raw result
+ * @return int
+ */
+ abstract protected function mysqlNumFields( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @param $n string
+ * @return string
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return $this->mysqlFieldName( $res, $n );
+ }
+
+ /**
+ * Get the name of the specified field in a result
+ *
+ * @param $res Raw result
+ * @param $n int
+ * @return string
+ */
+ abstract protected function mysqlFieldName( $res, $n );
+
+ /**
+ * @param $res ResultWrapper
+ * @param $row
+ * @return bool
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return $this->mysqlDataSeek( $res, $row );
+ }
+
+ /**
+ * Move internal result pointer
+ *
+ * @param $res Raw result
+ * @param $row int
+ * @return bool
+ */
+ abstract protected function mysqlDataSeek( $res, $row );
+
+ /**
+ * @return string
+ */
+ function lastError() {
+ if ( $this->mConn ) {
+ # Even if it's non-zero, it can still be invalid
+ wfSuppressWarnings();
+ $error = $this->mysqlError( $this->mConn );
+ if ( !$error ) {
+ $error = $this->mysqlError();
+ }
+ wfRestoreWarnings();
+ } else {
+ $error = $this->mysqlError();
+ }
+ if ( $error ) {
+ $error .= ' (' . $this->mServer . ')';
+ }
+ return $error;
+ }
+
+ /**
+ * Returns the text of the error message from previous MySQL operation
+ *
+ * @param $conn Raw connection
+ * @return string
+ */
+ abstract protected function mysqlError( $conn = null );
+
+ /**
+ * @param $table string
+ * @param $uniqueIndexes
+ * @param $rows array
+ * @param $fname string
+ * @return ResultWrapper
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+ return $this->nativeReplace( $table, $rows, $fname );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ *
+ * @param $table string|array
+ * @param $vars string|array
+ * @param $conds string|array
+ * @param $fname string
+ * @param $options string|array
+ * @return int
+ */
+ public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) {
+ $options['EXPLAIN'] = true;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
+ if ( $res === false ) {
+ return false;
+ }
+ if ( !$this->numRows( $res ) ) {
+ return 0;
+ }
+
+ $rows = 1;
+ foreach ( $res as $plan ) {
+ $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
+ }
+ return $rows;
+ }
+
+ /**
+ * @param $table string
+ * @param $field string
+ * @return bool|MySQLField
+ */
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
+ if ( !$res ) {
+ return false;
+ }
+ $n = $this->mysqlNumFields( $res->result );
+ for ( $i = 0; $i < $n; $i++ ) {
+ $meta = $this->mysqlFetchField( $res->result, $i );
+ if ( $field == $meta->name ) {
+ return new MySQLField( $meta );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get column information from a result
+ *
+ * @param $res Raw result
+ * @param $n int
+ * @return stdClass
+ */
+ abstract protected function mysqlFetchField( $res, $n );
+
+ /**
+ * Get information about an index into an object
+ * Returns false if the index does not exist
+ *
+ * @param $table string
+ * @param $index string
+ * @param $fname string
+ * @return bool|array|null False or null on failure
+ */
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
+ # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+ # SHOW INDEX should work for 3.x and up:
+ # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+ $table = $this->tableName( $table );
+ $index = $this->indexName( $index );
+
+ $sql = 'SHOW INDEX FROM ' . $table;
+ $res = $this->query( $sql, $fname );
+
+ if ( !$res ) {
+ return null;
+ }
+
+ $result = array();
+
+ foreach ( $res as $row ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ return empty( $result ) ? false : $result;
+ }
+
+ /**
+ * @param $s string
+ *
+ * @return string
+ */
+ function strencode( $s ) {
+ $sQuoted = $this->mysqlRealEscapeString( $s );
+
+ if ( $sQuoted === false ) {
+ $this->ping();
+ $sQuoted = $this->mysqlRealEscapeString( $s );
+ }
+ return $sQuoted;
+ }
+
+ /**
+ * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
+ *
+ * @param $s string
+ *
+ * @return string
+ */
+ public function addIdentifierQuotes( $s ) {
+ // Characters in the range \u0001-\uFFFF are valid in a quoted identifier
+ // Remove NUL bytes and escape backticks by doubling
+ return '`' . str_replace( array( "\0", '`' ), array( '', '``' ), $s ) . '`';
+ }
+
+ /**
+ * @param $name string
+ * @return bool
+ */
+ public function isQuotedIdentifier( $name ) {
+ return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
+ }
+
+ /**
+ * @return bool
+ */
+ function ping() {
+ $ping = $this->mysqlPing();
+ if ( $ping ) {
+ return true;
+ }
+
+ $this->closeConnection();
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ return true;
+ }
+
+ /**
+ * Ping a server connection or reconnect if there is no connection
+ *
+ * @return bool
+ */
+ abstract protected function mysqlPing();
+
+ /**
+ * Returns slave lag.
+ *
+ * This will do a SHOW SLAVE STATUS
+ *
+ * @return int
+ */
+ function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
+ }
+
+ return $this->getLagFromSlaveStatus();
+ }
+
+ /**
+ * @return bool|int
+ */
+ function getLagFromSlaveStatus() {
+ $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
+ if ( !$res ) {
+ return false;
+ }
+ $row = $res->fetchObject();
+ if ( !$row ) {
+ return false;
+ }
+ if ( strval( $row->Seconds_Behind_Master ) === '' ) {
+ return false;
+ } else {
+ return intval( $row->Seconds_Behind_Master );
+ }
+ }
+
+ /**
+ * @deprecated in 1.19, use getLagFromSlaveStatus
+ *
+ * @return bool|int
+ */
+ function getLagFromProcesslist() {
+ wfDeprecated( __METHOD__, '1.19' );
+ $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
+ if ( !$res ) {
+ return false;
+ }
+ # Find slave SQL thread
+ foreach ( $res as $row ) {
+ /* This should work for most situations - when default db
+ * for thread is not specified, it had no events executed,
+ * and therefore it doesn't know yet how lagged it is.
+ *
+ * Relay log I/O thread does not select databases.
+ */
+ if ( $row->User == 'system user' &&
+ $row->State != 'Waiting for master to send event' &&
+ $row->State != 'Connecting to master' &&
+ $row->State != 'Queueing master event to the relay log' &&
+ $row->State != 'Waiting for master update' &&
+ $row->State != 'Requesting binlog dump' &&
+ $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 ) {
+ return false;
+ } else {
+ return $row->Time;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Wait for the slave to catch up to a given master position.
+ * @TODO: return values for this and base class are rubbish
+ *
+ * @param $pos DBMasterPos object
+ * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
+ * @return bool|string
+ */
+ function masterPosWait( DBMasterPos $pos, $timeout ) {
+ if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) {
+ return '0'; // http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html
+ }
+
+ wfProfileIn( __METHOD__ );
+ # Commit any open transactions
+ $this->commit( __METHOD__, 'flush' );
+
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $status = parent::masterPosWait( $pos, $timeout );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+ $encFile = $this->addQuotes( $pos->file );
+ $encPos = intval( $pos->pos );
+ $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
+ $res = $this->doQuery( $sql );
+
+ $status = false;
+ if ( $res && $row = $this->fetchRow( $res ) ) {
+ $status = $row[0]; // can be NULL, -1, or 0+ per the MySQL manual
+ if ( ctype_digit( $status ) ) { // success
+ $this->lastKnownSlavePos = $pos;
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * Get the position of the master from SHOW SLAVE STATUS
+ *
+ * @return MySQLMasterPos|bool
+ */
+ function getSlavePos() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ return parent::getSlavePos();
+ }
+
+ $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' );
+ $row = $this->fetchObject( $res );
+
+ if ( $row ) {
+ $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
+ return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the position of the master from SHOW MASTER STATUS
+ *
+ * @return MySQLMasterPos|bool
+ */
+ function getMasterPos() {
+ if ( $this->mFakeMaster ) {
+ return parent::getMasterPos();
+ }
+
+ $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' );
+ $row = $this->fetchObject( $res );
+
+ if ( $row ) {
+ return new MySQLMasterPos( $row->File, $row->Position );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param $index
+ * @return string
+ */
+ function useIndexClause( $index ) {
+ return "FORCE INDEX (" . $this->indexName( $index ) . ")";
+ }
+
+ /**
+ * @return string
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ /**
+ * @return string
+ */
+ public function getSoftwareLink() {
+ $version = $this->getServerVersion();
+ if ( strpos( $version, 'MariaDB' ) !== false ) {
+ return '[{{int:version-db-mariadb-url}} MariaDB]';
+ } elseif ( strpos( $version, 'percona' ) !== false ) {
+ return '[{{int:version-db-percona-url}} Percona Server]';
+ } else {
+ return '[{{int:version-db-mysql-url}} MySQL]';
+ }
+ }
+
+ /**
+ * @param $options array
+ */
+ public function setSessionOptions( array $options ) {
+ if ( isset( $options['connTimeout'] ) ) {
+ $timeout = (int)$options['connTimeout'];
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
+ }
+ }
+
+ public function streamStatementEnd( &$sql, &$newLine ) {
+ if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
+ preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
+ $this->delimiter = $m[1];
+ $newLine = '';
+ }
+ return parent::streamStatementEnd( $sql, $newLine );
+ }
+
+ /**
+ * Check to see if a named lock is available. This is non-blocking.
+ *
+ * @param string $lockName name of lock to poll
+ * @param string $method name of method calling us
+ * @return Boolean
+ * @since 1.20
+ */
+ public function lockIsFree( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ return ( $row->lockstatus == 1 );
+ }
+
+ /**
+ * @param $lockName string
+ * @param $method string
+ * @param $timeout int
+ * @return bool
+ */
+ 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 );
+
+ if ( $row->lockstatus == 1 ) {
+ return true;
+ } else {
+ wfDebug( __METHOD__ . " failed to acquire lock\n" );
+ return false;
+ }
+ }
+
+ /**
+ * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
+ * @param $lockName string
+ * @param $method string
+ * @return bool
+ */
+ 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 == 1 );
+ }
+
+ /**
+ * @param $read array
+ * @param $write array
+ * @param $method string
+ * @param $lowPriority bool
+ * @return bool
+ */
+ 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 );
+ return true;
+ }
+
+ /**
+ * @param $method string
+ * @return bool
+ */
+ public function unlockTables( $method ) {
+ $this->query( "UNLOCK TABLES", $method );
+ return true;
+ }
+
+ /**
+ * Get search engine class. All subclasses of this
+ * need to implement this if they wish to use searching.
+ *
+ * @return String
+ */
+ public function getSearchEngine() {
+ return 'SearchMySQL';
+ }
+
+ /**
+ * @param bool $value
+ * @return mixed
+ */
+ 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__ );
+ }
+
+ /**
+ * DELETE where the condition is a join. MySql uses multi-table deletes.
+ * @param $delTable string
+ * @param $joinTable string
+ * @param $delVar string
+ * @param $joinVar string
+ * @param $conds array|string
+ * @param bool|string $fname bool
+ * @throws DBUnexpectedError
+ * @return bool|ResultWrapper
+ */
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * @param string $table
+ * @param array $rows
+ * @param array $uniqueIndexes
+ * @param array $set
+ * @param string $fname
+ * @param array $options
+ * @return bool
+ */
+ public function upsert(
+ $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+ ) {
+ if ( !count( $rows ) ) {
+ return true; // nothing to do
+ }
+ $rows = is_array( reset( $rows ) ) ? $rows : array( $rows );
+
+ $table = $this->tableName( $table );
+ $columns = array_keys( $rows[0] );
+
+ $sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
+ $rowTuples = array();
+ foreach ( $rows as $row ) {
+ $rowTuples[] = '(' . $this->makeList( $row ) . ')';
+ }
+ $sql .= implode( ',', $rowTuples );
+ $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET );
+
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Determines how long the server has been up
+ *
+ * @return int
+ */
+ function getServerUptime() {
+ $vars = $this->getMysqlStatus( 'Uptime' );
+ return (int)$vars['Uptime'];
+ }
+
+ /**
+ * Determines if the last failure was due to a deadlock
+ *
+ * @return bool
+ */
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
+ }
+
+ /**
+ * Determines if the last failure was due to a lock timeout
+ *
+ * @return bool
+ */
+ function wasLockTimeout() {
+ return $this->lastErrno() == 1205;
+ }
+
+ /**
+ * Determines if the last query error was something that should be dealt
+ * with by pinging the connection and reissuing the query
+ *
+ * @return bool
+ */
+ function wasErrorReissuable() {
+ return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+ }
+
+ /**
+ * Determines if the last failure was due to the database being read-only.
+ *
+ * @return bool
+ */
+ function wasReadOnlyError() {
+ return $this->lastErrno() == 1223 ||
+ ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
+ }
+
+ /**
+ * @param $oldName
+ * @param $newName
+ * @param $temporary bool
+ * @param $fname string
+ */
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
+ $tmp = $temporary ? 'TEMPORARY ' : '';
+ $newName = $this->addIdentifierQuotes( $newName );
+ $oldName = $this->addIdentifierQuotes( $oldName );
+ $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
+ $this->query( $query, $fname );
+ }
+
+ /**
+ * List all tables on the database
+ *
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname calling function name
+ * @return array
+ */
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
+ $result = $this->query( "SHOW TABLES", $fname );
+
+ $endArray = array();
+
+ foreach ( $result as $table ) {
+ $vars = get_object_vars( $table );
+ $table = array_pop( $vars );
+
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ $endArray[] = $table;
+ }
+ }
+
+ return $endArray;
+ }
+
+ /**
+ * @param $tableName
+ * @param $fName string
+ * @return bool|ResultWrapper
+ */
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ if ( !$this->tableExists( $tableName, $fName ) ) {
+ return false;
+ }
+ return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
+ }
+
+ /**
+ * @return array
+ */
+ protected function getDefaultSchemaVars() {
+ $vars = parent::getDefaultSchemaVars();
+ $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
+ $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['wgDBTableOptions'] );
+ return $vars;
+ }
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ *
+ * @param $which string
+ * @return array
+ */
+ function getMysqlStatus( $which = "%" ) {
+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+ $status = array();
+
+ foreach ( $res as $row ) {
+ $status[$row->Variable_name] = $row->Value;
+ }
+
+ return $status;
+ }
+
+ /**
+ * Lists VIEWs in the database
+ *
+ * @param string $prefix Only show VIEWs with this prefix, eg.
+ * unit_test_, or $wgDBprefix. Default: null, would return all views.
+ * @param string $fname Name of calling function
+ * @return array
+ * @since 1.22
+ */
+ public function listViews( $prefix = null, $fname = __METHOD__ ) {
+
+ if ( !isset( $this->allViews ) ) {
+
+ // The name of the column containing the name of the VIEW
+ $propertyName = 'Tables_in_' . $this->mDBname;
+
+ // Query for the VIEWS
+ $result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
+ $this->allViews = array();
+ while ( ($row = $this->fetchRow($result)) !== false ) {
+ array_push( $this->allViews, $row[$propertyName] );
+ }
+ }
+
+ if ( is_null($prefix) || $prefix === '' ) {
+ return $this->allViews;
+ }
+
+ $filteredViews = array();
+ foreach ( $this->allViews as $viewName ) {
+ // Does the name of this VIEW start with the table-prefix?
+ if ( strpos( $viewName, $prefix ) === 0 ) {
+ array_push( $filteredViews, $viewName );
+ }
+ }
+ return $filteredViews;
+ }
+
+ /**
+ * Differentiates between a TABLE and a VIEW.
+ *
+ * @param $name string: Name of the TABLE/VIEW to test
+ * @return bool
+ * @since 1.22
+ */
+ public function isView( $name, $prefix = null ) {
+ return in_array( $name, $this->listViews( $prefix ) );
+ }
+
+}
+
+
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField implements Field {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary;
+
+ function __construct( $info ) {
+ $this->name = $info->name;
+ $this->tablename = $info->table;
+ $this->default = $info->def;
+ $this->max_length = $info->max_length;
+ $this->nullable = !$info->not_null;
+ $this->is_pk = $info->primary_key;
+ $this->is_unique = $info->unique_key;
+ $this->is_multiple = $info->multiple_key;
+ $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
+ $this->type = $info->type;
+ $this->binary = isset( $info->binary ) ? $info->binary : false;
+ }
+
+ /**
+ * @return string
+ */
+ function name() {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ function tableName() {
+ return $this->tableName;
+ }
+
+ /**
+ * @return string
+ */
+ function type() {
+ return $this->type;
+ }
+
+ /**
+ * @return bool
+ */
+ function isNullable() {
+ return $this->nullable;
+ }
+
+ function defaultValue() {
+ return $this->default;
+ }
+
+ /**
+ * @return bool
+ */
+ function isKey() {
+ return $this->is_key;
+ }
+
+ /**
+ * @return bool
+ */
+ function isMultipleKey() {
+ return $this->is_multiple;
+ }
+
+ function isBinary() {
+ return $this->binary;
+ }
+}
+
+class MySQLMasterPos implements DBMasterPos {
+ var $file, $pos;
+
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ // e.g db1034-bin.000976/843431247
+ return "{$this->file}/{$this->pos}";
+ }
+
+ /**
+ * @return array|false (int, int)
+ */
+ protected function getCoordinates() {
+ $m = array();
+ if ( preg_match( '!\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
+ return array( (int)$m[1], (int)$m[2] );
+ }
+ return false;
+ }
+
+ function hasReached( MySQLMasterPos $pos ) {
+ $thisPos = $this->getCoordinates();
+ $thatPos = $pos->getCoordinates();
+ return ( $thisPos && $thatPos && $thisPos >= $thatPos );
+ }
+}
diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php
new file mode 100644
index 00000000..7761abe9
--- /dev/null
+++ b/includes/db/DatabaseMysqli.php
@@ -0,0 +1,194 @@
+<?php
+/**
+ * This is the MySQLi database abstraction layer.
+ *
+ * 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 Database
+ */
+
+/**
+ * Database abstraction object for PHP extension mysqli.
+ *
+ * @ingroup Database
+ * @since 1.22
+ * @see Database
+ */
+class DatabaseMysqli extends DatabaseMysqlBase {
+
+ /**
+ * @param $sql string
+ * @return resource
+ */
+ protected function doQuery( $sql ) {
+ if ( $this->bufferResults() ) {
+ $ret = $this->mConn->query( $sql );
+ } else {
+ $ret = $this->mConn->query( $sql, MYSQLI_USE_RESULT );
+ }
+ return $ret;
+ }
+
+ protected function mysqlConnect( $realServer ) {
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if ( !function_exists( 'mysqli_init' ) ) {
+ throw new DBConnectionError( $this, "MySQLi functions missing,"
+ . " have you compiled PHP with the --with-mysqli option?\n" );
+ }
+
+ $connFlags = 0;
+ if ( $this->mFlags & DBO_SSL ) {
+ $connFlags |= MYSQLI_CLIENT_SSL;
+ }
+ if ( $this->mFlags & DBO_COMPRESS ) {
+ $connFlags |= MYSQLI_CLIENT_COMPRESS;
+ }
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $realServer = 'p:' . $realServer;
+ }
+
+ $mysqli = mysqli_init();
+ $numAttempts = 2;
+
+ for ( $i = 0; $i < $numAttempts; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ( $mysqli->real_connect( $realServer, $this->mUser,
+ $this->mPassword, $this->mDBname, null, null, $connFlags ) )
+ {
+ return $mysqli;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function closeConnection() {
+ return $this->mConn->close();
+ }
+
+ /**
+ * @return int
+ */
+ function insertId() {
+ return $this->mConn->insert_id;
+ }
+
+ /**
+ * @return int
+ */
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return $this->mConn->errno;
+ } else {
+ return mysqli_connect_errno();
+ }
+ }
+
+ /**
+ * @return int
+ */
+ function affectedRows() {
+ return $this->mConn->affected_rows;
+ }
+
+ /**
+ * @param $db
+ * @return bool
+ */
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return $this->mConn->select_db( $db );
+ }
+
+ /**
+ * @return string
+ */
+ function getServerVersion() {
+ return $this->mConn->server_info;
+ }
+
+ protected function mysqlFreeResult( $res ) {
+ $res->free_result();
+ return true;
+ }
+
+ protected function mysqlFetchObject( $res ) {
+ $object = $res->fetch_object();
+ if ( $object === null ) {
+ return false;
+ }
+ return $object;
+ }
+
+ protected function mysqlFetchArray( $res ) {
+ $array = $res->fetch_array();
+ if ( $array === null ) {
+ return false;
+ }
+ return $array;
+ }
+
+ protected function mysqlNumRows( $res ) {
+ return $res->num_rows;
+ }
+
+ protected function mysqlNumFields( $res ) {
+ return $res->field_count;
+ }
+
+ protected function mysqlFetchField( $res, $n ) {
+ $field = $res->fetch_field_direct( $n );
+ $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG;
+ $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG;
+ $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG;
+ $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG;
+ $field->binary = $field->flags & MYSQLI_BINARY_FLAG;
+ return $field;
+ }
+
+ protected function mysqlFieldName( $res, $n ) {
+ $field = $res->fetch_field_direct( $n );
+ return $field->name;
+ }
+
+ protected function mysqlDataSeek( $res, $row ) {
+ return $res->data_seek( $row );
+ }
+
+ protected function mysqlError( $conn = null ) {
+ if ($conn === null) {
+ return mysqli_connect_error();
+ } else {
+ return $conn->error;
+ }
+ }
+
+ protected function mysqlRealEscapeString( $s ) {
+ return $this->mConn->real_escape_string( $s );
+ }
+
+ protected function mysqlPing() {
+ return $this->mConn->ping();
+ }
+
+}
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
index 7d8884fb..32d4d984 100644
--- a/includes/db/DatabaseOracle.php
+++ b/includes/db/DatabaseOracle.php
@@ -23,7 +23,7 @@
/**
* The oci8 extension is fairly weak and doesn't support oci_num_rows, among
- * other things. We use a wrapper class to handle that and other
+ * other things. We use a wrapper class to handle that and other
* Oracle-specific bits, like converting column names back to lowercase.
* @ingroup Database
*/
@@ -69,7 +69,7 @@ class ORAResult {
$this->nrows = count( $this->rows );
}
- if ($this->nrows > 0) {
+ if ( $this->nrows > 0 ) {
foreach ( $this->rows[0] as $k => $v ) {
$this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) );
}
@@ -80,7 +80,7 @@ class ORAResult {
}
public function free() {
- unset($this->db);
+ unset( $this->db );
}
public function seek( $row ) {
@@ -92,7 +92,7 @@ class ORAResult {
}
public function numFields() {
- return count($this->columns);
+ return count( $this->columns );
}
public function fetchObject() {
@@ -206,7 +206,7 @@ class DatabaseOracle extends DatabaseBase {
}
function __destruct() {
- if ($this->mOpened) {
+ if ( $this->mOpened ) {
wfSuppressWarnings();
$this->close();
wfRestoreWarnings();
@@ -241,9 +241,15 @@ class DatabaseOracle extends DatabaseBase {
/**
* Usually aborts on failure
+ * @param string $server
+ * @param string $user
+ * @param string $password
+ * @param string $dbName
+ * @throws DBConnectionError
* @return DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
+ global $wgDBOracleDRCP;
if ( !function_exists( 'oci_connect' ) ) {
throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
}
@@ -271,9 +277,16 @@ class DatabaseOracle extends DatabaseBase {
return;
}
+ if ( $wgDBOracleDRCP ) {
+ $this->setFlag( DBO_PERSISTENT );
+ }
+
$session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
+
wfSuppressWarnings();
- if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $this->mConn = oci_pconnect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode );
+ } elseif ( $this->mFlags & DBO_DEFAULT ) {
$this->mConn = oci_new_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode );
} else {
$this->mConn = oci_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode );
@@ -313,13 +326,13 @@ class DatabaseOracle extends DatabaseBase {
protected function doQuery( $sql ) {
wfDebug( "SQL: [$sql]\n" );
- if ( !mb_check_encoding( $sql ) ) {
+ if ( !StringUtils::isUtf8( $sql ) ) {
throw new MWException( "SQL encoding is invalid\n$sql" );
}
// handle some oracle specifics
// remove AS column/table/subquery namings
- if( !$this->getFlag( DBO_DDLMODE ) ) {
+ if ( !$this->getFlag( DBO_DDLMODE ) ) {
$sql = preg_replace( '/ as /i', ' ', $sql );
}
@@ -328,7 +341,7 @@ class DatabaseOracle extends DatabaseBase {
$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' );
+ $explain_id = MWTimestamp::getLocalInstance()->format( 'dmYHis' );
$sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count );
@@ -451,15 +464,15 @@ class DatabaseOracle extends DatabaseBase {
* If errors are explicitly ignored, returns NULL on failure
* @return bool
*/
- function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
return false;
}
- function indexUnique( $table, $index, $fname = 'DatabaseOracle::indexUnique' ) {
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
return false;
}
- function insert( $table, $a, $fname = 'DatabaseOracle::insert', $options = array() ) {
+ function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
if ( !count( $a ) ) {
return true;
}
@@ -488,7 +501,7 @@ class DatabaseOracle extends DatabaseBase {
return $retVal;
}
- private function fieldBindStatement ( $table, $col, &$val, $includeCol = false ) {
+ private function fieldBindStatement( $table, $col, &$val, $includeCol = false ) {
$col_info = $this->fieldInfoMulti( $table, $col );
$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
@@ -619,7 +632,7 @@ class DatabaseOracle extends DatabaseBase {
oci_free_statement( $stmt );
}
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseOracle::insertSelect',
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
@@ -628,7 +641,7 @@ class DatabaseOracle extends DatabaseBase {
}
list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
}
@@ -672,7 +685,7 @@ class DatabaseOracle extends DatabaseBase {
Using uppercase because that's the only way Oracle can handle
quoted tablenames
*/
- switch( $name ) {
+ switch ( $name ) {
case 'user':
$name = 'MWUSER';
break;
@@ -681,12 +694,12 @@ class DatabaseOracle extends DatabaseBase {
break;
}
- return parent::tableName( strtoupper( $name ), $format );
+ return strtoupper( parent::tableName( $name, $format ) );
}
function tableNameInternal( $name ) {
$name = $this->tableName( $name );
- return preg_replace( '/.*\.(.*)/', '$1', $name);
+ return preg_replace( '/.*\.(.*)/', '$1', $name );
}
/**
* Return the next in a sequence, save the value for retrieval via insertId()
@@ -751,14 +764,14 @@ class DatabaseOracle extends DatabaseBase {
function unionQueries( $sqls, $all ) {
$glue = ' UNION ALL ';
- return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ;
+ return 'SELECT * ' . ( $all ? '' : '/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')';
}
function wasDeadlock() {
return $this->lastErrno() == 'OCI-00060';
}
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) {
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
$temporary = $temporary ? 'TRUE' : 'FALSE';
$newName = strtoupper( $newName );
@@ -771,10 +784,10 @@ class DatabaseOracle extends DatabaseBase {
return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', '$oldPrefix', '$newPrefix', $temporary ); END;" );
}
- function listTables( $prefix = null, $fname = 'DatabaseOracle::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
$listWhere = '';
- if (!empty($prefix)) {
- $listWhere = ' AND table_name LIKE \''.strtoupper($prefix).'%\'';
+ if ( !empty( $prefix ) ) {
+ $listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\'';
}
$owner = strtoupper( $this->mDBname );
@@ -782,21 +795,22 @@ class DatabaseOracle extends DatabaseBase {
// dirty code ... i know
$endArray = array();
- $endArray[] = strtoupper($prefix.'MWUSER');
- $endArray[] = strtoupper($prefix.'PAGE');
- $endArray[] = strtoupper($prefix.'IMAGE');
+ $endArray[] = strtoupper( $prefix . 'MWUSER' );
+ $endArray[] = strtoupper( $prefix . 'PAGE' );
+ $endArray[] = strtoupper( $prefix . 'IMAGE' );
$fixedOrderTabs = $endArray;
- while (($row = $result->fetchRow()) !== false) {
- if (!in_array($row['table_name'], $fixedOrderTabs))
+ while ( ( $row = $result->fetchRow() ) !== false ) {
+ if ( !in_array( $row['table_name'], $fixedOrderTabs ) ) {
$endArray[] = $row['table_name'];
+ }
}
return $endArray;
}
- public function dropTable( $tableName, $fName = 'DatabaseOracle::dropTable' ) {
- $tableName = $this->tableName($tableName);
- if( !$this->tableExists( $tableName ) ) {
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ $tableName = $this->tableName( $tableName );
+ if ( !$this->tableExists( $tableName ) ) {
return false;
}
@@ -831,8 +845,8 @@ class DatabaseOracle extends DatabaseBase {
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
- return '[http://www.oracle.com/ Oracle]';
+ public function getSoftwareLink() {
+ return '[{{int:version-db-oracle-url}} Oracle]';
}
/**
@@ -841,7 +855,7 @@ class DatabaseOracle extends DatabaseBase {
function getServerVersion() {
//better version number, fallback on driver
$rset = $this->doQuery( 'SELECT version FROM product_component_version WHERE UPPER(product) LIKE \'ORACLE DATABASE%\'' );
- if ( !( $row = $rset->fetchRow() ) ) {
+ if ( !( $row = $rset->fetchRow() ) ) {
return oci_server_version( $this->mConn );
}
return $row['version'];
@@ -851,7 +865,7 @@ class DatabaseOracle extends DatabaseBase {
* Query whether a given index exists
* @return bool
*/
- function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
+ function indexExists( $table, $index, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
$table = strtoupper( $this->removeIdentifierQuotes( $table ) );
$index = strtoupper( $index );
@@ -869,7 +883,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Query whether a given table exists (in the given schema, or the default mw one if not given)
- * @return int
+ * @return bool
*/
function tableExists( $table, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
@@ -877,13 +891,14 @@ class DatabaseOracle extends DatabaseBase {
$owner = $this->addQuotes( strtoupper( $this->mDBname ) );
$SQL = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table";
$res = $this->doQuery( $SQL );
- if ( $res ) {
- $count = $res->numRows();
- $res->free();
+ if ( $res && $res->numRows() > 0 ) {
+ $exists = true;
} else {
- $count = 0;
+ $exists = false;
}
- return $count;
+
+ $res->free();
+ return $exists;
}
/**
@@ -901,8 +916,8 @@ class DatabaseOracle extends DatabaseBase {
if ( is_array( $table ) ) {
$table = array_map( array( &$this, 'tableNameInternal' ), $table );
$tableWhere = 'IN (';
- foreach( $table as &$singleTable ) {
- $singleTable = $this->removeIdentifierQuotes($singleTable);
+ foreach ( $table as &$singleTable ) {
+ $singleTable = $this->removeIdentifierQuotes( $singleTable );
if ( isset( $this->mFieldInfoCache["$singleTable.$field"] ) ) {
return $this->mFieldInfoCache["$singleTable.$field"];
}
@@ -910,14 +925,14 @@ class DatabaseOracle extends DatabaseBase {
}
$tableWhere = rtrim( $tableWhere, ',' ) . ')';
} else {
- $table = $this->removeIdentifierQuotes( $this->tableNameInternal( $table ) );
+ $table = $this->removeIdentifierQuotes( $this->tableNameInternal( $table ) );
if ( isset( $this->mFieldInfoCache["$table.$field"] ) ) {
return $this->mFieldInfoCache["$table.$field"];
}
- $tableWhere = '= \''.$table.'\'';
+ $tableWhere = '= \'' . $table . '\'';
}
- $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name '.$tableWhere.' and column_name = \''.$field.'\'' );
+ $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name ' . $tableWhere . ' and column_name = \'' . $field . '\'' );
if ( oci_execute( $fieldInfoStmt, $this->execFlags() ) === false ) {
$e = oci_error( $fieldInfoStmt );
$this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ );
@@ -926,7 +941,7 @@ class DatabaseOracle extends DatabaseBase {
$res = new ORAResult( $this, $fieldInfoStmt );
if ( $res->numRows() == 0 ) {
if ( is_array( $table ) ) {
- foreach( $table as &$singleTable ) {
+ foreach ( $table as &$singleTable ) {
$this->mFieldInfoCache["$singleTable.$field"] = false;
}
} else {
@@ -952,15 +967,15 @@ class DatabaseOracle extends DatabaseBase {
if ( is_array( $table ) ) {
throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' );
}
- return $this->fieldInfoMulti ($table, $field);
+ return $this->fieldInfoMulti( $table, $field );
}
- protected function doBegin( $fname = 'DatabaseOracle::begin' ) {
+ protected function doBegin( $fname = __METHOD__ ) {
$this->mTrxLevel = 1;
$this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' );
}
- protected function doCommit( $fname = 'DatabaseOracle::commit' ) {
+ protected function doCommit( $fname = __METHOD__ ) {
if ( $this->mTrxLevel ) {
$ret = oci_commit( $this->mConn );
if ( !$ret ) {
@@ -971,7 +986,7 @@ class DatabaseOracle extends DatabaseBase {
}
}
- protected function doRollback( $fname = 'DatabaseOracle::rollback' ) {
+ protected function doRollback( $fname = __METHOD__ ) {
if ( $this->mTrxLevel ) {
oci_rollback( $this->mConn );
$this->mTrxLevel = 0;
@@ -981,7 +996,7 @@ class DatabaseOracle extends DatabaseBase {
/* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */
function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = 'DatabaseOracle::sourceStream', $inputCallback = false ) {
+ $fname = __METHOD__, $inputCallback = false ) {
$cmd = '';
$done = false;
$dollarquote = false;
@@ -1061,7 +1076,7 @@ class DatabaseOracle extends DatabaseBase {
if ( $db == null || $db == $this->mUser ) {
return true;
}
- $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper($db);
+ $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db );
$stmt = oci_parse( $this->mConn, $sql );
wfSuppressWarnings();
$success = oci_execute( $stmt );
@@ -1096,11 +1111,11 @@ class DatabaseOracle extends DatabaseBase {
}
public function removeIdentifierQuotes( $s ) {
- return strpos($s, '/*Q*/') === FALSE ? $s : substr($s, 5);
+ return strpos( $s, '/*Q*/' ) === false ? $s : substr( $s, 5 );
}
public function isQuotedIdentifier( $s ) {
- return strpos($s, '/*Q*/') !== FALSE;
+ return strpos( $s, '/*Q*/' ) !== false;
}
private function wrapFieldForWhere( $table, &$col, &$val ) {
@@ -1111,21 +1126,21 @@ class DatabaseOracle extends DatabaseBase {
if ( $col_type == 'CLOB' ) {
$col = 'TO_CHAR(' . $col . ')';
$val = $wgContLang->checkTitleEncoding( $val );
- } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) {
+ } elseif ( $col_type == 'VARCHAR2' ) {
$val = $wgContLang->checkTitleEncoding( $val );
}
}
- private function wrapConditionsForWhere ( $table, $conds, $parentCol = null ) {
+ private function wrapConditionsForWhere( $table, $conds, $parentCol = null ) {
$conds2 = array();
foreach ( $conds as $col => $val ) {
if ( is_array( $val ) ) {
- $conds2[$col] = $this->wrapConditionsForWhere ( $table, $val, $col );
+ $conds2[$col] = $this->wrapConditionsForWhere( $table, $val, $col );
} else {
if ( is_numeric( $col ) && $parentCol != null ) {
- $this->wrapFieldForWhere ( $table, $parentCol, $val );
+ $this->wrapFieldForWhere( $table, $parentCol, $val );
} else {
- $this->wrapFieldForWhere ( $table, $col, $val );
+ $this->wrapFieldForWhere( $table, $col, $val );
}
$conds2[$col] = $val;
}
@@ -1133,8 +1148,8 @@ class DatabaseOracle extends DatabaseBase {
return $conds2;
}
- function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) {
- if ( is_array($conds) ) {
+ function selectRow( $table, $vars, $conds, $fname = __METHOD__, $options = array(), $join_conds = array() ) {
+ if ( is_array( $conds ) ) {
$conds = $this->wrapConditionsForWhere( $table, $conds );
}
return parent::selectRow( $table, $vars, $conds, $fname, $options, $join_conds );
@@ -1146,7 +1161,7 @@ class DatabaseOracle extends DatabaseBase {
*
* @private
*
- * @param $options Array: an associative array of options to be turned into
+ * @param array $options an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
*/
@@ -1161,15 +1176,14 @@ class DatabaseOracle extends DatabaseBase {
}
}
- if ( isset( $options['GROUP BY'] ) ) {
- $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
- }
- if ( isset( $options['ORDER BY'] ) ) {
- $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+ $preLimitTail .= $this->makeGroupByWithHaving( $options );
+
+ $preLimitTail .= $this->makeOrderBy( $options );
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
+ $postLimitTail .= ' FOR UPDATE';
}
- # 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';
}
@@ -1183,14 +1197,14 @@ class DatabaseOracle extends DatabaseBase {
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
- public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) {
- if ( is_array($conds) ) {
+ public function delete( $table, $conds, $fname = __METHOD__ ) {
+ if ( is_array( $conds ) ) {
$conds = $this->wrapConditionsForWhere( $table, $conds );
}
// a hack for deleting pages, users and images (which have non-nullable FKs)
// all deletions on these tables have transactions so final failure rollbacks these updates
$table = $this->tableName( $table );
- if ( $table == $this->tableName( 'user' ) ) {
+ if ( $table == $this->tableName( 'user' ) ) {
$this->update( 'archive', array( 'ar_user' => 0 ), array( 'ar_user' => $conds['user_id'] ), $fname );
$this->update( 'ipblocks', array( 'ipb_user' => 0 ), array( 'ipb_user' => $conds['user_id'] ), $fname );
$this->update( 'image', array( 'img_user' => 0 ), array( 'img_user' => $conds['user_id'] ), $fname );
@@ -1200,13 +1214,13 @@ class DatabaseOracle extends DatabaseBase {
$this->update( 'uploadstash', array( 'us_user' => 0 ), array( 'us_user' => $conds['user_id'] ), $fname );
$this->update( 'recentchanges', array( 'rc_user' => 0 ), array( 'rc_user' => $conds['user_id'] ), $fname );
$this->update( 'logging', array( 'log_user' => 0 ), array( 'log_user' => $conds['user_id'] ), $fname );
- } elseif ( $table == $this->tableName( 'image' ) ) {
+ } elseif ( $table == $this->tableName( 'image' ) ) {
$this->update( 'oldimage', array( 'oi_name' => 0 ), array( 'oi_name' => $conds['img_name'] ), $fname );
}
return parent::delete( $table, $conds, $fname );
}
- function update( $table, $values, $conds, $fname = 'DatabaseOracle::update', $options = array() ) {
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
global $wgContLang;
$table = $this->tableName( $table );
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
index 457bf384..aed35f10 100644
--- a/includes/db/DatabasePostgres.php
+++ b/includes/db/DatabasePostgres.php
@@ -117,7 +117,7 @@ SQL;
* @since 1.19
*/
function defaultValue() {
- if( $this->has_default ) {
+ if ( $this->has_default ) {
return $this->default;
} else {
return false;
@@ -139,15 +139,15 @@ class PostgresTransactionState {
array(
"desc" => "%s: Connection state changed from %s -> %s\n",
"states" => array(
- PGSQL_CONNECTION_OK => "OK",
- PGSQL_CONNECTION_BAD => "BAD"
+ PGSQL_CONNECTION_OK => "OK",
+ PGSQL_CONNECTION_BAD => "BAD"
)
),
array(
"desc" => "%s: Transaction state changed from %s -> %s\n",
"states" => array(
- PGSQL_TRANSACTION_IDLE => "IDLE",
- PGSQL_TRANSACTION_ACTIVE => "ACTIVE",
+ PGSQL_TRANSACTION_IDLE => "IDLE",
+ PGSQL_TRANSACTION_ACTIVE => "ACTIVE",
PGSQL_TRANSACTION_INTRANS => "TRANS",
PGSQL_TRANSACTION_INERROR => "ERROR",
PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN"
@@ -176,8 +176,8 @@ class PostgresTransactionState {
$old = reset( $this->mCurrentState );
$new = reset( $this->mNewState );
foreach ( self::$WATCHED as $watched ) {
- if ($old !== $new) {
- $this->log_changed($old, $new, $watched);
+ if ( $old !== $new ) {
+ $this->log_changed( $old, $new, $watched );
}
$old = next( $this->mCurrentState );
$new = next( $this->mNewState );
@@ -189,7 +189,7 @@ class PostgresTransactionState {
}
protected function describe_changed( $status, $desc_table ) {
- if( isset( $desc_table[$status] ) ) {
+ if ( isset( $desc_table[$status] ) ) {
return $desc_table[$status];
} else {
return "STATUS " . $status;
@@ -197,11 +197,11 @@ class PostgresTransactionState {
}
protected function log_changed( $old, $new, $watched ) {
- wfDebug(sprintf($watched["desc"],
+ wfDebug( sprintf( $watched["desc"],
$this->mConn,
$this->describe_changed( $old, $watched["states"] ),
- $this->describe_changed( $new, $watched["states"] ))
- );
+ $this->describe_changed( $new, $watched["states"] )
+ ) );
}
}
@@ -218,7 +218,7 @@ class SavepointPostgres {
protected $id;
protected $didbegin;
- public function __construct ($dbw, $id) {
+ public function __construct( $dbw, $id ) {
$this->dbw = $dbw;
$this->id = $id;
$this->didbegin = false;
@@ -232,12 +232,14 @@ class SavepointPostgres {
public function __destruct() {
if ( $this->didbegin ) {
$this->dbw->rollback();
+ $this->didbegin = false;
}
}
public function commit() {
if ( $this->didbegin ) {
$this->dbw->commit();
+ $this->didbegin = false;
}
}
@@ -245,29 +247,29 @@ class SavepointPostgres {
global $wgDebugDBTransactions;
if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
if ( $wgDebugDBTransactions ) {
- wfDebug( sprintf ($msg_ok, $this->id ) );
+ wfDebug( sprintf ( $msg_ok, $this->id ) );
}
} else {
- wfDebug( sprintf ($msg_failed, $this->id ) );
+ wfDebug( sprintf ( $msg_failed, $this->id ) );
}
}
public function savepoint() {
- $this->query("SAVEPOINT",
+ $this->query( "SAVEPOINT",
"Transaction state: savepoint \"%s\" established.\n",
"Transaction state: establishment of savepoint \"%s\" FAILED.\n"
);
}
public function release() {
- $this->query("RELEASE",
+ $this->query( "RELEASE",
"Transaction state: savepoint \"%s\" released.\n",
"Transaction state: release of savepoint \"%s\" FAILED.\n"
);
}
public function rollback() {
- $this->query("ROLLBACK TO",
+ $this->query( "ROLLBACK TO",
"Transaction state: savepoint \"%s\" rolled back.\n",
"Transaction state: rollback of savepoint \"%s\" FAILED.\n"
);
@@ -318,13 +320,18 @@ class DatabasePostgres extends DatabaseBase {
function hasConstraint( $name ) {
$SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'";
+ pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
$res = $this->doQuery( $SQL );
return $this->numRows( $res );
}
/**
* Usually aborts on failure
+ * @param string $server
+ * @param string $user
+ * @param string $password
+ * @param string $dbName
+ * @throws DBConnectionError
* @return DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
@@ -386,6 +393,9 @@ class DatabasePostgres extends DatabaseBase {
$this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
$this->query( "SET timezone = 'GMT'", __METHOD__ );
$this->query( "SET standard_conforming_strings = on", __METHOD__ );
+ if ( $this->getServerVersion() >= 9.0 ) {
+ $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
+ }
global $wgDBmwschema;
$this->determineCoreSchema( $wgDBmwschema );
@@ -428,7 +438,7 @@ class DatabasePostgres extends DatabaseBase {
$sql = mb_convert_encoding( $sql, 'UTF-8' );
}
$this->mTransactionState->check();
- if( pg_send_query( $this->mConn, $sql ) === false ) {
+ if ( pg_send_query( $this->mConn, $sql ) === false ) {
throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
}
$this->mLastResult = pg_get_result( $this->mConn );
@@ -440,7 +450,7 @@ class DatabasePostgres extends DatabaseBase {
return $this->mLastResult;
}
- protected function dumpError () {
+ protected function dumpError() {
$diags = array( PGSQL_DIAG_SEVERITY,
PGSQL_DIAG_SQLSTATE,
PGSQL_DIAG_MESSAGE_PRIMARY,
@@ -454,7 +464,7 @@ class DatabasePostgres extends DatabaseBase {
PGSQL_DIAG_SOURCE_LINE,
PGSQL_DIAG_SOURCE_FUNCTION );
foreach ( $diags as $d ) {
- wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) );
+ wfDebug( sprintf( "PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) );
}
}
@@ -472,8 +482,7 @@ class DatabasePostgres extends DatabaseBase {
parent::reportQueryError( $error, $errno, $sql, $fname, false );
}
-
- function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) {
+ function queryIgnore( $sql, $fname = __METHOD__ ) {
return $this->query( $sql, $fname, true );
}
@@ -500,7 +509,7 @@ class DatabasePostgres extends DatabaseBase {
# @todo hashar: not sure if the following test really trigger if the object
# fetching failed.
- if( pg_last_error( $this->mConn ) ) {
+ if ( pg_last_error( $this->mConn ) ) {
throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
}
return $row;
@@ -513,7 +522,7 @@ class DatabasePostgres extends DatabaseBase {
wfSuppressWarnings();
$row = pg_fetch_array( $res );
wfRestoreWarnings();
- if( pg_last_error( $this->mConn ) ) {
+ if ( pg_last_error( $this->mConn ) ) {
throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
}
return $row;
@@ -526,7 +535,7 @@ class DatabasePostgres extends DatabaseBase {
wfSuppressWarnings();
$n = pg_num_rows( $res );
wfRestoreWarnings();
- if( pg_last_error( $this->mConn ) ) {
+ if ( pg_last_error( $this->mConn ) ) {
throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
}
return $n;
@@ -547,8 +556,10 @@ class DatabasePostgres extends DatabaseBase {
}
/**
- * This must be called after nextSequenceVal
- * @return null
+ * Return the result of the last call to nextSequenceValue();
+ * This must be called after nextSequenceValue().
+ *
+ * @return integer|null
*/
function insertId() {
return $this->mInsertId;
@@ -585,7 +596,7 @@ class DatabasePostgres extends DatabaseBase {
// Forced result for simulated queries
return $this->mAffectedRows;
}
- if( empty( $this->mLastResult ) ) {
+ if ( empty( $this->mLastResult ) ) {
return 0;
}
return pg_affected_rows( $this->mLastResult );
@@ -599,14 +610,14 @@ class DatabasePostgres extends DatabaseBase {
* Takes same arguments as Database::select()
* @return int
*/
- function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
+ function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) {
$options['EXPLAIN'] = true;
$res = $this->select( $table, $vars, $conds, $fname, $options );
$rows = -1;
if ( $res ) {
$row = $this->fetchRow( $res );
$count = array();
- if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+ if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
$rows = $count[1];
}
}
@@ -618,7 +629,7 @@ class DatabasePostgres extends DatabaseBase {
* If errors are explicitly ignored, returns NULL on failure
* @return bool|null
*/
- function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
$res = $this->query( $sql, $fname );
if ( !$res ) {
@@ -638,9 +649,10 @@ class DatabasePostgres extends DatabaseBase {
* @since 1.19
* @return Array
*/
- function indexAttributes ( $index, $schema = false ) {
- if ( $schema === false )
+ function indexAttributes( $index, $schema = false ) {
+ if ( $schema === false ) {
$schema = $this->getCoreSchema();
+ }
/*
* A subquery would be not needed if we didn't care about the order
* of attributes, but we do
@@ -677,7 +689,7 @@ class DatabasePostgres extends DatabaseBase {
AND i.indclass[s.g] = opcls.oid
AND pg_am.oid = opcls.opcmethod
__INDEXATTR__;
- $res = $this->query($sql, __METHOD__);
+ $res = $this->query( $sql, __METHOD__ );
$a = array();
if ( $res ) {
foreach ( $res as $row ) {
@@ -685,7 +697,7 @@ __INDEXATTR__;
$row->attname,
$row->opcname,
$row->amname,
- $row->option);
+ $row->option );
}
} else {
return null;
@@ -693,9 +705,8 @@ __INDEXATTR__;
return $a;
}
-
- function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
" AND indexdef LIKE 'CREATE UNIQUE%(" .
$this->strencode( $this->indexName( $index ) ) .
")'";
@@ -710,6 +721,29 @@ __INDEXATTR__;
}
/**
+ * Change the FOR UPDATE option as necessary based on the join conditions. Then pass
+ * to the parent function to get the actual SQL text.
+ *
+ * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
+ * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do
+ * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly.
+ */
+ function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) {
+ $forUpdateKey = array_search( 'FOR UPDATE', $options );
+ if ( $forUpdateKey !== false && $join_conds ) {
+ unset( $options[$forUpdateKey] );
+
+ foreach ( $join_conds as $table => $join_cond ) {
+ if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
+ $options['FOR UPDATE'][] = $table;
+ }
+ }
+ }
+
+ return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+ }
+
+ /**
* INSERT wrapper, inserts an array into a table
*
* $args may be a single associative array, or an array of these with numeric keys,
@@ -718,17 +752,17 @@ __INDEXATTR__;
* @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
+ * @param string $options or Array. Valid options: IGNORE
*
* @return bool Success of insert operation. IGNORE always returns true.
*/
- function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
+ function insert( $table, $args, $fname = __METHOD__, $options = array() ) {
if ( !count( $args ) ) {
return true;
}
$table = $this->tableName( $table );
- if (! isset( $this->numeric_version ) ) {
+ if ( !isset( $this->numeric_version ) ) {
$this->getServerVersion();
}
@@ -839,12 +873,12 @@ __INDEXATTR__;
* @todo FIXME: Implement this a little better (seperate select/insert)?
* @return bool
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
- if( !is_array( $insertOptions ) ) {
+ if ( !is_array( $insertOptions ) ) {
$insertOptions = array( $insertOptions );
}
@@ -860,11 +894,11 @@ __INDEXATTR__;
$savepoint->savepoint();
}
- if( !is_array( $selectOptions ) ) {
+ if ( !is_array( $selectOptions ) ) {
$selectOptions = array( $selectOptions );
}
list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if( is_array( $srcTable ) ) {
+ if ( is_array( $srcTable ) ) {
$srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
@@ -881,9 +915,9 @@ __INDEXATTR__;
$sql .= " $tailOpts";
$res = (bool)$this->query( $sql, $fname, $savepoint );
- if( $savepoint ) {
+ if ( $savepoint ) {
$bar = pg_last_error();
- if( $bar != false ) {
+ if ( $bar != false ) {
$savepoint->rollback();
} else {
$savepoint->release();
@@ -904,7 +938,7 @@ __INDEXATTR__;
function tableName( $name, $format = 'quoted' ) {
# Replace reserved words with better ones
- switch( $name ) {
+ switch ( $name ) {
case 'user':
return $this->realTableName( 'mwuser', $format );
case 'text':
@@ -950,7 +984,7 @@ __INDEXATTR__;
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 );
+ $res = $this->query( $sql );
$row = $this->fetchObject( $res );
if ( $row->ftype == 'varchar' ) {
$size = $row->size - 4;
@@ -968,21 +1002,21 @@ __INDEXATTR__;
return $this->lastErrno() == '40P01';
}
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) {
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
$newName = $this->addIdentifierQuotes( $newName );
$oldName = $this->addIdentifierQuotes( $oldName );
return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname );
}
- function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
$eschema = $this->addQuotes( $this->getCoreSchema() );
$result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
$endArray = array();
- foreach( $result as $table ) {
- $vars = get_object_vars($table);
+ foreach ( $result as $table ) {
+ $vars = get_object_vars( $table );
$table = array_pop( $vars );
- if( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
$endArray[] = $table;
}
}
@@ -1013,26 +1047,26 @@ __INDEXATTR__;
* @return string
*/
function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
- if( false === $limit ) {
- $limit = strlen( $text )-1;
+ if ( false === $limit ) {
+ $limit = strlen( $text ) - 1;
$output = array();
}
- if( '{}' == $text ) {
+ if ( '{}' == $text ) {
return $output;
}
do {
- if ( '{' != $text{$offset} ) {
+ if ( '{' != $text[$offset] ) {
preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
$text, $match, 0, $offset );
$offset += strlen( $match[0] );
- $output[] = ( '"' != $match[1]{0}
+ $output[] = ( '"' != $match[1][0]
? $match[1]
: stripcslashes( substr( $match[1], 1, -1 ) ) );
if ( '},' == $match[3] ) {
return $output;
}
} else {
- $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 );
+ $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
}
} while ( $limit > $offset );
return $output;
@@ -1048,11 +1082,10 @@ __INDEXATTR__;
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
- return '[http://www.postgresql.org/ PostgreSQL]';
+ public function getSoftwareLink() {
+ return '[{{int:version-db-postgres-url}} PostgreSQL]';
}
-
/**
* Return current schema (executes SELECT current_schema())
* Needs transaction
@@ -1061,7 +1094,7 @@ __INDEXATTR__;
* @return string return default schema for the current session
*/
function getCurrentSchema() {
- $res = $this->query( "SELECT current_schema()", __METHOD__);
+ $res = $this->query( "SELECT current_schema()", __METHOD__ );
$row = $this->fetchRow( $res );
return $row[0];
}
@@ -1071,17 +1104,17 @@ __INDEXATTR__;
* This is list does not contain magic keywords like "$user"
* Needs transaction
*
- * @seealso getSearchPath()
- * @seealso setSearchPath()
+ * @see getSearchPath()
+ * @see setSearchPath()
* @since 1.19
* @return array list of actual schemas for the current sesson
*/
function getSchemas() {
- $res = $this->query( "SELECT current_schemas(false)", __METHOD__);
+ $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
$row = $this->fetchRow( $res );
$schemas = array();
/* PHP pgsql support does not support array type, "{a,b}" string is returned */
- return $this->pg_array_parse($row[0], $schemas);
+ return $this->pg_array_parse( $row[0], $schemas );
}
/**
@@ -1094,10 +1127,10 @@ __INDEXATTR__;
* @return array how to search for table names schemas for the current user
*/
function getSearchPath() {
- $res = $this->query( "SHOW search_path", __METHOD__);
+ $res = $this->query( "SHOW search_path", __METHOD__ );
$row = $this->fetchRow( $res );
/* PostgreSQL returns SHOW values as strings */
- return explode(",", $row[0]);
+ return explode( ",", $row[0] );
}
/**
@@ -1108,7 +1141,7 @@ __INDEXATTR__;
* @param $search_path array list of schemas to be searched by default
*/
function setSearchPath( $search_path ) {
- $this->query( "SET search_path = " . implode(", ", $search_path) );
+ $this->query( "SET search_path = " . implode( ", ", $search_path ) );
}
/**
@@ -1129,7 +1162,7 @@ __INDEXATTR__;
if ( $this->schemaExists( $desired_schema ) ) {
if ( in_array( $desired_schema, $this->getSchemas() ) ) {
$this->mCoreSchema = $desired_schema;
- wfDebug("Schema \"" . $desired_schema . "\" already in the search path\n");
+ wfDebug( "Schema \"" . $desired_schema . "\" already in the search path\n" );
} else {
/**
* Prepend our schema (e.g. 'mediawiki') in front
@@ -1141,11 +1174,11 @@ __INDEXATTR__;
$this->addIdentifierQuotes( $desired_schema ));
$this->setSearchPath( $search_path );
$this->mCoreSchema = $desired_schema;
- wfDebug("Schema \"" . $desired_schema . "\" added to the search path\n");
+ wfDebug( "Schema \"" . $desired_schema . "\" added to the search path\n" );
}
} else {
$this->mCoreSchema = $this->getCurrentSchema();
- wfDebug("Schema \"" . $desired_schema . "\" not found, using current \"". $this->mCoreSchema ."\"\n");
+ wfDebug( "Schema \"" . $desired_schema . "\" not found, using current \"" . $this->mCoreSchema . "\"\n" );
}
/* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
$this->commit( __METHOD__ );
@@ -1251,8 +1284,8 @@ SQL;
}
function constraintExists( $table, $constraint ) {
- $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints ".
- "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+ $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
$this->addQuotes( $this->getCoreSchema() ),
$this->addQuotes( $table ),
$this->addQuotes( $constraint )
@@ -1340,7 +1373,7 @@ SQL;
*
* @private
*
- * @param $ins String: SQL string, read from a stream (usually tables.sql)
+ * @param string $ins SQL string, read from a stream (usually tables.sql)
*
* @return string SQL string
*/
@@ -1364,7 +1397,7 @@ SQL;
*
* @private
*
- * @param $options Array: an associative array of options to be turned into
+ * @param array $options an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
*/
@@ -1379,23 +1412,9 @@ SQL;
}
}
- if ( isset( $options['GROUP BY'] ) ) {
- $gb = is_array( $options['GROUP BY'] )
- ? implode( ',', $options['GROUP BY'] )
- : $options['GROUP BY'];
- $preLimitTail .= " GROUP BY {$gb}";
- }
-
- if ( isset( $options['HAVING'] ) ) {
- $preLimitTail .= " HAVING {$options['HAVING']}";
- }
+ $preLimitTail .= $this->makeGroupByWithHaving( $options );
- if ( isset( $options['ORDER BY'] ) ) {
- $ob = is_array( $options['ORDER BY'] )
- ? implode( ',', $options['ORDER BY'] )
- : $options['ORDER BY'];
- $preLimitTail .= " ORDER BY {$ob}";
- }
+ $preLimitTail .= $this->makeOrderBy( $options );
//if ( isset( $options['LIMIT'] ) ) {
// $tailOpts .= $this->limitResult( '', $options['LIMIT'],
@@ -1403,9 +1422,12 @@ SQL;
// : false );
//}
- if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
+ if ( isset( $options['FOR UPDATE'] ) ) {
+ $postLimitTail .= ' FOR UPDATE OF ' . implode( ', ', $options['FOR UPDATE'] );
+ } else if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
$postLimitTail .= ' FOR UPDATE';
}
+
if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
$startOpts .= 'DISTINCT';
}
@@ -1413,7 +1435,8 @@ SQL;
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
- function setFakeMaster( $enabled = true ) {}
+ function setFakeMaster( $enabled = true ) {
+ }
function getDBname() {
return $this->mDBname;
@@ -1443,4 +1466,65 @@ SQL;
}
return parent::streamStatementEnd( $sql, $newLine );
}
+
+ /**
+ * Check to see if a named lock is available. This is non-blocking.
+ * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ *
+ * @param string $lockName name of lock to poll
+ * @param string $method name of method calling us
+ * @return Boolean
+ * @since 1.20
+ */
+ public function lockIsFree( $lockName, $method ) {
+ $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+ $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
+ WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ return ( $row->lockstatus === 't' );
+ }
+
+ /**
+ * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ * @param $lockName string
+ * @param $method string
+ * @param $timeout int
+ * @return bool
+ */
+ public function lock( $lockName, $method, $timeout = 5 ) {
+ $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+ for ( $attempts = 1; $attempts <= $timeout; ++$attempts ) {
+ $result = $this->query(
+ "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ if ( $row->lockstatus === 't' ) {
+ return true;
+ } else {
+ sleep( 1 );
+ }
+ }
+ wfDebug( __METHOD__ . " failed to acquire lock\n" );
+ return false;
+ }
+
+ /**
+ * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ * @param $lockName string
+ * @param $method string
+ * @return bool
+ */
+ public function unlock( $lockName, $method ) {
+ $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+ $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ return ( $row->lockstatus === 't' );
+ }
+
+ /**
+ * @param string $lockName
+ * @return string Integer
+ */
+ private function bigintFromLockName( $lockName ) {
+ return wfBaseConvert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
+ }
} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
index f1e553d7..3e034649 100644
--- a/includes/db/DatabaseSqlite.php
+++ b/includes/db/DatabaseSqlite.php
@@ -52,9 +52,9 @@ class DatabaseSqlite extends DatabaseBase {
$this->mName = $dbName;
parent::__construct( $server, $user, $password, $dbName, $flags );
// parent doesn't open when $user is false, but we can work with $dbName
- if( $dbName ) {
+ if ( $dbName && !$this->isOpen() ) {
global $wgSharedDB;
- if( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) {
+ if ( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) {
$this->attachDatabase( $wgSharedDB );
}
}
@@ -68,7 +68,7 @@ class DatabaseSqlite extends DatabaseBase {
}
/**
- * @todo: check if it should be true like parent class
+ * @todo Check if it should be true like parent class
*
* @return bool
*/
@@ -79,16 +79,18 @@ class DatabaseSqlite extends DatabaseBase {
/** Open an SQLite database and return a resource handle to it
* NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
*
- * @param $server
- * @param $user
- * @param $pass
- * @param $dbName
+ * @param string $server
+ * @param string $user
+ * @param string $pass
+ * @param string $dbName
*
+ * @throws DBConnectionError
* @return PDO
*/
function open( $server, $user, $pass, $dbName ) {
global $wgSQLiteDataDir;
+ $this->close();
$fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
if ( !is_readable( $fileName ) ) {
$this->mConn = false;
@@ -103,6 +105,7 @@ class DatabaseSqlite extends DatabaseBase {
*
* @param $fileName string
*
+ * @throws DBConnectionError
* @return PDO|bool SQL connection or false if failed
*/
function openFile( $fileName ) {
@@ -125,6 +128,8 @@ class DatabaseSqlite extends DatabaseBase {
# set error codes only, don't raise exceptions
if ( $this->mOpened ) {
$this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+ # Enforce LIKE to be case sensitive, just like MySQL
+ $this->query( 'PRAGMA case_sensitive_like = 1' );
return true;
}
}
@@ -140,8 +145,8 @@ class DatabaseSqlite extends DatabaseBase {
/**
* Generates a database file name. Explicitly public for installer.
- * @param $dir String: Directory where database resides
- * @param $dbName String: Database name
+ * @param string $dir Directory where database resides
+ * @param string $dbName Database name
* @return String
*/
public static function generateFileName( $dir, $dbName ) {
@@ -159,7 +164,7 @@ class DatabaseSqlite extends DatabaseBase {
$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
if ( $res ) {
$row = $res->fetchRow();
- self::$fulltextEnabled = stristr($row['sql'], 'fts' ) !== false;
+ self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
}
}
return self::$fulltextEnabled;
@@ -190,13 +195,13 @@ class DatabaseSqlite extends DatabaseBase {
* Attaches external database to our connection, see http://sqlite.org/lang_attach.html
* for details.
*
- * @param $name String: database name to be used in queries like SELECT foo FROM dbname.table
- * @param $file String: database file name. If omitted, will be generated using $name and $wgSQLiteDataDir
- * @param $fname String: calling function name
+ * @param string $name database name to be used in queries like SELECT foo FROM dbname.table
+ * @param string $file database file name. If omitted, will be generated using $name and $wgSQLiteDataDir
+ * @param string $fname calling function name
*
* @return ResultWrapper
*/
- function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) {
+ function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
global $wgSQLiteDataDir;
if ( !$file ) {
$file = self::generateFileName( $wgSQLiteDataDir, $name );
@@ -248,7 +253,7 @@ class DatabaseSqlite extends DatabaseBase {
/**
* @param $res ResultWrapper
- * @return
+ * @return object|bool
*/
function fetchObject( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -274,7 +279,7 @@ class DatabaseSqlite extends DatabaseBase {
/**
* @param $res ResultWrapper
- * @return bool|mixed
+ * @return array|bool
*/
function fetchRow( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -416,7 +421,7 @@ class DatabaseSqlite extends DatabaseBase {
*
* @return array
*/
- function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
$res = $this->query( $sql, $fname );
if ( !$res ) {
@@ -438,7 +443,7 @@ class DatabaseSqlite extends DatabaseBase {
* @param $fname string
* @return bool|null
*/
- function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
$row = $this->selectRow( 'sqlite_master', '*',
array(
'type' => 'index',
@@ -467,7 +472,7 @@ class DatabaseSqlite extends DatabaseBase {
*/
function makeSelectOptions( $options ) {
foreach ( $options as $k => $v ) {
- if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) {
+ if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
$options[$k] = '';
}
}
@@ -510,7 +515,7 @@ class DatabaseSqlite extends DatabaseBase {
* Based on generic method (parent) with some prior SQLite-sepcific adjustments
* @return bool
*/
- function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
+ function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
if ( !count( $a ) ) {
return true;
}
@@ -537,8 +542,10 @@ class DatabaseSqlite extends DatabaseBase {
* @param $fname string
* @return bool|ResultWrapper
*/
- function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
- if ( !count( $rows ) ) return true;
+ function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+ 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] ) ) {
@@ -593,7 +600,7 @@ class DatabaseSqlite extends DatabaseBase {
* @return bool
*/
function wasErrorReissuable() {
- return $this->lastErrno() == 17; // SQLITE_SCHEMA;
+ return $this->lastErrno() == 17; // SQLITE_SCHEMA;
}
/**
@@ -606,8 +613,8 @@ class DatabaseSqlite extends DatabaseBase {
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
- return "[http://sqlite.org/ SQLite]";
+ public function getSoftwareLink() {
+ return "[{{int:version-db-sqlite-url}} SQLite]";
}
/**
@@ -649,7 +656,11 @@ class DatabaseSqlite extends DatabaseBase {
if ( $this->mTrxLevel == 1 ) {
$this->commit( __METHOD__ );
}
- $this->mConn->beginTransaction();
+ try {
+ $this->mConn->beginTransaction();
+ } catch ( PDOException $e ) {
+ throw new DBUnexpectedError( $this, 'Error in BEGIN query: ' . $e->getMessage() );
+ }
$this->mTrxLevel = 1;
}
@@ -657,7 +668,11 @@ class DatabaseSqlite extends DatabaseBase {
if ( $this->mTrxLevel == 0 ) {
return;
}
- $this->mConn->commit();
+ try {
+ $this->mConn->commit();
+ } catch ( PDOException $e ) {
+ throw new DBUnexpectedError( $this, 'Error in COMMIT query: ' . $e->getMessage() );
+ }
$this->mTrxLevel = 0;
}
@@ -703,6 +718,16 @@ class DatabaseSqlite extends DatabaseBase {
function addQuotes( $s ) {
if ( $s instanceof Blob ) {
return "x'" . bin2hex( $s->fetch() ) . "'";
+ } elseif ( is_bool( $s ) ) {
+ return (int)$s;
+ } elseif ( strpos( $s, "\0" ) !== false ) {
+ // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
+ // This is a known limitation of SQLite's mprintf function which PDO should work around,
+ // but doesn't. I have reported this to php.net as bug #63419:
+ // https://bugs.php.net/bug.php?id=63419
+ // There was already a similar report for SQLite3::escapeString, bug #62361:
+ // https://bugs.php.net/bug.php?id=62361
+ return "x'" . bin2hex( $s ) . "'";
} else {
return $this->mConn->quote( $s );
}
@@ -801,7 +826,7 @@ class DatabaseSqlite extends DatabaseBase {
* @param $fname string
* @return bool|ResultWrapper
*/
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname );
$obj = $this->fetchObject( $res );
if ( !$obj ) {
@@ -819,16 +844,15 @@ class DatabaseSqlite extends DatabaseBase {
return $this->query( $sql, $fname );
}
-
/**
* List all tables on the database
*
- * @param $prefix string Only show tables with this prefix, e.g. mw_
- * @param $fname String: calling function name
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname calling function name
*
* @return array
*/
- function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
$result = $this->select(
'sqlite_master',
'name',
@@ -837,11 +861,11 @@ class DatabaseSqlite extends DatabaseBase {
$endArray = array();
- foreach( $result as $table ) {
- $vars = get_object_vars($table);
+ foreach ( $result as $table ) {
+ $vars = get_object_vars( $table );
$table = array_pop( $vars );
- if( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
if ( strpos( $table, 'sqlite_' ) !== 0 ) {
$endArray[] = $table;
}
diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php
index c846788d..de58bab6 100644
--- a/includes/db/DatabaseUtility.php
+++ b/includes/db/DatabaseUtility.php
@@ -1,6 +1,6 @@
<?php
/**
- * This file contains database-related utiliy classes.
+ * This file contains database-related utility classes.
*
* 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
@@ -138,7 +138,7 @@ class ResultWrapper implements Iterator {
/**
* Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
+ * form. Fields are retrieved with $row['fieldname'].
*
* @return Array
* @throws DBUnexpectedError Thrown if the database returns an error
@@ -219,9 +219,9 @@ class ResultWrapper implements Iterator {
* 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 $result = array();
+ var $db = null; // And it's going to stay that way :D
+ var $pos = 0;
var $currentRow = null;
function __construct( $array ) {
@@ -253,7 +253,8 @@ class FakeResultWrapper extends ResultWrapper {
$this->pos = $row;
}
- function free() {}
+ function free() {
+ }
// Callers want to be able to access fields with $this->fieldName
function fetchObject() {
@@ -285,7 +286,7 @@ class LikeMatch {
/**
* Store a string into a LikeMatch marker object.
*
- * @param String $s
+ * @param string $s
*/
public function __construct( $s ) {
$this->str = $s;
@@ -306,4 +307,3 @@ class LikeMatch {
*/
interface DBMasterPos {
}
-
diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php
index e99ba6cc..39411791 100644
--- a/includes/db/IORMRow.php
+++ b/includes/db/IORMRow.php
@@ -27,28 +27,17 @@
* @file
* @ingroup ORM
*
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
interface IORMRow {
-
- /**
- * Constructor.
- *
- * @since 1.20
- *
- * @param IORMTable $table
- * @param array|null $fields
- * @param boolean $loadDefaults
- */
- public function __construct( IORMTable $table, $fields = null, $loadDefaults = false );
-
/**
* Load the specified fields from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|null $fields
* @param boolean $override
@@ -75,8 +64,9 @@ interface IORMRow {
* Gets the value of a field but first loads it if not done so already.
*
* @since 1.20
+ * @deprecated since 1.22
*
- * @param string$name
+ * @param string $name
*
* @return mixed
*/
@@ -156,6 +146,7 @@ interface IORMRow {
* Load the default values, via getDefaults.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $override
*/
@@ -168,6 +159,7 @@ interface IORMRow {
* @since 1.20
*
* @param string|null $functionName
+ * @deprecated since 1.22
*
* @return boolean Success indicator
*/
@@ -177,6 +169,7 @@ interface IORMRow {
* Removes the object from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return boolean Success indicator
*/
@@ -216,9 +209,9 @@ interface IORMRow {
/**
* Add an amount (can be negative) to the specified field (needs to be numeric).
- * TODO: most off this stuff makes more sense in the table class
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param string $field
* @param integer $amount
@@ -240,6 +233,7 @@ interface IORMRow {
* Computes and updates the values of the summary fields.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|string|null $summaryFields
*/
@@ -249,6 +243,7 @@ interface IORMRow {
* Sets the value for the @see $updateSummaries field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $update
*/
@@ -258,6 +253,7 @@ interface IORMRow {
* Sets the value for the @see $inSummaryMode field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $summaryMode
*/
@@ -267,9 +263,10 @@ interface IORMRow {
* Returns the table this IORMRow is a row in.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return IORMTable
*/
public function getTable();
-} \ No newline at end of file
+}
diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php
index 99413f99..36865655 100644
--- a/includes/db/IORMTable.php
+++ b/includes/db/IORMTable.php
@@ -23,7 +23,7 @@
* @file
* @ingroup ORM
*
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
@@ -97,6 +97,8 @@ interface IORMTable {
* Selects the the specified fields of the records matching the provided
* conditions and returns them as DBDataObject. Field names get prefixed.
*
+ * @see DatabaseBase::select()
+ *
* @since 1.20
*
* @param array|string|null $fields
@@ -104,7 +106,8 @@ interface IORMTable {
* @param array $options
* @param string|null $functionName
*
- * @return ORMResult
+ * @return ORMResult The result set
+ * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode)
*/
public function select( $fields = null, array $conditions = array(),
array $options = array(), $functionName = null );
@@ -136,6 +139,7 @@ interface IORMTable {
* @param null|string $functionName
*
* @return ResultWrapper
+ * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode)
*/
public function rawSelect( $fields = null, array $conditions = array(),
array $options = array(), $functionName = null );
@@ -230,6 +234,15 @@ interface IORMTable {
public function has( array $conditions = array() );
/**
+ * Checks if the table exists
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function exists();
+
+ /**
* Returns the amount of matching records.
* Condition field names get prefixed.
*
@@ -299,6 +312,71 @@ interface IORMTable {
public function setReadDb( $db );
/**
+ * Get the ID of the any foreign wiki to use as a target for database operations
+ *
+ * @since 1.20
+ *
+ * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used)
+ */
+ public function getTargetWiki();
+
+ /**
+ * Set the ID of the any foreign wiki to use as a target for database operations
+ *
+ * @param string|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used)
+ *
+ * @since 1.20
+ */
+ public function setTargetWiki( $wiki );
+
+ /**
+ * Get the database type used for read operations.
+ * This is to be used instead of wfGetDB.
+ *
+ * @see LoadBalancer::getConnection
+ *
+ * @since 1.20
+ *
+ * @return DatabaseBase The database object
+ */
+ public function getReadDbConnection();
+
+ /**
+ * Get the database type used for read operations.
+ * This is to be used instead of wfGetDB.
+ *
+ * @see LoadBalancer::getConnection
+ *
+ * @since 1.20
+ *
+ * @return DatabaseBase The database object
+ */
+ public function getWriteDbConnection();
+
+ /**
+ * Get the database type used for read operations.
+ *
+ * @see wfGetLB
+ *
+ * @since 1.20
+ *
+ * @return LoadBalancer The database load balancer object
+ */
+ public function getLoadBalancer();
+
+ /**
+ * Releases the lease on the given database connection. This is useful mainly
+ * for connections to a foreign wiki. It does nothing for connections to the local wiki.
+ *
+ * @see LoadBalancer::reuseConnection
+ *
+ * @param DatabaseBase $db the database
+ *
+ * @since 1.20
+ */
+ public function releaseConnection( DatabaseBase $db );
+
+ /**
* Update the records matching the provided conditions by
* setting the fields that are keys in the $values param to
* their corresponding values.
@@ -381,15 +459,6 @@ interface IORMTable {
public function unprefixFieldName( $fieldName );
/**
- * Get an instance of this class.
- *
- * @since 1.20
- *
- * @return IORMTable
- */
- public static function singleton();
-
- /**
* Get an array with fields from a database result,
* that can be fed directly to the constructor or
* to setFields.
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
index e82c54ba..16c43a00 100644
--- a/includes/db/LBFactory.php
+++ b/includes/db/LBFactory.php
@@ -86,7 +86,7 @@ abstract class LBFactory {
* 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 $wiki String: wiki ID, or false for the current wiki
+ * @param string $wiki wiki ID, or false for the current wiki
* @return LoadBalancer
*/
abstract function newMainLB( $wiki = false );
@@ -94,7 +94,7 @@ abstract class LBFactory {
/**
* Get a cached (tracked) load balancer object.
*
- * @param $wiki String: wiki ID, or false for the current wiki
+ * @param string $wiki wiki ID, or false for the current wiki
* @return LoadBalancer
*/
abstract function getMainLB( $wiki = false );
@@ -104,8 +104,8 @@ abstract class LBFactory {
* untracked, not chronology-protected, and the caller is responsible for
* cleaning it up.
*
- * @param $cluster String: external storage cluster, or false for core
- * @param $wiki String: wiki ID, or false for the current wiki
+ * @param string $cluster external storage cluster, or false for core
+ * @param string $wiki wiki ID, or false for the current wiki
*
* @return LoadBalancer
*/
@@ -114,8 +114,8 @@ abstract class LBFactory {
/**
* Get a cached (tracked) load balancer for external storage
*
- * @param $cluster String: external storage cluster, or false for core
- * @param $wiki String: wiki ID, or false for the current wiki
+ * @param string $cluster external storage cluster, or false for core
+ * @param string $wiki wiki ID, or false for the current wiki
*
* @return LoadBalancer
*/
@@ -134,7 +134,8 @@ abstract class LBFactory {
* Prepare all tracked load balancers for shutdown
* STUB
*/
- function shutdown() {}
+ function shutdown() {
+ }
/**
* Call a method of each tracked load balancer
@@ -201,7 +202,7 @@ class LBFactory_Simple extends LBFactory {
$flags |= DBO_COMPRESS;
}
- $servers = array(array(
+ $servers = array( array(
'host' => $wgDBserver,
'user' => $wgDBuser,
'password' => $wgDBpassword,
@@ -240,7 +241,7 @@ class LBFactory_Simple extends LBFactory {
function newExternalLB( $cluster, $wiki = false ) {
global $wgExternalServers;
if ( !isset( $wgExternalServers[$cluster] ) ) {
- throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
}
return new LoadBalancer( array(
'servers' => $wgExternalServers[$cluster]
@@ -256,6 +257,7 @@ class LBFactory_Simple extends LBFactory {
if ( !isset( $this->extLBs[$cluster] ) ) {
$this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
$this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ $this->chronProt->initLB( $this->extLBs[$cluster] );
}
return $this->extLBs[$cluster];
}
@@ -280,6 +282,9 @@ class LBFactory_Simple extends LBFactory {
if ( $this->mainLB ) {
$this->chronProt->shutdownLB( $this->mainLB );
}
+ foreach ( $this->extLBs as $extLB ) {
+ $this->chronProt->shutdownLB( $extLB );
+ }
$this->chronProt->shutdown();
$this->commitMasterChanges();
}
@@ -292,21 +297,27 @@ class LBFactory_Simple extends LBFactory {
* LBFactory::enableBackend() to return to normal behavior
*/
class LBFactory_Fake extends LBFactory {
- function __construct( $conf ) {}
+ function __construct( $conf ) {
+ }
- function newMainLB( $wiki = false) {
+ function newMainLB( $wiki = false ) {
throw new DBAccessError;
}
+
function getMainLB( $wiki = false ) {
throw new DBAccessError;
}
+
function newExternalLB( $cluster, $wiki = false ) {
throw new DBAccessError;
}
+
function &getExternalLB( $cluster, $wiki = false ) {
throw new DBAccessError;
}
- function forEachLB( $callback, $params = array() ) {}
+
+ function forEachLB( $callback, $params = array() ) {
+ }
}
/**
@@ -317,76 +328,3 @@ class DBAccessError extends MWException {
parent::__construct( "Mediawiki tried to access the database via wfGetDB(). This is not allowed." );
}
}
-
-/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
- * Kind of like Hawking's [[Chronology Protection Agency]].
- */
-class ChronologyProtector {
- var $startupPos;
- var $shutdownPos = array();
-
- /**
- * Initialise a LoadBalancer to give it appropriate chronology protection.
- *
- * @param $lb LoadBalancer
- */
- function initLB( $lb ) {
- if ( $this->startupPos === null ) {
- if ( !empty( $_SESSION[__CLASS__] ) ) {
- $this->startupPos = $_SESSION[__CLASS__];
- }
- }
- if ( !$this->startupPos ) {
- return;
- }
- $masterName = $lb->getServerName( 0 );
-
- if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
- $info = $lb->parentInfo();
- $pos = $this->startupPos[$masterName];
- wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
- $lb->waitFor( $this->startupPos[$masterName] );
- }
- }
-
- /**
- * Notify the ChronologyProtector that the LoadBalancer is about to shut
- * down. Saves replication positions.
- *
- * @param $lb LoadBalancer
- */
- function shutdownLB( $lb ) {
- // Don't start a session, don't bother with non-replicated setups
- if ( strval( session_id() ) == '' || $lb->getServerCount() <= 1 ) {
- return;
- }
- $masterName = $lb->getServerName( 0 );
- if ( isset( $this->shutdownPos[$masterName] ) ) {
- // Already done
- return;
- }
- // Only save the position if writes have been done on the connection
- $db = $lb->getAnyOpenConnection( 0 );
- $info = $lb->parentInfo();
- if ( !$db || !$db->doneWrites() ) {
- wfDebug( __METHOD__.": LB {$info['id']}, no writes done\n" );
- return;
- }
- $pos = $db->getMasterPos();
- wfDebug( __METHOD__.": LB {$info['id']} has master pos $pos\n" );
- $this->shutdownPos[$masterName] = $pos;
- }
-
- /**
- * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
- * May commit chronology data to persistent storage.
- */
- function shutdown() {
- if ( session_id() != '' && count( $this->shutdownPos ) ) {
- wfDebug( __METHOD__.": saving master pos for " .
- count( $this->shutdownPos ) . " master(s)\n" );
- $_SESSION[__CLASS__] = $this->shutdownPos;
- }
- }
-}
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
index 6008813b..3043946a 100644
--- a/includes/db/LBFactory_Multi.php
+++ b/includes/db/LBFactory_Multi.php
@@ -21,7 +21,6 @@
* @ingroup Database
*/
-
/**
* A multi-wiki, multi-master factory for Wikimedia and similar installations.
* Ignores the old configuration globals
@@ -70,6 +69,7 @@ class LBFactory_Multi extends LBFactory {
/**
* @param $conf array
+ * @throws MWException
*/
function __construct( $conf ) {
$this->chronProt = new ChronologyProtector;
@@ -82,7 +82,7 @@ class LBFactory_Multi extends LBFactory {
foreach ( $required as $key ) {
if ( !isset( $conf[$key] ) ) {
- throw new MWException( __CLASS__.": $key is required in configuration" );
+ throw new MWException( __CLASS__ . ": $key is required in configuration" );
}
$this->$key = $conf[$key];
}
@@ -145,21 +145,22 @@ class LBFactory_Multi extends LBFactory {
$section = $this->getSectionForWiki( $wiki );
if ( !isset( $this->mainLBs[$section] ) ) {
$lb = $this->newMainLB( $wiki, $section );
- $this->chronProt->initLB( $lb );
$lb->parentInfo( array( 'id' => "main-$section" ) );
+ $this->chronProt->initLB( $lb );
$this->mainLBs[$section] = $lb;
}
return $this->mainLBs[$section];
}
/**
- * @param $cluster
- * @param $wiki
+ * @param string $cluster
+ * @param bool $wiki
+ * @throws MWException
* @return LoadBalancer
*/
function newExternalLB( $cluster, $wiki = false ) {
if ( !isset( $this->externalLoads[$cluster] ) ) {
- throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
}
$template = $this->serverTemplate;
if ( isset( $this->externalTemplateOverrides ) ) {
@@ -180,6 +181,7 @@ class LBFactory_Multi extends LBFactory {
if ( !isset( $this->extLBs[$cluster] ) ) {
$this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
$this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ $this->chronProt->initLB( $this->extLBs[$cluster] );
}
return $this->extLBs[$cluster];
}
@@ -295,6 +297,9 @@ class LBFactory_Multi extends LBFactory {
foreach ( $this->mainLBs as $lb ) {
$this->chronProt->shutdownLB( $lb );
}
+ foreach ( $this->extLBs as $extLB ) {
+ $this->chronProt->shutdownLB( $extLB );
+ }
$this->chronProt->shutdown();
$this->commitMasterChanges();
}
diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactory_Single.php
index 4b165b2a..7dca06d7 100644
--- a/includes/db/LBFactory_Single.php
+++ b/includes/db/LBFactory_Single.php
@@ -28,7 +28,7 @@ class LBFactory_Single extends LBFactory {
protected $lb;
/**
- * @param $conf array An associative array with one member:
+ * @param array $conf An associative array with one member:
* - connection: The DatabaseBase connection object
*/
function __construct( $conf ) {
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
index 0e455e0c..857109db 100644
--- a/includes/db/LoadBalancer.php
+++ b/includes/db/LoadBalancer.php
@@ -37,14 +37,15 @@ class LoadBalancer {
private $mLoadMonitorClass, $mLoadMonitor;
/**
- * @param $params Array with keys:
+ * @param array $params with keys:
* servers Required. Array of server info structures.
* masterWaitTimeout Replication lag wait timeout
* loadMonitor Name of a class used to fetch server lag and load.
+ * @throws MWException
*/
function __construct( $params ) {
if ( !isset( $params['servers'] ) ) {
- throw new MWException( __CLASS__.': missing servers parameter' );
+ throw new MWException( __CLASS__ . ': missing servers parameter' );
}
$this->mServers = $params['servers'];
@@ -77,7 +78,7 @@ class LoadBalancer {
}
}
- foreach( $params['servers'] as $i => $server ) {
+ foreach ( $params['servers'] as $i => $server ) {
$this->mLoads[$i] = $server['load'];
if ( isset( $server['groupLoads'] ) ) {
foreach ( $server['groupLoads'] as $group => $ratio ) {
@@ -116,34 +117,14 @@ class LoadBalancer {
* Given an array of non-normalised probabilities, this function will select
* an element and return the appropriate key
*
+ * @deprecated since 1.21, use ArrayUtils::pickRandom()
+ *
* @param $weights array
*
- * @return int
+ * @return bool|int|string
*/
function pickRandom( $weights ) {
- if ( !is_array( $weights ) || count( $weights ) == 0 ) {
- return false;
- }
-
- $sum = array_sum( $weights );
- if ( $sum == 0 ) {
- # No loads on any of them
- # In previous versions, this triggered an unweighted random selection,
- # but this feature has been removed as of April 2006 to allow for strict
- # separation of query groups.
- return false;
- }
- $max = mt_getrandmax();
- $rand = mt_rand( 0, $max ) / $max * $sum;
-
- $sum = 0;
- foreach ( $weights as $i => $w ) {
- $sum += $w;
- if ( $sum >= $rand ) {
- break;
- }
- }
- return $i;
+ return ArrayUtils::pickRandom( $weights );
}
/**
@@ -186,7 +167,7 @@ class LoadBalancer {
#wfDebugLog( 'connect', var_export( $loads, true ) );
# Return a random representative of the remainder
- return $this->pickRandom( $loads );
+ return ArrayUtils::pickRandom( $loads );
}
/**
@@ -197,17 +178,18 @@ class LoadBalancer {
* Side effect: opens connections to databases
* @param $group bool
* @param $wiki bool
+ * @throws MWException
* @return bool|int|string
*/
function getReaderIndex( $group = false, $wiki = false ) {
global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
# @todo FIXME: For now, only go through all this for mysql databases
- if ($wgDBtype != 'mysql') {
+ if ( $wgDBtype != 'mysql' ) {
return $this->getWriterIndex();
}
- if ( count( $this->mServers ) == 1 ) {
+ if ( count( $this->mServers ) == 1 ) {
# Skip the load balancing if there's only one server
return 0;
} elseif ( $group === false and $this->mReadIndex >= 0 ) {
@@ -228,7 +210,7 @@ class LoadBalancer {
$nonErrorLoads = $this->mGroupLoads[$group];
} else {
# No loads for this group, return false and the caller can use some other group
- wfDebug( __METHOD__.": no loads for group $group\n" );
+ wfDebug( __METHOD__ . ": no loads for group $group\n" );
wfProfileOut( __METHOD__ );
return false;
}
@@ -237,6 +219,7 @@ class LoadBalancer {
}
if ( !$nonErrorLoads ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Empty server array given to LoadBalancer" );
}
@@ -253,15 +236,15 @@ class LoadBalancer {
$currentLoads = $nonErrorLoads;
while ( count( $currentLoads ) ) {
if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
- $i = $this->pickRandom( $currentLoads );
+ $i = ArrayUtils::pickRandom( $currentLoads );
} else {
$i = $this->getRandomNonLagged( $currentLoads, $wiki );
- if ( $i === false && count( $currentLoads ) != 0 ) {
+ if ( $i === false && count( $currentLoads ) != 0 ) {
# All slaves lagged. Switch to read-only mode
wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode\n" );
$wgReadOnly = 'The database has been automatically locked ' .
'while the slave database servers catch up to the master';
- $i = $this->pickRandom( $currentLoads );
+ $i = ArrayUtils::pickRandom( $currentLoads );
$laggedSlaveMode = true;
}
}
@@ -270,16 +253,16 @@ class LoadBalancer {
# pickRandom() returned false
# This is permanent and means the configuration or the load monitor
# wants us to return false.
- wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
+ wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false\n" );
wfProfileOut( __METHOD__ );
return false;
}
- wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+ wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
$conn = $this->openConnection( $i, $wiki );
if ( !$conn ) {
- wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
+ wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki\n" );
unset( $nonErrorLoads[$i] );
unset( $currentLoads[$i] );
continue;
@@ -318,7 +301,7 @@ class LoadBalancer {
# Some servers must have been overloaded
if ( $overloadedServers == 0 ) {
- throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
+ throw new MWException( __METHOD__ . ": unexpectedly found no overloaded servers" );
}
# Back off for a while
# Scale the sleep time by the number of connected threads, to produce a
@@ -341,7 +324,7 @@ class LoadBalancer {
$this->mServers[$i]['slave pos'] = $conn->getSlavePos();
}
}
- if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
+ if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $i !== false ) {
$this->mReadIndex = $i;
}
}
@@ -356,7 +339,7 @@ class LoadBalancer {
*/
function sleep( $t ) {
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__.": waiting $t us\n" );
+ wfDebug( __METHOD__ . ": waiting $t us\n" );
usleep( $t );
wfProfileOut( __METHOD__ );
return $t;
@@ -366,7 +349,7 @@ class LoadBalancer {
* Set the master wait position
* If a DB_SLAVE connection has been opened already, waits
* Otherwise sets a variable telling it to wait if such a connection is opened
- * @param $pos int
+ * @param $pos DBMasterPos
*/
public function waitFor( $pos ) {
wfProfileIn( __METHOD__ );
@@ -384,13 +367,13 @@ class LoadBalancer {
/**
* Set the master wait position and wait for ALL slaves to catch up to it
- * @param $pos int
+ * @param $pos DBMasterPos
*/
public function waitForAll( $pos ) {
wfProfileIn( __METHOD__ );
$this->mWaitForPos = $pos;
for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
- $this->doWait( $i , true );
+ $this->doWait( $i, true );
}
wfProfileOut( __METHOD__ );
}
@@ -417,7 +400,7 @@ class LoadBalancer {
* @param $open bool
* @return bool
*/
- function doWait( $index, $open = false ) {
+ protected function doWait( $index, $open = false ) {
# Find a connection to wait on
$conn = $this->getAnyOpenConnection( $index );
if ( !$conn ) {
@@ -425,7 +408,7 @@ class LoadBalancer {
wfDebug( __METHOD__ . ": no connection open\n" );
return false;
} else {
- $conn = $this->openConnection( $index );
+ $conn = $this->openConnection( $index, '' );
if ( !$conn ) {
wfDebug( __METHOD__ . ": failed to open connection\n" );
return false;
@@ -433,15 +416,15 @@ class LoadBalancer {
}
}
- wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
+ wfDebug( __METHOD__ . ": Waiting for slave #$index to catch up...\n" );
$result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
if ( $result == -1 || is_null( $result ) ) {
# Timed out waiting for slave, use master instead
- wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
+ wfDebug( __METHOD__ . ": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
return false;
} else {
- wfDebug( __METHOD__.": Done\n" );
+ wfDebug( __METHOD__ . ": Done\n" );
return true;
}
}
@@ -451,17 +434,20 @@ class LoadBalancer {
* This is the main entry point for this class.
*
* @param $i Integer: server index
- * @param $groups Array: query groups
- * @param $wiki String: wiki ID
+ * @param array $groups query groups
+ * @param bool|string $wiki Wiki ID
*
+ * @throws MWException
* @return DatabaseBase
*/
public function &getConnection( $i, $groups = array(), $wiki = false ) {
wfProfileIn( __METHOD__ );
if ( $i == DB_LAST ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( 'Attempt to call ' . __METHOD__ . ' with deprecated server index DB_LAST' );
} elseif ( $i === null || $i === false ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' );
}
@@ -476,7 +462,7 @@ class LoadBalancer {
$groupIndex = $this->getReaderIndex( $groups, $wiki );
if ( $groupIndex !== false ) {
$serverName = $this->getServerName( $groupIndex );
- wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
+ wfDebug( __METHOD__ . ": using server $serverName for group $groups\n" );
$i = $groupIndex;
}
} else {
@@ -484,7 +470,7 @@ class LoadBalancer {
$groupIndex = $this->getReaderIndex( $group, $wiki );
if ( $groupIndex !== false ) {
$serverName = $this->getServerName( $groupIndex );
- wfDebug( __METHOD__.": using server $serverName for group $group\n" );
+ wfDebug( __METHOD__ . ": using server $serverName for group $group\n" );
$i = $groupIndex;
break;
}
@@ -493,20 +479,21 @@ class LoadBalancer {
# Operation-based index
if ( $i == DB_SLAVE ) {
+ $this->mLastError = 'Unknown error'; // reset error string
$i = $this->getReaderIndex( false, $wiki );
# Couldn't find a working server in getReaderIndex()?
if ( $i === false ) {
$this->mLastError = 'No working slave server: ' . $this->mLastError;
- $this->reportConnectionError( $this->mErrorConnection );
wfProfileOut( __METHOD__ );
- return false;
+ return $this->reportConnectionError();
}
}
# Now we have an explicit index into the servers array
$conn = $this->openConnection( $i, $wiki );
if ( !$conn ) {
- $this->reportConnectionError( $this->mErrorConnection );
+ wfProfileOut( __METHOD__ );
+ return $this->reportConnectionError();
}
wfProfileOut( __METHOD__ );
@@ -519,10 +506,11 @@ class LoadBalancer {
* the same number of times as getConnection() to work.
*
* @param DatabaseBase $conn
+ * @throws MWException
*/
public function reuseConnection( $conn ) {
- $serverIndex = $conn->getLBInfo('serverIndex');
- $refCount = $conn->getLBInfo('foreignPoolRefCount');
+ $serverIndex = $conn->getLBInfo( 'serverIndex' );
+ $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
$dbName = $conn->getDBname();
$prefix = $conn->tablePrefix();
if ( strval( $prefix ) !== '' ) {
@@ -531,7 +519,7 @@ class LoadBalancer {
$wiki = $dbName;
}
if ( $serverIndex === null || $refCount === null ) {
- wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
+ wfDebug( __METHOD__ . ": this connection was not opened as a foreign connection\n" );
/**
* This can happen in code like:
* foreach ( $dbs as $db ) {
@@ -545,19 +533,51 @@ class LoadBalancer {
return;
}
if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
- throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
+ throw new MWException( __METHOD__ . ": connection not found, has the connection been freed already?" );
}
$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
if ( $refCount <= 0 ) {
$this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
- wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
+ wfDebug( __METHOD__ . ": freed connection $serverIndex/$wiki\n" );
} else {
- wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
+ wfDebug( __METHOD__ . ": reference count for $serverIndex/$wiki reduced to $refCount\n" );
}
}
/**
+ * Get a database connection handle reference
+ *
+ * The handle's methods wrap simply wrap those of a DatabaseBase handle
+ *
+ * @see LoadBalancer::getConnection() for parameter information
+ *
+ * @param integer $db
+ * @param mixed $groups
+ * @param string $wiki
+ * @return DBConnRef
+ */
+ public function getConnectionRef( $db, $groups = array(), $wiki = false ) {
+ return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
+ }
+
+ /**
+ * Get a database connection handle reference without connecting yet
+ *
+ * The handle's methods wrap simply wrap those of a DatabaseBase handle
+ *
+ * @see LoadBalancer::getConnection() for parameter information
+ *
+ * @param integer $db
+ * @param mixed $groups
+ * @param string $wiki
+ * @return DBConnRef
+ */
+ public function getLazyConnectionRef( $db, $groups = array(), $wiki = false ) {
+ return new DBConnRef( $this, array( $db, $groups, $wiki ) );
+ }
+
+ /**
* Open a connection to the server given by the specified index
* Index must be an actual index into the array.
* If the server is already open, returns it.
@@ -566,7 +586,7 @@ class LoadBalancer {
* error will be available via $this->mErrorConnection.
*
* @param $i Integer server index
- * @param $wiki String wiki ID to open
+ * @param string $wiki wiki ID to open
* @return DatabaseBase
*
* @access private
@@ -575,7 +595,7 @@ class LoadBalancer {
wfProfileIn( __METHOD__ );
if ( $wiki !== false ) {
$conn = $this->openForeignConnection( $i, $wiki );
- wfProfileOut( __METHOD__);
+ wfProfileOut( __METHOD__ );
return $conn;
}
if ( isset( $this->mConns['local'][$i][0] ) ) {
@@ -585,6 +605,7 @@ class LoadBalancer {
$server['serverIndex'] = $i;
$conn = $this->reallyOpenConnection( $server, false );
if ( $conn->isOpen() ) {
+ wfDebug( "Connected to database $i at {$this->mServers[$i]['host']}\n" );
$this->mConns['local'][$i][0] = $conn;
} else {
wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
@@ -611,22 +632,22 @@ class LoadBalancer {
* error will be available via $this->mErrorConnection.
*
* @param $i Integer: server index
- * @param $wiki String: wiki ID to open
+ * @param string $wiki wiki ID to open
* @return DatabaseBase
*/
function openForeignConnection( $i, $wiki ) {
- wfProfileIn(__METHOD__);
+ wfProfileIn( __METHOD__ );
list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
// Reuse an already-used connection
$conn = $this->mConns['foreignUsed'][$i][$wiki];
- wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
+ wfDebug( __METHOD__ . ": reusing connection $i/$wiki\n" );
} elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
// Reuse a free connection for the same wiki
$conn = $this->mConns['foreignFree'][$i][$wiki];
unset( $this->mConns['foreignFree'][$i][$wiki] );
$this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
+ wfDebug( __METHOD__ . ": reusing free connection $i/$wiki\n" );
} elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
// Reuse a connection from another wiki
$conn = reset( $this->mConns['foreignFree'][$i] );
@@ -641,22 +662,23 @@ class LoadBalancer {
$conn->tablePrefix( $prefix );
unset( $this->mConns['foreignFree'][$i][$oldWiki] );
$this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
+ wfDebug( __METHOD__ . ": reusing free connection from $oldWiki for $wiki\n" );
}
} else {
// Open a new connection
$server = $this->mServers[$i];
$server['serverIndex'] = $i;
$server['foreignPoolRefCount'] = 0;
+ $server['foreign'] = true;
$conn = $this->reallyOpenConnection( $server, $dbName );
if ( !$conn->isOpen() ) {
- wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
+ wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" );
$this->mErrorConnection = $conn;
$conn = false;
} else {
$conn->tablePrefix( $prefix );
$this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
+ wfDebug( __METHOD__ . ": opened new connection for $i/$wiki\n" );
}
}
@@ -665,7 +687,7 @@ class LoadBalancer {
$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
$conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
}
- wfProfileOut(__METHOD__);
+ wfProfileOut( __METHOD__ );
return $conn;
}
@@ -677,7 +699,7 @@ class LoadBalancer {
* @return bool
*/
function isOpen( $index ) {
- if( !is_integer( $index ) ) {
+ if ( !is_integer( $index ) ) {
return false;
}
return (bool)$this->getAnyOpenConnection( $index );
@@ -690,23 +712,20 @@ class LoadBalancer {
*
* @param $server
* @param $dbNameOverride bool
+ * @throws MWException
* @return DatabaseBase
*/
function reallyOpenConnection( $server, $dbNameOverride = false ) {
- if( !is_array( $server ) ) {
+ if ( !is_array( $server ) ) {
throw new MWException( 'You must update your load-balancing configuration. ' .
'See DefaultSettings.php entry for $wgDBservers.' );
}
- $host = $server['host'];
- $dbname = $server['dbname'];
-
if ( $dbNameOverride !== false ) {
- $server['dbname'] = $dbname = $dbNameOverride;
+ $server['dbname'] = $dbNameOverride;
}
# Create object
- wfDebug( "Connecting to $host $dbname...\n" );
try {
$db = DatabaseBase::factory( $server['type'], $server );
} catch ( DBConnectionError $e ) {
@@ -715,11 +734,6 @@ class LoadBalancer {
$db = $e->db;
}
- if ( $db->isOpen() ) {
- wfDebug( "Connected to $host $dbname.\n" );
- } else {
- wfDebug( "Connection failed to $host $dbname.\n" );
- }
$db->setLBInfo( $server );
if ( isset( $server['fakeSlaveLag'] ) ) {
$db->setFakeSlaveLag( $server['fakeSlaveLag'] );
@@ -731,24 +745,24 @@ class LoadBalancer {
}
/**
- * @param $conn
* @throws DBConnectionError
+ * @return bool
*/
- function reportConnectionError( &$conn ) {
- wfProfileIn( __METHOD__ );
+ private function reportConnectionError() {
+ $conn = $this->mErrorConnection; // The connection which caused the error
if ( !is_object( $conn ) ) {
// No last connection, probably due to all servers being too busy
wfLogDBError( "LB failure with no last connection. Connection error: {$this->mLastError}\n" );
- $conn = new Database;
+
// If all servers were busy, mLastError will contain something sensible
- throw new DBConnectionError( $conn, $this->mLastError );
+ throw new DBConnectionError( null, $this->mLastError );
} else {
$server = $conn->getProperty( 'mServer' );
wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" );
- $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
+ $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); // throws DBConnectionError
}
- wfProfileOut( __METHOD__ );
+ return false; /* not reached */
}
/**
@@ -853,7 +867,7 @@ class LoadBalancer {
*/
function closeAll() {
foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns2 as $conns3 ) {
foreach ( $conns3 as $conn ) {
$conn->close();
}
@@ -909,7 +923,9 @@ class LoadBalancer {
foreach ( $this->mConns as $conns2 ) {
foreach ( $conns2 as $conns3 ) {
foreach ( $conns3 as $conn ) {
- $conn->commit( __METHOD__ );
+ if ( $conn->trxLevel() ) {
+ $conn->commit( __METHOD__, 'flush' );
+ }
}
}
}
@@ -926,8 +942,8 @@ class LoadBalancer {
continue;
}
foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->writesOrCallbacksPending() ) {
- $conn->commit( __METHOD__ );
+ if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
+ $conn->commit( __METHOD__, 'flush' );
}
}
}
@@ -954,10 +970,11 @@ class LoadBalancer {
* @return bool
*/
function allowLagged( $mode = null ) {
- if ( $mode === null) {
+ if ( $mode === null ) {
return $this->mAllowLagged;
}
$this->mAllowLagged = $mode;
+ return $this->mAllowLagged;
}
/**
@@ -999,7 +1016,7 @@ class LoadBalancer {
* May attempt to open connections to slaves on the default DB. If there is
* no lag, the maximum lag will be reported as -1.
*
- * @param $wiki string Wiki ID, or false for the default database
+ * @param string $wiki Wiki ID, or false for the default database
*
* @return array ( host, max lag, index of max lagged host )
*/
@@ -1084,3 +1101,46 @@ class LoadBalancer {
$this->mLagTimes = null;
}
}
+
+/**
+ * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
+ * as well handling deferring the actual network connection until the handle is used
+ *
+ * @ingroup Database
+ * @since 1.22
+ */
+class DBConnRef implements IDatabase {
+ /** @var LoadBalancer */
+ protected $lb;
+ /** @var DatabaseBase|null */
+ protected $conn;
+ /** @var Array|null */
+ protected $params;
+
+ /**
+ * @param $lb LoadBalancer
+ * @param $conn DatabaseBase|array Connection or (server index, group, wiki ID) array
+ */
+ public function __construct( LoadBalancer $lb, $conn ) {
+ $this->lb = $lb;
+ if ( $conn instanceof DatabaseBase ) {
+ $this->conn = $conn;
+ } else {
+ $this->params = $conn;
+ }
+ }
+
+ public function __call( $name, $arguments ) {
+ if ( $this->conn === null ) {
+ list( $db, $groups, $wiki ) = $this->params;
+ $this->conn = $this->lb->getConnection( $db, $groups, $wiki );
+ }
+ return call_user_func_array( array( $this->conn, $name ), $arguments );
+ }
+
+ function __destruct() {
+ if ( $this->conn !== null ) {
+ $this->lb->reuseConnection( $this->conn );
+ }
+ }
+}
diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php
index 146ac61e..519e2dfd 100644
--- a/includes/db/LoadMonitor.php
+++ b/includes/db/LoadMonitor.php
@@ -37,7 +37,7 @@ interface LoadMonitor {
/**
* Perform pre-connection load ratio adjustment.
* @param $loads array
- * @param $group String: the selected query group
+ * @param string $group the selected query group
* @param $wiki String
*/
function scaleLoads( &$loads, $group = false, $wiki = false );
@@ -135,18 +135,19 @@ class LoadMonitor_MySQL implements LoadMonitor {
$requestRate = 10;
global $wgMemc;
- if ( empty( $wgMemc ) )
+ if ( empty( $wgMemc ) ) {
$wgMemc = wfGetMainCache();
+ }
$masterName = $this->parent->getServerName( 0 );
$memcKey = wfMemcKey( 'lag_times', $masterName );
$times = $wgMemc->get( $memcKey );
- if ( $times ) {
+ if ( is_array( $times ) ) {
# Randomly recache with probability rising over $expiry
$elapsed = time() - $times['timestamp'];
$chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
if ( mt_rand( 0, $chance ) != 0 ) {
- unset( $times['timestamp'] );
+ unset( $times['timestamp'] ); // hide from caller
wfProfileOut( __METHOD__ );
return $times;
}
@@ -156,10 +157,21 @@ class LoadMonitor_MySQL implements LoadMonitor {
}
# Cache key missing or expired
+ if ( $wgMemc->add( "$memcKey:lock", 1, 10 ) ) {
+ # Let this process alone update the cache value
+ $unlocker = new ScopedCallback( function() use ( $wgMemc, $memcKey ) {
+ $wgMemc->delete( $memcKey );
+ } );
+ } elseif ( is_array( $times ) ) {
+ # Could not acquire lock but an old cache exists, so use it
+ unset( $times['timestamp'] ); // hide from caller
+ wfProfileOut( __METHOD__ );
+ return $times;
+ }
$times = array();
foreach ( $serverIndexes as $i ) {
- if ($i == 0) { # Master
+ if ( $i == 0 ) { # Master
$times[$i] = 0;
} elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) {
$times[$i] = $conn->getLag();
@@ -170,14 +182,11 @@ class LoadMonitor_MySQL implements LoadMonitor {
# Add a timestamp key so we know when it was cached
$times['timestamp'] = time();
- $wgMemc->set( $memcKey, $times, $expiry );
-
- # But don't give the timestamp to the caller
- unset($times['timestamp']);
- $lagTimes = $times;
+ $wgMemc->set( $memcKey, $times, $expiry + 10 );
+ unset( $times['timestamp'] ); // hide from caller
wfProfileOut( __METHOD__ );
- return $lagTimes;
+ return $times;
}
/**
@@ -189,7 +198,7 @@ class LoadMonitor_MySQL implements LoadMonitor {
if ( !$threshold ) {
return 0;
}
- $status = $conn->getMysqlStatus("Thread%");
+ $status = $conn->getMysqlStatus( "Thread%" );
if ( $status['Threads_running'] > $threshold ) {
$server = $conn->getProperty( 'mServer' );
wfLogDBError( "LB backoff from $server - Threads_running = {$status['Threads_running']}\n" );
@@ -199,4 +208,3 @@ class LoadMonitor_MySQL implements LoadMonitor {
}
}
}
-
diff --git a/includes/db/ORMIterator.php b/includes/db/ORMIterator.php
index 090b8932..077eab0f 100644
--- a/includes/db/ORMIterator.php
+++ b/includes/db/ORMIterator.php
@@ -23,9 +23,9 @@
* @file
* @ingroup ORM
*
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
interface ORMIterator extends Iterator {
-} \ No newline at end of file
+}
diff --git a/includes/db/ORMResult.php b/includes/db/ORMResult.php
index 2a5837a1..160033c4 100644
--- a/includes/db/ORMResult.php
+++ b/includes/db/ORMResult.php
@@ -25,7 +25,7 @@
* @file ORMResult.php
* @ingroup ORM
*
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php
index 303f3a20..5ce3794d 100644
--- a/includes/db/ORMRow.php
+++ b/includes/db/ORMRow.php
@@ -27,11 +27,11 @@
* @file ORMRow.php
* @ingroup ORM
*
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
-abstract class ORMRow implements IORMRow {
+class ORMRow implements IORMRow {
/**
* The fields of the object.
@@ -43,17 +43,12 @@ abstract class ORMRow implements IORMRow {
protected $fields = array( 'id' => null );
/**
- * @since 1.20
- * @var ORMTable
- */
- protected $table;
-
- /**
* If the object should update summaries of linked items when changed.
* For example, update the course_count field in universities when a course in courses is deleted.
* Settings this to false can prevent needless updating work in situations
* such as deleting a university, which will then delete all it's courses.
*
+ * @deprecated since 1.22
* @since 1.20
* @var bool
*/
@@ -64,21 +59,29 @@ abstract class ORMRow implements IORMRow {
* This mode indicates that only summary fields got updated,
* which allows for optimizations.
*
+ * @deprecated since 1.22
* @since 1.20
* @var bool
*/
protected $inSummaryMode = false;
/**
+ * @deprecated since 1.22
+ * @since 1.20
+ * @var ORMTable|null
+ */
+ protected $table;
+
+ /**
* Constructor.
*
* @since 1.20
*
- * @param IORMTable $table
+ * @param IORMTable|null $table Deprecated since 1.22
* @param array|null $fields
- * @param boolean $loadDefaults
+ * @param boolean $loadDefaults Deprecated since 1.22
*/
- public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ) {
+ public function __construct( IORMTable $table = null, $fields = null, $loadDefaults = false ) {
$this->table = $table;
if ( !is_array( $fields ) ) {
@@ -96,6 +99,7 @@ abstract class ORMRow implements IORMRow {
* Load the specified fields from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|null $fields
* @param boolean $override
@@ -120,7 +124,8 @@ abstract class ORMRow implements IORMRow {
$result = $this->table->rawSelectRow(
$this->table->getPrefixedFields( $fields ),
array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
- array( 'LIMIT' => 1 )
+ array( 'LIMIT' => 1 ),
+ __METHOD__
);
if ( $result !== false ) {
@@ -138,8 +143,9 @@ abstract class ORMRow implements IORMRow {
*
* @since 1.20
*
- * @param string $name
- * @param mixed $default
+ * @param string $name Field name
+ * @param $default mixed: Default value to return when none is found
+ * (default: null)
*
* @throws MWException
* @return mixed
@@ -158,8 +164,9 @@ abstract class ORMRow implements IORMRow {
* Gets the value of a field but first loads it if not done so already.
*
* @since 1.20
+ * @deprecated since 1.22
*
- * @param string$name
+ * @param $name string
*
* @return mixed
*/
@@ -230,25 +237,10 @@ abstract class ORMRow implements IORMRow {
}
/**
- * Sets multiple fields.
- *
- * @since 1.20
- *
- * @param array $fields The fields to set
- * @param boolean $override Override already set fields with the provided values?
- */
- public function setFields( array $fields, $override = true ) {
- foreach ( $fields as $name => $value ) {
- if ( $override || !$this->hasField( $name ) ) {
- $this->setField( $name, $value );
- }
- }
- }
-
- /**
* Gets the fields => values to write to the table.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return array
*/
@@ -259,11 +251,18 @@ abstract class ORMRow implements IORMRow {
if ( array_key_exists( $name, $this->fields ) ) {
$value = $this->fields[$name];
+ // Skip null id fields so that the DBMS can set the default.
+ if ( $name === 'id' && is_null ( $value ) ) {
+ continue;
+ }
+
switch ( $type ) {
case 'array':
$value = (array)$value;
+ // fall-through!
case 'blob':
$value = serialize( $value );
+ // fall-through!
}
$values[$this->table->getPrefixedField( $name )] = $value;
@@ -274,6 +273,22 @@ abstract class ORMRow implements IORMRow {
}
/**
+ * Sets multiple fields.
+ *
+ * @since 1.20
+ *
+ * @param array $fields The fields to set
+ * @param boolean $override Override already set fields with the provided values?
+ */
+ public function setFields( array $fields, $override = true ) {
+ foreach ( $fields as $name => $value ) {
+ if ( $override || !$this->hasField( $name ) ) {
+ $this->setField( $name, $value );
+ }
+ }
+ }
+
+ /**
* Serializes the object to an associative array which
* can then easily be converted into JSON or similar.
*
@@ -311,6 +326,7 @@ abstract class ORMRow implements IORMRow {
* Load the default values, via getDefaults.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $override
*/
@@ -323,6 +339,7 @@ abstract class ORMRow implements IORMRow {
* when it already exists, or inserting it when it doesn't.
*
* @since 1.20
+ * @deprecated since 1.22 Use IORMTable->updateRow or ->insertRow
*
* @param string|null $functionName
*
@@ -330,9 +347,9 @@ abstract class ORMRow implements IORMRow {
*/
public function save( $functionName = null ) {
if ( $this->hasIdField() ) {
- return $this->saveExisting( $functionName );
+ return $this->table->updateRow( $this, $functionName );
} else {
- return $this->insert( $functionName );
+ return $this->table->insertRow( $this, $functionName );
}
}
@@ -340,13 +357,14 @@ abstract class ORMRow implements IORMRow {
* Updates the object in the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param string|null $functionName
*
* @return boolean Success indicator
*/
protected function saveExisting( $functionName = null ) {
- $dbw = wfGetDB( DB_MASTER );
+ $dbw = $this->table->getWriteDbConnection();
$success = $dbw->update(
$this->table->getName(),
@@ -355,6 +373,8 @@ abstract class ORMRow implements IORMRow {
is_null( $functionName ) ? __METHOD__ : $functionName
);
+ $this->table->releaseConnection( $dbw );
+
// DatabaseBase::update does not always return true for success as documented...
return $success !== false;
}
@@ -375,6 +395,7 @@ abstract class ORMRow implements IORMRow {
* Inserts the object into the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param string|null $functionName
* @param array|null $options
@@ -382,13 +403,13 @@ abstract class ORMRow implements IORMRow {
* @return boolean Success indicator
*/
protected function insert( $functionName = null, array $options = null ) {
- $dbw = wfGetDB( DB_MASTER );
+ $dbw = $this->table->getWriteDbConnection();
$success = $dbw->insert(
$this->table->getName(),
$this->getWriteValues(),
is_null( $functionName ) ? __METHOD__ : $functionName,
- is_null( $options ) ? array( 'IGNORE' ) : $options
+ $options
);
// DatabaseBase::insert does not always return true for success as documented...
@@ -398,6 +419,8 @@ abstract class ORMRow implements IORMRow {
$this->setField( 'id', $dbw->insertId() );
}
+ $this->table->releaseConnection( $dbw );
+
return $success;
}
@@ -405,16 +428,14 @@ abstract class ORMRow implements IORMRow {
* Removes the object from the database.
*
* @since 1.20
+ * @deprecated since 1.22, use IORMTable->removeRow
*
* @return boolean Success indicator
*/
public function remove() {
$this->beforeRemove();
- $success = $this->table->delete( array( 'id' => $this->getId() ) );
-
- // DatabaseBase::delete does not always return true for success as documented...
- $success = $success !== false;
+ $success = $this->table->removeRow( $this, __METHOD__ );
if ( $success ) {
$this->onRemoved();
@@ -427,6 +448,7 @@ abstract class ORMRow implements IORMRow {
* Gets called before an object is removed from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*/
protected function beforeRemove() {
$this->loadFields( $this->getBeforeRemoveFields(), false, true );
@@ -446,10 +468,11 @@ abstract class ORMRow implements IORMRow {
}
/**
- * Gets called after successfull removal.
- * Can be overriden to get rid of linked data.
+ * Gets called after successful removal.
+ * Can be overridden to get rid of linked data.
*
* @since 1.20
+ * @deprecated since 1.22
*/
protected function onRemoved() {
$this->setField( 'id', null );
@@ -490,55 +513,14 @@ abstract class ORMRow implements IORMRow {
* @throws MWException
*/
public function setField( $name, $value ) {
- $fields = $this->table->getFields();
-
- if ( array_key_exists( $name, $fields ) ) {
- switch ( $fields[$name] ) {
- case 'int':
- $value = (int)$value;
- break;
- case 'float':
- $value = (float)$value;
- break;
- case 'bool':
- if ( is_string( $value ) ) {
- $value = $value !== '0';
- } elseif ( is_int( $value ) ) {
- $value = $value !== 0;
- }
- break;
- case 'array':
- if ( is_string( $value ) ) {
- $value = unserialize( $value );
- }
-
- if ( !is_array( $value ) ) {
- $value = array();
- }
- break;
- case 'blob':
- if ( is_string( $value ) ) {
- $value = unserialize( $value );
- }
- break;
- case 'id':
- if ( is_string( $value ) ) {
- $value = (int)$value;
- }
- break;
- }
-
- $this->fields[$name] = $value;
- } else {
- throw new MWException( 'Attempted to set unknown field ' . $name );
- }
+ $this->fields[$name] = $value;
}
/**
* Add an amount (can be negative) to the specified field (needs to be numeric).
- * TODO: most off this stuff makes more sense in the table class
*
* @since 1.20
+ * @deprecated since 1.22, use IORMTable->addToField
*
* @param string $field
* @param integer $amount
@@ -546,39 +528,14 @@ abstract class ORMRow implements IORMRow {
* @return boolean Success indicator
*/
public function addToField( $field, $amount ) {
- if ( $amount == 0 ) {
- return true;
- }
-
- if ( !$this->hasIdField() ) {
- return false;
- }
-
- $absoluteAmount = abs( $amount );
- $isNegative = $amount < 0;
-
- $dbw = wfGetDB( DB_MASTER );
-
- $fullField = $this->table->getPrefixedField( $field );
-
- $success = $dbw->update(
- $this->table->getName(),
- array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
- array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
- __METHOD__
- );
-
- if ( $success && $this->hasField( $field ) ) {
- $this->setField( $field, $this->getField( $field ) + $amount );
- }
-
- return $success;
+ return $this->table->addToField( $this->getUpdateConditions(), $field, $amount );
}
/**
* Return the names of the fields.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return array
*/
@@ -590,6 +547,7 @@ abstract class ORMRow implements IORMRow {
* Computes and updates the values of the summary fields.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|string|null $summaryFields
*/
@@ -601,6 +559,7 @@ abstract class ORMRow implements IORMRow {
* Sets the value for the @see $updateSummaries field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $update
*/
@@ -612,6 +571,7 @@ abstract class ORMRow implements IORMRow {
* Sets the value for the @see $inSummaryMode field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $summaryMode
*/
@@ -620,39 +580,10 @@ abstract class ORMRow implements IORMRow {
}
/**
- * Return if any fields got changed.
- *
- * @since 1.20
- *
- * @param IORMRow $object
- * @param boolean|array $excludeSummaryFields
- * When set to true, summary field changes are ignored.
- * Can also be an array of fields to ignore.
- *
- * @return boolean
- */
- protected function fieldsChanged( IORMRow $object, $excludeSummaryFields = false ) {
- $exclusionFields = array();
-
- if ( $excludeSummaryFields !== false ) {
- $exclusionFields = is_array( $excludeSummaryFields ) ? $excludeSummaryFields : $this->table->getSummaryFields();
- }
-
- foreach ( $this->fields as $name => $value ) {
- $excluded = $excludeSummaryFields && in_array( $name, $exclusionFields );
-
- if ( !$excluded && $object->getField( $name ) !== $value ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
* Returns the table this IORMRow is a row in.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return IORMTable
*/
diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php
index a77074ff..5f6723b9 100644
--- a/includes/db/ORMTable.php
+++ b/includes/db/ORMTable.php
@@ -19,43 +19,150 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @since 1.20
+ * Non-abstract since 1.21
*
* @file ORMTable.php
* @ingroup ORM
*
- * @licence GNU GPL v2 or later
+ * @license GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
-abstract class ORMTable implements IORMTable {
+class ORMTable extends DBAccessBase implements IORMTable {
/**
- * Gets the db field prefix.
+ * Cache for instances, used by the singleton method.
*
* @since 1.20
+ * @deprecated since 1.21
*
- * @return string
+ * @var ORMTable[]
*/
- protected abstract function getFieldPrefix();
+ protected static $instanceCache = array();
/**
- * Cache for instances, used by the singleton method.
+ * @since 1.21
*
- * @since 1.20
- * @var array of DBTable
+ * @var string
*/
- protected static $instanceCache = array();
+ protected $tableName;
+
+ /**
+ * @since 1.21
+ *
+ * @var string[]
+ */
+ protected $fields = array();
+
+ /**
+ * @since 1.21
+ *
+ * @var string
+ */
+ protected $fieldPrefix = '';
+
+ /**
+ * @since 1.21
+ *
+ * @var string
+ */
+ protected $rowClass = 'ORMRow';
+
+ /**
+ * @since 1.21
+ *
+ * @var array
+ */
+ protected $defaults = array();
/**
- * The database connection to use for read operations.
+ * ID of the database connection to use for read operations.
* Can be changed via @see setReadDb.
*
* @since 1.20
+ *
* @var integer DB_ enum
*/
protected $readDb = DB_SLAVE;
/**
+ * Constructor.
+ *
+ * @since 1.21
+ *
+ * @param string $tableName
+ * @param string[] $fields
+ * @param array $defaults
+ * @param string|null $rowClass
+ * @param string $fieldPrefix
+ */
+ public function __construct( $tableName = '', array $fields = array(), array $defaults = array(), $rowClass = null, $fieldPrefix = '' ) {
+ $this->tableName = $tableName;
+ $this->fields = $fields;
+ $this->defaults = $defaults;
+
+ if ( is_string( $rowClass ) ) {
+ $this->rowClass = $rowClass;
+ }
+
+ $this->fieldPrefix = $fieldPrefix;
+ }
+
+ /**
+ * @see IORMTable::getName
+ *
+ * @since 1.21
+ *
+ * @return string
+ * @throws MWException
+ */
+ public function getName() {
+ if ( $this->tableName === '' ) {
+ throw new MWException( 'The table name needs to be set' );
+ }
+
+ return $this->tableName;
+ }
+
+ /**
+ * Gets the db field prefix.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ protected function getFieldPrefix() {
+ return $this->fieldPrefix;
+ }
+
+ /**
+ * @see IORMTable::getRowClass
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getRowClass() {
+ return $this->rowClass;
+ }
+
+ /**
+ * @see ORMTable::getFields
+ *
+ * @since 1.21
+ *
+ * @return array
+ * @throws MWException
+ */
+ public function getFields() {
+ if ( $this->fields === array() ) {
+ throw new MWException( 'The table needs to have one or more fields' );
+ }
+
+ return $this->fields;
+ }
+
+ /**
* Returns a list of default field values.
* field name => field value
*
@@ -64,7 +171,7 @@ abstract class ORMTable implements IORMTable {
* @return array
*/
public function getDefaults() {
- return array();
+ return $this->defaults;
}
/**
@@ -94,8 +201,9 @@ abstract class ORMTable implements IORMTable {
* @return ORMResult
*/
public function select( $fields = null, array $conditions = array(),
- array $options = array(), $functionName = null ) {
- return new ORMResult( $this, $this->rawSelect( $fields, $conditions, $options, $functionName ) );
+ array $options = array(), $functionName = null ) {
+ $res = $this->rawSelect( $fields, $conditions, $options, $functionName );
+ return new ORMResult( $this, $res );
}
/**
@@ -109,10 +217,11 @@ abstract class ORMTable implements IORMTable {
* @param array $options
* @param string|null $functionName
*
- * @return array of self
+ * @return array of row objects
+ * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode).
*/
public function selectObjects( $fields = null, array $conditions = array(),
- array $options = array(), $functionName = null ) {
+ array $options = array(), $functionName = null ) {
$result = $this->selectFields( $fields, $conditions, $options, false, $functionName );
$objects = array();
@@ -130,14 +239,15 @@ abstract class ORMTable implements IORMTable {
* @since 1.20
*
* @param null|string|array $fields
- * @param array $conditions
- * @param array $options
- * @param null|string $functionName
+ * @param array $conditions
+ * @param array $options
+ * @param null|string $functionName
*
* @return ResultWrapper
+ * @throws DBQueryError if the quey failed (even if the database was in ignoreErrors mode).
*/
public function rawSelect( $fields = null, array $conditions = array(),
- array $options = array(), $functionName = null ) {
+ array $options = array(), $functionName = null ) {
if ( is_null( $fields ) ) {
$fields = array_keys( $this->getFields() );
}
@@ -145,13 +255,39 @@ abstract class ORMTable implements IORMTable {
$fields = (array)$fields;
}
- return wfGetDB( $this->getReadDb() )->select(
+ $dbr = $this->getReadDbConnection();
+ $result = $dbr->select(
$this->getName(),
$this->getPrefixedFields( $fields ),
$this->getPrefixedValues( $conditions ),
is_null( $functionName ) ? __METHOD__ : $functionName,
$options
);
+
+ /* @var Exception $error */
+ $error = null;
+
+ if ( $result === false ) {
+ // Database connection was in "ignoreErrors" mode. We don't like that.
+ // So, we emulate the DBQueryError that should have been thrown.
+ $error = new DBQueryError(
+ $dbr,
+ $dbr->lastError(),
+ $dbr->lastErrno(),
+ $dbr->lastQuery(),
+ is_null( $functionName ) ? __METHOD__ : $functionName
+ );
+ }
+
+ $this->releaseConnection( $dbr );
+
+ if ( $error ) {
+ // Note: construct the error before releasing the connection,
+ // but throw it after.
+ throw $error;
+ }
+
+ return $result;
}
/**
@@ -177,7 +313,7 @@ abstract class ORMTable implements IORMTable {
* @return array of array
*/
public function selectFields( $fields = null, array $conditions = array(),
- array $options = array(), $collapse = true, $functionName = null ) {
+ array $options = array(), $collapse = true, $functionName = null ) {
$objects = array();
$result = $this->rawSelect( $fields, $conditions, $options, $functionName );
@@ -223,7 +359,7 @@ abstract class ORMTable implements IORMTable {
$objects = $this->select( $fields, $conditions, $options, $functionName );
- return $objects->isEmpty() ? false : $objects->current();
+ return ( !$objects || $objects->isEmpty() ) ? false : $objects->current();
}
/**
@@ -241,15 +377,18 @@ abstract class ORMTable implements IORMTable {
*/
public function rawSelectRow( array $fields, array $conditions = array(),
array $options = array(), $functionName = null ) {
- $dbr = wfGetDB( $this->getReadDb() );
+ $dbr = $this->getReadDbConnection();
- return $dbr->selectRow(
+ $result = $dbr->selectRow(
$this->getName(),
$fields,
$conditions,
is_null( $functionName ) ? __METHOD__ : $functionName,
$options
);
+
+ $this->releaseConnection( $dbr );
+ return $result;
}
/**
@@ -293,6 +432,21 @@ abstract class ORMTable implements IORMTable {
}
/**
+ * Checks if the table exists
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function exists() {
+ $dbr = $this->getReadDbConnection();
+ $exists = $dbr->tableExists( $this->getName() );
+ $this->releaseConnection( $dbr );
+
+ return $exists;
+ }
+
+ /**
* Returns the amount of matching records.
* Condition field names get prefixed.
*
@@ -310,7 +464,8 @@ abstract class ORMTable implements IORMTable {
$res = $this->rawSelectRow(
array( 'rowcount' => 'COUNT(*)' ),
$this->getPrefixedValues( $conditions ),
- $options
+ $options,
+ __METHOD__
);
return $res->rowcount;
@@ -327,13 +482,18 @@ abstract class ORMTable implements IORMTable {
* @return boolean Success indicator
*/
public function delete( array $conditions, $functionName = null ) {
- return wfGetDB( DB_MASTER )->delete(
+ $dbw = $this->getWriteDbConnection();
+
+ $result = $dbw->delete(
$this->getName(),
$conditions === array() ? '*' : $this->getPrefixedValues( $conditions ),
- $functionName
+ is_null( $functionName ) ? __METHOD__ : $functionName
) !== false; // DatabaseBase::delete does not always return true for success as documented...
+
+ $this->releaseConnection( $dbw );
+ return $result;
}
-
+
/**
* Get API parameters for the fields supported by this object.
*
@@ -397,7 +557,7 @@ abstract class ORMTable implements IORMTable {
}
/**
- * Get the database type used for read operations.
+ * Get the database ID used for read operations.
*
* @since 1.20
*
@@ -408,7 +568,7 @@ abstract class ORMTable implements IORMTable {
}
/**
- * Set the database type to use for read operations.
+ * Set the database ID to use for read operations, use DB_XXX constants or an index to the load balancer setup.
*
* @param integer $db
*
@@ -419,6 +579,70 @@ abstract class ORMTable implements IORMTable {
}
/**
+ * Get the ID of the any foreign wiki to use as a target for database operations
+ *
+ * @since 1.20
+ *
+ * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used)
+ */
+ public function getTargetWiki() {
+ return $this->wiki;
+ }
+
+ /**
+ * Set the ID of the any foreign wiki to use as a target for database operations
+ *
+ * @param string|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used)
+ *
+ * @since 1.20
+ */
+ public function setTargetWiki( $wiki ) {
+ $this->wiki = $wiki;
+ }
+
+ /**
+ * Get the database type used for read operations.
+ * This is to be used instead of wfGetDB.
+ *
+ * @see LoadBalancer::getConnection
+ *
+ * @since 1.20
+ *
+ * @return DatabaseBase The database object
+ */
+ public function getReadDbConnection() {
+ return $this->getConnection( $this->getReadDb(), array() );
+ }
+
+ /**
+ * Get the database type used for read operations.
+ * This is to be used instead of wfGetDB.
+ *
+ * @see LoadBalancer::getConnection
+ *
+ * @since 1.20
+ *
+ * @return DatabaseBase The database object
+ */
+ public function getWriteDbConnection() {
+ return $this->getConnection( DB_MASTER, array() );
+ }
+
+ /**
+ * Releases the lease on the given database connection. This is useful mainly
+ * for connections to a foreign wiki. It does nothing for connections to the local wiki.
+ *
+ * @see LoadBalancer::reuseConnection
+ *
+ * @param DatabaseBase $db the database
+ *
+ * @since 1.20
+ */
+ public function releaseConnection( DatabaseBase $db ) {
+ parent::releaseConnection( $db ); // just make it public
+ }
+
+ /**
* Update the records matching the provided conditions by
* setting the fields that are keys in the $values param to
* their corresponding values.
@@ -431,14 +655,17 @@ abstract class ORMTable implements IORMTable {
* @return boolean Success indicator
*/
public function update( array $values, array $conditions = array() ) {
- $dbw = wfGetDB( DB_MASTER );
+ $dbw = $this->getWriteDbConnection();
- return $dbw->update(
+ $result = $dbw->update(
$this->getName(),
$this->getPrefixedValues( $values ),
$this->getPrefixedValues( $conditions ),
__METHOD__
) !== false; // DatabaseBase::update does not always return true for success as documented...
+
+ $this->releaseConnection( $dbw );
+ return $result;
}
/**
@@ -450,6 +677,7 @@ abstract class ORMTable implements IORMTable {
* @param array $conditions
*/
public function updateSummaryFields( $summaryFields = null, array $conditions = array() ) {
+ $slave = $this->getReadDb();
$this->setReadDb( DB_MASTER );
/**
@@ -461,7 +689,7 @@ abstract class ORMTable implements IORMTable {
$item->save();
}
- $this->setReadDb( DB_SLAVE );
+ $this->setReadDb( $slave );
}
/**
@@ -559,6 +787,7 @@ abstract class ORMTable implements IORMTable {
* Get an instance of this class.
*
* @since 1.20
+ * @deprecated since 1.21
*
* @return IORMTable
*/
@@ -585,10 +814,59 @@ abstract class ORMTable implements IORMTable {
*/
public function getFieldsFromDBResult( stdClass $result ) {
$result = (array)$result;
- return array_combine(
+
+ $rawFields = array_combine(
$this->unprefixFieldNames( array_keys( $result ) ),
array_values( $result )
);
+
+ $fieldDefinitions = $this->getFields();
+ $fields = array();
+
+ foreach ( $rawFields as $name => $value ) {
+ if ( array_key_exists( $name, $fieldDefinitions ) ) {
+ switch ( $fieldDefinitions[$name] ) {
+ case 'int':
+ $value = (int)$value;
+ break;
+ case 'float':
+ $value = (float)$value;
+ break;
+ case 'bool':
+ if ( is_string( $value ) ) {
+ $value = $value !== '0';
+ } elseif ( is_int( $value ) ) {
+ $value = $value !== 0;
+ }
+ break;
+ case 'array':
+ if ( is_string( $value ) ) {
+ $value = unserialize( $value );
+ }
+
+ if ( !is_array( $value ) ) {
+ $value = array();
+ }
+ break;
+ case 'blob':
+ if ( is_string( $value ) ) {
+ $value = unserialize( $value );
+ }
+ break;
+ case 'id':
+ if ( is_string( $value ) ) {
+ $value = (int)$value;
+ }
+ break;
+ }
+
+ $fields[$name] = $value;
+ } else {
+ throw new MWException( 'Attempted to set unknown field ' . $name );
+ }
+ }
+
+ return $fields;
}
/**
@@ -638,14 +916,15 @@ abstract class ORMTable implements IORMTable {
*
* @since 1.20
*
- * @param array $data
+ * @param array $fields
* @param boolean $loadDefaults
*
* @return IORMRow
*/
- public function newRow( array $data, $loadDefaults = false ) {
+ public function newRow( array $fields, $loadDefaults = false ) {
$class = $this->getRowClass();
- return new $class( $this, $data, $loadDefaults );
+
+ return new $class( $this, $fields, $loadDefaults );
}
/**
@@ -672,4 +951,157 @@ abstract class ORMTable implements IORMTable {
return array_key_exists( $name, $this->getFields() );
}
+ /**
+ * Updates the provided row in the database.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row The row to save
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function updateRow( IORMRow $row, $functionName = null ) {
+ $dbw = $this->getWriteDbConnection();
+
+ $success = $dbw->update(
+ $this->getName(),
+ $this->getWriteValues( $row ),
+ $this->getPrefixedValues( array( 'id' => $row->getId() ) ),
+ is_null( $functionName ) ? __METHOD__ : $functionName
+ );
+
+ $this->releaseConnection( $dbw );
+
+ // DatabaseBase::update does not always return true for success as documented...
+ return $success !== false;
+ }
+
+ /**
+ * Inserts the provided row into the database.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row
+ * @param string|null $functionName
+ * @param array|null $options
+ *
+ * @return boolean Success indicator
+ */
+ public function insertRow( IORMRow $row, $functionName = null, array $options = null ) {
+ $dbw = $this->getWriteDbConnection();
+
+ $success = $dbw->insert(
+ $this->getName(),
+ $this->getWriteValues( $row ),
+ is_null( $functionName ) ? __METHOD__ : $functionName,
+ $options
+ );
+
+ // DatabaseBase::insert does not always return true for success as documented...
+ $success = $success !== false;
+
+ if ( $success ) {
+ $row->setField( 'id', $dbw->insertId() );
+ }
+
+ $this->releaseConnection( $dbw );
+
+ return $success;
+ }
+
+ /**
+ * Gets the fields => values to write to the table.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row
+ *
+ * @return array
+ */
+ protected function getWriteValues( IORMRow $row ) {
+ $values = array();
+
+ $rowFields = $row->getFields();
+
+ foreach ( $this->getFields() as $name => $type ) {
+ if ( array_key_exists( $name, $rowFields ) ) {
+ $value = $rowFields[$name];
+
+ switch ( $type ) {
+ case 'array':
+ $value = (array)$value;
+ // fall-through!
+ case 'blob':
+ $value = serialize( $value );
+ // fall-through!
+ }
+
+ $values[$this->getPrefixedField( $name )] = $value;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Removes the provided row from the database.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function removeRow( IORMRow $row, $functionName = null ) {
+ $success = $this->delete(
+ array( 'id' => $row->getId() ),
+ is_null( $functionName ) ? __METHOD__ : $functionName
+ );
+
+ // DatabaseBase::delete does not always return true for success as documented...
+ return $success !== false;
+ }
+
+ /**
+ * Add an amount (can be negative) to the specified field (needs to be numeric).
+ *
+ * @since 1.22
+ *
+ * @param array $conditions
+ * @param string $field
+ * @param integer $amount
+ *
+ * @return boolean Success indicator
+ * @throws MWException
+ */
+ public function addToField( array $conditions, $field, $amount ) {
+ if ( !array_key_exists( $field, $this->fields ) ) {
+ throw new MWException( 'Unknown field "' . $field . '" provided' );
+ }
+
+ if ( $amount == 0 ) {
+ return true;
+ }
+
+ $absoluteAmount = abs( $amount );
+ $isNegative = $amount < 0;
+
+ $fullField = $this->getPrefixedField( $field );
+
+ $dbw = $this->getWriteDbConnection();
+
+ $success = $dbw->update(
+ $this->getName(),
+ array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
+ $this->getPrefixedValues( $conditions ),
+ __METHOD__
+ ) !== false; // DatabaseBase::update does not always return true for success as documented...
+
+ $this->releaseConnection( $dbw );
+
+ return $success;
+ }
+
}
diff --git a/includes/debug/Debug.php b/includes/debug/Debug.php
index d02bcf53..6e9ccc41 100644
--- a/includes/debug/Debug.php
+++ b/includes/debug/Debug.php
@@ -1,6 +1,6 @@
<?php
/**
- * Debug toolbar related code
+ * Debug toolbar related code.
*
* 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
@@ -21,7 +21,7 @@
*/
/**
- * New debugger system that outputs a toolbar on page view
+ * New debugger system that outputs a toolbar on page view.
*
* By default, most methods do nothing ( self::$enabled = false ). You have
* to explicitly call MWDebug::init() to enabled them.
@@ -35,28 +35,28 @@ class MWDebug {
/**
* Log lines
*
- * @var array
+ * @var array $log
*/
protected static $log = array();
/**
- * Debug messages from wfDebug()
+ * Debug messages from wfDebug().
*
- * @var array
+ * @var array $debug
*/
protected static $debug = array();
/**
- * Queries
+ * SQL statements of the databses queries.
*
- * @var array
+ * @var array $query
*/
protected static $query = array();
/**
* Is the debugger enabled?
*
- * @var bool
+ * @var bool $enabled
*/
protected static $enabled = false;
@@ -64,7 +64,7 @@ class MWDebug {
* Array of functions that have already been warned, formatted
* function-caller to prevent a buttload of warnings
*
- * @var array
+ * @var array $deprecationWarnings
*/
protected static $deprecationWarnings = array();
@@ -135,9 +135,24 @@ class MWDebug {
* @since 1.19
* @param $msg string
* @param $callerOffset int
+ * @param $level int A PHP error level. See sendWarning()
+ * @param $log string: 'production' will always trigger a php error, 'auto'
+ * will trigger an error if $wgDevelopmentWarnings is true, and 'debug'
+ * will only write to the debug log(s).
+ *
* @return mixed
*/
- public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
+ public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
+ global $wgDevelopmentWarnings;
+
+ if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
+ $log = 'debug';
+ }
+
+ if ( $log === 'debug' ) {
+ $level = false;
+ }
+
$callerDescription = self::getCallerDescription( $callerOffset );
self::sendWarning( $msg, $callerDescription, $level );
@@ -161,9 +176,9 @@ class MWDebug {
* - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false.
*
* @since 1.19
- * @param $function string: Function that is deprecated.
- * @param $version string|bool: Version in which the function was deprecated.
- * @param $component string|bool: Component to which the function belongs.
+ * @param string $function Function that is deprecated.
+ * @param string|bool $version Version in which the function was deprecated.
+ * @param string|bool $component Component to which the function belongs.
* If false, it is assumbed the function is in MediaWiki core.
* @param $callerOffset integer: How far up the callstack is the original
* caller. 2 = function that called the function that called
@@ -211,7 +226,8 @@ class MWDebug {
}
if ( $sendToLog ) {
- self::sendWarning( $msg, $callerDescription, E_USER_DEPRECATED );
+ global $wgDevelopmentWarnings; // we could have a more specific $wgDeprecationWarnings setting.
+ self::sendWarning( $msg, $callerDescription, $wgDevelopmentWarnings ? E_USER_DEPRECATED : false );
}
if ( self::$enabled ) {
@@ -266,23 +282,21 @@ class MWDebug {
}
/**
- * Send a warning either to the debug log or by triggering an user PHP
- * error depending on $wgDevelopmentWarnings.
+ * Send a warning to the debug log and optionally also trigger a PHP
+ * error, depending on the $level argument.
*
* @param $msg string Message to send
* @param $caller array caller description get from getCallerDescription()
- * @param $level error level to use if $wgDevelopmentWarnings is true
+ * @param $level int|bool error level to use; set to false to not trigger an error
*/
private static function sendWarning( $msg, $caller, $level ) {
- global $wgDevelopmentWarnings;
-
$msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
- if ( $wgDevelopmentWarnings ) {
+ if ( $level !== false ) {
trigger_error( $msg, $level );
- } else {
- wfDebug( "$msg\n" );
}
+
+ wfDebug( "$msg\n" );
}
/**
@@ -296,7 +310,7 @@ class MWDebug {
global $wgDebugComments, $wgShowDebug;
if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
- self::$debug[] = rtrim( $str );
+ self::$debug[] = rtrim( UtfNormal::cleanUp( $str ) );
}
}
@@ -318,7 +332,7 @@ class MWDebug {
self::$query[] = array(
'sql' => $sql,
'function' => $function,
- 'master' => (bool) $isMaster,
+ 'master' => (bool)$isMaster,
'time' => 0.0,
'_start' => microtime( true ),
);
@@ -470,10 +484,10 @@ class MWDebug {
// output errors as debug info, when display_errors is on
// this is necessary for all non html output of the api, because that clears all errors first
$obContents = ob_get_contents();
- if( $obContents ) {
+ if ( $obContents ) {
$obContentArray = explode( '<br />', $obContents );
- foreach( $obContentArray as $obContent ) {
- if( trim( $obContent ) ) {
+ foreach ( $obContentArray as $obContent ) {
+ if ( trim( $obContent ) ) {
self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
}
}
@@ -484,15 +498,10 @@ class MWDebug {
$result->setIndexedTagName( $debugInfo, 'debuginfo' );
$result->setIndexedTagName( $debugInfo['log'], 'line' );
- foreach( $debugInfo['debugLog'] as $index => $debugLogText ) {
- $vals = array();
- ApiResult::setContent( $vals, $debugLogText );
- $debugInfo['debugLog'][$index] = $vals; //replace
- }
$result->setIndexedTagName( $debugInfo['debugLog'], 'msg' );
$result->setIndexedTagName( $debugInfo['queries'], 'query' );
$result->setIndexedTagName( $debugInfo['includes'], 'queries' );
- $result->addValue( array(), 'debuginfo', $debugInfo );
+ $result->addValue( null, 'debuginfo', $debugInfo );
}
/**
diff --git a/includes/diff/DairikiDiff.php b/includes/diff/DairikiDiff.php
index 72eb5d3c..298d7240 100644
--- a/includes/diff/DairikiDiff.php
+++ b/includes/diff/DairikiDiff.php
@@ -5,6 +5,21 @@
* Copyright © 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
* You may copy this code freely under the conditions of the GPL.
*
+ * 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 DifferenceEngine
* @defgroup DifferenceEngine DifferenceEngine
@@ -28,14 +43,14 @@ class _DiffOp {
* @return int
*/
function norig() {
- return $this->orig ? sizeof( $this->orig ) : 0;
+ return $this->orig ? count( $this->orig ) : 0;
}
/**
* @return int
*/
function nclosing() {
- return $this->closing ? sizeof( $this->closing ) : 0;
+ return $this->closing ? count( $this->closing ) : 0;
}
}
@@ -152,7 +167,7 @@ class _DiffOp_Change extends _DiffOp {
*/
class _DiffEngine {
- const MAX_XREF_LENGTH = 10000;
+ const MAX_XREF_LENGTH = 10000;
protected $xchanged, $ychanged;
@@ -168,7 +183,7 @@ class _DiffEngine {
* @param $to_lines
* @return array
*/
- function diff ( $from_lines, $to_lines ) {
+ function diff( $from_lines, $to_lines ) {
wfProfileIn( __METHOD__ );
// Diff and store locally
@@ -179,8 +194,8 @@ class _DiffEngine {
$this->_shift_boundaries( $to_lines, $this->ychanged, $this->xchanged );
// Compute the edit operations.
- $n_from = sizeof( $from_lines );
- $n_to = sizeof( $to_lines );
+ $n_from = count( $from_lines );
+ $n_to = count( $to_lines );
$edits = array();
$xi = $yi = 0;
@@ -206,7 +221,7 @@ class _DiffEngine {
}
$add = array();
- while ( $yi < $n_to && $this->ychanged[$yi] ) {
+ while ( $yi < $n_to && $this->ychanged[$yi] ) {
$add[] = $to_lines[$yi++];
}
@@ -226,7 +241,7 @@ class _DiffEngine {
* @param $from_lines
* @param $to_lines
*/
- function diff_local ( $from_lines, $to_lines ) {
+ function diff_local( $from_lines, $to_lines ) {
global $wgExternalDiffEngine;
wfProfileIn( __METHOD__ );
@@ -239,8 +254,8 @@ class _DiffEngine {
unset( $wikidiff3 );
} else {
// old diff
- $n_from = sizeof( $from_lines );
- $n_to = sizeof( $to_lines );
+ $n_from = count( $from_lines );
+ $n_to = count( $to_lines );
$this->xchanged = $this->ychanged = array();
$this->xv = $this->yv = array();
$this->xind = $this->yind = array();
@@ -256,7 +271,8 @@ class _DiffEngine {
$this->xchanged[$skip] = $this->ychanged[$skip] = false;
}
// Skip trailing common lines.
- $xi = $n_from; $yi = $n_to;
+ $xi = $n_from;
+ $yi = $n_to;
for ( $endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++ ) {
if ( $from_lines[$xi] !== $to_lines[$yi] ) {
break;
@@ -288,7 +304,7 @@ class _DiffEngine {
}
// Find the LCS.
- $this->_compareseq( 0, sizeof( $this->xv ), 0, sizeof( $this->yv ) );
+ $this->_compareseq( 0, count( $this->xv ), 0, count( $this->yv ) );
}
wfProfileOut( __METHOD__ );
}
@@ -311,7 +327,7 @@ class _DiffEngine {
* [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
* sized segments.
*
- * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
+ * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
* array of NCHUNKS+1 (X, Y) indexes giving the diving points between
* sub sequences. The first sub-sequence is contained in [X0, X1),
* [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
@@ -379,7 +395,7 @@ class _DiffEngine {
break;
}
}
- while ( list ( , $y ) = each( $matches ) ) {
+ while ( list( , $y ) = each( $matches ) ) {
if ( $y > $this->seq[$k -1] ) {
assert( '$y < $this->seq[$k]' );
// Optimization: this is a common case:
@@ -454,10 +470,9 @@ class _DiffEngine {
* @param $yoff
* @param $ylim
*/
- function _compareseq ( $xoff, $xlim, $yoff, $ylim ) {
+ function _compareseq( $xoff, $xlim, $yoff, $ylim ) {
// Slide down the bottom initial diagonal.
- while ( $xoff < $xlim && $yoff < $ylim
- && $this->xv[$xoff] == $this->yv[$yoff] ) {
+ while ( $xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff] ) {
++$xoff;
++$yoff;
}
@@ -476,8 +491,7 @@ class _DiffEngine {
// $nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
// $nchunks = max(2,min(8,(int)$nchunks));
$nchunks = min( 7, $xlim - $xoff, $ylim - $yoff ) + 1;
- list ( $lcs, $seps )
- = $this->_diag( $xoff, $xlim, $yoff, $ylim, $nchunks );
+ list( $lcs, $seps ) = $this->_diag( $xoff, $xlim, $yoff, $ylim, $nchunks );
}
if ( $lcs == 0 ) {
@@ -494,7 +508,7 @@ class _DiffEngine {
reset( $seps );
$pt1 = $seps[0];
while ( $pt2 = next( $seps ) ) {
- $this->_compareseq ( $pt1[0], $pt2[0], $pt1[1], $pt2[1] );
+ $this->_compareseq( $pt1[0], $pt2[0], $pt1[1], $pt2[1] );
$pt1 = $pt2;
}
}
@@ -518,9 +532,9 @@ class _DiffEngine {
$i = 0;
$j = 0;
- assert( 'sizeof($lines) == sizeof($changed)' );
- $len = sizeof( $lines );
- $other_len = sizeof( $other_changed );
+ assert( 'count($lines) == count($changed)' );
+ $len = count( $lines );
+ $other_len = count( $other_changed );
while ( 1 ) {
/*
@@ -540,9 +554,11 @@ class _DiffEngine {
while ( $i < $len && ! $changed[$i] ) {
assert( '$j < $other_len && ! $other_changed[$j]' );
- $i++; $j++;
- while ( $j < $other_len && $other_changed[$j] )
+ $i++;
$j++;
+ while ( $j < $other_len && $other_changed[$j] ) {
+ $j++;
+ }
}
if ( $i == $len ) {
@@ -676,7 +692,7 @@ class Diff {
/**
* Check for empty diff.
*
- * @return bool True iff two sequences were identical.
+ * @return bool True if two sequences were identical.
*/
function isEmpty() {
foreach ( $this->edits as $edit ) {
@@ -698,7 +714,7 @@ class Diff {
$lcs = 0;
foreach ( $this->edits as $edit ) {
if ( $edit->type == 'copy' ) {
- $lcs += sizeof( $edit->orig );
+ $lcs += count( $edit->orig );
}
}
return $lcs;
@@ -717,7 +733,7 @@ class Diff {
foreach ( $this->edits as $edit ) {
if ( $edit->orig ) {
- array_splice( $lines, sizeof( $lines ), 0, $edit->orig );
+ array_splice( $lines, count( $lines ), 0, $edit->orig );
}
}
return $lines;
@@ -736,7 +752,7 @@ class Diff {
foreach ( $this->edits as $edit ) {
if ( $edit->closing ) {
- array_splice( $lines, sizeof( $lines ), 0, $edit->closing );
+ array_splice( $lines, count( $lines ), 0, $edit->closing );
}
}
return $lines;
@@ -766,7 +782,6 @@ class Diff {
trigger_error( "Reversed closing doesn't match", E_USER_ERROR );
}
-
$prevtype = 'none';
foreach ( $this->edits as $edit ) {
if ( $prevtype == $edit->type ) {
@@ -814,23 +829,23 @@ class MappedDiff extends Diff {
$mapped_from_lines, $mapped_to_lines ) {
wfProfileIn( __METHOD__ );
- assert( 'sizeof( $from_lines ) == sizeof( $mapped_from_lines )' );
- assert( 'sizeof( $to_lines ) == sizeof( $mapped_to_lines )' );
+ assert( 'count( $from_lines ) == count( $mapped_from_lines )' );
+ assert( 'count( $to_lines ) == count( $mapped_to_lines )' );
parent::__construct( $mapped_from_lines, $mapped_to_lines );
$xi = $yi = 0;
- for ( $i = 0; $i < sizeof( $this->edits ); $i++ ) {
+ for ( $i = 0; $i < count( $this->edits ); $i++ ) {
$orig = &$this->edits[$i]->orig;
if ( is_array( $orig ) ) {
- $orig = array_slice( $from_lines, $xi, sizeof( $orig ) );
- $xi += sizeof( $orig );
+ $orig = array_slice( $from_lines, $xi, count( $orig ) );
+ $xi += count( $orig );
}
$closing = &$this->edits[$i]->closing;
if ( is_array( $closing ) ) {
- $closing = array_slice( $to_lines, $yi, sizeof( $closing ) );
- $yi += sizeof( $closing );
+ $closing = array_slice( $to_lines, $yi, count( $closing ) );
+ $yi += count( $closing );
}
}
wfProfileOut( __METHOD__ );
@@ -885,7 +900,7 @@ class DiffFormatter {
foreach ( $diff->edits as $edit ) {
if ( $edit->type == 'copy' ) {
if ( is_array( $block ) ) {
- if ( sizeof( $edit->orig ) <= $nlead + $ntrail ) {
+ if ( count( $edit->orig ) <= $nlead + $ntrail ) {
$block[] = $edit;
} else {
if ( $ntrail ) {
@@ -901,9 +916,9 @@ class DiffFormatter {
$context = $edit->orig;
} else {
if ( !is_array( $block ) ) {
- $context = array_slice( $context, sizeof( $context ) - $nlead );
- $x0 = $xi - sizeof( $context );
- $y0 = $yi - sizeof( $context );
+ $context = array_slice( $context, count( $context ) - $nlead );
+ $x0 = $xi - count( $context );
+ $y0 = $yi - count( $context );
$block = array();
if ( $context ) {
$block[] = new _DiffOp_Copy( $context );
@@ -913,10 +928,10 @@ class DiffFormatter {
}
if ( $edit->orig ) {
- $xi += sizeof( $edit->orig );
+ $xi += count( $edit->orig );
}
if ( $edit->closing ) {
- $yi += sizeof( $edit->closing );
+ $yi += count( $edit->closing );
}
}
@@ -1096,7 +1111,7 @@ class ArrayDiffFormatter extends DiffFormatter {
$newline = 1;
$retval = array();
foreach ( $diff->edits as $edit ) {
- switch( $edit->type ) {
+ switch ( $edit->type ) {
case 'add':
foreach ( $edit->closing as $l ) {
$retval[] = array(
@@ -1191,7 +1206,7 @@ class _HWLDF_WordAccumulator {
* @param $words
* @param $tag string
*/
- function addWords ( $words, $tag = '' ) {
+ function addWords( $words, $tag = '' ) {
if ( $tag != $this->_tag ) {
$this->_flushGroup( $tag );
}
@@ -1231,7 +1246,7 @@ class WordLevelDiff extends MappedDiff {
* @param $orig_lines
* @param $closing_lines
*/
- function __construct ( $orig_lines, $closing_lines ) {
+ function __construct( $orig_lines, $closing_lines ) {
wfProfileIn( __METHOD__ );
list( $orig_words, $orig_stripped ) = $this->_split( $orig_lines );
@@ -1269,8 +1284,12 @@ class WordLevelDiff extends MappedDiff {
if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
$line, $m ) )
{
- $words = array_merge( $words, $m[0] );
- $stripped = array_merge( $stripped, $m[1] );
+ foreach ( $m[0] as $word ) {
+ $words[] = $word;
+ }
+ foreach ( $m[1] as $stripped_word ) {
+ $stripped[] = $stripped_word;
+ }
}
}
}
@@ -1350,7 +1369,7 @@ class TableDiffFormatter extends DiffFormatter {
*/
function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
$r = '<tr><td colspan="2" class="diff-lineno"><!--LINE ' . $xbeg . "--></td>\n" .
- '<td colspan="2" class="diff-lineno"><!--LINE ' . $ybeg . "--></td></tr>\n";
+ '<td colspan="2" class="diff-lineno"><!--LINE ' . $ybeg . "--></td></tr>\n";
return $r;
}
diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php
index c7156fb2..e436f58d 100644
--- a/includes/diff/DifferenceEngine.php
+++ b/includes/diff/DifferenceEngine.php
@@ -38,14 +38,17 @@ class DifferenceEngine extends ContextSource {
* @private
*/
var $mOldid, $mNewid;
- var $mOldtext, $mNewtext;
+ var $mOldTags, $mNewTags;
+ /**
+ * @var Content
+ */
+ var $mOldContent, $mNewContent;
protected $mDiffLang;
/**
* @var Title
*/
var $mOldPage, $mNewPage;
- var $mRcidMarkPatrolled;
/**
* @var Revision
@@ -78,7 +81,7 @@ class DifferenceEngine extends ContextSource {
* @param $context IContextSource context to use, anything else will be ignored
* @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 $rcid Integer Deprecated, no longer used!
* @param $refreshCache boolean If set, refreshes the diff cache
* @param $unhide boolean If set, allow viewing deleted revs
*/
@@ -93,7 +96,6 @@ class DifferenceEngine extends ContextSource {
$this->mOldid = $old;
$this->mNewid = $new;
- $this->mRcidMarkPatrolled = intval( $rcid ); # force it to be an integer
$this->mRefreshCache = $refreshCache;
$this->unhide = $unhide;
}
@@ -149,7 +151,7 @@ class DifferenceEngine extends ContextSource {
function deletedLink( $id ) {
if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
$dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow('archive', '*',
+ $row = $dbr->selectRow( 'archive', '*',
array( 'ar_rev_id' => $id ),
__METHOD__ );
if ( $row ) {
@@ -183,10 +185,14 @@ class DifferenceEngine extends ContextSource {
$out = $this->getOutput();
$missing = array();
- if ( $this->mOldRev === null ) {
+ if ( $this->mOldRev === null ||
+ ( $this->mOldRev && $this->mOldContent === null )
+ ) {
$missing[] = $this->deletedIdMarker( $this->mOldid );
}
- if ( $this->mNewRev === null ) {
+ if ( $this->mNewRev === null ||
+ ( $this->mNewRev && $this->mNewContent === null )
+ ) {
$missing[] = $this->deletedIdMarker( $this->mNewid );
}
@@ -220,29 +226,6 @@ class DifferenceEngine extends ContextSource {
throw new PermissionsError( 'read', $permErrors );
}
- # 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 ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
- $urls = array(
- 'File' => array( 'Extension' => 'wiki', 'URL' =>
- # This should be mOldPage, but it may not be set, see below.
- $this->mNewPage->getCanonicalURL( array(
- 'action' => 'raw', 'oldid' => $this->mOldid ) )
- ),
- 'File2' => array( 'Extension' => 'wiki', 'URL' =>
- $this->mNewPage->getCanonicalURL( array(
- 'action' => 'raw', 'oldid' => $this->mNewid ) )
- ),
- );
-
- $externalEditor = new ExternalEdit( $this->getContext(), $urls );
- $externalEditor->execute();
-
- wfProfileOut( __METHOD__ );
- return;
- }
-
$rollback = '';
$undoLink = '';
@@ -260,6 +243,8 @@ class DifferenceEngine extends ContextSource {
$deleted = $suppressed = false;
$allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
+ $revisionTools = array();
+
# mOldRev 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.
@@ -270,11 +255,6 @@ class DifferenceEngine extends ContextSource {
} else {
wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
- $sk = $this->getSkin();
- if ( method_exists( $sk, 'suppressQuickbar' ) ) {
- $sk->suppressQuickbar();
- }
-
if ( $this->mNewPage->equals( $this->mOldPage ) ) {
$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
$samePage = true;
@@ -287,20 +267,23 @@ class DifferenceEngine extends ContextSource {
if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
- $out->preventClickjacking();
- $rollback = '&#160;&#160;&#160;' . Linker::generateRollback( $this->mNewRev, $this->getContext() );
+ $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
+ if ( $rollbackLink ) {
+ $out->preventClickjacking();
+ $rollback = '&#160;&#160;&#160;' . $rollbackLink;
+ }
}
if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams(
- Html::element( 'a', array(
- 'href' => $this->mNewPage->getLocalUrl( array(
+ $undoLink = Html::element( 'a', array(
+ 'href' => $this->mNewPage->getLocalURL( array(
'action' => 'edit',
'undoafter' => $this->mOldid,
'undo' => $this->mNewid ) ),
'title' => Linker::titleAttrib( 'undo' )
),
$this->msg( 'editundo' )->text()
- ) )->escaped();
+ );
+ $revisionTools[] = $undoLink;
}
}
@@ -324,12 +307,14 @@ class DifferenceEngine extends ContextSource {
$ldel = $this->revisionDeleteLink( $this->mOldRev );
$oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
+ $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff' );
$oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
'<div id="mw-diff-otitle2">' .
Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
'<div id="mw-diff-otitle3">' . $oldminor .
Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
+ '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
'<div id="mw-diff-otitle4">' . $prevlink . '</div>';
if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
@@ -366,19 +351,30 @@ class DifferenceEngine extends ContextSource {
# Handle RevisionDelete links...
$rdel = $this->revisionDeleteLink( $this->mNewRev );
- $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . $undoLink;
+
+ # Allow extensions to define their own revision tools
+ wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools ) );
+ $formattedRevisionTools = array();
+ // Put each one in parentheses (poor man's button)
+ foreach ( $revisionTools as $tool ) {
+ $formattedRevisionTools[] = $this->msg( 'parentheses' )->rawParams( $tool )->escaped();
+ }
+ $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . ' ' . implode( ' ', $formattedRevisionTools );
+ $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff' );
$newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
'<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
" $rollback</div>" .
'<div id="mw-diff-ntitle3">' . $newminor .
Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
+ '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
'<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
$deleted = true; // new revisions text is hidden
- if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+ if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
$suppressed = true; // also suppressed
+ }
}
# If the diff cannot be shown due to a deleted revision, then output
@@ -394,7 +390,7 @@ class DifferenceEngine extends ContextSource {
array( $msg ) );
} else {
# Give explanation and add a link to view the diff...
- $link = $this->getTitle()->getFullUrl( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) );
+ $link = $this->getTitle()->getFullURL( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) );
$msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
$out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) );
}
@@ -417,49 +413,54 @@ class DifferenceEngine extends ContextSource {
/**
* Get a link to mark the change as patrolled, or '' if there's either no
* revision to patrol or the user is not allowed to to it.
- * Side effect: this method will call OutputPage::preventClickjacking()
- * when a link is builded.
+ * Side effect: When the patrol link is build, this method will call
+ * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
*
* @return String
*/
protected function markPatrolledLink() {
- global $wgUseRCPatrol;
+ global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
+ $user = $this->getUser();
if ( $this->mMarkPatrolledLink === null ) {
// Prepare a change patrol link, if applicable
- if ( $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $this->getUser() ) ) {
- // 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;
+ if (
+ // Is patrolling enabled and the user allowed to?
+ $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
+ // Only do this if the revision isn't more than 6 hours older
+ // than the Max RC age (6h because the RC might not be cleaned out regularly)
+ RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
+ ) {
+ // Look for an unpatrolled change corresponding to this diff
+
+ $db = wfGetDB( DB_SLAVE );
+ $change = RecentChange::newFromConds(
+ array(
+ 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
+ 'rc_this_oldid' => $this->mNewid,
+ 'rc_patrolled' => 0
+ ),
+ __METHOD__,
+ array( 'USE INDEX' => 'rc_timestamp' )
+ );
+
+ if ( $change && $change->getPerformer()->getName() !== $user->getName() ) {
+ $rcid = $change->getAttribute( 'rc_id' );
} 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;
- }
+ // None found or the page has been created by the current user.
+ // If the user could patrol this it already would be patrolled
+ $rcid = 0;
}
// Build the link
if ( $rcid ) {
$this->getOutput()->preventClickjacking();
- $token = $this->getUser()->getEditToken( $rcid );
+ if ( $wgEnableAPI && $wgEnableWriteAPI
+ && $user->isAllowed( 'writeapi' )
+ ) {
+ $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
+ }
+
+ $token = $user->getEditToken( $rcid );
$this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown(
$this->mNewPage,
$this->msg( 'markaspatrolleddiff' )->escaped(),
@@ -510,19 +511,23 @@ class DifferenceEngine extends ContextSource {
$out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
$out->setArticleFlag( true );
+ // NOTE: only needed for B/C: custom rendering of JS/CSS via hook
if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
// Stolen from Article::view --AG 2007-10-11
// Give hooks a chance to customise the output
- // @TODO: standardize this crap into one function
- if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
- // Wrap the whole lot in a <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
- $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $out->addHTML( htmlspecialchars( $this->mNewtext ) );
- $out->addHTML( "\n</pre>\n" );
+ // @todo standardize this crap into one function
+ if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+ // NOTE: deprecated hook, B/C only
+ // use the content object's own rendering
+ $cnt = $this->mNewRev->getContent();
+ $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null;
+ $txt = $po ? $po->getText() : '';
+ $out->addHTML( $txt );
}
- } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
+ } elseif ( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+ // Handled by extension
+ } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+ // NOTE: deprecated hook, B/C only
// Handled by extension
} else {
// Normal page
@@ -536,16 +541,21 @@ class DifferenceEngine extends ContextSource {
$wikiPage = WikiPage::factory( $this->mNewPage );
}
- $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
+ $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
- if ( !$this->mNewRev->isCurrent() ) {
- $parserOptions->setEditSection( false );
- }
+ # Also try to load it as a redirect
+ $rt = $this->mNewContent ? $this->mNewContent->getRedirectTarget() : null;
- $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
+ if ( $rt ) {
+ $article = Article::newFromTitle( $this->mNewPage, $this->getContext() );
+ $out->addHTML( $article->viewRedirect( $rt ) );
- # WikiPage::getParserOutput() should not return false, but just in case
- if( $parserOutput ) {
+ # WikiPage::getParserOutput() should not return false, but just in case
+ if ( $parserOutput ) {
+ # Show categories etc.
+ $out->addParserOutputNoText( $parserOutput );
+ }
+ } elseif ( $parserOutput ) {
$out->addParserOutput( $parserOutput );
}
}
@@ -556,6 +566,17 @@ class DifferenceEngine extends ContextSource {
wfProfileOut( __METHOD__ );
}
+ protected function getParserOutput( WikiPage $page, Revision $rev ) {
+ $parserOptions = $page->makeParserOptions( $this->getContext() );
+
+ if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) {
+ $parserOptions->setEditSection( false );
+ }
+
+ $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
+ return $parserOutput;
+ }
+
/**
* Get the diff text, send it to the OutputPage object
* Returns false if the diff could not be generated, otherwise returns true
@@ -584,9 +605,9 @@ class DifferenceEngine extends ContextSource {
/**
* Get complete diff table, including header
*
- * @param $otitle Title: old title
- * @param $ntitle Title: new title
- * @param $notice String: HTML between diff header and body
+ * @param string|bool $otitle Header for old text or false
+ * @param string|bool $ntitle Header for new text or false
+ * @param string $notice HTML between diff header and body
* @return mixed
*/
function getDiff( $otitle, $ntitle, $notice = '' ) {
@@ -595,6 +616,10 @@ class DifferenceEngine extends ContextSource {
return false;
} else {
$multi = $this->getMultiNotice();
+ // Display a message when the diff is empty
+ if ( $body === '' ) {
+ $notice .= '<div class="mw-diff-empty">' . $this->msg( 'diff-empty' )->parse() . "</div>\n";
+ }
return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
}
}
@@ -620,7 +645,6 @@ class DifferenceEngine extends ContextSource {
return false;
}
// Short-circuit
- // If mOldRev is false, it means that the
if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
&& $this->mOldRev->getID() == $this->mNewRev->getID() ) )
{
@@ -652,7 +676,7 @@ class DifferenceEngine extends ContextSource {
return false;
}
- $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+ $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
// Save to cache for 7 days
if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
@@ -672,31 +696,64 @@ class DifferenceEngine extends ContextSource {
}
/**
- * Make sure the proper modules are loaded before we try to
- * make the diff
+ * Generate a diff, no caching.
+ *
+ * This implementation uses generateTextDiffBody() to generate a diff based on the default
+ * serialization of the given Content objects. This will fail if $old or $new are not
+ * instances of TextContent.
+ *
+ * Subclasses may override this to provide a different rendering for the diff,
+ * perhaps taking advantage of the content's native form. This is required for all content
+ * models that are not text based.
+ *
+ * @param $old Content: old content
+ * @param $new Content: new content
+ *
+ * @return bool|string
+ * @since 1.21
+ * @throws MWException if $old or $new are not instances of TextContent.
*/
- private function initDiffEngines() {
- global $wgExternalDiffEngine;
- if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
- wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
- wfDl( 'php_wikidiff' );
- wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
+ function generateContentDiffBody( Content $old, Content $new ) {
+ if ( !( $old instanceof TextContent ) ) {
+ throw new MWException( "Diff not implemented for " . get_class( $old ) . "; "
+ . "override generateContentDiffBody to fix this." );
}
- elseif ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
- wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
- wfDl( 'wikidiff2' );
- wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
+
+ if ( !( $new instanceof TextContent ) ) {
+ throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
+ . "override generateContentDiffBody to fix this." );
}
+
+ $otext = $old->serialize();
+ $ntext = $new->serialize();
+
+ return $this->generateTextDiffBody( $otext, $ntext );
}
/**
* Generate a diff, no caching
*
- * @param $otext String: old text, must be already segmented
- * @param $ntext String: new text, must be already segmented
+ * @param string $otext old text, must be already segmented
+ * @param string $ntext new text, must be already segmented
* @return bool|string
+ * @deprecated since 1.21, use generateContentDiffBody() instead!
*/
function generateDiffBody( $otext, $ntext ) {
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+
+ return $this->generateTextDiffBody( $otext, $ntext );
+ }
+
+ /**
+ * Generate a diff, no caching
+ *
+ * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
+ *
+ * @param string $otext old text, must be already segmented
+ * @param string $ntext new text, must be already segmented
+ * @return bool|string
+ */
+ function generateTextDiffBody( $otext, $ntext ) {
global $wgExternalDiffEngine, $wgContLang;
wfProfileIn( __METHOD__ );
@@ -704,8 +761,6 @@ class DifferenceEngine extends ContextSource {
$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
@@ -800,11 +855,12 @@ class DifferenceEngine extends ContextSource {
}
function localiseLineNumbersCb( $matches ) {
- if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
+ if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
+ return '';
+ }
return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
}
-
/**
* If there are revisions between the ones being compared, return a note saying so.
* @return string
@@ -855,11 +911,11 @@ class DifferenceEngine extends ContextSource {
* Get a header for a specified revision.
*
* @param $rev Revision
- * @param $complete String: 'complete' to get the header wrapped depending
+ * @param string $complete 'complete' to get the header wrapped depending
* the visibility of the revision and a link to edit the page.
* @return String HTML fragment
*/
- private function getRevisionHeader( Revision $rev, $complete = '' ) {
+ protected function getRevisionHeader( Revision $rev, $complete = '' ) {
$lang = $this->getLanguage();
$user = $this->getUser();
$revtimestamp = $rev->getTimestamp();
@@ -932,11 +988,13 @@ class DifferenceEngine extends ContextSource {
$colspan = 1;
$multiColspan = 2;
}
- $header .= "
- <tr style='vertical-align: top;'>
- <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
- <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
- </tr>";
+ if ( $otitle || $ntitle ) {
+ $header .= "
+ <tr style='vertical-align: top;'>
+ <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
+ <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
+ </tr>";
+ }
}
if ( $multi != '' ) {
@@ -951,10 +1009,25 @@ class DifferenceEngine extends ContextSource {
/**
* Use specified text instead of loading from the database
+ * @deprecated since 1.21, use setContent() instead.
*/
function setText( $oldText, $newText ) {
- $this->mOldtext = $oldText;
- $this->mNewtext = $newText;
+ ContentHandler::deprecated( __METHOD__, "1.21" );
+
+ $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
+ $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
+
+ $this->setContent( $oldContent, $newContent );
+ }
+
+ /**
+ * Use specified text instead of loading from the database
+ * @since 1.21
+ */
+ function setContent( Content $oldContent, Content $newContent ) {
+ $this->mOldContent = $oldContent;
+ $this->mNewContent = $newContent;
+
$this->mTextLoaded = 2;
$this->mRevisionsLoaded = true;
}
@@ -1062,6 +1135,25 @@ class DifferenceEngine extends ContextSource {
$this->mOldPage = $this->mOldRev->getTitle();
}
+ // Load tags information for both revisions
+ $dbr = wfGetDB( DB_SLAVE );
+ if ( $this->mOldid !== false ) {
+ $this->mOldTags = $dbr->selectField(
+ 'tag_summary',
+ 'ts_tags',
+ array( 'ts_rev_id' => $this->mOldid ),
+ __METHOD__
+ );
+ } else {
+ $this->mOldTags = false;
+ }
+ $this->mNewTags = $dbr->selectField(
+ 'tag_summary',
+ 'ts_tags',
+ array( 'ts_rev_id' => $this->mNewid ),
+ __METHOD__
+ );
+
return true;
}
@@ -1073,26 +1165,29 @@ class DifferenceEngine extends ContextSource {
function loadText() {
if ( $this->mTextLoaded == 2 ) {
return true;
- } else {
- // Whether it succeeds or fails, we don't want to try again
- $this->mTextLoaded = 2;
}
+ // 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 ) {
+ $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( $this->mOldContent === null ) {
return false;
}
}
+
if ( $this->mNewRev ) {
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
- if ( $this->mNewtext === false ) {
+ $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( $this->mNewContent === null ) {
return false;
}
}
+
return true;
}
@@ -1104,13 +1199,16 @@ class DifferenceEngine extends ContextSource {
function loadNewText() {
if ( $this->mTextLoaded >= 1 ) {
return true;
- } else {
- $this->mTextLoaded = 1;
}
+
+ $this->mTextLoaded = 1;
+
if ( !$this->loadRevisionData() ) {
return false;
}
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
+
+ $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+
return true;
}
}
diff --git a/includes/diff/WikiDiff3.php b/includes/diff/WikiDiff3.php
index 66727445..ea6f6e5d 100644
--- a/includes/diff/WikiDiff3.php
+++ b/includes/diff/WikiDiff3.php
@@ -29,7 +29,7 @@
* (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
* "An O(NP) Sequence Comparison Algorithm").
*
- * This implementation supports an upper bound on the excution time.
+ * This implementation supports an upper bound on the execution time.
*
* Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
*
@@ -64,7 +64,7 @@ class WikiDiff3 {
public function diff( /*array*/ $from, /*array*/ $to ) {
// remember initial lengths
- $m = sizeof( $from );
+ $m = count( $from );
$n = count( $to );
$this->heuristicUsed = false;
@@ -490,7 +490,6 @@ class WikiDiff3 {
$temp = array( 0, 0, 0 );
-
$max_progress = array_fill( 0, ceil( max( $forward_end_diag - $forward_start_diag,
$backward_end_diag - $backward_start_diag ) / 2 ), $temp );
$num_progress = 0; // the 1st entry is current, it is initialized
diff --git a/includes/extauth/Hardcoded.php b/includes/extauth/Hardcoded.php
deleted file mode 100644
index dfb46742..00000000
--- a/includes/extauth/Hardcoded.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-/**
- * External authentication with hardcoded user names and passwords
- *
- * Copyright © 2009 Aryeh Gregor
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * This class supports external authentication from a literal array dumped in
- * LocalSettings.php. It's mostly useful for testing. Example configuration:
- *
- * $wgExternalAuthType = 'ExternalUser_Hardcoded';
- * $wgExternalAuthConf = array(
- * 'Bob Smith' => array(
- * 'password' => 'literal string',
- * 'emailaddress' => 'bob@example.com',
- * ),
- * );
- *
- * Multiple names may be provided. The keys of the inner arrays can be either
- * 'password', or the name of any preference.
- *
- * @ingroup ExternalUser
- */
-class ExternalUser_Hardcoded extends ExternalUser {
- private $mName;
-
- protected function initFromName( $name ) {
- global $wgExternalAuthConf;
-
- if ( isset( $wgExternalAuthConf[$name] ) ) {
- $this->mName = $name;
- return true;
- }
- return false;
- }
-
- protected function initFromId( $id ) {
- return $this->initFromName( $id );
- }
-
- public function getId() {
- return $this->mName;
- }
-
- public function getName() {
- return $this->mName;
- }
-
- public function authenticate( $password ) {
- global $wgExternalAuthConf;
-
- return isset( $wgExternalAuthConf[$this->mName]['password'] )
- && $wgExternalAuthConf[$this->mName]['password'] == $password;
- }
-
- public function getPref( $pref ) {
- global $wgExternalAuthConf;
-
- if ( isset( $wgExternalAuthConf[$this->mName][$pref] ) ) {
- return $wgExternalAuthConf[$this->mName][$pref];
- }
- return null;
- }
-
- # TODO: Implement setPref() via regex on LocalSettings. (Just kidding.)
-}
diff --git a/includes/extauth/MediaWiki.php b/includes/extauth/MediaWiki.php
deleted file mode 100644
index 0a5efae6..00000000
--- a/includes/extauth/MediaWiki.php
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php
-/**
- * External authentication with external MediaWiki database.
- *
- * Copyright © 2009 Aryeh Gregor
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * This class supports authentication against an external MediaWiki database,
- * probably any version back to 1.5 or something. Example configuration:
- *
- * $wgExternalAuthType = 'ExternalUser_MediaWiki';
- * $wgExternalAuthConf = array(
- * 'DBtype' => 'mysql',
- * 'DBserver' => 'localhost',
- * 'DBname' => 'wikidb',
- * 'DBuser' => 'quasit',
- * 'DBpassword' => 'a5Cr:yf9u-6[{`g',
- * 'DBprefix' => '',
- * );
- *
- * All fields must be present. These mean the same things as $wgDBtype,
- * $wgDBserver, etc. This implementation is quite crude; it could easily
- * support multiple database servers, for instance, and memcached, and it
- * probably has bugs. Kind of hard to reuse code when things might rely on who
- * knows what configuration globals.
- *
- * If either wiki uses the UserComparePasswords hook, password authentication
- * might fail unexpectedly unless they both do the exact same validation.
- * There may be other corner cases like this where this will fail, but it
- * should be unlikely.
- *
- * @ingroup ExternalUser
- */
-class ExternalUser_MediaWiki extends ExternalUser {
- private $mRow;
-
- /**
- * @var DatabaseBase
- */
- private $mDb;
-
- /**
- * @param $name string
- * @return bool
- */
- 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 ) );
- }
-
- /**
- * @param $id int
- * @return bool
- */
- protected function initFromId( $id ) {
- return $this->initFromCond( array( 'user_id' => $id ) );
- }
-
- /**
- * @param $cond array
- * @return bool
- */
- private function initFromCond( $cond ) {
- global $wgExternalAuthConf;
-
- $this->mDb = DatabaseBase::factory( $wgExternalAuthConf['DBtype'],
- array(
- 'host' => $wgExternalAuthConf['DBserver'],
- 'user' => $wgExternalAuthConf['DBuser'],
- 'password' => $wgExternalAuthConf['DBpassword'],
- 'dbname' => $wgExternalAuthConf['DBname'],
- 'tablePrefix' => $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;
- }
-
- /**
- * @return string
- */
- 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 ) {
- # @todo 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;
- }
-
- /**
- * @return array
- */
- public function getGroups() {
- # @todo 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
deleted file mode 100644
index 0565a2e3..00000000
--- a/includes/extauth/vB.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-/**
- * External authentication with a vBulletin database.
- *
- * Copyright © 2009 Aryeh Gregor
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * This class supports the proprietary vBulletin forum system
- * <http://www.vbulletin.com>, versions 3.5 and up. It calls no functions or
- * code, only reads from the database. Example lines to put in
- * LocalSettings.php:
- *
- * $wgExternalAuthType = 'ExternalUser_vB';
- * $wgExternalAuthConf = array(
- * 'server' => 'localhost',
- * 'username' => 'forum',
- * 'password' => 'udE,jSqDJ<""p=fI.K9',
- * 'dbname' => 'forum',
- * 'tablePrefix' => '',
- * 'cookieprefix' => 'bb'
- * );
- *
- * @ingroup ExternalUser
- */
-class ExternalUser_vB extends ExternalUser {
- private $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, $wgRequest;
- if ( !isset( $wgExternalAuthConf['cookieprefix'] ) ) {
- $prefix = 'bb';
- } else {
- $prefix = $wgExternalAuthConf['cookieprefix'];
- }
- if ( $wgRequest->getCookie( 'sessionhash', $prefix ) === null ) {
- return false;
- }
-
- $db = $this->getDb();
-
- $row = $db->selectRow(
- array( 'session', 'user' ),
- $this->getFields(),
- array(
- 'session.userid = user.userid',
- 'sessionhash' => $wgRequest->getCookie( 'sessionhash', $prefix ),
- ),
- __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 DatabaseBase::factory( 'mysql',
- array(
- 'host' => $wgExternalAuthConf['server'],
- 'user' => $wgExternalAuthConf['username'],
- 'password' => $wgExternalAuthConf['password'],
- 'dbname' => $wgExternalAuthConf['dbname'],
- 'tablePrefix' => $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/externalstore/ExternalStore.php b/includes/externalstore/ExternalStore.php
new file mode 100644
index 00000000..462b0b90
--- /dev/null
+++ b/includes/externalstore/ExternalStore.php
@@ -0,0 +1,227 @@
+<?php
+/**
+ * @defgroup ExternalStorage ExternalStorage
+ */
+
+/**
+ * Interface for data storage in external repositories.
+ *
+ * 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
+ */
+
+/**
+ * Constructor class for key/value blob data kept in external repositories.
+ *
+ * Objects in external stores are defined by a special URL. The URL is of
+ * the form "<store protocol>://<location>/<object name>". The protocol is used
+ * to determine what ExternalStoreMedium class is used. The location identifies
+ * particular storage instances or database clusters for store class to use.
+ *
+ * When an object is inserted into a store, the calling code uses a partial URL of
+ * the form "<store protocol>://<location>" and receives the full object URL on success.
+ * This is useful since object names can be sequential IDs, UUIDs, or hashes.
+ * Callers are not responsible for unique name generation.
+ *
+ * External repositories might be populated by maintenance/async
+ * scripts, thus partial moving of data may be possible, as well
+ * as the possibility to have any storage format (i.e. for archives).
+ *
+ * @ingroup ExternalStorage
+ */
+class ExternalStore {
+ /**
+ * Get an external store object of the given type, with the given parameters
+ *
+ * @param string $proto Type of external storage, should be a value in $wgExternalStores
+ * @param array $params Associative array of ExternalStoreMedium parameters
+ * @return ExternalStoreMedium|bool The store class or false on error
+ */
+ public static function getStoreObject( $proto, array $params = array() ) {
+ global $wgExternalStores;
+
+ if ( !$wgExternalStores || !in_array( $proto, $wgExternalStores ) ) {
+ return false; // protocol not enabled
+ }
+
+ $class = 'ExternalStore' . ucfirst( $proto );
+ // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
+ return class_exists( $class ) ? new $class( $params ) : false;
+ }
+
+ /**
+ * Fetch data from given URL
+ *
+ * @param string $url The URL of the text to get
+ * @param array $params Associative array of ExternalStoreMedium parameters
+ * @return string|bool The text stored or false on error
+ * @throws MWException
+ */
+ public static function fetchFromURL( $url, array $params = array() ) {
+ $parts = explode( '://', $url, 2 );
+ if ( count( $parts ) != 2 ) {
+ return false; // invalid URL
+ }
+
+ list( $proto, $path ) = $parts;
+ if ( $path == '' ) { // bad URL
+ return false;
+ }
+
+ $store = self::getStoreObject( $proto, $params );
+ if ( $store === false ) {
+ return false;
+ }
+
+ return $store->fetchFromURL( $url );
+ }
+
+ /**
+ * Fetch data from multiple URLs with a minimum of round trips
+ *
+ * @param array $urls The URLs of the text to get
+ * @return array Map from url to its data. Data is either string when found
+ * or false on failure.
+ */
+ public static function batchFetchFromURLs( array $urls ) {
+ $batches = array();
+ foreach ( $urls as $url ) {
+ $scheme = parse_url( $url, PHP_URL_SCHEME );
+ if ( $scheme ) {
+ $batches[$scheme][] = $url;
+ }
+ }
+ $retval = array();
+ foreach ( $batches as $proto => $batchedUrls ) {
+ $store = self::getStoreObject( $proto );
+ if ( $store === false ) {
+ continue;
+ }
+ $retval += $store->batchFetchFromURLs( $batchedUrls );
+ }
+ // invalid, not found, db dead, etc.
+ $missing = array_diff( $urls, array_keys( $retval ) );
+ if ( $missing ) {
+ foreach ( $missing as $url ) {
+ $retval[$url] = false;
+ }
+ }
+ return $retval;
+ }
+
+ /**
+ * 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.
+ *
+ * @param string $url A partial external store URL ("<store type>://<location>")
+ * @param $data string
+ * @param array $params Associative array of ExternalStoreMedium parameters
+ * @return string|bool The URL of the stored data item, or false on error
+ * @throws MWException
+ */
+ public static function insert( $url, $data, array $params = array() ) {
+ $parts = explode( '://', $url, 2 );
+ if ( count( $parts ) != 2 ) {
+ return false; // invalid URL
+ }
+
+ list( $proto, $path ) = $parts;
+ if ( $path == '' ) { // bad URL
+ return false;
+ }
+
+ $store = self::getStoreObject( $proto, $params );
+ if ( $store === false ) {
+ return false;
+ } else {
+ return $store->store( $path, $data );
+ }
+ }
+
+ /**
+ * Like insert() above, but does more of the work for us.
+ * This function does not need a url param, it builds it by
+ * itself. It also fails-over to the next possible clusters
+ * provided by $wgDefaultExternalStore.
+ *
+ * @param string $data
+ * @param array $params Associative array of ExternalStoreMedium parameters
+ * @return string|bool The URL of the stored data item, or false on error
+ * @throws MWException
+ */
+ public static function insertToDefault( $data, array $params = array() ) {
+ global $wgDefaultExternalStore;
+
+ return self::insertWithFallback( (array)$wgDefaultExternalStore, $data, $params );
+ }
+
+ /**
+ * Like insert() above, but does more of the work for us.
+ * This function does not need a url param, it builds it by
+ * itself. It also fails-over to the next possible clusters
+ * as provided in the first parameter.
+ *
+ * @param array $tryStores refer to $wgDefaultExternalStore
+ * @param string $data
+ * @param array $params Associative array of ExternalStoreMedium parameters
+ * @return string|bool The URL of the stored data item, or false on error
+ * @throws MWException
+ */
+ public static function insertWithFallback( array $tryStores, $data, array $params = array() ) {
+ $error = false;
+ while ( count( $tryStores ) > 0 ) {
+ $index = mt_rand( 0, count( $tryStores ) - 1 );
+ $storeUrl = $tryStores[$index];
+ wfDebug( __METHOD__ . ": trying $storeUrl\n" );
+ list( $proto, $path ) = explode( '://', $storeUrl, 2 );
+ $store = self::getStoreObject( $proto, $params );
+ if ( $store === false ) {
+ throw new MWException( "Invalid external storage protocol - $storeUrl" );
+ }
+ try {
+ $url = $store->store( $path, $data ); // Try to save the object
+ } catch ( MWException $error ) {
+ $url = false;
+ }
+ if ( strlen( $url ) ) {
+ return $url; // Done!
+ } else {
+ unset( $tryStores[$index] ); // Don't try this one again!
+ $tryStores = array_values( $tryStores ); // Must have consecutive keys
+ wfDebugLog( 'ExternalStorage',
+ "Unable to store text to external storage $storeUrl" );
+ }
+ }
+ // All stores failed
+ if ( $error ) {
+ throw $error; // rethrow the last error
+ } else {
+ throw new MWException( "Unable to store text to external storage" );
+ }
+ }
+
+ /**
+ * @param $data string
+ * @param $wiki string
+ * @return string|bool The URL of the stored data item, or false on error
+ * @throws MWException
+ */
+ public static function insertToForeignDefault( $data, $wiki ) {
+ return self::insertToDefault( $data, array( 'wiki' => $wiki ) );
+ }
+}
diff --git a/includes/externalstore/ExternalStoreDB.php b/includes/externalstore/ExternalStoreDB.php
new file mode 100644
index 00000000..46a89379
--- /dev/null
+++ b/includes/externalstore/ExternalStoreDB.php
@@ -0,0 +1,283 @@
+<?php
+/**
+ * External storage in SQL database.
+ *
+ * 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
+ */
+
+/**
+ * DB accessable external objects.
+ *
+ * In this system, each store "location" maps to a database "cluster".
+ * The clusters must be defined in the normal LBFactory configuration.
+ *
+ * @ingroup ExternalStorage
+ */
+class ExternalStoreDB extends ExternalStoreMedium {
+ /**
+ * The provided URL is in the form of DB://cluster/id
+ * or DB://cluster/id/itemid for concatened storage.
+ *
+ * @see ExternalStoreMedium::fetchFromURL()
+ */
+ public function fetchFromURL( $url ) {
+ list( $cluster, $id, $itemID ) = $this->parseURL( $url );
+ $ret =& $this->fetchBlob( $cluster, $id, $itemID );
+
+ if ( $itemID !== false && $ret !== false ) {
+ return $ret->getItem( $itemID );
+ }
+ return $ret;
+ }
+
+ /**
+ * Fetch data from given external store URLs.
+ * The provided URLs are in the form of DB://cluster/id
+ * or DB://cluster/id/itemid for concatened storage.
+ *
+ * @param array $urls An array of external store URLs
+ * @return array A map from url to stored content. Failed results
+ * are not represented.
+ */
+ public function batchFetchFromURLs( array $urls ) {
+ $batched = $inverseUrlMap = array();
+ foreach ( $urls as $url ) {
+ list( $cluster, $id, $itemID ) = $this->parseURL( $url );
+ $batched[$cluster][$id][] = $itemID;
+ // false $itemID gets cast to int, but should be ok
+ // since we do === from the $itemID in $batched
+ $inverseUrlMap[$cluster][$id][$itemID] = $url;
+ }
+ $ret = array();
+ foreach ( $batched as $cluster => $batchByCluster ) {
+ $res = $this->batchFetchBlobs( $cluster, $batchByCluster );
+ foreach ( $res as $id => $blob ) {
+ foreach ( $batchByCluster[$id] as $itemID ) {
+ $url = $inverseUrlMap[$cluster][$id][$itemID];
+ if ( $itemID === false ) {
+ $ret[$url] = $blob;
+ } else {
+ $ret[$url] = $blob->getItem( $itemID );
+ }
+ }
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @see ExternalStoreMedium::store()
+ */
+ public function store( $cluster, $data ) {
+ $dbw = $this->getMaster( $cluster );
+ $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' );
+ $dbw->insert( $this->getTable( $dbw ),
+ 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->commit( __METHOD__ );
+ }
+ return "DB://$cluster/$id";
+ }
+
+ /**
+ * Get a LoadBalancer for the specified cluster
+ *
+ * @param string $cluster cluster name
+ * @return LoadBalancer object
+ */
+ function &getLoadBalancer( $cluster ) {
+ $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
+
+ return wfGetLBFactory()->getExternalLB( $cluster, $wiki );
+ }
+
+ /**
+ * Get a slave database connection for the specified cluster
+ *
+ * @param string $cluster cluster name
+ * @return DatabaseBase object
+ */
+ function &getSlave( $cluster ) {
+ global $wgDefaultExternalStore;
+
+ $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
+ $lb =& $this->getLoadBalancer( $cluster );
+
+ if ( !in_array( "DB://" . $cluster, (array)$wgDefaultExternalStore ) ) {
+ wfDebug( "read only external store" );
+ $lb->allowLagged( true );
+ } else {
+ wfDebug( "writable external store" );
+ }
+
+ return $lb->getConnection( DB_SLAVE, array(), $wiki );
+ }
+
+ /**
+ * Get a master database connection for the specified cluster
+ *
+ * @param string $cluster cluster name
+ * @return DatabaseBase object
+ */
+ function &getMaster( $cluster ) {
+ $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
+ $lb =& $this->getLoadBalancer( $cluster );
+ return $lb->getConnection( DB_MASTER, array(), $wiki );
+ }
+
+ /**
+ * 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 ) ) {
+ $table = 'blobs';
+ }
+ return $table;
+ }
+
+ /**
+ * Fetch a blob item out of the database; a cache of the last-loaded
+ * blob will be kept so that multiple loads out of a multi-item blob
+ * can avoid redundant database access and decompression.
+ * @param $cluster
+ * @param $id
+ * @param $itemID
+ * @return mixed
+ * @private
+ */
+ function &fetchBlob( $cluster, $id, $itemID ) {
+ /**
+ * One-step cache variable to hold base blobs; operations that
+ * pull multiple revisions may often pull multiple times from
+ * the same blob. By keeping the last-used one open, we avoid
+ * redundant unserialization and decompression overhead.
+ */
+ static $externalBlobCache = array();
+
+ $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/";
+ if ( isset( $externalBlobCache[$cacheID] ) ) {
+ wfDebugLog( 'ExternalStoreDB-cache',
+ "ExternalStoreDB::fetchBlob cache hit on $cacheID\n" );
+ return $externalBlobCache[$cacheID];
+ }
+
+ wfDebugLog( 'ExternalStoreDB-cache',
+ "ExternalStoreDB::fetchBlob cache miss on $cacheID\n" );
+
+ $dbr =& $this->getSlave( $cluster );
+ $ret = $dbr->selectField( $this->getTable( $dbr ),
+ 'blob_text', array( 'blob_id' => $id ), __METHOD__ );
+ if ( $ret === false ) {
+ wfDebugLog( 'ExternalStoreDB',
+ "ExternalStoreDB::fetchBlob master fallback on $cacheID\n" );
+ // Try the master
+ $dbw =& $this->getMaster( $cluster );
+ $ret = $dbw->selectField( $this->getTable( $dbw ),
+ 'blob_text', array( 'blob_id' => $id ), __METHOD__ );
+ if ( $ret === false ) {
+ wfDebugLog( 'ExternalStoreDB',
+ "ExternalStoreDB::fetchBlob master failed to find $cacheID\n" );
+ }
+ }
+ if ( $itemID !== false && $ret !== false ) {
+ // Unserialise object; caller extracts item
+ $ret = unserialize( $ret );
+ }
+
+ $externalBlobCache = array( $cacheID => &$ret );
+ return $ret;
+ }
+
+ /**
+ * Fetch multiple blob items out of the database
+ *
+ * @param string $cluster A cluster name valid for use with LBFactory
+ * @param array $ids A map from the blob_id's to look for to the requested itemIDs in the blobs
+ * @return array A map from the blob_id's requested to their content. Unlocated ids are not represented
+ */
+ function batchFetchBlobs( $cluster, array $ids ) {
+ $dbr = $this->getSlave( $cluster );
+ $res = $dbr->select( $this->getTable( $dbr ),
+ array( 'blob_id', 'blob_text' ), array( 'blob_id' => array_keys( $ids ) ), __METHOD__ );
+ $ret = array();
+ if ( $res !== false ) {
+ $this->mergeBatchResult( $ret, $ids, $res );
+ }
+ if ( $ids ) {
+ wfDebugLog( __CLASS__, __METHOD__ .
+ " master fallback on '$cluster' for: " .
+ implode( ',', array_keys( $ids ) ) . "\n" );
+ // Try the master
+ $dbw = $this->getMaster( $cluster );
+ $res = $dbw->select( $this->getTable( $dbr ),
+ array( 'blob_id', 'blob_text' ),
+ array( 'blob_id' => array_keys( $ids ) ),
+ __METHOD__ );
+ if ( $res === false ) {
+ wfDebugLog( __CLASS__, __METHOD__ . " master failed on '$cluster'\n" );
+ } else {
+ $this->mergeBatchResult( $ret, $ids, $res );
+ }
+ }
+ if ( $ids ) {
+ wfDebugLog( __CLASS__, __METHOD__ .
+ " master on '$cluster' failed locating items: " .
+ implode( ',', array_keys( $ids ) ) . "\n" );
+ }
+ return $ret;
+ }
+
+ /**
+ * Helper function for self::batchFetchBlobs for merging master/slave results
+ * @param array &$ret Current self::batchFetchBlobs return value
+ * @param array &$ids Map from blob_id to requested itemIDs
+ * @param mixed $res DB result from DatabaseBase::select
+ */
+ private function mergeBatchResult( array &$ret, array &$ids, $res ) {
+ foreach ( $res as $row ) {
+ $id = $row->blob_id;
+ $itemIDs = $ids[$id];
+ unset( $ids[$id] ); // to track if everything is found
+ if ( count( $itemIDs ) === 1 && reset( $itemIDs ) === false ) {
+ // single result stored per blob
+ $ret[$id] = $row->blob_text;
+ } else {
+ // multi result stored per blob
+ $ret[$id] = unserialize( $row->blob_text );
+ }
+ }
+ }
+
+ protected function parseURL( $url ) {
+ $path = explode( '/', $url );
+ return array(
+ $path[2], // cluster
+ $path[3], // id
+ isset( $path[4] ) ? $path[4] : false // itemID
+ );
+ }
+}
diff --git a/includes/ExternalStoreHttp.php b/includes/externalstore/ExternalStoreHttp.php
index 311e32b3..345c17be 100644
--- a/includes/ExternalStoreHttp.php
+++ b/includes/externalstore/ExternalStoreHttp.php
@@ -26,20 +26,18 @@
*
* @ingroup ExternalStorage
*/
-class ExternalStoreHttp {
-
+class ExternalStoreHttp extends ExternalStoreMedium {
/**
- * Fetch data from given URL
- *
- * @param $url String: the URL
- * @return String: the content at $url
+ * @see ExternalStoreMedium::fetchFromURL()
*/
- function fetchFromURL( $url ) {
- $ret = Http::get( $url );
- return $ret;
+ public function fetchFromURL( $url ) {
+ return Http::get( $url );
}
- /* XXX: may require other methods, for store, delete,
- * whatever, for initial ext storage
+ /**
+ * @see ExternalStoreMedium::store()
*/
+ public function store( $cluster, $data ) {
+ throw new MWException( "ExternalStoreHttp is read-only and does not support store()." );
+ }
}
diff --git a/includes/externalstore/ExternalStoreMedium.php b/includes/externalstore/ExternalStoreMedium.php
new file mode 100644
index 00000000..02bdcb51
--- /dev/null
+++ b/includes/externalstore/ExternalStoreMedium.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * External storage in some particular medium.
+ *
+ * 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 ExternalStorage
+ * @author Aaron Schulz
+ */
+
+/**
+ * Accessable external objects in a particular storage medium
+ *
+ * @ingroup ExternalStorage
+ * @since 1.21
+ */
+abstract class ExternalStoreMedium {
+ /** @var Array */
+ protected $params = array();
+
+ /**
+ * @param array $params Options
+ */
+ public function __construct( array $params = array() ) {
+ $this->params = $params;
+ }
+
+ /**
+ * Fetch data from given external store URL
+ *
+ * @param string $url An external store URL
+ * @return string|bool The text stored or false on error
+ * @throws MWException
+ */
+ abstract public function fetchFromURL( $url );
+
+ /**
+ * Fetch data from given external store URLs.
+ *
+ * @param array $urls A list of external store URLs
+ * @return array Map from the url to the text stored. Unfound data is not represented
+ */
+ public function batchFetchFromURLs( array $urls ) {
+ $retval = array();
+ foreach ( $urls as $url ) {
+ $data = $this->fetchFromURL( $url );
+ // Dont return when false to allow for simpler implementations.
+ // errored urls are handled in ExternalStore::batchFetchFromURLs
+ if ( $data !== false ) {
+ $retval[$urls] = $data;
+ }
+ }
+ return $retval;
+ }
+
+ /**
+ * Insert a data item into a given location
+ *
+ * @param string $location the location name
+ * @param string $data the data item
+ * @return string|bool The URL of the stored data item, or false on error
+ * @throws MWException
+ */
+ abstract public function store( $location, $data );
+}
diff --git a/includes/externalstore/ExternalStoreMwstore.php b/includes/externalstore/ExternalStoreMwstore.php
new file mode 100644
index 00000000..aa486796
--- /dev/null
+++ b/includes/externalstore/ExternalStoreMwstore.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * External storage in a file backend.
+ *
+ * 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
+ */
+
+/**
+ * File backend accessable external objects.
+ *
+ * In this system, each store "location" maps to the name of a file backend.
+ * The file backends must be defined in $wgFileBackends and must be global
+ * and fully qualified with a global "wikiId" prefix in the configuration.
+ *
+ * @ingroup ExternalStorage
+ * @since 1.21
+ */
+class ExternalStoreMwstore extends ExternalStoreMedium {
+ /**
+ * The URL returned is of the form of the form mwstore://backend/container/wiki/id
+ *
+ * @see ExternalStoreMedium::fetchFromURL()
+ */
+ public function fetchFromURL( $url ) {
+ $be = FileBackendGroup::singleton()->backendFromPath( $url );
+ if ( $be instanceof FileBackend ) {
+ // We don't need "latest" since objects are immutable and
+ // backends should at least have "read-after-create" consistency.
+ return $be->getFileContents( array( 'src' => $url ) );
+ }
+ return false;
+ }
+
+ /**
+ * Fetch data from given external store URLs.
+ * The URL returned is of the form of the form mwstore://backend/container/wiki/id
+ *
+ * @param array $urls An array of external store URLs
+ * @return array A map from url to stored content. Failed results are not represented.
+ */
+ public function batchFetchFromURLs( array $urls ) {
+ $pathsByBackend = array();
+ foreach ( $urls as $url ) {
+ $be = FileBackendGroup::singleton()->backendFromPath( $url );
+ if ( $be instanceof FileBackend ) {
+ $pathsByBackend[$be->getName()][] = $url;
+ }
+ }
+ $blobs = array();
+ foreach ( $pathsByBackend as $backendName => $paths ) {
+ $be = FileBackendGroup::get( $backendName );
+ $blobs = $blobs + $be->getFileContentsMulti( array( 'srcs' => $paths ) );
+ }
+ return $blobs;
+ }
+
+ /**
+ * @see ExternalStoreMedium::store()
+ */
+ public function store( $backend, $data ) {
+ $be = FileBackendGroup::singleton()->get( $backend );
+ if ( $be instanceof FileBackend ) {
+ // Get three random base 36 characters to act as shard directories
+ $rand = wfBaseConvert( mt_rand( 0, 46655 ), 10, 36, 3 );
+ // Make sure ID is roughly lexicographically increasing for performance
+ $id = str_pad( UIDGenerator::newTimestampedUID128( 32 ), 26, '0', STR_PAD_LEFT );
+ // Segregate items by wiki ID for the sake of bookkeeping
+ $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : wfWikiID();
+
+ $url = $be->getContainerStoragePath( 'data' ) . '/' .
+ rawurlencode( $wiki ) . "/{$rand[0]}/{$rand[1]}/{$rand[2]}/{$id}";
+
+ $be->prepare( array( 'dir' => dirname( $url ), 'noAccess' => 1, 'noListing' => 1 ) );
+ if ( $be->create( array( 'dst' => $url, 'content' => $data ) )->isOK() ) {
+ return $url;
+ }
+ }
+ return false;
+ }
+}
diff --git a/includes/filebackend/FSFile.php b/includes/filebackend/FSFile.php
index e07c99d4..8f0a1334 100644
--- a/includes/filebackend/FSFile.php
+++ b/includes/filebackend/FSFile.php
@@ -28,11 +28,12 @@
*/
class FSFile {
protected $path; // path to file
+ protected $sha1Base36; // file SHA-1 in base 36
/**
* Sets up the file object
*
- * @param $path string Path to temporary file on local disk
+ * @param string $path Path to temporary file on local disk
* @throws MWException
*/
public function __construct( $path ) {
@@ -86,8 +87,8 @@ class FSFile {
/**
* Guess the MIME type from the file contents alone
- *
- * @return string
+ *
+ * @return string
*/
public function getMimeType() {
return MimeMagic::singleton()->guessMimeType( $this->path, false );
@@ -97,14 +98,14 @@ class FSFile {
* Get an associative array containing information about
* a file with the given storage path.
*
- * @param $ext Mixed: the file extension, or true to extract it from the filename.
+ * @param Mixed $ext: the file extension, or true to extract it from the filename.
* Set it to false to ignore the extension.
*
* @return array
*/
public function getProps( $ext = true ) {
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__.": Getting file info for $this->path\n" );
+ wfDebug( __METHOD__ . ": Getting file info for $this->path\n" );
$info = self::placeholderProps();
$info['fileExists'] = $this->exists();
@@ -131,7 +132,7 @@ class FSFile {
# Height, width and metadata
$handler = MediaHandler::getHandler( $info['mime'] );
if ( $handler ) {
- $tempImage = (object)array();
+ $tempImage = (object)array(); // XXX (hack for File object)
$info['metadata'] = $handler->getMetadata( $tempImage, $this->path );
$gis = $handler->getImageSize( $tempImage, $this->path, $info['metadata'] );
if ( is_array( $gis ) ) {
@@ -140,9 +141,9 @@ class FSFile {
}
$info['sha1'] = $this->getSha1Base36();
- wfDebug(__METHOD__.": $this->path loaded, {$info['size']} bytes, {$info['mime']}.\n");
+ wfDebug( __METHOD__ . ": $this->path loaded, {$info['size']} bytes, {$info['mime']}.\n" );
} else {
- wfDebug(__METHOD__.": $this->path NOT FOUND!\n");
+ wfDebug( __METHOD__ . ": $this->path NOT FOUND!\n" );
}
wfProfileOut( __METHOD__ );
@@ -170,7 +171,7 @@ class FSFile {
/**
* Exract image size information
*
- * @param $gis array
+ * @param array $gis
* @return Array
*/
protected function extractImageSizeInfo( array $gis ) {
@@ -193,26 +194,33 @@ class FSFile {
* 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
* fairly neatly.
*
+ * @param bool $recache
* @return bool|string False on failure
*/
- public function getSha1Base36() {
+ public function getSha1Base36( $recache = false ) {
wfProfileIn( __METHOD__ );
+ if ( $this->sha1Base36 !== null && !$recache ) {
+ wfProfileOut( __METHOD__ );
+ return $this->sha1Base36;
+ }
+
wfSuppressWarnings();
- $hash = sha1_file( $this->path );
+ $this->sha1Base36 = sha1_file( $this->path );
wfRestoreWarnings();
- if ( $hash !== false ) {
- $hash = wfBaseConvert( $hash, 16, 36, 31 );
+
+ if ( $this->sha1Base36 !== false ) {
+ $this->sha1Base36 = wfBaseConvert( $this->sha1Base36, 16, 36, 31 );
}
wfProfileOut( __METHOD__ );
- return $hash;
+ return $this->sha1Base36;
}
/**
* Get the final file extension from a file system path
- *
- * @param $path string
+ *
+ * @param string $path
* @return string
*/
public static function extensionFromPath( $path ) {
@@ -223,10 +231,9 @@ class FSFile {
/**
* Get an associative array containing information about a file in the local filesystem.
*
- * @param $path String: absolute local filesystem path
- * @param $ext Mixed: the file extension, or true to extract it from the filename.
+ * @param string $path absolute local filesystem path
+ * @param Mixed $ext: the file extension, or true to extract it from the filename.
* Set it to false to ignore the extension.
- *
* @return array
*/
public static function getPropsFromPath( $path, $ext = true ) {
@@ -241,8 +248,7 @@ class FSFile {
* 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
* fairly neatly.
*
- * @param $path string
- *
+ * @param string $path
* @return bool|string False on failure
*/
public static function getSha1Base36FromPath( $path ) {
diff --git a/includes/filebackend/FSFileBackend.php b/includes/filebackend/FSFileBackend.php
index 93495340..6d642162 100644
--- a/includes/filebackend/FSFileBackend.php
+++ b/includes/filebackend/FSFileBackend.php
@@ -46,6 +46,7 @@ class FSFileBackend extends FileBackendStore {
protected $fileOwner; // string; required OS username to own files
protected $currentUser; // string; OS username running this script
+ /** @var Array */
protected $hadWarningErrors = array();
/**
@@ -69,7 +70,7 @@ class FSFileBackend extends FileBackendStore {
if ( isset( $config['containerPaths'] ) ) {
$this->containerPaths = (array)$config['containerPaths'];
foreach ( $this->containerPaths as &$path ) {
- $path = rtrim( $path, '/' ); // remove trailing slash
+ $path = rtrim( $path, '/' ); // remove trailing slash
}
}
@@ -81,12 +82,6 @@ class FSFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::resolveContainerPath()
- * @param $container string
- * @param $relStoragePath string
- * @return null|string
- */
protected function resolveContainerPath( $container, $relStoragePath ) {
// Check that container has a root directory
if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
@@ -101,7 +96,7 @@ class FSFileBackend extends FileBackendStore {
/**
* Sanity check a relative file system path for validity
*
- * @param $path string Normalized relative path
+ * @param string $path Normalized relative path
* @return bool
*/
protected function isLegalRelPath( $path ) {
@@ -120,8 +115,8 @@ class FSFileBackend extends FileBackendStore {
* Given the short (unresolved) and full (resolved) name of
* a container, return the file system path of the container.
*
- * @param $shortCont string
- * @param $fullCont string
+ * @param string $shortCont
+ * @param string $fullCont
* @return string|null
*/
protected function containerFSRoot( $shortCont, $fullCont ) {
@@ -136,7 +131,7 @@ class FSFileBackend extends FileBackendStore {
/**
* Get the absolute file system path for a storage path
*
- * @param $storagePath string Storage path
+ * @param string $storagePath Storage path
* @return string|null
*/
protected function resolveToFSPath( $storagePath ) {
@@ -144,7 +139,7 @@ class FSFileBackend extends FileBackendStore {
if ( $relPath === null ) {
return null; // invalid
}
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
$fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
if ( $relPath != '' ) {
$fsPath .= "/{$relPath}";
@@ -152,10 +147,6 @@ class FSFileBackend extends FileBackendStore {
return $fsPath;
}
- /**
- * @see FileBackendStore::isPathUsableInternal()
- * @return bool
- */
public function isPathUsableInternal( $storagePath ) {
$fsPath = $this->resolveToFSPath( $storagePath );
if ( $fsPath === null ) {
@@ -177,11 +168,7 @@ class FSFileBackend extends FileBackendStore {
return $ok;
}
- /**
- * @see FileBackendStore::doStoreInternal()
- * @return Status
- */
- protected function doStoreInternal( array $params ) {
+ protected function doCreateInternal( array $params ) {
$status = Status::newGood();
$dest = $this->resolveToFSPath( $params['dst'] );
@@ -190,27 +177,70 @@ class FSFileBackend extends FileBackendStore {
return $status;
}
- if ( file_exists( $dest ) ) {
- if ( !empty( $params['overwrite'] ) ) {
- $ok = unlink( $dest );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['dst'] );
- return $status;
- }
- } else {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
+ if ( !empty( $params['async'] ) ) { // deferred
+ $tempFile = TempFSFile::factory( 'create_', 'tmp' );
+ if ( !$tempFile ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
return $status;
}
+ $this->trapWarnings();
+ $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
+ $this->untrapWarnings();
+ if ( $bytes === false ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ return $status;
+ }
+ $cmd = implode( ' ', array(
+ wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
+ wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Create', $cmd, $dest );
+ $tempFile->bind( $status->value );
+ } else { // immediate write
+ $this->trapWarnings();
+ $bytes = file_put_contents( $dest, $params['content'] );
+ $this->untrapWarnings();
+ if ( $bytes === false ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ return $status;
+ }
+ $this->chmod( $dest );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCreate( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
+ protected function doStoreInternal( array $params ) {
+ $status = Status::newGood();
+
+ $dest = $this->resolveToFSPath( $params['dst'] );
+ if ( $dest === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+ return $status;
}
if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ $cmd = implode( ' ', array(
+ wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
) );
$status->value = new FSFileOpHandle( $this, $params, 'Store', $cmd, $dest );
} else { // immediate write
+ $this->trapWarnings();
$ok = copy( $params['src'], $dest );
+ $this->untrapWarnings();
// In some cases (at least over NFS), copy() returns true when it fails
if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
if ( $ok ) { // PHP bug
@@ -236,10 +266,6 @@ class FSFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doCopyInternal()
- * @return Status
- */
protected function doCopyInternal( array $params ) {
$status = Status::newGood();
@@ -255,31 +281,30 @@ class FSFileBackend extends FileBackendStore {
return $status;
}
- if ( file_exists( $dest ) ) {
- if ( !empty( $params['overwrite'] ) ) {
- $ok = unlink( $dest );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['dst'] );
- return $status;
- }
- } else {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
+ if ( !is_file( $source ) ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'] );
}
+ return $status; // do nothing; either OK or bad status
}
if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ $cmd = implode( ' ', array(
+ wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
) );
$status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd, $dest );
} else { // immediate write
- $ok = copy( $source, $dest );
+ $this->trapWarnings();
+ $ok = ( $source === $dest ) ? true : copy( $source, $dest );
+ $this->untrapWarnings();
// In some cases (at least over NFS), copy() returns true when it fails
if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
if ( $ok ) { // PHP bug
+ $this->trapWarnings();
unlink( $dest ); // remove broken file
+ $this->untrapWarnings();
trigger_error( __METHOD__ . ": copy() failed but returned true." );
}
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
@@ -301,10 +326,6 @@ class FSFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doMoveInternal()
- * @return Status
- */
protected function doMoveInternal( array $params ) {
$status = Status::newGood();
@@ -320,30 +341,24 @@ class FSFileBackend extends FileBackendStore {
return $status;
}
- if ( file_exists( $dest ) ) {
- if ( !empty( $params['overwrite'] ) ) {
- // Windows does not support moving over existing files
- if ( wfIsWindows() ) {
- $ok = unlink( $dest );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['dst'] );
- return $status;
- }
- }
- } else {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
+ if ( !is_file( $source ) ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'] );
}
+ return $status; // do nothing; either OK or bad status
}
if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', array( wfIsWindows() ? 'MOVE' : 'mv',
+ $cmd = implode( ' ', array(
+ wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite)
wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
) );
$status->value = new FSFileOpHandle( $this, $params, 'Move', $cmd );
} else { // immediate write
- $ok = rename( $source, $dest );
+ $this->trapWarnings();
+ $ok = ( $source === $dest ) ? true : rename( $source, $dest );
+ $this->untrapWarnings();
clearstatcache(); // file no longer at source
if ( !$ok ) {
$status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
@@ -364,10 +379,6 @@ class FSFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doDeleteInternal()
- * @return Status
- */
protected function doDeleteInternal( array $params ) {
$status = Status::newGood();
@@ -385,12 +396,15 @@ class FSFileBackend extends FileBackendStore {
}
if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', array( wfIsWindows() ? 'DEL' : 'unlink',
+ $cmd = implode( ' ', array(
+ wfIsWindows() ? 'DEL' : 'unlink',
wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
) );
$status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd );
} else { // immediate write
+ $this->trapWarnings();
$ok = unlink( $source );
+ $this->untrapWarnings();
if ( !$ok ) {
$status->fatal( 'backend-fail-delete', $params['src'] );
return $status;
@@ -410,174 +424,96 @@ class FSFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doCreateInternal()
- * @return Status
- */
- protected function doCreateInternal( array $params ) {
- $status = Status::newGood();
-
- $dest = $this->resolveToFSPath( $params['dst'] );
- if ( $dest === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
- return $status;
- }
-
- if ( file_exists( $dest ) ) {
- if ( !empty( $params['overwrite'] ) ) {
- $ok = unlink( $dest );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['dst'] );
- return $status;
- }
- } else {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
- }
- }
-
- if ( !empty( $params['async'] ) ) { // deferred
- $tempFile = TempFSFile::factory( 'create_', 'tmp' );
- if ( !$tempFile ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
- return $status;
- }
- $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
- return $status;
- }
- $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
- wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
- wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
- ) );
- $status->value = new FSFileOpHandle( $this, $params, 'Create', $cmd, $dest );
- $tempFile->bind( $status->value );
- } else { // immediate write
- $bytes = file_put_contents( $dest, $params['content'] );
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
- return $status;
- }
- $this->chmod( $dest );
- }
-
- return $status;
- }
-
- /**
- * @see FSFileBackend::doExecuteOpHandlesInternal()
- */
- protected function _getResponseCreate( $errors, Status $status, array $params, $cmd ) {
- if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
- trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
- }
- }
-
- /**
- * @see FileBackendStore::doPrepareInternal()
- * @return Status
- */
protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
$existed = is_dir( $dir ); // already there?
- if ( !wfMkdirParents( $dir ) ) { // make directory and its parents
+ // Create the directory and its parents as needed...
+ $this->trapWarnings();
+ if ( !wfMkdirParents( $dir ) ) {
$status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
} elseif ( !is_writable( $dir ) ) {
$status->fatal( 'directoryreadonlyerror', $params['dir'] );
} elseif ( !is_readable( $dir ) ) {
$status->fatal( 'directorynotreadableerror', $params['dir'] );
}
+ $this->untrapWarnings();
+ // Respect any 'noAccess' or 'noListing' flags...
if ( is_dir( $dir ) && !$existed ) {
- // Respect any 'noAccess' or 'noListing' flags...
$status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
}
return $status;
}
- /**
- * @see FileBackendStore::doSecureInternal()
- * @return Status
- */
protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
// Seed new directories with a blank index.html, to prevent crawling...
if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
+ $this->trapWarnings();
$bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
+ $this->untrapWarnings();
if ( $bytes === false ) {
$status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
- return $status;
}
}
// Add a .htaccess file to the root of the container...
if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
+ $this->trapWarnings();
$bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
+ $this->untrapWarnings();
if ( $bytes === false ) {
$storeDir = "mwstore://{$this->name}/{$shortCont}";
$status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
- return $status;
}
}
return $status;
}
- /**
- * @see FileBackendStore::doPublishInternal()
- * @return Status
- */
protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
// Unseed new directories with a blank index.html, to allow crawling...
if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
$exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
+ $this->trapWarnings();
if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
$status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
- return $status;
}
+ $this->untrapWarnings();
}
// Remove the .htaccess file from the root of the container...
if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
$exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
+ $this->trapWarnings();
if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
$storeDir = "mwstore://{$this->name}/{$shortCont}";
$status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
- return $status;
}
+ $this->untrapWarnings();
}
return $status;
}
- /**
- * @see FileBackendStore::doCleanInternal()
- * @return Status
- */
protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- wfSuppressWarnings();
+ $this->trapWarnings();
if ( is_dir( $dir ) ) {
rmdir( $dir ); // remove directory if empty
}
- wfRestoreWarnings();
+ $this->untrapWarnings();
return $status;
}
- /**
- * @see FileBackendStore::doFileExists()
- * @return array|bool|null
- */
protected function doGetFileStat( array $params ) {
$source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
@@ -591,7 +527,7 @@ class FSFileBackend extends FileBackendStore {
if ( $stat ) {
return array(
'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
- 'size' => $stat['size']
+ 'size' => $stat['size']
);
} elseif ( !$hadError ) {
return false; // file does not exist
@@ -607,12 +543,8 @@ class FSFileBackend extends FileBackendStore {
clearstatcache(); // clear the PHP file stat cache
}
- /**
- * @see FileBackendStore::doDirectoryExists()
- * @return bool|null
- */
protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
@@ -628,7 +560,7 @@ class FSFileBackend extends FileBackendStore {
* @return Array|null
*/
public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
$exists = is_dir( $dir );
@@ -644,10 +576,10 @@ class FSFileBackend extends FileBackendStore {
/**
* @see FileBackendStore::getFileListInternal()
- * @return array|FSFileBackendFileList|null
+ * @return Array|FSFileBackendFileList|null
*/
public function getFileListInternal( $fullCont, $dirRel, array $params ) {
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
$exists = is_dir( $dir );
@@ -661,59 +593,57 @@ class FSFileBackend extends FileBackendStore {
return new FSFileBackendFileList( $dir, $params );
}
- /**
- * @see FileBackendStore::getLocalReference()
- * @return FSFile|null
- */
- public function getLocalReference( array $params ) {
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- return null;
- }
- return new FSFile( $source );
- }
+ protected function doGetLocalReferenceMulti( array $params ) {
+ $fsFiles = array(); // (path => FSFile)
- /**
- * @see FileBackendStore::getLocalCopy()
- * @return null|TempFSFile
- */
- public function getLocalCopy( array $params ) {
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- return null;
+ foreach ( $params['srcs'] as $src ) {
+ $source = $this->resolveToFSPath( $src );
+ if ( $source === null || !is_file( $source ) ) {
+ $fsFiles[$src] = null; // invalid path or file does not exist
+ } else {
+ $fsFiles[$src] = new FSFile( $source );
+ }
}
- // Create a new temporary file with the same extension...
- $ext = FileBackend::extensionFromPath( $params['src'] );
- $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
- if ( !$tmpFile ) {
- return null;
- }
- $tmpPath = $tmpFile->getPath();
+ return $fsFiles;
+ }
- // Copy the source file over the temp file
- $ok = copy( $source, $tmpPath );
- if ( !$ok ) {
- return null;
- }
+ protected function doGetLocalCopyMulti( array $params ) {
+ $tmpFiles = array(); // (path => TempFSFile)
- $this->chmod( $tmpPath );
+ foreach ( $params['srcs'] as $src ) {
+ $source = $this->resolveToFSPath( $src );
+ if ( $source === null ) {
+ $tmpFiles[$src] = null; // invalid path
+ } else {
+ // Create a new temporary file with the same extension...
+ $ext = FileBackend::extensionFromPath( $src );
+ $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
+ if ( !$tmpFile ) {
+ $tmpFiles[$src] = null;
+ } else {
+ $tmpPath = $tmpFile->getPath();
+ // Copy the source file over the temp file
+ $this->trapWarnings();
+ $ok = copy( $source, $tmpPath );
+ $this->untrapWarnings();
+ if ( !$ok ) {
+ $tmpFiles[$src] = null;
+ } else {
+ $this->chmod( $tmpPath );
+ $tmpFiles[$src] = $tmpFile;
+ }
+ }
+ }
+ }
- return $tmpFile;
+ return $tmpFiles;
}
- /**
- * @see FileBackendStore::directoriesAreVirtual()
- * @return bool
- */
protected function directoriesAreVirtual() {
return false;
}
- /**
- * @see FileBackendStore::doExecuteOpHandlesInternal()
- * @return Array List of corresponding Status objects
- */
protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
$statuses = array();
@@ -747,13 +677,13 @@ class FSFileBackend extends FileBackendStore {
/**
* Chmod a file, suppressing the warnings
*
- * @param $path string Absolute file system path
+ * @param string $path Absolute file system path
* @return bool Success
*/
protected function chmod( $path ) {
- wfSuppressWarnings();
+ $this->trapWarnings();
$ok = chmod( $path, $this->fileMode );
- wfRestoreWarnings();
+ $this->untrapWarnings();
return $ok;
}
@@ -779,7 +709,7 @@ class FSFileBackend extends FileBackendStore {
/**
* Clean up directory separators for the given OS
*
- * @param $path string FS path
+ * @param string $path FS path
* @return string
*/
protected function cleanPathSlashes( $path ) {
@@ -789,12 +719,11 @@ class FSFileBackend extends FileBackendStore {
/**
* Listen for E_WARNING errors and track whether any happen
*
- * @return bool
+ * @return void
*/
protected function trapWarnings() {
$this->hadWarningErrors[] = false; // push to stack
set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
- return false; // invoke normal PHP error handler
}
/**
@@ -808,9 +737,13 @@ class FSFileBackend extends FileBackendStore {
}
/**
+ * @param integer $errno
+ * @param string $errstr
* @return bool
+ * @access private
*/
- private function handleWarning() {
+ public function handleWarning( $errno, $errstr ) {
+ wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging
$this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
return true; // suppress from PHP handler
}
@@ -824,13 +757,15 @@ class FSFileOpHandle extends FileBackendStoreOpHandle {
public $chmodPath; // string; file to chmod
/**
- * @param $backend
- * @param $params array
- * @param $call
- * @param $cmd
- * @param $chmodPath null
+ * @param FSFileBackend $backend
+ * @param array $params
+ * @param string $call
+ * @param string $cmd
+ * @param integer|null $chmodPath
*/
- public function __construct( $backend, array $params, $call, $cmd, $chmodPath = null ) {
+ public function __construct(
+ FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
+ ) {
$this->backend = $backend;
$this->params = $params;
$this->call = $call;
@@ -855,16 +790,19 @@ abstract class FSFileBackendList implements Iterator {
protected $params = array();
/**
- * @param $dir string file system directory
- * @param $params array
+ * @param string $dir file system directory
+ * @param array $params
*/
public function __construct( $dir, array $params ) {
- $dir = realpath( $dir ); // normalize
- $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
+ $path = realpath( $dir ); // normalize
+ if ( $path === false ) {
+ $path = $dir;
+ }
+ $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
$this->params = $params;
try {
- $this->iter = $this->initIterator( $dir );
+ $this->iter = $this->initIterator( $path );
} catch ( UnexpectedValueException $e ) {
$this->iter = null; // bad permissions? deleted?
}
@@ -873,7 +811,7 @@ abstract class FSFileBackendList implements Iterator {
/**
* Return an appropriate iterator object to wrap
*
- * @param $dir string file system directory
+ * @param string $dir file system directory
* @return Iterator
*/
protected function initIterator( $dir ) {
@@ -916,8 +854,8 @@ abstract class FSFileBackendList implements Iterator {
try {
$this->iter->next();
$this->filterViaNext();
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null;
+ } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
+ throw new FileBackendError( "File iterator gave UnexpectedValueException." );
}
++$this->pos;
}
@@ -931,8 +869,8 @@ abstract class FSFileBackendList implements Iterator {
try {
$this->iter->rewind();
$this->filterViaNext();
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null;
+ } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
+ throw new FileBackendError( "File iterator gave UnexpectedValueException." );
}
}
@@ -953,11 +891,15 @@ abstract class FSFileBackendList implements Iterator {
* Return only the relative path and normalize slashes to FileBackend-style.
* Uses the "real path" since the suffix is based upon that.
*
- * @param $path string
+ * @param string $path
* @return string
*/
- protected function getRelPath( $path ) {
- return strtr( substr( realpath( $path ), $this->suffixStart ), '\\', '/' );
+ protected function getRelPath( $dir ) {
+ $path = realpath( $dir );
+ if ( $path === false ) {
+ $path = $dir;
+ }
+ return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
}
}
diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php
index 76c761b0..f586578b 100644
--- a/includes/filebackend/FileBackend.php
+++ b/includes/filebackend/FileBackend.php
@@ -1,7 +1,6 @@
<?php
/**
* @defgroup FileBackend File backend
- * @ingroup FileRepo
*
* File backend is used to interact with file storage systems,
* such as the local file system, NFS, or cloud storage systems.
@@ -37,14 +36,18 @@
* Outside callers can assume that all backends will have these functions.
*
* All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
- * The "<path>" portion is a relative path that uses UNIX file system (FS)
- * notation, though any particular backend may not actually be using a local
- * filesystem. Therefore, the relative paths are only virtual.
+ * The "backend" portion is unique name for MediaWiki to refer to a backend, while
+ * the "container" portion is a top-level directory of the backend. The "path" portion
+ * is a relative path that uses UNIX file system (FS) notation, though any particular
+ * backend may not actually be using a local filesystem. Therefore, the relative paths
+ * are only virtual.
*
* Backend contents are stored under wiki-specific container names by default.
- * For legacy reasons, this has no effect for the FS backend class, and per-wiki
- * segregation must be done by setting the container paths appropriately.
+ * Global (qualified) backends are achieved by configuring the "wiki ID" to a constant.
+ * For legacy reasons, the FSFileBackend class allows manually setting the paths of
+ * containers to ones that do not respect the "wiki ID".
*
+ * In key/value stores, the container is the only hierarchy (the rest is emulated).
* FS-based backends are somewhat more restrictive due to the existence of real
* directory files; a regular file cannot have the same name as a directory. Other
* backends with virtual directories may not have this limitation. Callers should
@@ -75,9 +78,13 @@ abstract class FileBackend {
* $config includes:
* - name : The unique name of this backend.
* This should consist of alphanumberic, '-', and '_' characters.
- * This name should not be changed after use.
- * - wikiId : Prefix to container names that is unique to this wiki.
+ * This name should not be changed after use (e.g. with journaling).
+ * Note that the name is *not* used in actual container names.
+ * - wikiId : Prefix to container names that is unique to this backend.
+ * If not provided, this defaults to the current wiki ID.
* It should only consist of alphanumberic, '-', and '_' characters.
+ * This ID is what avoids collisions if multiple logical backends
+ * use the same storage system, so this should be set carefully.
* - lockManager : Registered name of a file lock manager to use.
* - fileJournal : File journal configuration; see FileJournal::factory().
* Journals simply log changes to files stored in the backend.
@@ -87,7 +94,7 @@ abstract class FileBackend {
* Allowed values are "implicit", "explicit" and "off".
* - concurrency : How many file operations can be done in parallel.
*
- * @param $config Array
+ * @param array $config
* @throws MWException
*/
public function __construct( array $config ) {
@@ -100,7 +107,7 @@ abstract class FileBackend {
: wfWikiID(); // e.g. "my_wiki-en_"
$this->lockManager = ( $config['lockManager'] instanceof LockManager )
? $config['lockManager']
- : LockManagerGroup::singleton()->get( $config['lockManager'] );
+ : LockManagerGroup::singleton( $this->wikiId )->get( $config['lockManager'] );
$this->fileJournal = isset( $config['fileJournal'] )
? ( ( $config['fileJournal'] instanceof FileJournal )
? $config['fileJournal']
@@ -129,7 +136,8 @@ abstract class FileBackend {
}
/**
- * Get the wiki identifier used for this backend (possibly empty)
+ * Get the wiki identifier used for this backend (possibly empty).
+ * Note that this might *not* be in the same format as wfWikiID().
*
* @return string
* @since 1.20
@@ -171,6 +179,7 @@ abstract class FileBackend {
* - copy
* - move
* - delete
+ * - describe (since 1.21)
* - null
*
* a) Create a new file in storage with the contents of a string
@@ -181,7 +190,7 @@ abstract class FileBackend {
* 'content' => <string of new file contents>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
- * 'disposition' => <Content-Disposition header value>
+ * 'headers' => <HTTP header name/value map> # since 1.21
* );
* @endcode
*
@@ -193,7 +202,7 @@ abstract class FileBackend {
* 'dst' => <storage path>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
- * 'disposition' => <Content-Disposition header value>
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
*
@@ -205,7 +214,8 @@ abstract class FileBackend {
* 'dst' => <storage path>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
- * 'disposition' => <Content-Disposition header value>
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
*
@@ -217,7 +227,8 @@ abstract class FileBackend {
* 'dst' => <storage path>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
- * 'disposition' => <Content-Disposition header value>
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
*
@@ -230,7 +241,16 @@ abstract class FileBackend {
* )
* @endcode
*
- * f) Do nothing (no-op)
+ * f) Update metadata for a file within storage
+ * @code
+ * array(
+ * 'op' => 'describe',
+ * 'src' => <storage path>,
+ * 'headers' => <HTTP header name/value map>
+ * )
+ * @endcode
+ *
+ * g) Do nothing (no-op)
* @code
* array(
* 'op' => 'null',
@@ -241,31 +261,35 @@ abstract class FileBackend {
* - ignoreMissingSource : The operation will simply succeed and do
* nothing if the source file does not exist.
* - overwrite : Any destination file will be overwritten.
- * - overwriteSame : An error will not be given if a file already
- * exists at the destination that has the same
- * contents as the new contents to be written there.
- * - disposition : When supplied, the backend will add a Content-Disposition
- * header when GETs/HEADs of the destination file are made.
- * Backends that don't support file metadata will ignore this.
- * See http://tools.ietf.org/html/rfc6266 (since 1.20).
+ * - overwriteSame : If a file already exists at the destination with the
+ * same contents, then do nothing to the destination file
+ * instead of giving an error. This does not compare headers.
+ * This option is ignored if 'overwrite' is already provided.
+ * - headers : If supplied, the result of merging these headers with any
+ * existing source file headers (replacing conflicting ones)
+ * will be set as the destination file headers. Headers are
+ * deleted if their value is set to the empty string. When a
+ * file has headers they are included in responses to GET and
+ * HEAD requests to the backing store for that file.
+ * Header values should be no larger than 255 bytes, except for
+ * Content-Disposition. The system might ignore or truncate any
+ * headers that are too long to store (exact limits will vary).
+ * Backends that don't support metadata ignore this. (since 1.21)
*
* $opts is an associative of boolean flags, including:
* - force : Operation precondition errors no longer trigger an abort.
* Any remaining operations are still attempted. Unexpected
- * failures may still cause remaning operations to be aborted.
+ * failures may still cause remaining operations to be aborted.
* - nonLocking : No locks are acquired for the operations.
* This can increase performance for non-critical writes.
* This has no effect unless the 'force' flag is set.
- * - allowStale : Don't require the latest available data.
- * This can increase performance for non-critical writes.
- * This has no effect unless the 'force' flag is set.
* - nonJournaled : Don't log this operation batch in the file journal.
* This limits the ability of recovery scripts.
* - parallelize : Try to do operations in parallel when possible.
- * - bypassReadOnly : Allow writes in read-only mode (since 1.20).
+ * - bypassReadOnly : Allow writes in read-only mode. (since 1.20)
* - preserveCache : Don't clear the process cache before checking files.
* This should only be used if all entries in the process
- * cache were added after the files were already locked (since 1.20).
+ * cache were added after the files were already locked. (since 1.20)
*
* @remarks Remarks on locking:
* File system paths given to operations should refer to files that are
@@ -282,28 +306,26 @@ abstract class FileBackend {
* - a) unexpected operation errors occurred (network partitions, disk full...)
* - b) significant operation errors occurred and 'force' was not set
*
- * @param $ops Array List of operations to execute in order
- * @param $opts Array Batch operation options
+ * @param array $ops List of operations to execute in order
+ * @param array $opts Batch operation options
* @return Status
*/
final public function doOperations( array $ops, array $opts = array() ) {
if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
}
+ if ( !count( $ops ) ) {
+ return Status::newGood(); // nothing to do
+ }
if ( empty( $opts['force'] ) ) { // sanity
unset( $opts['nonLocking'] );
- unset( $opts['allowStale'] );
}
- $opts['concurrency'] = 1; // off
- if ( $this->parallelize === 'implicit' ) {
- if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
- $opts['concurrency'] = $this->concurrency;
- }
- } elseif ( $this->parallelize === 'explicit' ) {
- if ( !empty( $opts['parallelize'] ) ) {
- $opts['concurrency'] = $this->concurrency;
+ foreach ( $ops as &$op ) {
+ if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20)
+ $op['headers']['Content-Disposition'] = $op['disposition'];
}
}
+ $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
return $this->doOperationsInternal( $ops, $opts );
}
@@ -319,8 +341,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperations()
*
- * @param $op Array Operation
- * @param $opts Array Operation options
+ * @param array $op Operation
+ * @param array $opts Operation options
* @return Status
*/
final public function doOperation( array $op, array $opts = array() ) {
@@ -333,8 +355,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperation()
*
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
+ * @param array $params Operation parameters
+ * @param array $opts Operation options
* @return Status
*/
final public function create( array $params, array $opts = array() ) {
@@ -347,8 +369,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperation()
*
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
+ * @param array $params Operation parameters
+ * @param array $opts Operation options
* @return Status
*/
final public function store( array $params, array $opts = array() ) {
@@ -361,8 +383,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperation()
*
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
+ * @param array $params Operation parameters
+ * @param array $opts Operation options
* @return Status
*/
final public function copy( array $params, array $opts = array() ) {
@@ -375,8 +397,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperation()
*
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
+ * @param array $params Operation parameters
+ * @param array $opts Operation options
* @return Status
*/
final public function move( array $params, array $opts = array() ) {
@@ -389,8 +411,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperation()
*
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
+ * @param array $params Operation parameters
+ * @param array $opts Operation options
* @return Status
*/
final public function delete( array $params, array $opts = array() ) {
@@ -398,6 +420,21 @@ abstract class FileBackend {
}
/**
+ * Performs a single describe operation.
+ * This sets $params['op'] to 'describe' and passes it to doOperation().
+ *
+ * @see FileBackend::doOperation()
+ *
+ * @param array $params Operation parameters
+ * @param array $opts Operation options
+ * @return Status
+ * @since 1.21
+ */
+ final public function describe( array $params, array $opts = array() ) {
+ return $this->doOperation( array( 'op' => 'describe' ) + $params, $opts );
+ }
+
+ /**
* Perform a set of independent file operations on some files.
*
* This does no locking, nor journaling, and possibly no stat calls.
@@ -410,6 +447,7 @@ abstract class FileBackend {
* - copy
* - move
* - delete
+ * - describe (since 1.21)
* - null
*
* a) Create a new file in storage with the contents of a string
@@ -418,36 +456,42 @@ abstract class FileBackend {
* 'op' => 'create',
* 'dst' => <storage path>,
* 'content' => <string of new file contents>,
- * 'disposition' => <Content-Disposition header value>
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
+ *
* b) Copy a file system file into storage
* @code
* array(
* 'op' => 'store',
* 'src' => <file system path>,
* 'dst' => <storage path>,
- * 'disposition' => <Content-Disposition header value>
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
+ *
* c) Copy a file within storage
* @code
* array(
* 'op' => 'copy',
* 'src' => <storage path>,
* 'dst' => <storage path>,
- * 'disposition' => <Content-Disposition header value>
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
+ *
* d) Move a file within storage
* @code
* array(
* 'op' => 'move',
* 'src' => <storage path>,
* 'dst' => <storage path>,
- * 'disposition' => <Content-Disposition header value>
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
+ *
* e) Delete a file within storage
* @code
* array(
@@ -456,7 +500,17 @@ abstract class FileBackend {
* 'ignoreMissingSource' => <boolean>
* )
* @endcode
- * f) Do nothing (no-op)
+ *
+ * f) Update metadata for a file within storage
+ * @code
+ * array(
+ * 'op' => 'describe',
+ * 'src' => <storage path>,
+ * 'headers' => <HTTP header name/value map>
+ * )
+ * @endcode
+ *
+ * g) Do nothing (no-op)
* @code
* array(
* 'op' => 'null',
@@ -466,10 +520,15 @@ abstract class FileBackend {
* @par Boolean flags for operations (operation-specific):
* - ignoreMissingSource : The operation will simply succeed and do
* nothing if the source file does not exist.
- * - disposition : When supplied, the backend will add a Content-Disposition
- * header when GETs/HEADs of the destination file are made.
- * Backends that don't support file metadata will ignore this.
- * See http://tools.ietf.org/html/rfc6266 (since 1.20).
+ * - headers : If supplied with a header name/value map, the backend will
+ * reply with these headers when GETs/HEADs of the destination
+ * file are made. Header values should be smaller than 256 bytes.
+ * Content-Disposition headers can be longer, though the system
+ * might ignore or truncate ones that are too long to store.
+ * Existing headers will remain, but these will replace any
+ * conflicting previous headers, and headers will be removed
+ * if they are set to an empty string.
+ * Backends that don't support metadata ignore this. (since 1.21)
*
* $opts is an associative of boolean flags, including:
* - bypassReadOnly : Allow writes in read-only mode (since 1.20)
@@ -480,8 +539,8 @@ abstract class FileBackend {
* will reflect each operation attempted for the given files. The status will be
* considered "OK" as long as no fatal errors occurred.
*
- * @param $ops Array Set of operations to execute
- * @param $opts Array Batch operation options
+ * @param array $ops Set of operations to execute
+ * @param array $opts Batch operation options
* @return Status
* @since 1.20
*/
@@ -489,9 +548,16 @@ abstract class FileBackend {
if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
}
+ if ( !count( $ops ) ) {
+ return Status::newGood(); // nothing to do
+ }
foreach ( $ops as &$op ) {
$op['overwrite'] = true; // avoids RTTs in key/value stores
+ if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20)
+ $op['headers']['Content-Disposition'] = $op['disposition'];
+ }
}
+ $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
return $this->doQuickOperationsInternal( $ops );
}
@@ -507,7 +573,7 @@ abstract class FileBackend {
*
* @see FileBackend::doQuickOperations()
*
- * @param $op Array Operation
+ * @param array $op Operation
* @return Status
* @since 1.20
*/
@@ -521,7 +587,7 @@ abstract class FileBackend {
*
* @see FileBackend::doQuickOperation()
*
- * @param $params Array Operation parameters
+ * @param array $params Operation parameters
* @return Status
* @since 1.20
*/
@@ -535,7 +601,7 @@ abstract class FileBackend {
*
* @see FileBackend::doQuickOperation()
*
- * @param $params Array Operation parameters
+ * @param array $params Operation parameters
* @return Status
* @since 1.20
*/
@@ -549,7 +615,7 @@ abstract class FileBackend {
*
* @see FileBackend::doQuickOperation()
*
- * @param $params Array Operation parameters
+ * @param array $params Operation parameters
* @return Status
* @since 1.20
*/
@@ -563,7 +629,7 @@ abstract class FileBackend {
*
* @see FileBackend::doQuickOperation()
*
- * @param $params Array Operation parameters
+ * @param array $params Operation parameters
* @return Status
* @since 1.20
*/
@@ -577,7 +643,7 @@ abstract class FileBackend {
*
* @see FileBackend::doQuickOperation()
*
- * @param $params Array Operation parameters
+ * @param array $params Operation parameters
* @return Status
* @since 1.20
*/
@@ -586,15 +652,30 @@ abstract class FileBackend {
}
/**
+ * Performs a single quick describe operation.
+ * This sets $params['op'] to 'describe' and passes it to doQuickOperation().
+ *
+ * @see FileBackend::doQuickOperation()
+ *
+ * @param array $params Operation parameters
+ * @return Status
+ * @since 1.21
+ */
+ final public function quickDescribe( array $params ) {
+ return $this->doQuickOperation( array( 'op' => 'describe' ) + $params );
+ }
+
+ /**
* Concatenate a list of storage files into a single file system file.
* The target path should refer to a file that is already locked or
* otherwise safe from modification from other processes. Normally,
* the file will be a new temp file, which should be adequate.
*
- * @param $params Array Operation parameters
+ * @param array $params Operation parameters
* $params include:
- * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
- * - dst : file system path to 0-byte temp file
+ * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
+ * - dst : file system path to 0-byte temp file
+ * - parallelize : try to do operations in parallel when possible
* @return Status
*/
abstract public function concatenate( array $params );
@@ -607,8 +688,10 @@ abstract class FileBackend {
* The 'noAccess' and 'noListing' parameters works the same as in secure(),
* except they are only applied *if* the directory/container had to be created.
* These flags should always be set for directories that have private files.
+ * However, setting them is not guaranteed to actually do anything.
+ * Additional server configuration may be needed to achieve the desired effect.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - dir : storage directory
* - noAccess : try to deny file access (since 1.20)
@@ -620,6 +703,7 @@ abstract class FileBackend {
if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
}
+ $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
return $this->doPrepare( $params );
}
@@ -633,9 +717,11 @@ abstract class FileBackend {
* the container it belongs to. FS backends might add .htaccess
* files whereas key/value store backends might revoke container
* access to the storage user representing end-users in web requests.
- * This is not guaranteed to actually do anything.
*
- * @param $params Array
+ * This is not guaranteed to actually make files or listings publically hidden.
+ * Additional server configuration may be needed to achieve the desired effect.
+ *
+ * @param array $params
* $params include:
* - dir : storage directory
* - noAccess : try to deny file access
@@ -647,6 +733,7 @@ abstract class FileBackend {
if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
}
+ $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
return $this->doSecure( $params );
}
@@ -662,7 +749,10 @@ abstract class FileBackend {
* access to the storage user representing end-users in web requests.
* This essentially can undo the result of secure() calls.
*
- * @param $params Array
+ * This is not guaranteed to actually make files or listings publically viewable.
+ * Additional server configuration may be needed to achieve the desired effect.
+ *
+ * @param array $params
* $params include:
* - dir : storage directory
* - access : try to allow file access
@@ -675,6 +765,7 @@ abstract class FileBackend {
if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
}
+ $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
return $this->doPublish( $params );
}
@@ -688,7 +779,7 @@ abstract class FileBackend {
* Backends using key/value stores may do nothing unless the directory
* is that of an empty container, in which case it will be deleted.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - dir : storage directory
* - recursive : recursively delete empty subdirectories first (since 1.20)
@@ -699,6 +790,7 @@ abstract class FileBackend {
if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
}
+ $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
return $this->doClean( $params );
}
@@ -708,10 +800,27 @@ abstract class FileBackend {
abstract protected function doClean( array $params );
/**
+ * Enter file operation scope.
+ * This just makes PHP ignore user aborts/disconnects until the return
+ * value leaves scope. This returns null and does nothing in CLI mode.
+ *
+ * @return ScopedCallback|null
+ */
+ final protected function getScopedPHPBehaviorForOps() {
+ if ( php_sapi_name() != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+ $old = ignore_user_abort( true ); // avoid half-finished operations
+ return new ScopedCallback( function() use ( $old ) {
+ ignore_user_abort( $old );
+ } );
+ }
+ return null;
+ }
+
+ /**
* Check if a file exists at a storage path in the backend.
* This returns false if only a directory exists at the path.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
@@ -722,7 +831,7 @@ abstract class FileBackend {
/**
* Get the last-modified timestamp of the file at a storage path.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
@@ -734,18 +843,40 @@ abstract class FileBackend {
* Get the contents of a file at a storage path in the backend.
* This should be avoided for potentially large files.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
* @return string|bool Returns false on failure
*/
- abstract public function getFileContents( array $params );
+ final public function getFileContents( array $params ) {
+ $contents = $this->getFileContentsMulti(
+ array( 'srcs' => array( $params['src'] ) ) + $params );
+
+ return $contents[$params['src']];
+ }
+
+ /**
+ * Like getFileContents() except it takes an array of storage paths
+ * and returns a map of storage paths to strings (or null on failure).
+ * The map keys (paths) are in the same order as the provided list of paths.
+ *
+ * @see FileBackend::getFileContents()
+ *
+ * @param array $params
+ * $params include:
+ * - srcs : list of source storage paths
+ * - latest : use the latest available data
+ * - parallelize : try to do operations in parallel when possible
+ * @return Array Map of (path name => string or false on failure)
+ * @since 1.20
+ */
+ abstract public function getFileContentsMulti( array $params );
/**
* Get the size (bytes) of a file at a storage path in the backend.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
@@ -761,7 +892,7 @@ abstract class FileBackend {
* - size : the file size (bytes)
* Additional values may be included for internal use only.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
@@ -772,7 +903,7 @@ abstract class FileBackend {
/**
* Get a SHA-1 hash of the file at a storage path in the backend.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
@@ -782,13 +913,13 @@ abstract class FileBackend {
/**
* Get the properties of the file at a storage path in the backend.
- * Returns FSFile::placeholderProps() on failure.
+ * This gives the result of FSFile::getProps() on a local copy of the file.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
- * @return Array
+ * @return Array Returns FSFile::placeholderProps() on failure
*/
abstract public function getFileProps( array $params );
@@ -799,7 +930,7 @@ abstract class FileBackend {
* will be sent if streaming began, while none will be sent otherwise.
* Implementations should flush the output buffer before sending data.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - headers : list of additional HTTP headers to send on success
@@ -821,26 +952,89 @@ abstract class FileBackend {
* In that later case, there are copies of the file that must stay in sync.
* Additionally, further calls to this function may return the same file.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
* @return FSFile|null Returns null on failure
*/
- abstract public function getLocalReference( array $params );
+ final public function getLocalReference( array $params ) {
+ $fsFiles = $this->getLocalReferenceMulti(
+ array( 'srcs' => array( $params['src'] ) ) + $params );
+
+ return $fsFiles[$params['src']];
+ }
+
+ /**
+ * Like getLocalReference() except it takes an array of storage paths
+ * and returns a map of storage paths to FSFile objects (or null on failure).
+ * The map keys (paths) are in the same order as the provided list of paths.
+ *
+ * @see FileBackend::getLocalReference()
+ *
+ * @param array $params
+ * $params include:
+ * - srcs : list of source storage paths
+ * - latest : use the latest available data
+ * - parallelize : try to do operations in parallel when possible
+ * @return Array Map of (path name => FSFile or null on failure)
+ * @since 1.20
+ */
+ abstract public function getLocalReferenceMulti( array $params );
/**
* Get a local copy on disk of the file at a storage path in the backend.
* The temporary copy will have the same file extension as the source.
* Temporary files may be purged when the file object falls out of scope.
*
- * @param $params Array
+ * @param array $params
* $params include:
* - src : source storage path
* - latest : use the latest available data
* @return TempFSFile|null Returns null on failure
*/
- abstract public function getLocalCopy( array $params );
+ final public function getLocalCopy( array $params ) {
+ $tmpFiles = $this->getLocalCopyMulti(
+ array( 'srcs' => array( $params['src'] ) ) + $params );
+
+ return $tmpFiles[$params['src']];
+ }
+
+ /**
+ * Like getLocalCopy() except it takes an array of storage paths and
+ * returns a map of storage paths to TempFSFile objects (or null on failure).
+ * The map keys (paths) are in the same order as the provided list of paths.
+ *
+ * @see FileBackend::getLocalCopy()
+ *
+ * @param array $params
+ * $params include:
+ * - srcs : list of source storage paths
+ * - latest : use the latest available data
+ * - parallelize : try to do operations in parallel when possible
+ * @return Array Map of (path name => TempFSFile or null on failure)
+ * @since 1.20
+ */
+ abstract public function getLocalCopyMulti( array $params );
+
+ /**
+ * Return an HTTP URL to a given file that requires no authentication to use.
+ * The URL may be pre-authenticated (via some token in the URL) and temporary.
+ * This will return null if the backend cannot make an HTTP URL for the file.
+ *
+ * This is useful for key/value stores when using scripts that seek around
+ * large files and those scripts (and the backend) support HTTP Range headers.
+ * Otherwise, one would need to use getLocalReference(), which involves loading
+ * the entire file on to local disk.
+ *
+ * @param array $params
+ * $params include:
+ * - src : source storage path
+ * - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
+ * @return string|null
+ * @since 1.21
+ */
+ abstract public function getFileHttpUrl( array $params );
/**
* Check if a directory exists at a given storage path.
@@ -849,7 +1043,7 @@ abstract class FileBackend {
*
* Storage backends with eventual consistency might return stale data.
*
- * @param $params array
+ * @param array $params
* $params include:
* - dir : storage directory
* @return bool|null Returns null on failure
@@ -867,7 +1061,9 @@ abstract class FileBackend {
*
* Storage backends with eventual consistency might return stale data.
*
- * @param $params array
+ * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+ *
+ * @param array $params
* $params include:
* - dir : storage directory
* - topOnly : only return direct child dirs of the directory
@@ -882,7 +1078,9 @@ abstract class FileBackend {
*
* Storage backends with eventual consistency might return stale data.
*
- * @param $params array
+ * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+ *
+ * @param array $params
* $params include:
* - dir : storage directory
* @return Traversable|Array|null Returns null on failure
@@ -902,10 +1100,13 @@ abstract class FileBackend {
*
* Storage backends with eventual consistency might return stale data.
*
- * @param $params array
+ * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+ *
+ * @param array $params
* $params include:
- * - dir : storage directory
- * - topOnly : only return direct child files of the directory (since 1.20)
+ * - dir : storage directory
+ * - topOnly : only return direct child files of the directory (since 1.20)
+ * - adviseStat : set to true if stat requests will be made on the files (since 1.22)
* @return Traversable|Array|null Returns null on failure
*/
abstract public function getFileList( array $params );
@@ -916,9 +1117,12 @@ abstract class FileBackend {
*
* Storage backends with eventual consistency might return stale data.
*
- * @param $params array
+ * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+ *
+ * @param array $params
* $params include:
- * - dir : storage directory
+ * - dir : storage directory
+ * - adviseStat : set to true if stat requests will be made on the files (since 1.22)
* @return Traversable|Array|null Returns null on failure
* @since 1.20
*/
@@ -930,7 +1134,7 @@ abstract class FileBackend {
* Preload persistent file stat and property cache into in-process cache.
* This should be used when stat calls will be made on a known list of a many files.
*
- * @param $paths Array Storage paths
+ * @param array $paths Storage paths
* @return void
*/
public function preloadCache( array $paths ) {}
@@ -939,7 +1143,7 @@ abstract class FileBackend {
* Invalidate any in-process file stat and property cache.
* If $paths is given, then only the cache for those files will be cleared.
*
- * @param $paths Array Storage paths (optional)
+ * @param array $paths Storage paths (optional)
* @return void
*/
public function clearCache( array $paths = null ) {}
@@ -950,22 +1154,24 @@ abstract class FileBackend {
*
* Callers should consider using getScopedFileLocks() instead.
*
- * @param $paths Array Storage paths
- * @param $type integer LockManager::LOCK_* constant
+ * @param array $paths Storage paths
+ * @param integer $type LockManager::LOCK_* constant
* @return Status
*/
final public function lockFiles( array $paths, $type ) {
+ $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
return $this->lockManager->lock( $paths, $type );
}
/**
* Unlock the files at the given storage paths in the backend.
*
- * @param $paths Array Storage paths
- * @param $type integer LockManager::LOCK_* constant
+ * @param array $paths Storage paths
+ * @param integer $type LockManager::LOCK_* constant
* @return Status
*/
final public function unlockFiles( array $paths, $type ) {
+ $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
return $this->lockManager->unlock( $paths, $type );
}
@@ -977,12 +1183,21 @@ abstract class FileBackend {
* Once the return value goes out scope, the locks will be released and
* the status updated. Unlock fatals will not change the status "OK" value.
*
- * @param $paths Array Storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @param $status Status Status to update on lock/unlock
+ * @see ScopedLock::factory()
+ *
+ * @param array $paths List of storage paths or map of lock types to path lists
+ * @param integer|string $type LockManager::LOCK_* constant or "mixed"
+ * @param Status $status Status to update on lock/unlock
* @return ScopedLock|null Returns null on failure
*/
final public function getScopedFileLocks( array $paths, $type, Status $status ) {
+ if ( $type === 'mixed' ) {
+ foreach ( $paths as &$typePaths ) {
+ $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
+ }
+ } else {
+ $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+ }
return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
}
@@ -997,8 +1212,8 @@ abstract class FileBackend {
*
* @see FileBackend::doOperations()
*
- * @param $ops Array List of file operations to FileBackend::doOperations()
- * @param $status Status Status to update on lock/unlock
+ * @param array $ops List of file operations to FileBackend::doOperations()
+ * @param Status $status Status to update on lock/unlock
* @return Array List of ScopedFileLocks or null values
* @since 1.20
*/
@@ -1016,6 +1231,17 @@ abstract class FileBackend {
}
/**
+ * Get the storage path for the given container for this backend
+ *
+ * @param string $container Container name
+ * @return string Storage path
+ * @since 1.21
+ */
+ final public function getContainerStoragePath( $container ) {
+ return $this->getRootStoragePath() . "/{$container}";
+ }
+
+ /**
* Get the file journal object for this backend
*
* @return FileJournal
@@ -1028,7 +1254,7 @@ abstract class FileBackend {
* Check if a given path is a "mwstore://" path.
* This does not do any further validation or any existence checks.
*
- * @param $path string
+ * @param string $path
* @return bool
*/
final public static function isStoragePath( $path ) {
@@ -1040,7 +1266,7 @@ abstract class FileBackend {
* and a relative file path. The relative path may be the empty string.
* This does not do any path normalization or traversal checks.
*
- * @param $storagePath string
+ * @param string $storagePath
* @return Array (backend, container, rel object) or (null, null, null)
*/
final public static function splitStoragePath( $storagePath ) {
@@ -1062,7 +1288,7 @@ abstract class FileBackend {
* Normalize a storage path by cleaning up directory separators.
* Returns null if the path is not of the format of a valid storage path.
*
- * @param $storagePath string
+ * @param string $storagePath
* @return string|null
*/
final public static function normalizeStoragePath( $storagePath ) {
@@ -1083,19 +1309,19 @@ abstract class FileBackend {
* This returns a path like "mwstore://backend/container",
* "mwstore://backend/container/...", or null if there is no parent.
*
- * @param $storagePath string
+ * @param string $storagePath
* @return string|null
*/
final public static function parentStoragePath( $storagePath ) {
$storagePath = dirname( $storagePath );
- list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
+ list( , , $rel ) = self::splitStoragePath( $storagePath );
return ( $rel === null ) ? null : $storagePath;
}
/**
* Get the final extension from a storage or FS path
*
- * @param $path string
+ * @param string $path
* @return string
*/
final public static function extensionFromPath( $path ) {
@@ -1106,7 +1332,7 @@ abstract class FileBackend {
/**
* Check if a relative path has no directory traversals
*
- * @param $path string
+ * @param string $path
* @return bool
* @since 1.20
*/
@@ -1117,8 +1343,9 @@ abstract class FileBackend {
/**
* Build a Content-Disposition header value per RFC 6266.
*
- * @param $type string One of (attachment, inline)
- * @param $filename string Suggested file name (should not contain slashes)
+ * @param string $type One of (attachment, inline)
+ * @param string $filename Suggested file name (should not contain slashes)
+ * @throws MWException
* @return string
* @since 1.20
*/
@@ -1145,7 +1372,7 @@ abstract class FileBackend {
*
* This uses the same traversal protection as Title::secureAndSplit().
*
- * @param $path string Storage path relative to a container
+ * @param string $path Storage path relative to a container
* @return string|null
*/
final protected static function normalizeContainerPath( $path ) {
@@ -1171,3 +1398,9 @@ abstract class FileBackend {
return $path;
}
}
+
+/**
+ * @ingroup FileBackend
+ * @since 1.22
+ */
+class FileBackendError extends MWException {}
diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php
index 8bbc96d0..be8a2076 100644
--- a/includes/filebackend/FileBackendGroup.php
+++ b/includes/filebackend/FileBackendGroup.php
@@ -87,21 +87,25 @@ class FileBackendGroup {
$thumbDir = isset( $info['thumbDir'] )
? $info['thumbDir']
: "{$directory}/thumb";
+ $transcodedDir = isset( $info['transcodedDir'] )
+ ? $info['transcodedDir']
+ : "{$directory}/transcoded";
$fileMode = isset( $info['fileMode'] )
? $info['fileMode']
: 0644;
// Get the FS backend configuration
$autoBackends[] = array(
- 'name' => $backendName,
- 'class' => 'FSFileBackend',
- 'lockManager' => 'fsLockManager',
+ 'name' => $backendName,
+ 'class' => 'FSFileBackend',
+ 'lockManager' => 'fsLockManager',
'containerPaths' => array(
- "{$repoName}-public" => "{$directory}",
- "{$repoName}-thumb" => $thumbDir,
+ "{$repoName}-public" => "{$directory}",
+ "{$repoName}-thumb" => $thumbDir,
+ "{$repoName}-transcoded" => $transcodedDir,
"{$repoName}-deleted" => $deletedDir,
- "{$repoName}-temp" => "{$directory}/temp"
+ "{$repoName}-temp" => "{$directory}/temp"
),
- 'fileMode' => $fileMode,
+ 'fileMode' => $fileMode,
);
}
@@ -112,7 +116,7 @@ class FileBackendGroup {
/**
* Register an array of file backend configurations
*
- * @param $configs Array
+ * @param Array $configs
* @return void
* @throws MWException
*/
@@ -122,15 +126,17 @@ class FileBackendGroup {
throw new MWException( "Cannot register a backend with no name." );
}
$name = $config['name'];
- if ( !isset( $config['class'] ) ) {
+ if ( isset( $this->backends[$name] ) ) {
+ throw new MWException( "Backend with name `{$name}` already registered." );
+ } elseif ( !isset( $config['class'] ) ) {
throw new MWException( "Cannot register backend `{$name}` with no class." );
}
$class = $config['class'];
unset( $config['class'] ); // backend won't need this
$this->backends[$name] = array(
- 'class' => $class,
- 'config' => $config,
+ 'class' => $class,
+ 'config' => $config,
'instance' => null
);
}
@@ -139,7 +145,7 @@ class FileBackendGroup {
/**
* Get the backend object with a given name
*
- * @param $name string
+ * @param string $name
* @return FileBackend
* @throws MWException
*/
@@ -159,7 +165,7 @@ class FileBackendGroup {
/**
* Get the config array for a backend object with a given name
*
- * @param $name string
+ * @param string $name
* @return Array
* @throws MWException
*/
@@ -174,11 +180,11 @@ class FileBackendGroup {
/**
* Get an appropriate backend object from a storage path
*
- * @param $storagePath string
+ * @param string $storagePath
* @return FileBackend|null Backend or null on failure
*/
public function backendFromPath( $storagePath ) {
- list( $backend, $c, $p ) = FileBackend::splitStoragePath( $storagePath );
+ list( $backend, , ) = FileBackend::splitStoragePath( $storagePath );
if ( $backend !== null && isset( $this->backends[$backend] ) ) {
return $this->get( $backend );
}
diff --git a/includes/filebackend/FileBackendMultiWrite.php b/includes/filebackend/FileBackendMultiWrite.php
index 4be03231..97584a71 100644
--- a/includes/filebackend/FileBackendMultiWrite.php
+++ b/includes/filebackend/FileBackendMultiWrite.php
@@ -62,7 +62,7 @@ class FileBackendMultiWrite extends FileBackend {
* Additional $config params include:
* - backends : Array of backend config and multi-backend settings.
* Each value is the config used in the constructor of a
- * FileBackendStore class, but with these additional settings:
+ * FileBackendStore class, but with these additional settings:
* - class : The name of the backend class
* - isMultiMaster : This must be set for one backend.
* - template: : If given a backend name, this will use
@@ -75,10 +75,13 @@ class FileBackendMultiWrite extends FileBackend {
* - autoResync : Automatically resync the clone backends to the master backend
* when pre-operation sync checks fail. This should only be used
* if the master backend is stable and not missing any files.
+ * Use "conservative" to limit resyncing to copying newer master
+ * backend files over older (or non-existing) clone backend files.
+ * Cases that cannot be handled will result in operation abortion.
* - noPushQuickOps : (hack) Only apply doQuickOperations() to the master backend.
* - noPushDirConts : (hack) Only apply directory functions to the master backend.
*
- * @param $config Array
+ * @param Array $config
* @throws MWException
*/
public function __construct( array $config ) {
@@ -86,7 +89,9 @@ class FileBackendMultiWrite extends FileBackend {
$this->syncChecks = isset( $config['syncChecks'] )
? $config['syncChecks']
: self::CHECK_SIZE;
- $this->autoResync = !empty( $config['autoResync'] );
+ $this->autoResync = isset( $config['autoResync'] )
+ ? $config['autoResync']
+ : false;
$this->noPushQuickOps = isset( $config['noPushQuickOps'] )
? $config['noPushQuickOps']
: false;
@@ -131,26 +136,15 @@ class FileBackendMultiWrite extends FileBackend {
}
}
- /**
- * @see FileBackend::doOperationsInternal()
- * @return Status
- */
final protected function doOperationsInternal( array $ops, array $opts ) {
$status = Status::newGood();
$mbe = $this->backends[$this->masterIndex]; // convenience
- // Get the paths to lock from the master backend
- $realOps = $this->substOpBatchPaths( $ops, $mbe );
- $paths = $mbe->getPathsToLockForOpsInternal( $mbe->getOperationsInternal( $realOps ) );
- // Get the paths under the proxy backend's name
- $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
- $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
// Try to lock those files for the scope of this function...
if ( empty( $opts['nonLocking'] ) ) {
// Try to lock those files for the scope of this function...
- $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
- $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
+ $scopeLock = $this->getScopedLocksForOps( $ops, $status );
if ( !$status->isOK() ) {
return $status; // abort
}
@@ -177,12 +171,14 @@ class FileBackendMultiWrite extends FileBackend {
}
}
// Actually attempt the operation batch on the master backend...
+ $realOps = $this->substOpBatchPaths( $ops, $mbe );
$masterStatus = $mbe->doOperations( $realOps, $opts );
$status->merge( $masterStatus );
- // Propagate the operations to the clone backends if there were no fatal errors.
- // If $ops only had one operation, this might avoid backend inconsistencies.
- // This also avoids inconsistency for expected errors (like "file already exists").
- if ( !count( $masterStatus->getErrorsArray() ) ) {
+ // Propagate the operations to the clone backends if there were no unexpected errors
+ // and if there were either no expected errors or if the 'force' option was used.
+ // However, if nothing succeeded at all, then don't replicate any of the operations.
+ // If $ops only had one operation, this might avoid backend sync inconsistencies.
+ if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
foreach ( $this->backends as $index => $backend ) {
if ( $index !== $this->masterIndex ) { // not done already
$realOps = $this->substOpBatchPaths( $ops, $backend );
@@ -203,7 +199,7 @@ class FileBackendMultiWrite extends FileBackend {
/**
* Check that a set of files are consistent across all internal backends
*
- * @param $paths Array List of storage paths
+ * @param array $paths List of storage paths
* @return Status
*/
public function consistencyCheck( array $paths ) {
@@ -269,7 +265,7 @@ class FileBackendMultiWrite extends FileBackend {
/**
* Check that a set of file paths are usable across all internal backends
*
- * @param $paths Array List of storage paths
+ * @param array $paths List of storage paths
* @return Status
*/
public function accessibilityCheck( array $paths ) {
@@ -294,7 +290,7 @@ class FileBackendMultiWrite extends FileBackend {
* Check that a set of files are consistent across all internal backends
* and re-synchronize those files againt the "multi master" if needed.
*
- * @param $paths Array List of storage paths
+ * @param array $paths List of storage paths
* @return Status
*/
public function resyncFiles( array $paths ) {
@@ -302,12 +298,12 @@ class FileBackendMultiWrite extends FileBackend {
$mBackend = $this->backends[$this->masterIndex];
foreach ( $paths as $path ) {
- $mPath = $this->substPaths( $path, $mBackend );
- $mSha1 = $mBackend->getFileSha1Base36( array( 'src' => $mPath ) );
- $mExist = $mBackend->fileExists( array( 'src' => $mPath ) );
- // Check if the master backend is available...
- if ( $mExist === null ) {
+ $mPath = $this->substPaths( $path, $mBackend );
+ $mSha1 = $mBackend->getFileSha1Base36( array( 'src' => $mPath, 'latest' => true ) );
+ $mStat = $mBackend->getFileStat( array( 'src' => $mPath, 'latest' => true ) );
+ if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
$status->fatal( 'backend-fail-internal', $this->name );
+ continue; // file is not available on the master backend...
}
// Check of all clone backends agree with the master...
foreach ( $this->backends as $index => $cBackend ) {
@@ -315,15 +311,31 @@ class FileBackendMultiWrite extends FileBackend {
continue; // master
}
$cPath = $this->substPaths( $path, $cBackend );
- $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath ) );
+ $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath, 'latest' => true ) );
+ $cStat = $cBackend->getFileStat( array( 'src' => $cPath, 'latest' => true ) );
+ if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
+ $status->fatal( 'backend-fail-internal', $cBackend->getName() );
+ continue; // file is not available on the clone backend...
+ }
if ( $mSha1 === $cSha1 ) {
// already synced; nothing to do
- } elseif ( $mSha1 ) { // file is in master
- $fsFile = $mBackend->getLocalReference( array( 'src' => $mPath ) );
+ } elseif ( $mSha1 !== false ) { // file is in master
+ if ( $this->autoResync === 'conservative'
+ && $cStat && $cStat['mtime'] > $mStat['mtime'] )
+ {
+ $status->fatal( 'backend-fail-synced', $path );
+ continue; // don't rollback data
+ }
+ $fsFile = $mBackend->getLocalReference(
+ array( 'src' => $mPath, 'latest' => true ) );
$status->merge( $cBackend->quickStore(
array( 'src' => $fsFile->getPath(), 'dst' => $cPath )
) );
- } elseif ( $mExist === false ) { // file is not in master
+ } elseif ( $mStat === false ) { // file is not in master
+ if ( $this->autoResync === 'conservative' ) {
+ $status->fatal( 'backend-fail-synced', $path );
+ continue; // don't delete data
+ }
$status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) );
}
}
@@ -335,14 +347,20 @@ class FileBackendMultiWrite extends FileBackend {
/**
* Get a list of file storage paths to read or write for a list of operations
*
- * @param $ops Array Same format as doOperations()
+ * @param array $ops Same format as doOperations()
* @return Array List of storage paths to files (does not include directories)
*/
protected function fileStoragePathsForOps( array $ops ) {
$paths = array();
foreach ( $ops as $op ) {
if ( isset( $op['src'] ) ) {
- $paths[] = $op['src'];
+ // For things like copy/move/delete with "ignoreMissingSource" and there
+ // is no source file, nothing should happen and there should be no errors.
+ if ( empty( $op['ignoreMissingSource'] )
+ || $this->fileExists( array( 'src' => $op['src'] ) ) )
+ {
+ $paths[] = $op['src'];
+ }
}
if ( isset( $op['srcs'] ) ) {
$paths = array_merge( $paths, $op['srcs'] );
@@ -351,15 +369,15 @@ class FileBackendMultiWrite extends FileBackend {
$paths[] = $op['dst'];
}
}
- return array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) );
+ return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
}
/**
* Substitute the backend name in storage path parameters
* for a set of operations with that of a given internal backend.
*
- * @param $ops Array List of file operation arrays
- * @param $backend FileBackendStore
+ * @param array $ops List of file operation arrays
+ * @param FileBackendStore $backend
* @return Array
*/
protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
@@ -379,8 +397,8 @@ class FileBackendMultiWrite extends FileBackend {
/**
* Same as substOpBatchPaths() but for a single operation
*
- * @param $ops array File operation array
- * @param $backend FileBackendStore
+ * @param array $ops File operation array
+ * @param FileBackendStore $backend
* @return Array
*/
protected function substOpPaths( array $ops, FileBackendStore $backend ) {
@@ -391,8 +409,8 @@ class FileBackendMultiWrite extends FileBackend {
/**
* Substitute the backend of storage paths with an internal backend's name
*
- * @param $paths Array|string List of paths or single string path
- * @param $backend FileBackendStore
+ * @param array|string $paths List of paths or single string path
+ * @param FileBackendStore $backend
* @return Array|string
*/
protected function substPaths( $paths, FileBackendStore $backend ) {
@@ -406,7 +424,7 @@ class FileBackendMultiWrite extends FileBackend {
/**
* Substitute the backend of internal storage paths with the proxy backend's name
*
- * @param $paths Array|string List of paths or single string path
+ * @param array|string $paths List of paths or single string path
* @return Array|string
*/
protected function unsubstPaths( $paths ) {
@@ -417,10 +435,6 @@ class FileBackendMultiWrite extends FileBackend {
);
}
- /**
- * @see FileBackend::doQuickOperationsInternal()
- * @return Status
- */
protected function doQuickOperationsInternal( array $ops ) {
$status = Status::newGood();
// Do the operations on the master backend; setting Status fields...
@@ -446,18 +460,14 @@ class FileBackendMultiWrite extends FileBackend {
}
/**
- * @param $path string Storage path
+ * @param string $path Storage path
* @return bool Path container should have dir changes pushed to all backends
*/
protected function replicateContainerDirChanges( $path ) {
- list( $b, $shortCont, $r ) = self::splitStoragePath( $path );
+ list( , $shortCont, ) = self::splitStoragePath( $path );
return !in_array( $shortCont, $this->noPushDirConts );
}
- /**
- * @see FileBackend::doPrepare()
- * @return Status
- */
protected function doPrepare( array $params ) {
$status = Status::newGood();
$replicate = $this->replicateContainerDirChanges( $params['dir'] );
@@ -470,11 +480,6 @@ class FileBackendMultiWrite extends FileBackend {
return $status;
}
- /**
- * @see FileBackend::doSecure()
- * @param $params array
- * @return Status
- */
protected function doSecure( array $params ) {
$status = Status::newGood();
$replicate = $this->replicateContainerDirChanges( $params['dir'] );
@@ -487,11 +492,6 @@ class FileBackendMultiWrite extends FileBackend {
return $status;
}
- /**
- * @see FileBackend::doPublish()
- * @param $params array
- * @return Status
- */
protected function doPublish( array $params ) {
$status = Status::newGood();
$replicate = $this->replicateContainerDirChanges( $params['dir'] );
@@ -504,11 +504,6 @@ class FileBackendMultiWrite extends FileBackend {
return $status;
}
- /**
- * @see FileBackend::doClean()
- * @param $params array
- * @return Status
- */
protected function doClean( array $params ) {
$status = Status::newGood();
$replicate = $this->replicateContainerDirChanges( $params['dir'] );
@@ -521,149 +516,100 @@ class FileBackendMultiWrite extends FileBackend {
return $status;
}
- /**
- * @see FileBackend::concatenate()
- * @param $params array
- * @return Status
- */
public function concatenate( array $params ) {
// We are writing to an FS file, so we don't need to do this per-backend
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->concatenate( $realParams );
}
- /**
- * @see FileBackend::fileExists()
- * @param $params array
- */
public function fileExists( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->fileExists( $realParams );
}
- /**
- * @see FileBackend::getFileTimestamp()
- * @param $params array
- * @return bool|string
- */
public function getFileTimestamp( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
}
- /**
- * @see FileBackend::getFileSize()
- * @param $params array
- * @return bool|int
- */
public function getFileSize( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getFileSize( $realParams );
}
- /**
- * @see FileBackend::getFileStat()
- * @param $params array
- * @return Array|bool|null
- */
public function getFileStat( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getFileStat( $realParams );
}
- /**
- * @see FileBackend::getFileContents()
- * @param $params array
- * @return bool|string
- */
- public function getFileContents( array $params ) {
+ public function getFileContentsMulti( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileContents( $realParams );
+ $contentsM = $this->backends[$this->masterIndex]->getFileContentsMulti( $realParams );
+
+ $contents = array(); // (path => FSFile) mapping using the proxy backend's name
+ foreach ( $contentsM as $path => $data ) {
+ $contents[$this->unsubstPaths( $path )] = $data;
+ }
+ return $contents;
}
- /**
- * @see FileBackend::getFileSha1Base36()
- * @param $params array
- * @return bool|string
- */
public function getFileSha1Base36( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
}
- /**
- * @see FileBackend::getFileProps()
- * @param $params array
- * @return Array
- */
public function getFileProps( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getFileProps( $realParams );
}
- /**
- * @see FileBackend::streamFile()
- * @param $params array
- * @return \Status
- */
public function streamFile( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->streamFile( $realParams );
}
- /**
- * @see FileBackend::getLocalReference()
- * @param $params array
- * @return FSFile|null
- */
- public function getLocalReference( array $params ) {
+ public function getLocalReferenceMulti( array $params ) {
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ $fsFilesM = $this->backends[$this->masterIndex]->getLocalReferenceMulti( $realParams );
+
+ $fsFiles = array(); // (path => FSFile) mapping using the proxy backend's name
+ foreach ( $fsFilesM as $path => $fsFile ) {
+ $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
+ }
+ return $fsFiles;
+ }
+
+ public function getLocalCopyMulti( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
+ $tempFilesM = $this->backends[$this->masterIndex]->getLocalCopyMulti( $realParams );
+
+ $tempFiles = array(); // (path => TempFSFile) mapping using the proxy backend's name
+ foreach ( $tempFilesM as $path => $tempFile ) {
+ $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
+ }
+ return $tempFiles;
}
- /**
- * @see FileBackend::getLocalCopy()
- * @param $params array
- * @return null|TempFSFile
- */
- public function getLocalCopy( array $params ) {
+ public function getFileHttpUrl( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
+ return $this->backends[$this->masterIndex]->getFileHttpUrl( $realParams );
}
- /**
- * @see FileBackend::directoryExists()
- * @param $params array
- * @return bool|null
- */
public function directoryExists( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->directoryExists( $realParams );
}
- /**
- * @see FileBackend::getSubdirectoryList()
- * @param $params array
- * @return Array|null|Traversable
- */
public function getDirectoryList( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
}
- /**
- * @see FileBackend::getFileList()
- * @param $params array
- * @return Array|null|\Traversable
- */
public function getFileList( array $params ) {
$realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
return $this->backends[$this->masterIndex]->getFileList( $realParams );
}
- /**
- * @see FileBackend::clearCache()
- */
public function clearCache( array $paths = null ) {
foreach ( $this->backends as $backend ) {
$realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
@@ -671,19 +617,17 @@ class FileBackendMultiWrite extends FileBackend {
}
}
- /**
- * @see FileBackend::getScopedLocksForOps()
- */
public function getScopedLocksForOps( array $ops, Status $status ) {
- $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops );
+ $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
+ $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
// Get the paths to lock from the master backend
$paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
// Get the paths under the proxy backend's name
- $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
- $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
- return array(
- $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
- $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
+ $pbPaths = array(
+ LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
+ LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
);
+ // Actually acquire the locks
+ return array( $this->getScopedFileLocks( $pbPaths, 'mixed', $status ) );
}
}
diff --git a/includes/filebackend/FileBackendStore.php b/includes/filebackend/FileBackendStore.php
index 083dfea9..0921e99f 100644
--- a/includes/filebackend/FileBackendStore.php
+++ b/includes/filebackend/FileBackendStore.php
@@ -38,26 +38,43 @@
abstract class FileBackendStore extends FileBackend {
/** @var BagOStuff */
protected $memCache;
- /** @var ProcessCacheLRU */
- protected $cheapCache; // Map of paths to small (RAM/disk) cache items
- /** @var ProcessCacheLRU */
- protected $expensiveCache; // Map of paths to large (RAM/disk) cache items
+ /** @var ProcessCacheLRU Map of paths to small (RAM/disk) cache items */
+ protected $cheapCache;
+ /** @var ProcessCacheLRU Map of paths to large (RAM/disk) cache items */
+ protected $expensiveCache;
- /** @var Array Map of container names to sharding settings */
- protected $shardViaHashLevels = array(); // (container name => config array)
+ /** @var Array Map of container names to sharding config */
+ protected $shardViaHashLevels = array();
+
+ /** @var callback Method to get the MIME type of files */
+ protected $mimeCallback;
protected $maxFileSize = 4294967296; // integer bytes (4GiB)
+ const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
+ const CACHE_CHEAP_SIZE = 300; // integer; max entries in "cheap cache"
+ const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
+
/**
* @see FileBackend::__construct()
+ * Additional $config params include:
+ * - mimeCallback : Callback that takes (storage path, content, file system path) and
+ * returns the MIME type of the file or 'unknown/unknown'. The file
+ * system path parameter should be used if the content one is null.
*
- * @param $config Array
+ * @param array $config
*/
public function __construct( array $config ) {
parent::__construct( $config );
- $this->memCache = new EmptyBagOStuff(); // disabled by default
- $this->cheapCache = new ProcessCacheLRU( 300 );
- $this->expensiveCache = new ProcessCacheLRU( 5 );
+ $this->mimeCallback = isset( $config['mimeCallback'] )
+ ? $config['mimeCallback']
+ : function( $storagePath, $content, $fsPath ) {
+ // @TODO: handle the case of extension-less files using the contents
+ return StreamFile::contentTypeFromPath( $storagePath ) ?: 'unknown/unknown';
+ };
+ $this->memCache = new EmptyBagOStuff(); // disabled by default
+ $this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
+ $this->expensiveCache = new ProcessCacheLRU( self::CACHE_EXPENSIVE_SIZE );
}
/**
@@ -72,124 +89,126 @@ abstract class FileBackendStore extends FileBackend {
}
/**
- * Check if a file can be created at a given storage path.
- * FS backends should check if the parent directory exists and the file is writable.
+ * Check if a file can be created or changed at a given storage path.
+ * FS backends should check if the parent directory exists, files can be
+ * written under it, and that any file already there is writable.
* Backends using key/value stores should check if the container exists.
*
- * @param $storagePath string
+ * @param string $storagePath
* @return bool
*/
abstract public function isPathUsableInternal( $storagePath );
/**
* Create a file in the backend with the given contents.
+ * This will overwrite any file that exists at the destination.
* Do not call this function from places outside FileBackend and FileOp.
*
* $params include:
- * - content : the raw file contents
- * - dst : destination storage path
- * - overwrite : overwrite any file that exists at the destination
- * - disposition : Content-Disposition header value for the destination
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
+ * - content : the raw file contents
+ * - dst : destination storage path
+ * - headers : HTTP header name/value map
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
+ * - dstExists : Whether a file exists at the destination (optimization).
+ * Callers can use "false" if no existing file is being changed.
*
- * @param $params Array
+ * @param array $params
* @return Status
*/
final public function createInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
$status = Status::newFatal( 'backend-fail-maxsize',
$params['dst'], $this->maxFileSizeInternal() );
} else {
$status = $this->doCreateInternal( $params );
$this->clearCache( array( $params['dst'] ) );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
+ if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
$this->deleteFileCache( $params['dst'] ); // persistent cache
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
/**
* @see FileBackendStore::createInternal()
+ * @return Status
*/
abstract protected function doCreateInternal( array $params );
/**
* Store a file into the backend from a file on disk.
+ * This will overwrite any file that exists at the destination.
* Do not call this function from places outside FileBackend and FileOp.
*
* $params include:
- * - src : source path on disk
- * - dst : destination storage path
- * - overwrite : overwrite any file that exists at the destination
- * - disposition : Content-Disposition header value for the destination
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
+ * - src : source path on disk
+ * - dst : destination storage path
+ * - headers : HTTP header name/value map
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
+ * - dstExists : Whether a file exists at the destination (optimization).
+ * Callers can use "false" if no existing file is being changed.
*
- * @param $params Array
+ * @param array $params
* @return Status
*/
final public function storeInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
$status = Status::newFatal( 'backend-fail-maxsize',
$params['dst'], $this->maxFileSizeInternal() );
} else {
$status = $this->doStoreInternal( $params );
$this->clearCache( array( $params['dst'] ) );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
+ if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
$this->deleteFileCache( $params['dst'] ); // persistent cache
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
/**
* @see FileBackendStore::storeInternal()
+ * @return Status
*/
abstract protected function doStoreInternal( array $params );
/**
* Copy a file from one storage path to another in the backend.
+ * This will overwrite any file that exists at the destination.
* Do not call this function from places outside FileBackend and FileOp.
*
* $params include:
- * - src : source storage path
- * - dst : destination storage path
- * - overwrite : overwrite any file that exists at the destination
- * - disposition : Content-Disposition header value for the destination
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
+ * - src : source storage path
+ * - dst : destination storage path
+ * - ignoreMissingSource : do nothing if the source file does not exist
+ * - headers : HTTP header name/value map
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
+ * - dstExists : Whether a file exists at the destination (optimization).
+ * Callers can use "false" if no existing file is being changed.
*
- * @param $params Array
+ * @param array $params
* @return Status
*/
final public function copyInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = $this->doCopyInternal( $params );
$this->clearCache( array( $params['dst'] ) );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
+ if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
$this->deleteFileCache( $params['dst'] ); // persistent cache
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
/**
* @see FileBackendStore::copyInternal()
+ * @return Status
*/
abstract protected function doCopyInternal( array $params );
@@ -204,52 +223,50 @@ abstract class FileBackendStore extends FileBackend {
* If the status is OK, then its value field will be
* set to a FileBackendStoreOpHandle object.
*
- * @param $params Array
+ * @param array $params
* @return Status
*/
final public function deleteInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = $this->doDeleteInternal( $params );
$this->clearCache( array( $params['src'] ) );
$this->deleteFileCache( $params['src'] ); // persistent cache
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
/**
* @see FileBackendStore::deleteInternal()
+ * @return Status
*/
abstract protected function doDeleteInternal( array $params );
/**
* Move a file from one storage path to another in the backend.
+ * This will overwrite any file that exists at the destination.
* Do not call this function from places outside FileBackend and FileOp.
*
* $params include:
- * - src : source storage path
- * - dst : destination storage path
- * - overwrite : overwrite any file that exists at the destination
- * - disposition : Content-Disposition header value for the destination
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
+ * - src : source storage path
+ * - dst : destination storage path
+ * - ignoreMissingSource : do nothing if the source file does not exist
+ * - headers : HTTP header name/value map
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
+ * - dstExists : Whether a file exists at the destination (optimization).
+ * Callers can use "false" if no existing file is being changed.
*
- * @param $params Array
+ * @param array $params
* @return Status
*/
final public function moveInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = $this->doMoveInternal( $params );
$this->clearCache( array( $params['src'], $params['dst'] ) );
$this->deleteFileCache( $params['src'] ); // persistent cache
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
+ if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
$this->deleteFileCache( $params['dst'] ); // persistent cache
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -259,10 +276,12 @@ abstract class FileBackendStore extends FileBackend {
*/
protected function doMoveInternal( array $params ) {
unset( $params['async'] ); // two steps, won't work here :)
+ $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
+ $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
// Copy source to dest
$status = $this->copyInternal( $params );
- if ( $status->isOK() ) {
- // Delete source (only fails due to races or medium going down)
+ if ( $nsrc !== $ndst && $status->isOK() ) {
+ // Delete source (only fails due to races or network problems)
$status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
$status->setResult( true, $status->value ); // ignore delete() errors
}
@@ -270,23 +289,52 @@ abstract class FileBackendStore extends FileBackend {
}
/**
- * No-op file operation that does nothing.
+ * Alter metadata for a file at the storage path.
* Do not call this function from places outside FileBackend and FileOp.
*
- * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - headers : HTTP header name/value map
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
+ *
+ * @param array $params
* @return Status
*/
- final public function nullInternal( array $params ) {
+ final public function describeInternal( array $params ) {
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
+ if ( count( $params['headers'] ) ) {
+ $status = $this->doDescribeInternal( $params );
+ $this->clearCache( array( $params['src'] ) );
+ $this->deleteFileCache( $params['src'] ); // persistent cache
+ } else {
+ $status = Status::newGood(); // nothing to do
+ }
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::describeInternal()
+ * @return Status
+ */
+ protected function doDescribeInternal( array $params ) {
return Status::newGood();
}
/**
- * @see FileBackend::concatenate()
+ * No-op file operation that does nothing.
+ * Do not call this function from places outside FileBackend and FileOp.
+ *
+ * @param array $params
* @return Status
*/
+ final public function nullInternal( array $params ) {
+ return Status::newGood();
+ }
+
final public function concatenate( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
// Try to lock the source files for the scope of this function
@@ -302,8 +350,6 @@ abstract class FileBackendStore extends FileBackend {
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -314,31 +360,41 @@ abstract class FileBackendStore extends FileBackend {
protected function doConcatenate( array $params ) {
$status = Status::newGood();
$tmpPath = $params['dst']; // convenience
+ unset( $params['latest'] ); // sanity
// Check that the specified temp file is valid...
wfSuppressWarnings();
- $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
+ $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
wfRestoreWarnings();
if ( !$ok ) { // not present or not empty
$status->fatal( 'backend-fail-opentemp', $tmpPath );
return $status;
}
- // Build up the temp file using the source chunks (in order)...
+ // Get local FS versions of the chunks needed for the concatenation...
+ $fsFiles = $this->getLocalReferenceMulti( $params );
+ foreach ( $fsFiles as $path => &$fsFile ) {
+ if ( !$fsFile ) { // chunk failed to download?
+ $fsFile = $this->getLocalReference( array( 'src' => $path ) );
+ if ( !$fsFile ) { // retry failed?
+ $status->fatal( 'backend-fail-read', $path );
+ return $status;
+ }
+ }
+ }
+ unset( $fsFile ); // unset reference so we can reuse $fsFile
+
+ // Get a handle for the destination temp file
$tmpHandle = fopen( $tmpPath, 'ab' );
if ( $tmpHandle === false ) {
$status->fatal( 'backend-fail-opentemp', $tmpPath );
return $status;
}
- foreach ( $params['srcs'] as $virtualSource ) {
- // Get a local FS version of the chunk
- $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) );
- if ( !$tmpFile ) {
- $status->fatal( 'backend-fail-read', $virtualSource );
- return $status;
- }
+
+ // Build up the temp file using the source chunks (in order)...
+ foreach ( $fsFiles as $virtualSource => $fsFile ) {
// Get a handle to the local FS version
- $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
+ $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
if ( $sourceHandle === false ) {
fclose( $tmpHandle );
$status->fatal( 'backend-fail-read', $virtualSource );
@@ -363,20 +419,13 @@ abstract class FileBackendStore extends FileBackend {
return $status;
}
- /**
- * @see FileBackend::doPrepare()
- * @return Status
- */
final protected function doPrepare( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
-
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
+
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status; // invalid storage path
}
@@ -384,14 +433,12 @@ abstract class FileBackendStore extends FileBackend {
$status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
} else { // directory is on several shards
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
$status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -403,20 +450,13 @@ abstract class FileBackendStore extends FileBackend {
return Status::newGood();
}
- /**
- * @see FileBackend::doSecure()
- * @return Status
- */
final protected function doSecure( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status; // invalid storage path
}
@@ -424,14 +464,12 @@ abstract class FileBackendStore extends FileBackend {
$status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
} else { // directory is on several shards
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
$status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -443,20 +481,13 @@ abstract class FileBackendStore extends FileBackend {
return Status::newGood();
}
- /**
- * @see FileBackend::doPublish()
- * @return Status
- */
final protected function doPublish( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status; // invalid storage path
}
@@ -464,14 +495,12 @@ abstract class FileBackendStore extends FileBackend {
$status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
} else { // directory is on several shards
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
$status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -483,13 +512,8 @@ abstract class FileBackendStore extends FileBackend {
return Status::newGood();
}
- /**
- * @see FileBackend::doClean()
- * @return Status
- */
final protected function doClean( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
// Recursive: first delete all empty subdirs recursively
@@ -500,14 +524,13 @@ abstract class FileBackendStore extends FileBackend {
$subDir = $params['dir'] . "/{$subDirRel}"; // full path
$status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
}
+ unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
}
}
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status; // invalid storage path
}
@@ -515,8 +538,6 @@ abstract class FileBackendStore extends FileBackend {
$filesLockEx = array( $params['dir'] );
$scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
if ( !$status->isOK() ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status; // abort
}
@@ -525,15 +546,13 @@ abstract class FileBackendStore extends FileBackend {
$this->deleteContainerCache( $fullCont ); // purge cache
} else { // directory is on several shards
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
$status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
$this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -545,68 +564,46 @@ abstract class FileBackendStore extends FileBackend {
return Status::newGood();
}
- /**
- * @see FileBackend::fileExists()
- * @return bool|null
- */
final public function fileExists( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$stat = $this->getFileStat( $params );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return ( $stat === null ) ? null : (bool)$stat; // null => failure
}
- /**
- * @see FileBackend::getFileTimestamp()
- * @return bool
- */
final public function getFileTimestamp( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$stat = $this->getFileStat( $params );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $stat ? $stat['mtime'] : false;
}
- /**
- * @see FileBackend::getFileSize()
- * @return bool
- */
final public function getFileSize( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$stat = $this->getFileStat( $params );
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $stat ? $stat['size'] : false;
}
- /**
- * @see FileBackend::getFileStat()
- * @return bool
- */
final public function getFileStat( array $params ) {
$path = self::normalizeStoragePath( $params['src'] );
if ( $path === null ) {
return false; // invalid storage path
}
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$latest = !empty( $params['latest'] ); // use latest data?
- if ( !$this->cheapCache->has( $path, 'stat' ) ) {
+ if ( !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
$this->primeFileCache( array( $path ) ); // check persistent cache
}
- if ( $this->cheapCache->has( $path, 'stat' ) ) {
+ if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
$stat = $this->cheapCache->get( $path, 'stat' );
// If we want the latest data, check that this cached
// value was in fact fetched with the latest available data.
- if ( !$latest || $stat['latest'] ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
- return $stat;
+ if ( is_array( $stat ) ) {
+ if ( !$latest || $stat['latest'] ) {
+ return $stat;
+ }
+ } elseif ( in_array( $stat, array( 'NOT_EXIST', 'NOT_EXIST_LATEST' ) ) ) {
+ if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
+ return false;
+ }
}
}
wfProfileIn( __METHOD__ . '-miss' );
@@ -614,7 +611,7 @@ abstract class FileBackendStore extends FileBackend {
$stat = $this->doGetFileStat( $params );
wfProfileOut( __METHOD__ . '-miss-' . $this->name );
wfProfileOut( __METHOD__ . '-miss' );
- if ( is_array( $stat ) ) { // don't cache negatives
+ if ( is_array( $stat ) ) { // file exists
$stat['latest'] = $latest;
$this->cheapCache->set( $path, 'stat', $stat );
$this->setFileCache( $path, $stat ); // update persistent cache
@@ -622,11 +619,14 @@ abstract class FileBackendStore extends FileBackend {
$this->cheapCache->set( $path, 'sha1',
array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
}
- } else {
+ } elseif ( $stat === false ) { // file does not exist
+ $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
+ $this->cheapCache->set( $path, 'sha1', // the SHA-1 must be false too
+ array( 'hash' => false, 'latest' => $latest ) );
wfDebug( __METHOD__ . ": File $path does not exist.\n" );
+ } else { // an error occurred
+ wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $stat;
}
@@ -635,46 +635,41 @@ abstract class FileBackendStore extends FileBackend {
*/
abstract protected function doGetFileStat( array $params );
+ public function getFileContentsMulti( array $params ) {
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
+
+ $params = $this->setConcurrencyFlags( $params );
+ $contents = $this->doGetFileContentsMulti( $params );
+
+ return $contents;
+ }
+
/**
- * @see FileBackend::getFileContents()
- * @return bool|string
+ * @see FileBackendStore::getFileContentsMulti()
+ * @return Array
*/
- public function getFileContents( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
- $tmpFile = $this->getLocalReference( $params );
- if ( !$tmpFile ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
- return false;
+ protected function doGetFileContentsMulti( array $params ) {
+ $contents = array();
+ foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
+ wfSuppressWarnings();
+ $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
+ wfRestoreWarnings();
}
- wfSuppressWarnings();
- $data = file_get_contents( $tmpFile->getPath() );
- wfRestoreWarnings();
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
- return $data;
+ return $contents;
}
- /**
- * @see FileBackend::getFileSha1Base36()
- * @return bool|string
- */
final public function getFileSha1Base36( array $params ) {
$path = self::normalizeStoragePath( $params['src'] );
if ( $path === null ) {
return false; // invalid storage path
}
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$latest = !empty( $params['latest'] ); // use latest data?
- if ( $this->cheapCache->has( $path, 'sha1' ) ) {
+ if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
$stat = $this->cheapCache->get( $path, 'sha1' );
// If we want the latest data, check that this cached
// value was in fact fetched with the latest available data.
if ( !$latest || $stat['latest'] ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $stat['hash'];
}
}
@@ -683,12 +678,7 @@ abstract class FileBackendStore extends FileBackend {
$hash = $this->doGetFileSha1Base36( $params );
wfProfileOut( __METHOD__ . '-miss-' . $this->name );
wfProfileOut( __METHOD__ . '-miss' );
- if ( $hash ) { // don't cache negatives
- $this->cheapCache->set( $path, 'sha1',
- array( 'hash' => $hash, 'latest' => $latest ) );
- }
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
+ $this->cheapCache->set( $path, 'sha1', array( 'hash' => $hash, 'latest' => $latest ) );
return $hash;
}
@@ -705,59 +695,80 @@ abstract class FileBackendStore extends FileBackend {
}
}
- /**
- * @see FileBackend::getFileProps()
- * @return Array
- */
final public function getFileProps( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$fsFile = $this->getLocalReference( $params );
$props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $props;
}
- /**
- * @see FileBackend::getLocalReference()
- * @return TempFSFile|null
- */
- public function getLocalReference( array $params ) {
- $path = self::normalizeStoragePath( $params['src'] );
- if ( $path === null ) {
- return null; // invalid storage path
- }
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ final public function getLocalReferenceMulti( array $params ) {
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
+
+ $params = $this->setConcurrencyFlags( $params );
+
+ $fsFiles = array(); // (path => FSFile)
$latest = !empty( $params['latest'] ); // use latest data?
- if ( $this->expensiveCache->has( $path, 'localRef' ) ) {
- $val = $this->expensiveCache->get( $path, 'localRef' );
- // If we want the latest data, check that this cached
- // value was in fact fetched with the latest available data.
- if ( !$latest || $val['latest'] ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
- return $val['object'];
+ // Reuse any files already in process cache...
+ foreach ( $params['srcs'] as $src ) {
+ $path = self::normalizeStoragePath( $src );
+ if ( $path === null ) {
+ $fsFiles[$src] = null; // invalid storage path
+ } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
+ $val = $this->expensiveCache->get( $path, 'localRef' );
+ // If we want the latest data, check that this cached
+ // value was in fact fetched with the latest available data.
+ if ( !$latest || $val['latest'] ) {
+ $fsFiles[$src] = $val['object'];
+ }
}
}
- $tmpFile = $this->getLocalCopy( $params );
- if ( $tmpFile ) { // don't cache negatives
- $this->expensiveCache->set( $path, 'localRef',
- array( 'object' => $tmpFile, 'latest' => $latest ) );
+ // Fetch local references of any remaning files...
+ $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
+ foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
+ $fsFiles[$path] = $fsFile;
+ if ( $fsFile ) { // update the process cache...
+ $this->expensiveCache->set( $path, 'localRef',
+ array( 'object' => $fsFile, 'latest' => $latest ) );
+ }
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
- return $tmpFile;
+
+ return $fsFiles;
}
/**
- * @see FileBackend::streamFile()
- * @return Status
+ * @see FileBackendStore::getLocalReferenceMulti()
+ * @return Array
+ */
+ protected function doGetLocalReferenceMulti( array $params ) {
+ return $this->doGetLocalCopyMulti( $params );
+ }
+
+ final public function getLocalCopyMulti( array $params ) {
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
+
+ $params = $this->setConcurrencyFlags( $params );
+ $tmpFiles = $this->doGetLocalCopyMulti( $params );
+
+ return $tmpFiles;
+ }
+
+ /**
+ * @see FileBackendStore::getLocalCopyMulti()
+ * @return Array
+ */
+ abstract protected function doGetLocalCopyMulti( array $params );
+
+ /**
+ * @see FileBackend::getFileHttpUrl()
+ * @return string|null
*/
+ public function getFileHttpUrl( array $params ) {
+ return null; // not supported
+ }
+
final public function streamFile( array $params ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
$info = $this->getFileStat( $params );
@@ -776,12 +787,18 @@ abstract class FileBackendStore extends FileBackend {
$status = $this->doStreamFile( $params );
wfProfileOut( __METHOD__ . '-send-' . $this->name );
wfProfileOut( __METHOD__ . '-send' );
+ if ( !$status->isOK() ) {
+ // Per bug 41113, nasty things can happen if bad cache entries get
+ // stuck in cache. It's also possible that this error can come up
+ // with simple race conditions. Clear out the stat cache to be safe.
+ $this->clearCache( array( $params['src'] ) );
+ $this->deleteFileCache( $params['src'] );
+ trigger_error( "Bad stat cache or race condition for file {$params['src']}." );
+ }
} else {
$status->fatal( 'backend-fail-stream', $params['src'] );
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -802,10 +819,6 @@ abstract class FileBackendStore extends FileBackend {
return $status;
}
- /**
- * @see FileBackend::directoryExists()
- * @return bool|null
- */
final public function directoryExists( array $params ) {
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) {
@@ -815,7 +828,7 @@ abstract class FileBackendStore extends FileBackend {
return $this->doDirectoryExists( $fullCont, $dir, $params );
} else { // directory is on several shards
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
$res = false; // response
foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
$exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
@@ -833,17 +846,13 @@ abstract class FileBackendStore extends FileBackend {
/**
* @see FileBackendStore::directoryExists()
*
- * @param $container string Resolved container name
- * @param $dir string Resolved path relative to container
- * @param $params Array
+ * @param string $container Resolved container name
+ * @param string $dir Resolved path relative to container
+ * @param array $params
* @return bool|null
*/
abstract protected function doDirectoryExists( $container, $dir, array $params );
- /**
- * @see FileBackend::getDirectoryList()
- * @return Traversable|Array|null Returns null on failure
- */
final public function getDirectoryList( array $params ) {
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) { // invalid storage path
@@ -855,7 +864,7 @@ abstract class FileBackendStore extends FileBackend {
} else {
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
// File listing spans multiple containers/shards
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
return new FileBackendStoreShardDirIterator( $this,
$fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
}
@@ -866,17 +875,13 @@ abstract class FileBackendStore extends FileBackend {
*
* @see FileBackendStore::getDirectoryList()
*
- * @param $container string Resolved container name
- * @param $dir string Resolved path relative to container
- * @param $params Array
+ * @param string $container Resolved container name
+ * @param string $dir Resolved path relative to container
+ * @param array $params
* @return Traversable|Array|null Returns null on failure
*/
abstract public function getDirectoryListInternal( $container, $dir, array $params );
- /**
- * @see FileBackend::getFileList()
- * @return Traversable|Array|null Returns null on failure
- */
final public function getFileList( array $params ) {
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) { // invalid storage path
@@ -888,7 +893,7 @@ abstract class FileBackendStore extends FileBackend {
} else {
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
// File listing spans multiple containers/shards
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
return new FileBackendStoreShardFileIterator( $this,
$fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
}
@@ -899,9 +904,9 @@ abstract class FileBackendStore extends FileBackend {
*
* @see FileBackendStore::getFileList()
*
- * @param $container string Resolved container name
- * @param $dir string Resolved path relative to container
- * @param $params Array
+ * @param string $container Resolved container name
+ * @param string $dir Resolved path relative to container
+ * @param array $params
* @return Traversable|Array|null Returns null on failure
*/
abstract public function getFileListInternal( $container, $dir, array $params );
@@ -913,18 +918,19 @@ abstract class FileBackendStore extends FileBackend {
* The result must have the same number of items as the input.
* An exception is thrown if an unsupported operation is requested.
*
- * @param $ops Array Same format as doOperations()
+ * @param array $ops Same format as doOperations()
* @return Array List of FileOp objects
* @throws MWException
*/
final public function getOperationsInternal( array $ops ) {
$supportedOps = array(
- 'store' => 'StoreFileOp',
- 'copy' => 'CopyFileOp',
- 'move' => 'MoveFileOp',
- 'delete' => 'DeleteFileOp',
- 'create' => 'CreateFileOp',
- 'null' => 'NullFileOp'
+ 'store' => 'StoreFileOp',
+ 'copy' => 'CopyFileOp',
+ 'move' => 'MoveFileOp',
+ 'delete' => 'DeleteFileOp',
+ 'create' => 'CreateFileOp',
+ 'describe' => 'DescribeFileOp',
+ 'null' => 'NullFileOp'
);
$performOps = array(); // array of FileOp objects
@@ -947,11 +953,13 @@ abstract class FileBackendStore extends FileBackend {
/**
* Get a list of storage paths to lock for a list of operations
- * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys,
- * each corresponding to a list of storage paths to be locked.
+ * Returns an array with LockManager::LOCK_UW (shared locks) and
+ * LockManager::LOCK_EX (exclusive locks) keys, each corresponding
+ * to a list of storage paths to be locked. All returned paths are
+ * normalized.
*
- * @param $performOps Array List of FileOp objects
- * @return Array ('sh' => list of paths, 'ex' => list of paths)
+ * @param array $performOps List of FileOp objects
+ * @return Array (LockManager::LOCK_UW => path list, LockManager::LOCK_EX => path list)
*/
final public function getPathsToLockForOpsInternal( array $performOps ) {
// Build up a list of files to lock...
@@ -965,30 +973,24 @@ abstract class FileBackendStore extends FileBackend {
// Get a shared lock on the parent directory of each path changed
$paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
- return $paths;
+ return array(
+ LockManager::LOCK_UW => $paths['sh'],
+ LockManager::LOCK_EX => $paths['ex']
+ );
}
- /**
- * @see FileBackend::getScopedLocksForOps()
- * @return Array
- */
public function getScopedLocksForOps( array $ops, Status $status ) {
$paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
- return array(
- $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
- $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
- );
+ return array( $this->getScopedFileLocks( $paths, 'mixed', $status ) );
}
- /**
- * @see FileBackend::doOperationsInternal()
- * @return Status
- */
final protected function doOperationsInternal( array $ops, array $opts ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
+ // Fix up custom header name/value pairs...
+ $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
+
// Build up a list of FileOps...
$performOps = $this->getOperationsInternal( $ops );
@@ -997,11 +999,8 @@ abstract class FileBackendStore extends FileBackend {
// Build up a list of files to lock...
$paths = $this->getPathsToLockForOpsInternal( $performOps );
// Try to lock those files for the scope of this function...
- $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
- $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
+ $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
if ( !$status->isOK() ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status; // abort
}
}
@@ -1016,29 +1015,28 @@ abstract class FileBackendStore extends FileBackend {
$this->primeContainerCache( $performOps );
// Actually attempt the operation batch...
+ $opts = $this->setConcurrencyFlags( $opts );
$subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
// Merge errors into status fields
$status->merge( $subStatus );
$status->success = $subStatus->success; // not done in merge()
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
- /**
- * @see FileBackend::doQuickOperationsInternal()
- * @return Status
- * @throws MWException
- */
final protected function doQuickOperationsInternal( array $ops ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$status = Status::newGood();
+ // Fix up custom header name/value pairs...
+ $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
+
+ // Clear any file cache entries
+ $this->clearCache();
+
$supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
- $async = ( $this->parallelize === 'implicit' );
+ $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
$maxConcurrency = $this->concurrency; // throttle
$statuses = array(); // array of (index => Status)
@@ -1047,8 +1045,6 @@ abstract class FileBackendStore extends FileBackend {
// Perform the sync-only ops and build up op handles for the async ops...
foreach ( $ops as $index => $params ) {
if ( !in_array( $params['op'], $supportedOps ) ) {
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
throw new MWException( "Operation '{$params['op']}' is not supported." );
}
$method = $params['op'] . 'Internal'; // e.g. "storeInternal"
@@ -1082,8 +1078,6 @@ abstract class FileBackendStore extends FileBackend {
}
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $status;
}
@@ -1092,13 +1086,12 @@ abstract class FileBackendStore extends FileBackend {
* The resulting Status object fields will correspond
* to the order in which the handles where given.
*
- * @param $handles Array List of FileBackendStoreOpHandle objects
+ * @param array $handles List of FileBackendStoreOpHandle objects
* @return Array Map of Status objects
* @throws MWException
*/
final public function executeOpHandlesInternal( array $fileOpHandles ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
foreach ( $fileOpHandles as $fileOpHandle ) {
if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
throw new MWException( "Given a non-FileBackendStoreOpHandle object." );
@@ -1110,13 +1103,13 @@ abstract class FileBackendStore extends FileBackend {
foreach ( $fileOpHandles as $fileOpHandle ) {
$fileOpHandle->closeResources();
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
return $res;
}
/**
* @see FileBackendStore::executeOpHandlesInternal()
+ * @param array $fileOpHandles
+ * @throws MWException
* @return Array List of corresponding Status objects
*/
protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
@@ -1127,12 +1120,34 @@ abstract class FileBackendStore extends FileBackend {
}
/**
- * @see FileBackend::preloadCache()
+ * Strip long HTTP headers from a file operation.
+ * Most headers are just numbers, but some are allowed to be long.
+ * This function is useful for cleaning up headers and avoiding backend
+ * specific errors, especially in the middle of batch file operations.
+ *
+ * @param array $op Same format as doOperation()
+ * @return Array
*/
+ protected function stripInvalidHeadersFromOp( array $op ) {
+ static $longs = array( 'Content-Disposition' );
+ if ( isset( $op['headers'] ) ) { // op sets HTTP headers
+ foreach ( $op['headers'] as $name => $value ) {
+ $maxHVLen = in_array( $name, $longs ) ? INF : 255;
+ if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
+ trigger_error( "Header '$name: $value' is too long." );
+ unset( $op['headers'][$name] );
+ } elseif ( !strlen( $value ) ) {
+ $op['headers'][$name] = ''; // null/false => ""
+ }
+ }
+ }
+ return $op;
+ }
+
final public function preloadCache( array $paths ) {
$fullConts = array(); // full container names
foreach ( $paths as $path ) {
- list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+ list( $fullCont, , ) = $this->resolveStoragePath( $path );
$fullConts[] = $fullCont;
}
// Load from the persistent file and container caches
@@ -1140,9 +1155,6 @@ abstract class FileBackendStore extends FileBackend {
$this->primeFileCache( $paths );
}
- /**
- * @see FileBackend::clearCache()
- */
final public function clearCache( array $paths = null ) {
if ( is_array( $paths ) ) {
$paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
@@ -1165,7 +1177,7 @@ abstract class FileBackendStore extends FileBackend {
*
* @see FileBackend::clearCache()
*
- * @param $paths Array Storage paths (optional)
+ * @param array $paths Storage paths (optional)
* @return void
*/
protected function doClearCache( array $paths = null ) {}
@@ -1183,7 +1195,7 @@ abstract class FileBackendStore extends FileBackend {
* Check if a container name is valid.
* This checks for for length and illegal characters.
*
- * @param $container string
+ * @param string $container
* @return bool
*/
final protected static function isValidContainerName( $container ) {
@@ -1205,7 +1217,7 @@ abstract class FileBackendStore extends FileBackend {
* this means that the path can only refer to a directory and can only
* be scanned by looking in all the container shards.
*
- * @param $storagePath string
+ * @param string $storagePath
* @return Array (container, path, container suffix) or (null, null, null) if invalid
*/
final protected function resolveStoragePath( $storagePath ) {
@@ -1235,16 +1247,22 @@ abstract class FileBackendStore extends FileBackend {
/**
* Like resolveStoragePath() except null values are returned if
- * the container is sharded and the shard could not be determined.
+ * the container is sharded and the shard could not be determined
+ * or if the path ends with '/'. The later case is illegal for FS
+ * backends and can confuse listings for object store backends.
+ *
+ * This function is used when resolving paths that must be valid
+ * locations for files. Directory and listing functions should
+ * generally just use resolveStoragePath() instead.
*
* @see FileBackendStore::resolveStoragePath()
*
- * @param $storagePath string
+ * @param string $storagePath
* @return Array (container, path) or (null, null) if invalid
*/
final protected function resolveStoragePathReal( $storagePath ) {
list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
- if ( $cShard !== null ) {
+ if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
return array( $container, $relPath );
}
return array( null, null );
@@ -1254,8 +1272,8 @@ abstract class FileBackendStore extends FileBackend {
* Get the container name shard suffix for a given path.
* Any empty suffix means the container is not sharded.
*
- * @param $container string Container name
- * @param $relPath string Storage path relative to the container
+ * @param string $container Container name
+ * @param string $relPath Storage path relative to the container
* @return string|null Returns null if shard could not be determined
*/
final protected function getContainerShard( $container, $relPath ) {
@@ -1291,11 +1309,11 @@ abstract class FileBackendStore extends FileBackend {
* Container dirs like "a", where the container shards on "x/xy",
* can reside on several shards. Such paths are tricky to handle.
*
- * @param $storagePath string Storage path
+ * @param string $storagePath Storage path
* @return bool
*/
final public function isSingleShardPathInternal( $storagePath ) {
- list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
+ list( , , $shard ) = $this->resolveStoragePath( $storagePath );
return ( $shard !== null );
}
@@ -1304,7 +1322,7 @@ abstract class FileBackendStore extends FileBackend {
* If greater than 0, then all file storage paths within
* the container are required to be hashed accordingly.
*
- * @param $container string
+ * @param string $container
* @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
*/
final protected function getContainerHashLevels( $container ) {
@@ -1324,7 +1342,7 @@ abstract class FileBackendStore extends FileBackend {
/**
* Get a list of full container shard suffixes for a container
*
- * @param $container string
+ * @param string $container
* @return Array
*/
final protected function getContainerSuffixes( $container ) {
@@ -1342,7 +1360,7 @@ abstract class FileBackendStore extends FileBackend {
/**
* Get the full container name, including the wiki ID prefix
*
- * @param $container string
+ * @param string $container
* @return string
*/
final protected function fullContainerName( $container ) {
@@ -1358,7 +1376,7 @@ abstract class FileBackendStore extends FileBackend {
* This is intended for internal use, such as encoding illegal chars.
* Subclasses can override this to be more restrictive.
*
- * @param $container string
+ * @param string $container
* @return string|null
*/
protected function resolveContainerName( $container ) {
@@ -1371,8 +1389,8 @@ abstract class FileBackendStore extends FileBackend {
* getting absolute paths (e.g. FS based backends). Note that the relative path
* may be the empty string (e.g. the path is simply to the container).
*
- * @param $container string Container name
- * @param $relStoragePath string Storage path relative to the container
+ * @param string $container Container name
+ * @param string $relStoragePath Storage path relative to the container
* @return string|null Path or null if not valid
*/
protected function resolveContainerPath( $container, $relStoragePath ) {
@@ -1382,7 +1400,7 @@ abstract class FileBackendStore extends FileBackend {
/**
* Get the cache key for a container
*
- * @param $container string Resolved container name
+ * @param string $container Resolved container name
* @return string
*/
private function containerCacheKey( $container ) {
@@ -1392,18 +1410,20 @@ abstract class FileBackendStore extends FileBackend {
/**
* Set the cached info for a container
*
- * @param $container string Resolved container name
- * @param $val mixed Information to cache
+ * @param string $container Resolved container name
+ * @param array $val Information to cache
+ * @return void
*/
- final protected function setContainerCache( $container, $val ) {
- $this->memCache->add( $this->containerCacheKey( $container ), $val, 14*86400 );
+ final protected function setContainerCache( $container, array $val ) {
+ $this->memCache->add( $this->containerCacheKey( $container ), $val, 14 * 86400 );
}
/**
* Delete the cached info for a container.
* The cache key is salted for a while to prevent race conditions.
*
- * @param $container string Resolved container name
+ * @param string $container Resolved container name
+ * @return void
*/
final protected function deleteContainerCache( $container ) {
if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) {
@@ -1414,13 +1434,13 @@ abstract class FileBackendStore extends FileBackend {
/**
* Do a batch lookup from cache for container stats for all containers
* used in a list of container names, storage paths, or FileOp objects.
+ * This loads the persistent cache values into the process cache.
*
- * @param $items Array
+ * @param Array $items
* @return void
*/
final protected function primeContainerCache( array $items ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$paths = array(); // list of storage paths
$contNames = array(); // (cache key => resolved container name)
@@ -1437,7 +1457,7 @@ abstract class FileBackendStore extends FileBackend {
}
// Get all the corresponding cache keys for paths...
foreach ( $paths as $path ) {
- list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+ list( $fullCont, , ) = $this->resolveStoragePath( $path );
if ( $fullCont !== null ) { // valid path for this backend
$contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
}
@@ -1452,9 +1472,6 @@ abstract class FileBackendStore extends FileBackend {
// Populate the container process cache for the backend...
$this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
-
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
}
/**
@@ -1462,7 +1479,7 @@ abstract class FileBackendStore extends FileBackend {
* resolved container names and their corresponding cached info.
* Only containers that actually exist should appear in the map.
*
- * @param $containerInfo Array Map of resolved container names to cached info
+ * @param array $containerInfo Map of resolved container names to cached info
* @return void
*/
protected function doPrimeContainerCache( array $containerInfo ) {}
@@ -1470,7 +1487,7 @@ abstract class FileBackendStore extends FileBackend {
/**
* Get the cache key for a file path
*
- * @param $path string Storage path
+ * @param string $path Normalized storage path
* @return string
*/
private function fileCacheKey( $path ) {
@@ -1482,20 +1499,34 @@ abstract class FileBackendStore extends FileBackend {
* Negatives (404s) are not cached. By not caching negatives, we can skip cache
* salting for the case when a file is created at a path were there was none before.
*
- * @param $path string Storage path
- * @param $val mixed Information to cache
+ * @param string $path Storage path
+ * @param array $val Stat information to cache
+ * @return void
*/
- final protected function setFileCache( $path, $val ) {
- $this->memCache->add( $this->fileCacheKey( $path ), $val, 7*86400 );
+ final protected function setFileCache( $path, array $val ) {
+ $path = FileBackend::normalizeStoragePath( $path );
+ if ( $path === null ) {
+ return; // invalid storage path
+ }
+ $age = time() - wfTimestamp( TS_UNIX, $val['mtime'] );
+ $ttl = min( 7 * 86400, max( 300, floor( .1 * $age ) ) );
+ $this->memCache->add( $this->fileCacheKey( $path ), $val, $ttl );
}
/**
* Delete the cached stat info for a file path.
* The cache key is salted for a while to prevent race conditions.
+ * Since negatives (404s) are not cached, this does not need to be called when
+ * a file is created at a path were there was none before.
*
- * @param $path string Storage path
+ * @param string $path Storage path
+ * @return void
*/
final protected function deleteFileCache( $path ) {
+ $path = FileBackend::normalizeStoragePath( $path );
+ if ( $path === null ) {
+ return; // invalid storage path
+ }
if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) {
trigger_error( "Unable to delete stat cache for file $path." );
}
@@ -1504,13 +1535,13 @@ abstract class FileBackendStore extends FileBackend {
/**
* Do a batch lookup from cache for file stats for all paths
* used in a list of storage paths or FileOp objects.
+ * This loads the persistent cache values into the process cache.
*
- * @param $items Array List of storage paths or FileOps
+ * @param array $items List of storage paths or FileOps
* @return void
*/
final protected function primeFileCache( array $items ) {
- wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
$paths = array(); // list of storage paths
$pathNames = array(); // (cache key => storage path)
@@ -1520,12 +1551,14 @@ abstract class FileBackendStore extends FileBackend {
$paths = array_merge( $paths, $item->storagePathsRead() );
$paths = array_merge( $paths, $item->storagePathsChanged() );
} elseif ( self::isStoragePath( $item ) ) {
- $paths[] = $item;
+ $paths[] = FileBackend::normalizeStoragePath( $item );
}
}
+ // Get rid of any paths that failed normalization...
+ $paths = array_filter( $paths, 'strlen' ); // remove nulls
// Get all the corresponding cache keys for paths...
foreach ( $paths as $path ) {
- list( $cont, $rel, $s ) = $this->resolveStoragePath( $path );
+ list( , $rel, ) = $this->resolveStoragePath( $path );
if ( $rel !== null ) { // valid path for this backend
$pathNames[$this->fileCacheKey( $path )] = $path;
}
@@ -1542,9 +1575,38 @@ abstract class FileBackendStore extends FileBackend {
}
}
}
+ }
+
+ /**
+ * Set the 'concurrency' option from a list of operation options
+ *
+ * @param array $opts Map of operation options
+ * @return Array
+ */
+ final protected function setConcurrencyFlags( array $opts ) {
+ $opts['concurrency'] = 1; // off
+ if ( $this->parallelize === 'implicit' ) {
+ if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
+ $opts['concurrency'] = $this->concurrency;
+ }
+ } elseif ( $this->parallelize === 'explicit' ) {
+ if ( !empty( $opts['parallelize'] ) ) {
+ $opts['concurrency'] = $this->concurrency;
+ }
+ }
+ return $opts;
+ }
- wfProfileOut( __METHOD__ . '-' . $this->name );
- wfProfileOut( __METHOD__ );
+ /**
+ * Get the content type to use in HEAD/GET requests for a file
+ *
+ * @param string $storagePath
+ * @param string|null $content File data
+ * @param string|null $fsPath File system path
+ * @return MIME type
+ */
+ protected function getContentType( $storagePath, $content, $fsPath ) {
+ return call_user_func_array( $this->mimeCallback, func_get_args() );
}
}
@@ -1582,30 +1644,24 @@ abstract class FileBackendStoreOpHandle {
*
* @ingroup FileBackend
*/
-abstract class FileBackendStoreShardListIterator implements Iterator {
+abstract class FileBackendStoreShardListIterator extends FilterIterator {
/** @var FileBackendStore */
protected $backend;
/** @var Array */
protected $params;
- /** @var Array */
- protected $shardSuffixes;
+
protected $container; // string; full container name
protected $directory; // string; resolved relative path
- /** @var Traversable */
- protected $iter;
- protected $curShard = 0; // integer
- protected $pos = 0; // integer
-
/** @var Array */
protected $multiShardPaths = array(); // (rel path => 1)
/**
- * @param $backend FileBackendStore
- * @param $container string Full storage container name
- * @param $dir string Storage directory relative to container
- * @param $suffixes Array List of container shard suffixes
- * @param $params Array
+ * @param FileBackendStore $backend
+ * @param string $container Full storage container name
+ * @param string $dir Storage directory relative to container
+ * @param array $suffixes List of container shard suffixes
+ * @param array $params
*/
public function __construct(
FileBackendStore $backend, $container, $dir, array $suffixes, array $params
@@ -1613,142 +1669,56 @@ abstract class FileBackendStoreShardListIterator implements Iterator {
$this->backend = $backend;
$this->container = $container;
$this->directory = $dir;
- $this->shardSuffixes = $suffixes;
$this->params = $params;
- }
-
- /**
- * @see Iterator::key()
- * @return integer
- */
- public function key() {
- return $this->pos;
- }
- /**
- * @see Iterator::valid()
- * @return bool
- */
- public function valid() {
- if ( $this->iter instanceof Iterator ) {
- return $this->iter->valid();
- } elseif ( is_array( $this->iter ) ) {
- return ( current( $this->iter ) !== false ); // no paths can have this value
+ $iter = new AppendIterator();
+ foreach ( $suffixes as $suffix ) {
+ $iter->append( $this->listFromShard( $this->container . $suffix ) );
}
- return false; // some failure?
- }
- /**
- * @see Iterator::current()
- * @return string|bool String or false
- */
- public function current() {
- return ( $this->iter instanceof Iterator )
- ? $this->iter->current()
- : current( $this->iter );
+ parent::__construct( $iter );
}
- /**
- * @see Iterator::next()
- * @return void
- */
- public function next() {
- ++$this->pos;
- ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter );
- do {
- $continue = false; // keep scanning shards?
- $this->filterViaNext(); // filter out duplicates
- // Find the next non-empty shard if no elements are left
- if ( !$this->valid() ) {
- $this->nextShardIteratorIfNotValid();
- $continue = $this->valid(); // re-filter unless we ran out of shards
- }
- } while ( $continue );
- }
-
- /**
- * @see Iterator::rewind()
- * @return void
- */
- public function rewind() {
- $this->pos = 0;
- $this->curShard = 0;
- $this->setIteratorFromCurrentShard();
- do {
- $continue = false; // keep scanning shards?
- $this->filterViaNext(); // filter out duplicates
- // Find the next non-empty shard if no elements are left
- if ( !$this->valid() ) {
- $this->nextShardIteratorIfNotValid();
- $continue = $this->valid(); // re-filter unless we ran out of shards
- }
- } while ( $continue );
- }
-
- /**
- * Filter out duplicate items by advancing to the next ones
- */
- protected function filterViaNext() {
- while ( $this->valid() ) {
- $rel = $this->iter->current(); // path relative to given directory
- $path = $this->params['dir'] . "/{$rel}"; // full storage path
- if ( $this->backend->isSingleShardPathInternal( $path ) ) {
- break; // path is only on one shard; no issue with duplicates
- } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
- // Don't keep listing paths that are on multiple shards
- ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter );
- } else {
- $this->multiShardPaths[$rel] = 1;
- break;
- }
- }
- }
-
- /**
- * If the list iterator for this container shard is out of items,
- * then move on to the next container that has items.
- * If there are none, then it advances to the last container.
- */
- protected function nextShardIteratorIfNotValid() {
- while ( !$this->valid() && ++$this->curShard < count( $this->shardSuffixes ) ) {
- $this->setIteratorFromCurrentShard();
+ public function accept() {
+ $rel = $this->getInnerIterator()->current(); // path relative to given directory
+ $path = $this->params['dir'] . "/{$rel}"; // full storage path
+ if ( $this->backend->isSingleShardPathInternal( $path ) ) {
+ return true; // path is only on one shard; no issue with duplicates
+ } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
+ // Don't keep listing paths that are on multiple shards
+ return false;
+ } else {
+ $this->multiShardPaths[$rel] = 1;
+ return true;
}
}
- /**
- * Set the list iterator to that of the current container shard
- */
- protected function setIteratorFromCurrentShard() {
- $this->iter = $this->listFromShard(
- $this->container . $this->shardSuffixes[$this->curShard],
- $this->directory, $this->params );
- // Start loading results so that current() works
- if ( $this->iter ) {
- ( $this->iter instanceof Iterator ) ? $this->iter->rewind() : reset( $this->iter );
- }
+ public function rewind() {
+ parent::rewind();
+ $this->multiShardPaths = array();
}
/**
* Get the list for a given container shard
*
- * @param $container string Resolved container name
- * @param $dir string Resolved path relative to container
- * @param $params Array
- * @return Traversable|Array|null
+ * @param string $container Resolved container name
+ * @return Iterator
*/
- abstract protected function listFromShard( $container, $dir, array $params );
+ abstract protected function listFromShard( $container );
}
/**
* Iterator for listing directories
*/
class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
- /**
- * @see FileBackendStoreShardListIterator::listFromShard()
- * @return Array|null|Traversable
- */
- protected function listFromShard( $container, $dir, array $params ) {
- return $this->backend->getDirectoryListInternal( $container, $dir, $params );
+ protected function listFromShard( $container ) {
+ $list = $this->backend->getDirectoryListInternal(
+ $container, $this->directory, $this->params );
+ if ( $list === null ) {
+ return new ArrayIterator( array() );
+ } else {
+ return is_array( $list ) ? new ArrayIterator( $list ) : $list;
+ }
}
}
@@ -1756,11 +1726,13 @@ class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator
* Iterator for listing regular files
*/
class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
- /**
- * @see FileBackendStoreShardListIterator::listFromShard()
- * @return Array|null|Traversable
- */
- protected function listFromShard( $container, $dir, array $params ) {
- return $this->backend->getFileListInternal( $container, $dir, $params );
+ protected function listFromShard( $container ) {
+ $list = $this->backend->getFileListInternal(
+ $container, $this->directory, $this->params );
+ if ( $list === null ) {
+ return new ArrayIterator( array() );
+ } else {
+ return is_array( $list ) ? new ArrayIterator( $list ) : $list;
+ }
}
}
diff --git a/includes/filebackend/FileOp.php b/includes/filebackend/FileOp.php
index 7c43c489..fe833084 100644
--- a/includes/filebackend/FileOp.php
+++ b/includes/filebackend/FileOp.php
@@ -42,11 +42,12 @@ abstract class FileOp {
protected $state = self::STATE_NEW; // integer
protected $failed = false; // boolean
protected $async = false; // boolean
- protected $useLatest = true; // boolean
protected $batchId; // string
+ protected $doOperation = true; // boolean; operation is not a no-op
protected $sourceSha1; // string
- protected $destSameAsSource; // boolean
+ protected $overwriteSameCase; // boolean
+ protected $destExists; // boolean
/* Object life-cycle */
const STATE_NEW = 1;
@@ -54,54 +55,81 @@ abstract class FileOp {
const STATE_ATTEMPTED = 3;
/**
- * Build a new file operation transaction
+ * Build a new batch file operation transaction
*
- * @param $backend FileBackendStore
- * @param $params Array
+ * @param FileBackendStore $backend
+ * @param Array $params
* @throws MWException
*/
final public function __construct( FileBackendStore $backend, array $params ) {
$this->backend = $backend;
list( $required, $optional ) = $this->allowedParams();
+ // @todo normalizeAnyStoragePaths() calls are overzealous, use a parameter list
foreach ( $required as $name ) {
if ( isset( $params[$name] ) ) {
- $this->params[$name] = $params[$name];
+ // Normalize paths so the paths to the same file have the same string
+ $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
} else {
throw new MWException( "File operation missing parameter '$name'." );
}
}
foreach ( $optional as $name ) {
if ( isset( $params[$name] ) ) {
- $this->params[$name] = $params[$name];
+ // Normalize paths so the paths to the same file have the same string
+ $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
}
}
$this->params = $params;
}
/**
- * Set the batch UUID this operation belongs to
+ * Normalize $item or anything in $item that is a valid storage path
*
- * @param $batchId string
- * @return void
+ * @param string $item|array
+ * @return string|Array
+ */
+ protected function normalizeAnyStoragePaths( $item ) {
+ if ( is_array( $item ) ) {
+ $res = array();
+ foreach ( $item as $k => $v ) {
+ $k = self::normalizeIfValidStoragePath( $k );
+ $v = self::normalizeIfValidStoragePath( $v );
+ $res[$k] = $v;
+ }
+ return $res;
+ } else {
+ return self::normalizeIfValidStoragePath( $item );
+ }
+ }
+
+ /**
+ * Normalize a string if it is a valid storage path
+ *
+ * @param string $path
+ * @return string
*/
- final public function setBatchId( $batchId ) {
- $this->batchId = $batchId;
+ protected static function normalizeIfValidStoragePath( $path ) {
+ if ( FileBackend::isStoragePath( $path ) ) {
+ $res = FileBackend::normalizeStoragePath( $path );
+ return ( $res !== null ) ? $res : $path;
+ }
+ return $path;
}
/**
- * Whether to allow stale data for file reads and stat checks
+ * Set the batch UUID this operation belongs to
*
- * @param $allowStale bool
+ * @param string $batchId
* @return void
*/
- final public function allowStaleReads( $allowStale ) {
- $this->useLatest = !$allowStale;
+ final public function setBatchId( $batchId ) {
+ $this->batchId = $batchId;
}
/**
* Get the value of the parameter with the given name
*
- * @param $name string
+ * @param string $name
* @return mixed Returns null if the parameter is not set
*/
final public function getParam( $name ) {
@@ -138,11 +166,11 @@ abstract class FileOp {
/**
* Update a dependency tracking array to account for this operation
*
- * @param $deps Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
* @return Array
*/
final public function applyDependencies( array $deps ) {
- $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
+ $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
$deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
return $deps;
}
@@ -150,7 +178,7 @@ abstract class FileOp {
/**
* Check if this operation changes files listed in $paths
*
- * @param $paths Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @param array $paths Prior path reads/writes; format of FileOp::newPredicates()
* @return boolean
*/
final public function dependsOn( array $deps ) {
@@ -170,33 +198,36 @@ abstract class FileOp {
/**
* Get the file journal entries for this file operation
*
- * @param $oPredicates Array Pre-op info about files (format of FileOp::newPredicates)
- * @param $nPredicates Array Post-op info about files (format of FileOp::newPredicates)
+ * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates)
+ * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates)
* @return Array
*/
final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
+ if ( !$this->doOperation ) {
+ return array(); // this is a no-op
+ }
$nullEntries = array();
$updateEntries = array();
$deleteEntries = array();
$pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
- foreach ( $pathsUsed as $path ) {
+ foreach ( array_unique( $pathsUsed ) as $path ) {
$nullEntries[] = array( // assertion for recovery
- 'op' => 'null',
- 'path' => $path,
+ 'op' => 'null',
+ 'path' => $path,
'newSha1' => $this->fileSha1( $path, $oPredicates )
);
}
foreach ( $this->storagePathsChanged() as $path ) {
if ( $nPredicates['sha1'][$path] === false ) { // deleted
$deleteEntries[] = array(
- 'op' => 'delete',
- 'path' => $path,
+ 'op' => 'delete',
+ 'path' => $path,
'newSha1' => ''
);
} else { // created/updated
$updateEntries[] = array(
- 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
- 'path' => $path,
+ 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
+ 'path' => $path,
'newSha1' => $nPredicates['sha1'][$path]
);
}
@@ -205,9 +236,11 @@ abstract class FileOp {
}
/**
- * Check preconditions of the operation without writing anything
+ * Check preconditions of the operation without writing anything.
+ * This must update $predicates for each path that the op can change
+ * except when a failing status object is returned.
*
- * @param $predicates Array
+ * @param Array $predicates
* @return Status
*/
final public function precheck( array &$predicates ) {
@@ -241,10 +274,14 @@ abstract class FileOp {
return Status::newFatal( 'fileop-fail-attempt-precheck' );
}
$this->state = self::STATE_ATTEMPTED;
- $status = $this->doAttempt();
- if ( !$status->isOK() ) {
- $this->failed = true;
- $this->logFailure( 'attempt' );
+ if ( $this->doOperation ) {
+ $status = $this->doAttempt();
+ if ( !$status->isOK() ) {
+ $this->failed = true;
+ $this->logFailure( 'attempt' );
+ }
+ } else { // no-op
+ $status = Status::newGood();
}
return $status;
}
@@ -280,7 +317,7 @@ abstract class FileOp {
/**
* Adjust params to FileBackendStore internal file calls
*
- * @param $params Array
+ * @param Array $params
* @return Array (required params list, optional params list)
*/
protected function setFlags( array $params ) {
@@ -292,15 +329,7 @@ abstract class FileOp {
*
* @return Array
*/
- final public function storagePathsRead() {
- return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() );
- }
-
- /**
- * @see FileOp::storagePathsRead()
- * @return Array
- */
- protected function doStoragePathsRead() {
+ public function storagePathsRead() {
return array();
}
@@ -309,24 +338,16 @@ abstract class FileOp {
*
* @return Array
*/
- final public function storagePathsChanged() {
- return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() );
- }
-
- /**
- * @see FileOp::storagePathsChanged()
- * @return Array
- */
- protected function doStoragePathsChanged() {
+ public function storagePathsChanged() {
return array();
}
/**
* Check for errors with regards to the destination file already existing.
- * This also updates the destSameAsSource and sourceSha1 member variables.
+ * Also set the destExists, overwriteSameCase and sourceSha1 member variables.
* A bad status will be returned if there is no chance it can be overwritten.
*
- * @param $predicates Array
+ * @param Array $predicates
* @return Status
*/
protected function precheckDestExistence( array $predicates ) {
@@ -336,8 +357,9 @@ abstract class FileOp {
if ( $this->sourceSha1 === null ) { // file in storage?
$this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
}
- $this->destSameAsSource = false;
- if ( $this->fileExists( $this->params['dst'], $predicates ) ) {
+ $this->overwriteSameCase = false;
+ $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
+ if ( $this->destExists ) {
if ( $this->getParam( 'overwrite' ) ) {
return $status; // OK
} elseif ( $this->getParam( 'overwriteSame' ) ) {
@@ -349,7 +371,7 @@ abstract class FileOp {
// Give an error if the files are not identical
$status->fatal( 'backend-fail-notsame', $this->params['dst'] );
} else {
- $this->destSameAsSource = true; // OK
+ $this->overwriteSameCase = true; // OK
}
return $status; // do nothing; either OK or bad status
} else {
@@ -362,7 +384,7 @@ abstract class FileOp {
/**
* precheckDestExistence() helper function to get the source file SHA-1.
- * Subclasses should overwride this iff the source is not in storage.
+ * Subclasses should overwride this if the source is not in storage.
*
* @return string|bool Returns false on failure
*/
@@ -373,15 +395,15 @@ abstract class FileOp {
/**
* Check if a file will exist in storage when this operation is attempted
*
- * @param $source string Storage path
- * @param $predicates Array
+ * @param string $source Storage path
+ * @param Array $predicates
* @return bool
*/
final protected function fileExists( $source, array $predicates ) {
if ( isset( $predicates['exists'][$source] ) ) {
return $predicates['exists'][$source]; // previous op assures this
} else {
- $params = array( 'src' => $source, 'latest' => $this->useLatest );
+ $params = array( 'src' => $source, 'latest' => true );
return $this->backend->fileExists( $params );
}
}
@@ -389,15 +411,17 @@ abstract class FileOp {
/**
* Get the SHA-1 of a file in storage when this operation is attempted
*
- * @param $source string Storage path
- * @param $predicates Array
+ * @param string $source Storage path
+ * @param Array $predicates
* @return string|bool False on failure
*/
final protected function fileSha1( $source, array $predicates ) {
if ( isset( $predicates['sha1'][$source] ) ) {
return $predicates['sha1'][$source]; // previous op assures this
+ } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
+ return false; // previous op assures this
} else {
- $params = array( 'src' => $source, 'latest' => $this->useLatest );
+ $params = array( 'src' => $source, 'latest' => true );
return $this->backend->getFileSha1Base36( $params );
}
}
@@ -414,7 +438,7 @@ abstract class FileOp {
/**
* Log a file operation failure and preserve any temp files
*
- * @param $action string
+ * @param string $action
* @return void
*/
final public function logFailure( $action ) {
@@ -430,42 +454,32 @@ abstract class FileOp {
}
/**
- * Store a file into the backend from a file on the file system.
+ * Create a file in the backend with the given content.
* Parameters for this operation are outlined in FileBackend::doOperations().
*/
-class StoreFileOp extends FileOp {
- /**
- * @return array
- */
+class CreateFileOp extends FileOp {
protected function allowedParams() {
- return array( array( 'src', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ return array( array( 'content', 'dst' ),
+ array( 'overwrite', 'overwriteSame', 'headers' ) );
}
- /**
- * @param $predicates array
- * @return Status
- */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
- // Check if the source file exists on the file system
- if ( !is_file( $this->params['src'] ) ) {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
- return $status;
- // Check if the source file is too big
- } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
+ // Check if the source data is too big
+ if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
$status->fatal( 'backend-fail-maxsize',
$this->params['dst'], $this->backend->maxFileSizeInternal() );
- $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
+ $status->fatal( 'backend-fail-create', $this->params['dst'] );
return $status;
- // Check if a file can be placed at the destination
+ // Check if a file can be placed/changed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
$status->fatal( 'backend-fail-usable', $this->params['dst'] );
- $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
+ $status->fatal( 'backend-fail-create', $this->params['dst'] );
return $status;
}
// Check if destination file exists
$status->merge( $this->precheckDestExistence( $predicates ) );
+ $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
if ( $status->isOK() ) {
// Update file existence predicates
$predicates['exists'][$this->params['dst']] = true;
@@ -474,61 +488,54 @@ class StoreFileOp extends FileOp {
return $status; // safe to call attempt()
}
- /**
- * @return Status
- */
protected function doAttempt() {
- // Store the file at the destination
- if ( !$this->destSameAsSource ) {
- return $this->backend->storeInternal( $this->setFlags( $this->params ) );
+ if ( !$this->overwriteSameCase ) {
+ // Create the file at the destination
+ return $this->backend->createInternal( $this->setFlags( $this->params ) );
}
return Status::newGood();
}
- /**
- * @return bool|string
- */
protected function getSourceSha1Base36() {
- wfSuppressWarnings();
- $hash = sha1_file( $this->params['src'] );
- wfRestoreWarnings();
- if ( $hash !== false ) {
- $hash = wfBaseConvert( $hash, 16, 36, 31 );
- }
- return $hash;
+ return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
}
- protected function doStoragePathsChanged() {
+ public function storagePathsChanged() {
return array( $this->params['dst'] );
}
}
/**
- * Create a file in the backend with the given content.
+ * Store a file into the backend from a file on the file system.
* Parameters for this operation are outlined in FileBackend::doOperations().
*/
-class CreateFileOp extends FileOp {
+class StoreFileOp extends FileOp {
protected function allowedParams() {
- return array( array( 'content', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ return array( array( 'src', 'dst' ),
+ array( 'overwrite', 'overwriteSame', 'headers' ) );
}
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
- // Check if the source data is too big
- if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
+ // Check if the source file exists on the file system
+ if ( !is_file( $this->params['src'] ) ) {
+ $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+ return $status;
+ // Check if the source file is too big
+ } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
$status->fatal( 'backend-fail-maxsize',
$this->params['dst'], $this->backend->maxFileSizeInternal() );
- $status->fatal( 'backend-fail-create', $this->params['dst'] );
+ $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
return $status;
- // Check if a file can be placed at the destination
+ // Check if a file can be placed/changed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
$status->fatal( 'backend-fail-usable', $this->params['dst'] );
- $status->fatal( 'backend-fail-create', $this->params['dst'] );
+ $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
return $status;
}
// Check if destination file exists
$status->merge( $this->precheckDestExistence( $predicates ) );
+ $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
if ( $status->isOK() ) {
// Update file existence predicates
$predicates['exists'][$this->params['dst']] = true;
@@ -537,28 +544,25 @@ class CreateFileOp extends FileOp {
return $status; // safe to call attempt()
}
- /**
- * @return Status
- */
protected function doAttempt() {
- if ( !$this->destSameAsSource ) {
- // Create the file at the destination
- return $this->backend->createInternal( $this->setFlags( $this->params ) );
+ if ( !$this->overwriteSameCase ) {
+ // Store the file at the destination
+ return $this->backend->storeInternal( $this->setFlags( $this->params ) );
}
return Status::newGood();
}
- /**
- * @return bool|String
- */
protected function getSourceSha1Base36() {
- return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
+ wfSuppressWarnings();
+ $hash = sha1_file( $this->params['src'] );
+ wfRestoreWarnings();
+ if ( $hash !== false ) {
+ $hash = wfBaseConvert( $hash, 16, 36, 31 );
+ }
+ return $hash;
}
- /**
- * @return array
- */
- protected function doStoragePathsChanged() {
+ public function storagePathsChanged() {
return array( $this->params['dst'] );
}
}
@@ -568,25 +572,26 @@ class CreateFileOp extends FileOp {
* Parameters for this operation are outlined in FileBackend::doOperations().
*/
class CopyFileOp extends FileOp {
- /**
- * @return array
- */
protected function allowedParams() {
return array( array( 'src', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ) );
}
- /**
- * @param $predicates array
- * @return Status
- */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
- return $status;
- // Check if a file can be placed at the destination
+ if ( $this->getParam( 'ignoreMissingSource' ) ) {
+ $this->doOperation = false; // no-op
+ // Update file existence predicates (cache 404s)
+ $predicates['exists'][$this->params['src']] = false;
+ $predicates['sha1'][$this->params['src']] = false;
+ return $status; // nothing to do
+ } else {
+ $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+ return $status;
+ }
+ // Check if a file can be placed/changed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
$status->fatal( 'backend-fail-usable', $this->params['dst'] );
$status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
@@ -594,6 +599,7 @@ class CopyFileOp extends FileOp {
}
// Check if destination file exists
$status->merge( $this->precheckDestExistence( $predicates ) );
+ $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
if ( $status->isOK() ) {
// Update file existence predicates
$predicates['exists'][$this->params['dst']] = true;
@@ -602,31 +608,27 @@ class CopyFileOp extends FileOp {
return $status; // safe to call attempt()
}
- /**
- * @return Status
- */
protected function doAttempt() {
- // Do nothing if the src/dst paths are the same
- if ( $this->params['src'] !== $this->params['dst'] ) {
- // Copy the file into the destination
- if ( !$this->destSameAsSource ) {
- return $this->backend->copyInternal( $this->setFlags( $this->params ) );
- }
+ if ( $this->overwriteSameCase ) {
+ $status = Status::newGood(); // nothing to do
+ } elseif ( $this->params['src'] === $this->params['dst'] ) {
+ // Just update the destination file headers
+ $headers = $this->getParam( 'headers' ) ?: array();
+ $status = $this->backend->describeInternal( $this->setFlags( array(
+ 'src' => $this->params['dst'], 'headers' => $headers
+ ) ) );
+ } else {
+ // Copy the file to the destination
+ $status = $this->backend->copyInternal( $this->setFlags( $this->params ) );
}
- return Status::newGood();
+ return $status;
}
- /**
- * @return array
- */
- protected function doStoragePathsRead() {
+ public function storagePathsRead() {
return array( $this->params['src'] );
}
- /**
- * @return array
- */
- protected function doStoragePathsChanged() {
+ public function storagePathsChanged() {
return array( $this->params['dst'] );
}
}
@@ -636,25 +638,26 @@ class CopyFileOp extends FileOp {
* Parameters for this operation are outlined in FileBackend::doOperations().
*/
class MoveFileOp extends FileOp {
- /**
- * @return array
- */
protected function allowedParams() {
return array( array( 'src', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ) );
}
- /**
- * @param $predicates array
- * @return Status
- */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
- return $status;
- // Check if a file can be placed at the destination
+ if ( $this->getParam( 'ignoreMissingSource' ) ) {
+ $this->doOperation = false; // no-op
+ // Update file existence predicates (cache 404s)
+ $predicates['exists'][$this->params['src']] = false;
+ $predicates['sha1'][$this->params['src']] = false;
+ return $status; // nothing to do
+ } else {
+ $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+ return $status;
+ }
+ // Check if a file can be placed/changed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
$status->fatal( 'backend-fail-usable', $this->params['dst'] );
$status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
@@ -662,6 +665,7 @@ class MoveFileOp extends FileOp {
}
// Check if destination file exists
$status->merge( $this->precheckDestExistence( $predicates ) );
+ $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
if ( $status->isOK() ) {
// Update file existence predicates
$predicates['exists'][$this->params['src']] = false;
@@ -672,35 +676,35 @@ class MoveFileOp extends FileOp {
return $status; // safe to call attempt()
}
- /**
- * @return Status
- */
protected function doAttempt() {
- // Do nothing if the src/dst paths are the same
- if ( $this->params['src'] !== $this->params['dst'] ) {
- if ( !$this->destSameAsSource ) {
- // Move the file into the destination
- return $this->backend->moveInternal( $this->setFlags( $this->params ) );
+ if ( $this->overwriteSameCase ) {
+ if ( $this->params['src'] === $this->params['dst'] ) {
+ // Do nothing to the destination (which is also the source)
+ $status = Status::newGood();
} else {
- // Just delete source as the destination needs no changes
- $params = array( 'src' => $this->params['src'] );
- return $this->backend->deleteInternal( $this->setFlags( $params ) );
+ // Just delete the source as the destination file needs no changes
+ $status = $this->backend->deleteInternal( $this->setFlags(
+ array( 'src' => $this->params['src'] )
+ ) );
}
+ } elseif ( $this->params['src'] === $this->params['dst'] ) {
+ // Just update the destination file headers
+ $headers = $this->getParam( 'headers' ) ?: array();
+ $status = $this->backend->describeInternal( $this->setFlags(
+ array( 'src' => $this->params['dst'], 'headers' => $headers )
+ ) );
+ } else {
+ // Move the file to the destination
+ $status = $this->backend->moveInternal( $this->setFlags( $this->params ) );
}
- return Status::newGood();
+ return $status;
}
- /**
- * @return array
- */
- protected function doStoragePathsRead() {
+ public function storagePathsRead() {
return array( $this->params['src'] );
}
- /**
- * @return array
- */
- protected function doStoragePathsChanged() {
+ public function storagePathsChanged() {
return array( $this->params['src'], $this->params['dst'] );
}
}
@@ -710,28 +714,29 @@ class MoveFileOp extends FileOp {
* Parameters for this operation are outlined in FileBackend::doOperations().
*/
class DeleteFileOp extends FileOp {
- /**
- * @return array
- */
protected function allowedParams() {
return array( array( 'src' ), array( 'ignoreMissingSource' ) );
}
- protected $needsDelete = true;
-
- /**
- * @param array $predicates
- * @return Status
- */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- if ( !$this->getParam( 'ignoreMissingSource' ) ) {
+ if ( $this->getParam( 'ignoreMissingSource' ) ) {
+ $this->doOperation = false; // no-op
+ // Update file existence predicates (cache 404s)
+ $predicates['exists'][$this->params['src']] = false;
+ $predicates['sha1'][$this->params['src']] = false;
+ return $status; // nothing to do
+ } else {
$status->fatal( 'backend-fail-notexists', $this->params['src'] );
return $status;
}
- $this->needsDelete = false;
+ // Check if a file can be placed/changed at the source
+ } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
+ $status->fatal( 'backend-fail-usable', $this->params['src'] );
+ $status->fatal( 'backend-fail-delete', $this->params['src'] );
+ return $status;
}
// Update file existence predicates
$predicates['exists'][$this->params['src']] = false;
@@ -739,21 +744,51 @@ class DeleteFileOp extends FileOp {
return $status; // safe to call attempt()
}
- /**
- * @return Status
- */
protected function doAttempt() {
- if ( $this->needsDelete ) {
- // Delete the source file
- return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
+ // Delete the source file
+ return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
+ }
+
+ public function storagePathsChanged() {
+ return array( $this->params['src'] );
+ }
+}
+
+/**
+ * Change metadata for a file at the given storage path in the backend.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class DescribeFileOp extends FileOp {
+ protected function allowedParams() {
+ return array( array( 'src' ), array( 'headers' ) );
+ }
+
+ protected function doPrecheck( array &$predicates ) {
+ $status = Status::newGood();
+ // Check if the source file exists
+ if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+ $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+ return $status;
+ // Check if a file can be placed/changed at the source
+ } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
+ $status->fatal( 'backend-fail-usable', $this->params['src'] );
+ $status->fatal( 'backend-fail-describe', $this->params['src'] );
+ return $status;
}
- return Status::newGood();
+ // Update file existence predicates
+ $predicates['exists'][$this->params['src']] =
+ $this->fileExists( $this->params['src'], $predicates );
+ $predicates['sha1'][$this->params['src']] =
+ $this->fileSha1( $this->params['src'], $predicates );
+ return $status; // safe to call attempt()
}
- /**
- * @return array
- */
- protected function doStoragePathsChanged() {
+ protected function doAttempt() {
+ // Update the source file's metadata
+ return $this->backend->describeInternal( $this->setFlags( $this->params ) );
+ }
+
+ public function storagePathsChanged() {
return array( $this->params['src'] );
}
}
diff --git a/includes/filebackend/FileOpBatch.php b/includes/filebackend/FileOpBatch.php
index 33558725..785c0bc9 100644
--- a/includes/filebackend/FileOpBatch.php
+++ b/includes/filebackend/FileOpBatch.php
@@ -42,9 +42,6 @@ class FileOpBatch {
* $opts is an array of options, including:
* - force : Errors that would normally cause a rollback do not.
* The remaining operations are still attempted if any fail.
- * - allowStale : Don't require the latest available data.
- * This can increase performance for non-critical writes.
- * This has no effect unless the 'force' flag is set.
* - nonJournaled : Don't log this operation batch in the file journal.
* - concurrency : Try to do this many operations in parallel when possible.
*
@@ -52,9 +49,9 @@ class FileOpBatch {
* - a) unexpected operation errors occurred (network partitions, disk full...)
* - b) significant operation errors occurred and 'force' was not set
*
- * @param $performOps Array List of FileOp operations
- * @param $opts Array Batch operation options
- * @param $journal FileJournal Journal to log operations to
+ * @param array $performOps List of FileOp operations
+ * @param array $opts Batch operation options
+ * @param FileJournal $journal Journal to log operations to
* @return Status
*/
public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
@@ -69,7 +66,6 @@ class FileOpBatch {
}
$batchId = $journal->getTimestampedUUID();
- $allowStale = !empty( $opts['allowStale'] );
$ignoreErrors = !empty( $opts['force'] );
$journaled = empty( $opts['nonJournaled'] );
$maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1;
@@ -84,7 +80,6 @@ class FileOpBatch {
foreach ( $performOps as $index => $fileOp ) {
$backendName = $fileOp->getBackend()->getName();
$fileOp->setBatchId( $batchId ); // transaction ID
- $fileOp->allowStaleReads( $allowStale ); // consistency level
// Decide if this op can be done concurrently within this sub-batch
// or if a new concurrent sub-batch must be started after this one...
if ( $fileOp->dependsOn( $curBatchDeps )
@@ -136,49 +131,13 @@ class FileOpBatch {
}
// Attempt each operation (in parallel if allowed and possible)...
- if ( count( $pPerformOps ) < count( $performOps ) ) {
- self::runBatchParallel( $pPerformOps, $status );
- } else {
- self::runBatchSeries( $performOps, $status );
- }
+ self::runParallelBatches( $pPerformOps, $status );
wfProfileOut( __METHOD__ );
return $status;
}
/**
- * Attempt a list of file operations in series.
- * This will abort remaining ops on failure.
- *
- * @param $performOps Array
- * @param $status Status
- * @return bool Success
- */
- protected static function runBatchSeries( array $performOps, Status $status ) {
- foreach ( $performOps as $index => $fileOp ) {
- if ( $fileOp->failed() ) {
- continue; // nothing to do
- }
- $subStatus = $fileOp->attempt();
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- $status->success[$index] = true;
- ++$status->successCount;
- } else {
- $status->success[$index] = false;
- ++$status->failCount;
- // We can't continue (even with $ignoreErrors) as $predicates is wrong.
- // Log the remaining ops as failed for recovery...
- for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
- $performOps[$i]->logFailure( 'attempt_aborted' );
- }
- return false; // bail out
- }
- }
- return true;
- }
-
- /**
* Attempt a list of file operations sub-batches in series.
*
* The operations *in* each sub-batch will be done in parallel.
@@ -186,12 +145,12 @@ class FileOpBatch {
* within any given sub-batch do not depend on each other.
* This will abort remaining ops on failure.
*
- * @param $pPerformOps Array
- * @param $status Status
+ * @param Array $pPerformOps
+ * @param Status $status
* @return bool Success
*/
- protected static function runBatchParallel( array $pPerformOps, Status $status ) {
- $aborted = false;
+ protected static function runParallelBatches( array $pPerformOps, Status $status ) {
+ $aborted = false; // set to true on unexpected errors
foreach ( $pPerformOps as $performOpsBatch ) {
if ( $aborted ) { // check batch op abort flag...
// We can't continue (even with $ignoreErrors) as $predicates is wrong.
@@ -205,11 +164,16 @@ class FileOpBatch {
$opHandles = array();
// Get the backend; all sub-batch ops belong to a single backend
$backend = reset( $performOpsBatch )->getBackend();
- // If attemptAsync() returns synchronously, it was either an
- // error Status or the backend just doesn't support async ops.
+ // Get the operation handles or actually do it if there is just one.
+ // If attemptAsync() returns a Status, it was either due to an error
+ // or the backend does not support async ops and did it synchronously.
foreach ( $performOpsBatch as $i => $fileOp ) {
if ( !$fileOp->failed() ) { // failed => already has Status
- $subStatus = $fileOp->attemptAsync();
+ // If the batch is just one operation, it's faster to avoid
+ // pipelining as that can involve creating new TCP connections.
+ $subStatus = ( count( $performOpsBatch ) > 1 )
+ ? $fileOp->attemptAsync()
+ : $fileOp->attempt();
if ( $subStatus->value instanceof FileBackendStoreOpHandle ) {
$opHandles[$i] = $subStatus->value; // deferred
} else {
diff --git a/includes/filebackend/README b/includes/filebackend/README
new file mode 100644
index 00000000..569f3376
--- /dev/null
+++ b/includes/filebackend/README
@@ -0,0 +1,208 @@
+/*!
+\ingroup FileBackend
+\page file_backend_design File backend design
+
+Some notes on the FileBackend architecture.
+
+\section intro Introduction
+
+To abstract away the differences among different types of storage media,
+MediaWiki is providing an interface known as FileBackend. Any MediaWiki
+interaction with stored files should thus use a FileBackend object.
+
+Different types of backing storage media are supported (ranging from local
+file system to distributed object stores). The types include:
+
+* FSFileBackend (used for mounted file systems)
+* SwiftFileBackend (used for Swift or Ceph Rados+RGW object stores)
+* FileBackendMultiWrite (useful for transitioning from one backend to another)
+
+Configuration documentation for each type of backend is to be found in their
+__construct() inline documentation.
+
+
+\section setup Setup
+
+File backends are registered in LocalSettings.php via the global variable
+$wgFileBackends. To access one of those defined backends, one would use
+FileBackendStore::get( <name> ) which will bring back a FileBackend object
+handle. Such handles are reused for any subsequent get() call (via singleton).
+The FileBackends objects are caching request calls such as file stats,
+SHA1 requests or TCP connection handles.
+
+\par Note:
+Some backends may require additional PHP extensions to be enabled or can rely on a
+MediaWiki extension. This is often the case when a FileBackend subclass makes use of an
+upstream client API for communicating with the backing store.
+
+
+\section fileoperations File operations
+
+The MediaWiki FileBackend API supports various operations on either files or
+directories. See FileBackend.php for full documentation for each function.
+
+
+\subsection reading Reading
+
+The following basic operations are supported for reading from a backend:
+
+On files:
+* stat a file for basic information (timestamp, size)
+* read a file into a string or several files into a map of path names to strings
+* download a file or set of files to a temporary file (on a mounted file system)
+* get the SHA1 hash of a file
+* get various properties of a file (stat information, content time, mime information, ...)
+
+On directories:
+* get a list of files directly under a directory
+* get a recursive list of files under a directory
+* get a list of directories directly under a directory
+* get a recursive list of directories under a directory
+
+\par Note:
+Backend handles should return directory listings as iterators, all though in some cases
+they may just be simple arrays (which can still be iterated over). Iterators allow for
+callers to traverse a large number of file listings without consuming excessive RAM in
+the process. Either the memory consumed is flatly bounded (if the iterator does paging)
+or it is proportional to the depth of the portion of the directory tree being traversed
+(if the iterator works via recursion).
+
+
+\subsection writing Writing
+
+The following basic operations are supported for writing or changing in the backend:
+
+On files:
+* store (copying a mounted file system file into storage)
+* create (creating a file within storage from a string)
+* copy (within storage)
+* move (within storage)
+* delete (within storage)
+* lock/unlock (lock or unlock a file in storage)
+
+The following operations are supported for writing directories in the backend:
+* prepare (create parent container and directories for a path)
+* secure (try to lock-down access to a container)
+* publish (try to reverse the effects of secure)
+* clean (remove empty containers or directories)
+
+
+\subsection invokingoperation Invoking an operation
+
+Generally, callers should use doOperations() or doQuickOperations() when doing
+batches of changes, rather than making a suite of single operation calls. This
+makes the system tolerate high latency much better by pipelining operations
+when possible.
+
+doOperations() should be used for working on important original data, i.e. when
+consistency is important. The former will only pipeline operations that do not
+depend on each other. It is best if the operations that do not depend on each
+other occur in consecutive groups. This function can also log file changes to
+a journal (see FileJournal), which can be used to sync two backend instances.
+One might use this function for user uploads of file for example.
+
+doQuickOperations() is more geared toward ephemeral items that can be easily
+regenerated from original data. It will always pipeline without checking for
+dependencies within the operation batch. One might use this function for
+creating and purging generated thumbnails of original files for example.
+
+
+\section consistency Consistency
+
+Not all backing stores are sequentially consistent by default. Various FileBackend
+functions offer a "latest" option that can be passed in to assure (or try to assure)
+that the latest version of the file is read. Some backing stores are consistent by
+default, but callers should always assume that without this option, stale data may
+be read. This is actually true for stores that have eventual consistency.
+
+Note that file listing functions have no "latest" flag, and thus some systems may
+return stale data. Thus callers should avoid assuming that listings contain changes
+made my the current client or any other client from a very short time ago. For example,
+creating a file under a directory and then immediately doing a file listing operation
+on that directory may result in a listing that does not include that file.
+
+
+\section locking Locking
+
+Locking is effective if and only if a proper lock manager is registered and is
+actually being used by the backend. Lock managers can be registered in LocalSettings.php
+using the $wgLockManagers global configuration variable.
+
+For object stores, locking is not generally useful for avoiding partially
+written or read objects, since most stores use Multi Version Concurrency
+Control (MVCC) to avoid this. However, locking can be important when:
+* One or more operations must be done without objects changing in the meantime.
+* It can also be useful when a file read is used to determine a file write or DB change.
+ For example, doOperations() first checks that there will be no "file already exists"
+ or "file does not exist" type errors before attempting an operation batch. This works
+ by stating the files first, and is only safe if the files are locked in the meantime.
+
+When locking, callers should use the latest available file data for reads.
+Also, one should always lock the file *before* reading it, not after. If stale data is
+used to determine a write, there will be some data corruption, even when reads of the
+original file finally start returning the updated data without needing the "latest"
+option (eventual consistency). The "scoped" lock functions are preferable since
+there is not the problem of forgetting to unlock due to early returns or exceptions.
+
+Since acquiring locks can fail, and lock managers can be non-blocking, callers should:
+* Acquire all required locks up font
+* Be prepared for the case where locks fail to be acquired
+* Possible retry acquiring certain locks
+
+MVCC is also a useful pattern to use on top of the backend interface, because operations
+are not atomic, even with doOperations(), so doing complex batch file changes or changing
+files and updating a database row can result in partially written "transactions". Thus one
+should avoid changing files once they have been stored, except perhaps with ephemeral data
+that are tolerant of some degree of inconsistency.
+
+Callers can use their own locking (e.g. SELECT FOR UPDATE) if it is more convenient, but
+note that all callers that change any of the files should then go through functions that
+acquire these locks. For example, if a caller just directly uses the file backend store()
+function, it will ignore any custom "FOR UPDATE" locks, which can cause problems.
+
+\section objectstore Object stores
+
+Support for object stores (like Amazon S3/Swift) drive much of the API and design
+decisions of FileBackend, but using any POSIX compliant file systems works fine.
+The system essentially stores "files" in "containers". For a mounted file system
+as a backing store, "files" will just be files under directories. For an object store
+as a backing store, the "files" will be objects stored in actual containers.
+
+
+\section file_obj_diffs File system and Object store differences
+
+An advantage of object stores is the reduced Round-Trip Times. This is
+achieved by avoiding the need to create each parent directory before placing a
+file somewhere. It gets worse the deeper the directory hierarchy is. Another
+advantage of object stores is that object listings tend to use databases, which
+scale better than the linked list directories that file sytems sometimes use.
+File systems like btrfs and xfs use tree structures, which scale better.
+For both object stores and file systems, using "/" in filenames will allow for the
+intuitive use of directory functions. For example, creating a file in Swift
+called "container/a/b/file1" will mean that:
+- a "directory listing" of "container/a" will contain "b",
+- and a "file listing" of "b" will contain "file1"
+
+This means that switching from an object store to a file system and vise versa
+using the FileBackend interface will generally be harmless. However, one must be
+aware of some important differences:
+
+* In a file system, you cannot have a file and a directory within the same path
+ whereas it is possible in an object stores. Calling code should avoid any layouts
+ which allow files and directories at the same path.
+* Some file systems have file name length restrictions or overall path length
+ restrictions that others do not. The same goes with object stores which might
+ have a maximum object length or a limitation regarding the number of files
+ under a container or volume.
+* Latency varies among systems, certain access patterns may not be tolerable for
+ certain backends but may hold up for others. Some backend subclasses use
+ MediaWiki's object caching for serving stat requests, which can greatly
+ reduce latency. Making sure that the backend has pipelining (see the
+ "parallelize" and "concurrency" settings) enabled can also mask latency in
+ batch operation scenarios.
+* File systems may implement directories as linked-lists or other structures
+ with poor scalability, so calling code should use layouts that shard the data.
+ Instead of storing files like "container/file.txt", one can store files like
+ "container/<x>/<y>/file.txt". It is best if "sharding" optional or configurable.
+
+*/
diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php
index b6f0aa60..db090a98 100644
--- a/includes/filebackend/SwiftFileBackend.php
+++ b/includes/filebackend/SwiftFileBackend.php
@@ -24,7 +24,7 @@
*/
/**
- * @brief Class for an OpenStack Swift based file backend.
+ * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
*
* This requires the SwiftCloudFiles MediaWiki extension, which includes
* the php-cloudfiles library (https://github.com/rackspace/php-cloudfiles).
@@ -40,11 +40,16 @@ class SwiftFileBackend extends FileBackendStore {
/** @var CF_Authentication */
protected $auth; // Swift authentication handler
protected $authTTL; // integer seconds
+ protected $swiftTempUrlKey; // string; shared secret value for making temp urls
protected $swiftAnonUser; // string; username to handle unauthenticated requests
protected $swiftUseCDN; // boolean; whether CloudFiles CDN is enabled
protected $swiftCDNExpiry; // integer; how long to cache things in the CDN
protected $swiftCDNPurgable; // boolean; whether object CDN purging is enabled
+ // Rados Gateway specific options
+ protected $rgwS3AccessKey; // string; S3 access key
+ protected $rgwS3SecretKey; // string; S3 authentication key
+
/** @var CF_Connection */
protected $conn; // Swift connection handle
protected $sessionStarted = 0; // integer UNIX timestamp
@@ -66,6 +71,8 @@ class SwiftFileBackend extends FileBackendStore {
* - swiftUser : Swift user used by MediaWiki (account:username)
* - swiftKey : Swift authentication key for the above user
* - swiftAuthTTL : Swift authentication TTL (seconds)
+ * - swiftTempUrlKey : Swift "X-Account-Meta-Temp-URL-Key" value on the account.
+ * Do not set this until it has been set in the backend.
* - swiftAnonUser : Swift user used for end-user requests (account:username).
* If set, then views of public containers are assumed to go
* through this user. If not set, then public containers are
@@ -84,10 +91,20 @@ class SwiftFileBackend extends FileBackendStore {
* - cacheAuthInfo : Whether to cache authentication tokens in APC, XCache, ect.
* If those are not available, then the main cache will be used.
* This is probably insecure in shared hosting environments.
+ * - rgwS3AccessKey : Ragos Gateway S3 "access key" value on the account.
+ * Do not set this until it has been set in the backend.
+ * This is used for generating expiring pre-authenticated URLs.
+ * Only use this when using rgw and to work around
+ * http://tracker.newdream.net/issues/3454.
+ * - rgwS3SecretKey : Ragos Gateway S3 "secret key" value on the account.
+ * Do not set this until it has been set in the backend.
+ * This is used for generating expiring pre-authenticated URLs.
+ * Only use this when using rgw and to work around
+ * http://tracker.newdream.net/issues/3454.
*/
public function __construct( array $config ) {
parent::__construct( $config );
- if ( !MWInit::classExists( 'CF_Constants' ) ) {
+ if ( !class_exists( 'CF_Constants' ) ) {
throw new MWException( 'SwiftCloudFiles extension not installed.' );
}
// Required settings
@@ -104,6 +121,9 @@ class SwiftFileBackend extends FileBackendStore {
$this->swiftAnonUser = isset( $config['swiftAnonUser'] )
? $config['swiftAnonUser']
: '';
+ $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
+ ? $config['swiftTempUrlKey']
+ : '';
$this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
? $config['shardViaHashLevels']
: '';
@@ -112,17 +132,23 @@ class SwiftFileBackend extends FileBackendStore {
: false;
$this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] )
? $config['swiftCDNExpiry']
- : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org)
+ : 12 * 3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org)
$this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] )
? $config['swiftCDNPurgable']
: true;
+ $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
+ ? $config['rgwS3AccessKey']
+ : '';
+ $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
+ ? $config['rgwS3SecretKey']
+ : '';
// Cache container information to mask latency
$this->memCache = wfGetMainCache();
// Process cache for container info
$this->connContainerCache = new ProcessCacheLRU( 300 );
// Cache auth token information to avoid RTTs
if ( !empty( $config['cacheAuthInfo'] ) ) {
- if ( php_sapi_name() === 'cli' ) {
+ if ( PHP_SAPI === 'cli' ) {
$this->srvCache = wfGetMainCache(); // preferrably memcached
} else {
try { // look for APC, XCache, WinCache, ect...
@@ -146,10 +172,6 @@ class SwiftFileBackend extends FileBackendStore {
return $relStoragePath;
}
- /**
- * @see FileBackendStore::isPathUsableInternal()
- * @return bool
- */
public function isPathUsableInternal( $storagePath ) {
list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
if ( $rel === null ) {
@@ -168,14 +190,26 @@ class SwiftFileBackend extends FileBackendStore {
}
/**
- * @param $disposition string Content-Disposition header value
+ * @param array $headers
+ * @return array
+ */
+ protected function sanitizeHdrs( array $headers ) {
+ // By default, Swift has annoyingly low maximum header value limits
+ if ( isset( $headers['Content-Disposition'] ) ) {
+ $headers['Content-Disposition'] = $this->truncDisp( $headers['Content-Disposition'] );
+ }
+ return $headers;
+ }
+
+ /**
+ * @param string $disposition Content-Disposition header value
* @return string Truncated Content-Disposition header value to meet Swift limits
*/
protected function truncDisp( $disposition ) {
$res = '';
foreach ( explode( ';', $disposition ) as $part ) {
$part = trim( $part );
- $new = ( $res === '' ) ? $part : "{$res};{$part}";
+ $new = ( $res === '' ) ? $part : "{$res};{$part}";
if ( strlen( $new ) <= 255 ) {
$res = $new;
} else {
@@ -185,10 +219,6 @@ class SwiftFileBackend extends FileBackendStore {
return $res;
}
- /**
- * @see FileBackendStore::doCreateInternal()
- * @return Status
- */
protected function doCreateInternal( array $params ) {
$status = Status::newGood();
@@ -201,12 +231,6 @@ class SwiftFileBackend extends FileBackendStore {
// (a) Check the destination container and object
try {
$dContObj = $this->getContainer( $dstCont );
- if ( empty( $params['overwrite'] ) &&
- $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
- {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
- }
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-create', $params['dst'] );
return $status;
@@ -223,31 +247,23 @@ class SwiftFileBackend extends FileBackendStore {
// Create a fresh CF_Object with no fields preloaded.
// We don't want to preserve headers, metadata, and such.
$obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
- // Note: metadata keys stored as [Upper case char][[Lower case char]...]
- $obj->metadata = array( 'Sha1base36' => $sha1Hash );
+ $obj->setMetadataValues( array( 'Sha1base36' => $sha1Hash ) );
// Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59).
// The MD5 here will be checked within Swift against its own MD5.
$obj->set_etag( md5( $params['content'] ) );
// Use the same content type as StreamFile for security
- $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
- if ( !strlen( $obj->content_type ) ) { // special case
- $obj->content_type = 'unknown/unknown';
- }
- // Set the Content-Disposition header if requested
- if ( isset( $params['disposition'] ) ) {
- $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
+ $obj->content_type = $this->getContentType( $params['dst'], $params['content'], null );
+ // Set any other custom headers if requested
+ if ( isset( $params['headers'] ) ) {
+ $obj->headers += $this->sanitizeHdrs( $params['headers'] );
}
if ( !empty( $params['async'] ) ) { // deferred
$op = $obj->write_async( $params['content'] );
$status->value = new SwiftFileOpHandle( $this, $params, 'Create', $op );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $status->value->affectedObjects[] = $obj;
- }
+ $status->value->affectedObjects[] = $obj;
} else { // actually write the object in Swift
$obj->write( $params['content'] );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $this->purgeCDNCache( array( $obj ) );
- }
+ $this->purgeCDNCache( array( $obj ) );
}
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
@@ -271,10 +287,6 @@ class SwiftFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doStoreInternal()
- * @return Status
- */
protected function doStoreInternal( array $params ) {
$status = Status::newGood();
@@ -287,12 +299,6 @@ class SwiftFileBackend extends FileBackendStore {
// (a) Check the destination container and object
try {
$dContObj = $this->getContainer( $dstCont );
- if ( empty( $params['overwrite'] ) &&
- $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
- {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
- }
} catch ( NoSuchContainerException $e ) {
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
return $status;
@@ -302,7 +308,9 @@ class SwiftFileBackend extends FileBackendStore {
}
// (b) Get a SHA-1 hash of the object
+ wfSuppressWarnings();
$sha1Hash = sha1_file( $params['src'] );
+ wfRestoreWarnings();
if ( $sha1Hash === false ) { // source doesn't exist?
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
return $status;
@@ -314,18 +322,14 @@ class SwiftFileBackend extends FileBackendStore {
// Create a fresh CF_Object with no fields preloaded.
// We don't want to preserve headers, metadata, and such.
$obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
- // Note: metadata keys stored as [Upper case char][[Lower case char]...]
- $obj->metadata = array( 'Sha1base36' => $sha1Hash );
+ $obj->setMetadataValues( array( 'Sha1base36' => $sha1Hash ) );
// The MD5 here will be checked within Swift against its own MD5.
$obj->set_etag( md5_file( $params['src'] ) );
// Use the same content type as StreamFile for security
- $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
- if ( !strlen( $obj->content_type ) ) { // special case
- $obj->content_type = 'unknown/unknown';
- }
- // Set the Content-Disposition header if requested
- if ( isset( $params['disposition'] ) ) {
- $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
+ $obj->content_type = $this->getContentType( $params['dst'], null, $params['src'] );
+ // Set any other custom headers if requested
+ if ( isset( $params['headers'] ) ) {
+ $obj->headers += $this->sanitizeHdrs( $params['headers'] );
}
if ( !empty( $params['async'] ) ) { // deferred
wfSuppressWarnings();
@@ -337,15 +341,11 @@ class SwiftFileBackend extends FileBackendStore {
$op = $obj->write_async( $fp, filesize( $params['src'] ), true );
$status->value = new SwiftFileOpHandle( $this, $params, 'Store', $op );
$status->value->resourcesToClose[] = $fp;
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $status->value->affectedObjects[] = $obj;
- }
+ $status->value->affectedObjects[] = $obj;
}
} else { // actually write the object in Swift
$obj->load_from_filename( $params['src'], true ); // calls $obj->write()
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $this->purgeCDNCache( array( $obj ) );
- }
+ $this->purgeCDNCache( array( $obj ) );
}
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
@@ -373,10 +373,6 @@ class SwiftFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doCopyInternal()
- * @return Status
- */
protected function doCopyInternal( array $params ) {
$status = Status::newGood();
@@ -396,14 +392,10 @@ class SwiftFileBackend extends FileBackendStore {
try {
$sContObj = $this->getContainer( $srcCont );
$dContObj = $this->getContainer( $dstCont );
- if ( empty( $params['overwrite'] ) &&
- $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
- {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
- }
} catch ( NoSuchContainerException $e ) {
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ if ( empty( $params['ignoreMissingSource'] ) || isset( $sContObj ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ }
return $status;
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, $status, __METHOD__, $params );
@@ -414,25 +406,24 @@ class SwiftFileBackend extends FileBackendStore {
try {
$dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
$hdrs = array(); // source file headers to override with new values
- if ( isset( $params['disposition'] ) ) {
- $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
+ // Set any other custom headers if requested
+ if ( isset( $params['headers'] ) ) {
+ $hdrs += $this->sanitizeHdrs( $params['headers'] );
}
if ( !empty( $params['async'] ) ) { // deferred
$op = $sContObj->copy_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs );
$status->value = new SwiftFileOpHandle( $this, $params, 'Copy', $op );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $status->value->affectedObjects[] = $dstObj;
- }
+ $status->value->affectedObjects[] = $dstObj;
} else { // actually write the object in Swift
$sContObj->copy_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $this->purgeCDNCache( array( $dstObj ) );
- }
+ $this->purgeCDNCache( array( $dstObj ) );
}
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
} catch ( NoSuchObjectException $e ) { // source object does not exist
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ }
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, $status, __METHOD__, $params );
}
@@ -451,10 +442,6 @@ class SwiftFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doMoveInternal()
- * @return Status
- */
protected function doMoveInternal( array $params ) {
$status = Status::newGood();
@@ -474,14 +461,10 @@ class SwiftFileBackend extends FileBackendStore {
try {
$sContObj = $this->getContainer( $srcCont );
$dContObj = $this->getContainer( $dstCont );
- if ( empty( $params['overwrite'] ) &&
- $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
- {
- $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
- return $status;
- }
} catch ( NoSuchContainerException $e ) {
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ if ( empty( $params['ignoreMissingSource'] ) || isset( $sContObj ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ }
return $status;
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, $status, __METHOD__, $params );
@@ -493,27 +476,26 @@ class SwiftFileBackend extends FileBackendStore {
$srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
$dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
$hdrs = array(); // source file headers to override with new values
- if ( isset( $params['disposition'] ) ) {
- $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
+ // Set any other custom headers if requested
+ if ( isset( $params['headers'] ) ) {
+ $hdrs += $this->sanitizeHdrs( $params['headers'] );
}
if ( !empty( $params['async'] ) ) { // deferred
$op = $sContObj->move_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs );
$status->value = new SwiftFileOpHandle( $this, $params, 'Move', $op );
$status->value->affectedObjects[] = $srcObj;
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $status->value->affectedObjects[] = $dstObj;
- }
+ $status->value->affectedObjects[] = $dstObj;
} else { // actually write the object in Swift
$sContObj->move_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs );
$this->purgeCDNCache( array( $srcObj ) );
- if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
- $this->purgeCDNCache( array( $dstObj ) );
- }
+ $this->purgeCDNCache( array( $dstObj ) );
}
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
} catch ( NoSuchObjectException $e ) { // source object does not exist
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ }
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, $status, __METHOD__, $params );
}
@@ -532,10 +514,6 @@ class SwiftFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doDeleteInternal()
- * @return Status
- */
protected function doDeleteInternal( array $params ) {
$status = Status::newGood();
@@ -559,7 +537,9 @@ class SwiftFileBackend extends FileBackendStore {
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
} catch ( NoSuchContainerException $e ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ }
} catch ( NoSuchObjectException $e ) {
if ( empty( $params['ignoreMissingSource'] ) ) {
$status->fatal( 'backend-fail-delete', $params['src'] );
@@ -586,16 +566,45 @@ class SwiftFileBackend extends FileBackendStore {
}
}
- /**
- * @see FileBackendStore::doPrepareInternal()
- * @return Status
- */
+ protected function doDescribeInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ try {
+ $sContObj = $this->getContainer( $srcCont );
+ // Get the latest version of the current metadata
+ $srcObj = $sContObj->get_object( $srcRel,
+ $this->headersFromParams( array( 'latest' => true ) ) );
+ // Merge in the metadata updates...
+ if ( isset( $params['headers'] ) ) {
+ $srcObj->headers = $this->sanitizeHdrs( $params['headers'] ) + $srcObj->headers;
+ }
+ $srcObj->sync_metadata(); // save to Swift
+ $this->purgeCDNCache( array( $srcObj ) );
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( NoSuchContainerException $e ) {
+ $status->fatal( 'backend-fail-describe', $params['src'] );
+ } catch ( NoSuchObjectException $e ) {
+ $status->fatal( 'backend-fail-describe', $params['src'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
protected function doPrepareInternal( $fullCont, $dir, array $params ) {
$status = Status::newGood();
// (a) Check if container already exists
try {
- $contObj = $this->getContainer( $fullCont );
+ $this->getContainer( $fullCont );
// NoSuchContainerException not thrown: container must exist
return $status; // already exists
} catch ( NoSuchContainerException $e ) {
@@ -703,10 +712,6 @@ class SwiftFileBackend extends FileBackendStore {
return $status;
}
- /**
- * @see FileBackendStore::doCleanInternal()
- * @return Status
- */
protected function doCleanInternal( $fullCont, $dir, array $params ) {
$status = Status::newGood();
@@ -742,10 +747,6 @@ class SwiftFileBackend extends FileBackendStore {
return $status;
}
- /**
- * @see FileBackendStore::doFileExists()
- * @return array|bool|null
- */
protected function doGetFileStat( array $params ) {
list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
if ( $srcRel === null ) {
@@ -760,8 +761,8 @@ class SwiftFileBackend extends FileBackendStore {
$stat = array(
// Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ),
- 'size' => (int)$srcObj->content_length,
- 'sha1' => $srcObj->metadata['Sha1base36']
+ 'size' => (int)$srcObj->content_length,
+ 'sha1' => $srcObj->getMetadataValue( 'Sha1base36' )
);
} catch ( NoSuchContainerException $e ) {
} catch ( NoSuchObjectException $e ) {
@@ -776,61 +777,103 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Fill in any missing object metadata and save it to Swift
*
- * @param $obj CF_Object
- * @param $path string Storage path to object
+ * @param CF_Object $obj
+ * @param string $path Storage path to object
* @return bool Success
* @throws Exception cloudfiles exceptions
*/
protected function addMissingMetadata( CF_Object $obj, $path ) {
- if ( isset( $obj->metadata['Sha1base36'] ) ) {
+ if ( $obj->getMetadataValue( 'Sha1base36' ) !== null ) {
return true; // nothing to do
}
wfProfileIn( __METHOD__ );
+ trigger_error( "$path was not stored with SHA-1 metadata.", E_USER_WARNING );
$status = Status::newGood();
$scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
if ( $status->isOK() ) {
- # Do not stat the file in getLocalCopy() to avoid infinite loops
- $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1, 'nostat' => 1 ) );
+ $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) );
if ( $tmpFile ) {
$hash = $tmpFile->getSha1Base36();
if ( $hash !== false ) {
- $obj->metadata['Sha1base36'] = $hash;
+ $obj->setMetadataValues( array( 'Sha1base36' => $hash ) );
$obj->sync_metadata(); // save to Swift
wfProfileOut( __METHOD__ );
return true; // success
}
}
}
- $obj->metadata['Sha1base36'] = false;
+ trigger_error( "Unable to set SHA-1 metadata for $path", E_USER_WARNING );
+ $obj->setMetadataValues( array( 'Sha1base36' => false ) );
wfProfileOut( __METHOD__ );
return false; // failed
}
- /**
- * @see FileBackend::getFileContents()
- * @return bool|null|string
- */
- public function getFileContents( array $params ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- return false; // invalid storage path
- }
+ protected function doGetFileContentsMulti( array $params ) {
+ $contents = array();
- if ( !$this->fileExists( $params ) ) {
- return null;
- }
+ $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
+ // Blindly create tmp files and stream to them, catching any exception if the file does
+ // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
+ foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) {
+ $cfOps = array(); // (path => CF_Async_Op)
- $data = false;
- try {
- $sContObj = $this->getContainer( $srcCont );
- $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
- $data = $obj->read( $this->headersFromParams( $params ) );
- } catch ( NoSuchContainerException $e ) {
- } catch ( CloudFilesException $e ) { // some other exception?
- $this->handleException( $e, null, __METHOD__, $params );
+ foreach ( $pathBatch as $path ) { // each path in this concurrent batch
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+ if ( $srcRel === null ) {
+ $contents[$path] = false;
+ continue;
+ }
+ $data = false;
+ try {
+ $sContObj = $this->getContainer( $srcCont );
+ $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
+ // Create a new temporary memory file...
+ $handle = fopen( 'php://temp', 'wb' );
+ if ( $handle ) {
+ $headers = $this->headersFromParams( $params );
+ if ( count( $pathBatch ) > 1 ) {
+ $cfOps[$path] = $obj->stream_async( $handle, $headers );
+ $cfOps[$path]->_file_handle = $handle; // close this later
+ } else {
+ $obj->stream( $handle, $headers );
+ rewind( $handle ); // start from the beginning
+ $data = stream_get_contents( $handle );
+ fclose( $handle );
+ }
+ } else {
+ $data = false;
+ }
+ } catch ( NoSuchContainerException $e ) {
+ $data = false;
+ } catch ( NoSuchObjectException $e ) {
+ $data = false;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $data = false;
+ $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
+ }
+ $contents[$path] = $data;
+ }
+
+ $batch = new CF_Async_Op_Batch( $cfOps );
+ $cfOps = $batch->execute();
+ foreach ( $cfOps as $path => $cfOp ) {
+ try {
+ $cfOp->getLastResponse();
+ rewind( $cfOp->_file_handle ); // start from the beginning
+ $contents[$path] = stream_get_contents( $cfOp->_file_handle );
+ } catch ( NoSuchContainerException $e ) {
+ $contents[$path] = false;
+ } catch ( NoSuchObjectException $e ) {
+ $contents[$path] = false;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $contents[$path] = false;
+ $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
+ }
+ fclose( $cfOp->_file_handle ); // close open handle
+ }
}
- return $data;
+ return $contents;
}
/**
@@ -871,27 +914,28 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Do not call this function outside of SwiftFileBackendFileList
*
- * @param $fullCont string Resolved container name
- * @param $dir string Resolved storage directory with no trailing slash
- * @param $after string|null Storage path of file to list items after
- * @param $limit integer Max number of items to list
- * @param $params Array Includes flag for 'topOnly'
- * @return Array List of relative paths of dirs directly under $dir
+ * @param string $fullCont Resolved container name
+ * @param string $dir Resolved storage directory with no trailing slash
+ * @param string|null $after Storage path of file to list items after
+ * @param integer $limit Max number of items to list
+ * @param array $params Parameters for getDirectoryList()
+ * @return Array List of resolved paths of directories directly under $dir
+ * @throws FileBackendError
*/
public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
$dirs = array();
if ( $after === INF ) {
return $dirs; // nothing more
}
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . '-' . $this->name );
try {
$container = $this->getContainer( $fullCont );
$prefix = ( $dir == '' ) ? null : "{$dir}/";
// Non-recursive: only list dirs right under $dir
if ( !empty( $params['topOnly'] ) ) {
$objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
- foreach ( $objects as $object ) { // files and dirs
+ foreach ( $objects as $object ) { // files and directories
if ( substr( $object, -1 ) === '/' ) {
$dirs[] = $object; // directories end in '/'
}
@@ -903,7 +947,7 @@ class SwiftFileBackend extends FileBackendStore {
$objects = $container->list_objects( $limit, $after, $prefix );
foreach ( $objects as $object ) { // files
$objectDir = $this->getParentDir( $object ); // directory of object
- if ( $objectDir !== false ) { // file has a parent dir
+ if ( $objectDir !== false && $objectDir !== $dir ) {
// Swift stores paths in UTF-8, using binary sorting.
// See function "create_container_table" in common/db.py.
// If a directory is not "greater" than the last one,
@@ -922,6 +966,7 @@ class SwiftFileBackend extends FileBackendStore {
}
}
}
+ // Page on the unfiltered directory listing (what is returned may be filtered)
if ( count( $objects ) < $limit ) {
$after = INF; // avoid a second RTT
} else {
@@ -931,9 +976,9 @@ class SwiftFileBackend extends FileBackendStore {
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, null, __METHOD__,
array( 'cont' => $fullCont, 'dir' => $dir ) );
+ throw new FileBackendError( "Got " . get_class( $e ) . " exception." );
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
return $dirs;
}
@@ -944,36 +989,52 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Do not call this function outside of SwiftFileBackendFileList
*
- * @param $fullCont string Resolved container name
- * @param $dir string Resolved storage directory with no trailing slash
- * @param $after string|null Storage path of file to list items after
- * @param $limit integer Max number of items to list
- * @param $params Array Includes flag for 'topOnly'
- * @return Array List of relative paths of files under $dir
+ * @param string $fullCont Resolved container name
+ * @param string $dir Resolved storage directory with no trailing slash
+ * @param string|null $after Storage path of file to list items after
+ * @param integer $limit Max number of items to list
+ * @param array $params Parameters for getDirectoryList()
+ * @return Array List of resolved paths of files under $dir
+ * @throws FileBackendError
*/
public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
$files = array();
if ( $after === INF ) {
return $files; // nothing more
}
- wfProfileIn( __METHOD__ . '-' . $this->name );
+ $section = new ProfileSection( __METHOD__ . '-' . $this->name );
try {
$container = $this->getContainer( $fullCont );
$prefix = ( $dir == '' ) ? null : "{$dir}/";
// Non-recursive: only list files right under $dir
if ( !empty( $params['topOnly'] ) ) { // files and dirs
- $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
- foreach ( $objects as $object ) {
- if ( substr( $object, -1 ) !== '/' ) {
- $files[] = $object; // directories end in '/'
+ if ( !empty( $params['adviseStat'] ) ) {
+ $limit = min( $limit, self::CACHE_CHEAP_SIZE );
+ // Note: get_objects() does not include directories
+ $objects = $this->loadObjectListing( $params, $dir,
+ $container->get_objects( $limit, $after, $prefix, null, '/' ) );
+ $files = $objects;
+ } else {
+ $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
+ foreach ( $objects as $object ) { // files and directories
+ if ( substr( $object, -1 ) !== '/' ) {
+ $files[] = $object; // directories end in '/'
+ }
}
}
// Recursive: list all files under $dir and its subdirs
} else { // files
- $objects = $container->list_objects( $limit, $after, $prefix );
+ if ( !empty( $params['adviseStat'] ) ) {
+ $limit = min( $limit, self::CACHE_CHEAP_SIZE );
+ $objects = $this->loadObjectListing( $params, $dir,
+ $container->get_objects( $limit, $after, $prefix ) );
+ } else {
+ $objects = $container->list_objects( $limit, $after, $prefix );
+ }
$files = $objects;
}
+ // Page on the unfiltered object listing (what is returned may be filtered)
if ( count( $objects ) < $limit ) {
$after = INF; // avoid a second RTT
} else {
@@ -983,29 +1044,57 @@ class SwiftFileBackend extends FileBackendStore {
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, null, __METHOD__,
array( 'cont' => $fullCont, 'dir' => $dir ) );
+ throw new FileBackendError( "Got " . get_class( $e ) . " exception." );
}
- wfProfileOut( __METHOD__ . '-' . $this->name );
return $files;
}
/**
- * @see FileBackendStore::doGetFileSha1base36()
- * @return bool
+ * Load a list of objects that belong under $dir into stat cache
+ * and return a list of the names of the objects in the same order.
+ *
+ * @param array $params Parameters for getDirectoryList()
+ * @param string $dir Resolved container directory path
+ * @param array $cfObjects List of CF_Object items
+ * @return array List of object names
*/
+ private function loadObjectListing( array $params, $dir, array $cfObjects ) {
+ $names = array();
+ $storageDir = rtrim( $params['dir'], '/' );
+ $suffixStart = ( $dir === '' ) ? 0 : strlen( $dir ) + 1; // size of "path/to/dir/"
+ // Iterate over the list *backwards* as this primes the stat cache, which is LRU.
+ // If this fills the cache and the caller stats an uncached file before stating
+ // the ones on the listing, there would be zero cache hits if this went forwards.
+ for ( end( $cfObjects ); key( $cfObjects ) !== null; prev( $cfObjects ) ) {
+ $object = current( $cfObjects );
+ $path = "{$storageDir}/" . substr( $object->name, $suffixStart );
+ $val = array(
+ // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
+ 'mtime' => wfTimestamp( TS_MW, $object->last_modified ),
+ 'size' => (int)$object->content_length,
+ 'latest' => false // eventually consistent
+ );
+ $this->cheapCache->set( $path, 'stat', $val );
+ $names[] = $object->name;
+ }
+ return array_reverse( $names ); // keep the paths in original order
+ }
+
protected function doGetFileSha1base36( array $params ) {
$stat = $this->getFileStat( $params );
if ( $stat ) {
+ if ( !isset( $stat['sha1'] ) ) {
+ // Stat entries filled by file listings don't include SHA1
+ $this->clearCache( array( $params['src'] ) );
+ $stat = $this->getFileStat( $params );
+ }
return $stat['sha1'];
} else {
return false;
}
}
- /**
- * @see FileBackendStore::doStreamFile()
- * @return Status
- */
protected function doStreamFile( array $params ) {
$status = Status::newGood();
@@ -1037,51 +1126,120 @@ class SwiftFileBackend extends FileBackendStore {
return $status;
}
- /**
- * @see FileBackendStore::getLocalCopy()
- * @return null|TempFSFile
- */
- public function getLocalCopy( array $params ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- return null;
- }
+ protected function doGetLocalCopyMulti( array $params ) {
+ $tmpFiles = array();
- // Blindly create a tmp file and stream to it, catching any exception if the file does
- // not exist. Also, doing a stat here will cause infinite loops when filling metadata.
- $tmpFile = null;
- try {
- $sContObj = $this->getContainer( $srcCont );
- $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
- // Get source file extension
- $ext = FileBackend::extensionFromPath( $srcRel );
- // Create a new temporary file...
- $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
- if ( $tmpFile ) {
- $handle = fopen( $tmpFile->getPath(), 'wb' );
- if ( $handle ) {
- $obj->stream( $handle, $this->headersFromParams( $params ) );
- fclose( $handle );
- } else {
- $tmpFile = null; // couldn't open temp file
+ $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
+ // Blindly create tmp files and stream to them, catching any exception if the file does
+ // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
+ foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) {
+ $cfOps = array(); // (path => CF_Async_Op)
+
+ foreach ( $pathBatch as $path ) { // each path in this concurrent batch
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+ if ( $srcRel === null ) {
+ $tmpFiles[$path] = null;
+ continue;
+ }
+ $tmpFile = null;
+ try {
+ $sContObj = $this->getContainer( $srcCont );
+ $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
+ // Get source file extension
+ $ext = FileBackend::extensionFromPath( $path );
+ // Create a new temporary file...
+ $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
+ if ( $tmpFile ) {
+ $handle = fopen( $tmpFile->getPath(), 'wb' );
+ if ( $handle ) {
+ $headers = $this->headersFromParams( $params );
+ if ( count( $pathBatch ) > 1 ) {
+ $cfOps[$path] = $obj->stream_async( $handle, $headers );
+ $cfOps[$path]->_file_handle = $handle; // close this later
+ } else {
+ $obj->stream( $handle, $headers );
+ fclose( $handle );
+ }
+ } else {
+ $tmpFile = null;
+ }
+ }
+ } catch ( NoSuchContainerException $e ) {
+ $tmpFile = null;
+ } catch ( NoSuchObjectException $e ) {
+ $tmpFile = null;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $tmpFile = null;
+ $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
}
+ $tmpFiles[$path] = $tmpFile;
+ }
+
+ $batch = new CF_Async_Op_Batch( $cfOps );
+ $cfOps = $batch->execute();
+ foreach ( $cfOps as $path => $cfOp ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( NoSuchContainerException $e ) {
+ $tmpFiles[$path] = null;
+ } catch ( NoSuchObjectException $e ) {
+ $tmpFiles[$path] = null;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $tmpFiles[$path] = null;
+ $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
+ }
+ fclose( $cfOp->_file_handle ); // close open handle
}
- } catch ( NoSuchContainerException $e ) {
- $tmpFile = null;
- } catch ( NoSuchObjectException $e ) {
- $tmpFile = null;
- } catch ( CloudFilesException $e ) { // some other exception?
- $tmpFile = null;
- $this->handleException( $e, null, __METHOD__, $params );
}
- return $tmpFile;
+ return $tmpFiles;
+ }
+
+ public function getFileHttpUrl( array $params ) {
+ if ( $this->swiftTempUrlKey != '' ||
+ ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' ) )
+ {
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ return null; // invalid path
+ }
+ try {
+ $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
+ $sContObj = $this->getContainer( $srcCont );
+ $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
+ if ( $this->swiftTempUrlKey != '' ) {
+ return $obj->get_temp_url( $this->swiftTempUrlKey, $ttl, "GET" );
+ } else { // give S3 API URL for rgw
+ $expires = time() + $ttl;
+ // Path for signature starts with the bucket
+ $spath = '/' . rawurlencode( $srcCont ) . '/' .
+ str_replace( '%2F', '/', rawurlencode( $srcRel ) );
+ // Calculate the hash
+ $signature = base64_encode( hash_hmac(
+ 'sha1',
+ "GET\n\n\n{$expires}\n{$spath}",
+ $this->rgwS3SecretKey,
+ true // raw
+ ) );
+ // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
+ // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
+ return wfAppendQuery(
+ str_replace( '/swift/v1', '', // S3 API is the rgw default
+ $sContObj->cfs_http->getStorageUrl() . $spath ),
+ array(
+ 'Signature' => $signature,
+ 'Expires' => $expires,
+ 'AWSAccessKeyId' => $this->rgwS3AccessKey )
+ );
+ }
+ } catch ( NoSuchContainerException $e ) {
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__, $params );
+ }
+ }
+ return null;
}
- /**
- * @see FileBackendStore::directoriesAreVirtual()
- * @return bool
- */
protected function directoriesAreVirtual() {
return true;
}
@@ -1091,7 +1249,7 @@ class SwiftFileBackend extends FileBackendStore {
* on a FileBackend params array, e.g. that of getLocalCopy().
* $params is currently only checked for a 'latest' flag.
*
- * @param $params Array
+ * @param array $params
* @return Array
*/
protected function headersFromParams( array $params ) {
@@ -1102,10 +1260,6 @@ class SwiftFileBackend extends FileBackendStore {
return $hdrs;
}
- /**
- * @see FileBackendStore::doExecuteOpHandlesInternal()
- * @return Array List of corresponding Status objects
- */
protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
$statuses = array();
@@ -1118,8 +1272,8 @@ class SwiftFileBackend extends FileBackendStore {
$cfOps = $batch->execute();
foreach ( $cfOps as $index => $cfOp ) {
$status = Status::newGood();
+ $function = '_getResponse' . $fileOpHandles[$index]->call;
try { // catch exceptions; update status
- $function = '_getResponse' . $fileOpHandles[$index]->call;
$this->$function( $cfOp, $status, $fileOpHandles[$index]->params );
$this->purgeCDNCache( $fileOpHandles[$index]->affectedObjects );
} catch ( CloudFilesException $e ) { // some other exception?
@@ -1137,12 +1291,12 @@ class SwiftFileBackend extends FileBackendStore {
*
* $readGrps is a list of the possible criteria for a request to have
* access to read a container. Each item is one of the following formats:
- * - account:user : Grants access if the request is by the given user
- * - .r:<regex> : Grants access if the request is from a referrer host that
- * matches the expression and the request is not for a listing.
- * Setting this to '*' effectively makes a container public.
- * - .rlistings:<regex> : Grants access if the request is from a referrer host that
- * matches the expression and the request for a listing.
+ * - account:user : Grants access if the request is by the given user
+ * - ".r:<regex>" : Grants access if the request is from a referrer host that
+ * matches the expression and the request is not for a listing.
+ * Setting this to '*' effectively makes a container public.
+ * -".rlistings:<regex>" : Grants access if the request is from a referrer host that
+ * matches the expression and the request is for a listing.
*
* $writeGrps is a list of the possible criteria for a request to have
* access to write to a container. Each item is of the following format:
@@ -1153,9 +1307,9 @@ class SwiftFileBackend extends FileBackendStore {
* In general, we don't allow listings to end-users. It's not useful, isn't well-defined
* (lists are truncated to 10000 item with no way to page), and is just a performance risk.
*
- * @param $contObj CF_Container Swift container
- * @param $readGrps Array List of read access routes
- * @param $writeGrps Array List of write access routes
+ * @param CF_Container $contObj Swift container
+ * @param array $readGrps List of read access routes
+ * @param array $writeGrps List of write access routes
* @return Status
*/
protected function setContainerAccess(
@@ -1178,7 +1332,7 @@ class SwiftFileBackend extends FileBackendStore {
* Purge the CDN cache of affected objects if CDN caching is enabled.
* This is for Rackspace/Akamai CDNs.
*
- * @param $objects Array List of CF_Object items
+ * @param array $objects List of CF_Object items
* @return void
*/
public function purgeCDNCache( array $objects ) {
@@ -1199,8 +1353,9 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Get an authenticated connection handle to the Swift proxy
*
- * @return CF_Connection|bool False on failure
* @throws CloudFilesException
+ * @throws CloudFilesException|Exception
+ * @return CF_Connection|bool False on failure
*/
protected function getConnection() {
if ( $this->connException instanceof CloudFilesException ) {
@@ -1222,12 +1377,12 @@ class SwiftFileBackend extends FileBackendStore {
if ( is_array( $creds ) ) { // cache hit
$this->auth->load_cached_credentials(
$creds['auth_token'], $creds['storage_url'], $creds['cdnm_url'] );
- $this->sessionStarted = time() - ceil( $this->authTTL/2 ); // skew for worst case
+ $this->sessionStarted = time() - ceil( $this->authTTL / 2 ); // skew for worst case
} else { // cache miss
try {
$this->auth->authenticate();
$creds = $this->auth->export_credentials();
- $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache
+ $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL / 2 ) ); // cache
$this->sessionStarted = time();
} catch ( CloudFilesException $e ) {
$this->connException = $e; // don't keep re-trying
@@ -1251,6 +1406,7 @@ class SwiftFileBackend extends FileBackendStore {
protected function closeConnection() {
if ( $this->conn ) {
$this->conn->close(); // close active cURL handles in CF_Http object
+ $this->conn = null;
$this->sessionStarted = 0;
$this->connContainerCache->clear();
}
@@ -1259,7 +1415,7 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Get the cache key for a container
*
- * @param $username string
+ * @param string $username
* @return string
*/
private function getCredsCacheKey( $username ) {
@@ -1267,18 +1423,11 @@ class SwiftFileBackend extends FileBackendStore {
}
/**
- * @see FileBackendStore::doClearCache()
- */
- protected function doClearCache( array $paths = null ) {
- $this->connContainerCache->clear(); // clear container object cache
- }
-
- /**
* Get a Swift container object, possibly from process cache.
* Use $reCache if the file count or byte count is needed.
*
- * @param $container string Container name
- * @param $bypassCache bool Bypass all caches and load from Swift
+ * @param string $container Container name
+ * @param bool $bypassCache Bypass all caches and load from Swift
* @return CF_Container
* @throws CloudFilesException
*/
@@ -1305,7 +1454,7 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Create a Swift container
*
- * @param $container string Container name
+ * @param string $container Container name
* @return CF_Container
* @throws CloudFilesException
*/
@@ -1319,7 +1468,7 @@ class SwiftFileBackend extends FileBackendStore {
/**
* Delete a Swift container
*
- * @param $container string Container name
+ * @param string $container Container name
* @return void
* @throws CloudFilesException
*/
@@ -1329,10 +1478,6 @@ class SwiftFileBackend extends FileBackendStore {
$conn->delete_container( $container );
}
- /**
- * @see FileBackendStore::doPrimeContainerCache()
- * @return void
- */
protected function doPrimeContainerCache( array $containerInfo ) {
try {
$conn = $this->getConnection(); // Swift proxy connection
@@ -1350,10 +1495,10 @@ class SwiftFileBackend extends FileBackendStore {
* Log an unexpected exception for this backend.
* This also sets the Status object to have a fatal error.
*
- * @param $e Exception
- * @param $status Status|null
- * @param $func string
- * @param $params Array
+ * @param Exception $e
+ * @param Status $status|null
+ * @param string $func
+ * @param array $params
* @return void
*/
protected function handleException( Exception $e, $status, $func, array $params ) {
@@ -1387,7 +1532,15 @@ class SwiftFileOpHandle extends FileBackendStoreOpHandle {
/** @var Array */
public $affectedObjects = array();
- public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) {
+ /**
+ * @param SwiftFileBackend $backend
+ * @param array $params
+ * @param string $call
+ * @param CF_Async_Op $cfOp
+ */
+ public function __construct(
+ SwiftFileBackend $backend, array $params, $call, CF_Async_Op $cfOp
+ ) {
$this->backend = $backend;
$this->params = $params;
$this->call = $call;
@@ -1419,10 +1572,10 @@ abstract class SwiftFileBackendList implements Iterator {
const PAGE_SIZE = 9000; // file listing buffer size
/**
- * @param $backend SwiftFileBackend
- * @param $fullCont string Resolved container name
- * @param $dir string Resolved directory relative to container
- * @param $params Array
+ * @param SwiftFileBackend $backend
+ * @param string $fullCont Resolved container name
+ * @param string $dir Resolved directory relative to container
+ * @param array $params
*/
public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
$this->backend = $backend;
@@ -1491,12 +1644,12 @@ abstract class SwiftFileBackendList implements Iterator {
/**
* Get the given list portion (page)
*
- * @param $container string Resolved container name
- * @param $dir string Resolved path relative to container
- * @param $after string|null
- * @param $limit integer
- * @param $params Array
- * @return Traversable|Array|null Returns null on failure
+ * @param string $container Resolved container name
+ * @param string $dir Resolved path relative to container
+ * @param string $after|null
+ * @param integer $limit
+ * @param array $params
+ * @return Traversable|Array
*/
abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
}
@@ -1515,7 +1668,7 @@ class SwiftFileBackendDirList extends SwiftFileBackendList {
/**
* @see SwiftFileBackendList::pageFromList()
- * @return Array|null
+ * @return Array
*/
protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
@@ -1536,7 +1689,7 @@ class SwiftFileBackendFileList extends SwiftFileBackendList {
/**
* @see SwiftFileBackendList::pageFromList()
- * @return Array|null
+ * @return Array
*/
protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
diff --git a/includes/filebackend/TempFSFile.php b/includes/filebackend/TempFSFile.php
index 5032bf68..8266e420 100644
--- a/includes/filebackend/TempFSFile.php
+++ b/includes/filebackend/TempFSFile.php
@@ -37,8 +37,8 @@ class TempFSFile extends FSFile {
* Make a new temporary file on the file system.
* Temporary files may be purged when the file object falls out of scope.
*
- * @param $prefix string
- * @param $extension string
+ * @param string $prefix
+ * @param string $extension
* @return TempFSFile|null
*/
public static function factory( $prefix, $extension = '' ) {
@@ -81,31 +81,38 @@ class TempFSFile extends FSFile {
/**
* Clean up the temporary file only after an object goes out of scope
*
- * @param $object Object
- * @return void
+ * @param Object $object
+ * @return TempFSFile This object
*/
public function bind( $object ) {
if ( is_object( $object ) ) {
+ if ( !isset( $object->tempFSFileReferences ) ) {
+ // Init first since $object might use __get() and return only a copy variable
+ $object->tempFSFileReferences = array();
+ }
$object->tempFSFileReferences[] = $this;
}
+ return $this;
}
/**
* Set flag to not clean up after the temporary file
*
- * @return void
+ * @return TempFSFile This object
*/
public function preserve() {
$this->canDelete = false;
+ return $this;
}
/**
* Set flag clean up after the temporary file
*
- * @return void
+ * @return TempFSFile This object
*/
public function autocollect() {
$this->canDelete = true;
+ return $this;
}
/**
diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php
index f6268c25..9250aa5e 100644
--- a/includes/filebackend/filejournal/DBFileJournal.php
+++ b/includes/filebackend/filejournal/DBFileJournal.php
@@ -65,16 +65,19 @@ class DBFileJournal extends FileJournal {
foreach ( $entries as $entry ) {
$data[] = array(
'fj_batch_uuid' => $batchId,
- 'fj_backend' => $this->backend,
- 'fj_op' => $entry['op'],
- 'fj_path' => $entry['path'],
- 'fj_new_sha1' => $entry['newSha1'],
- 'fj_timestamp' => $dbw->timestamp( $now )
+ 'fj_backend' => $this->backend,
+ 'fj_op' => $entry['op'],
+ 'fj_path' => $entry['path'],
+ 'fj_new_sha1' => $entry['newSha1'],
+ 'fj_timestamp' => $dbw->timestamp( $now )
);
}
try {
$dbw->insert( 'filejournal', $data, __METHOD__ );
+ if ( mt_rand( 0, 99 ) == 0 ) {
+ $this->purgeOldLogs(); // occasionally delete old logs
+ }
} catch ( DBError $e ) {
$status->fatal( 'filejournal-fail-dbquery', $this->backend );
return $status;
@@ -84,6 +87,35 @@ class DBFileJournal extends FileJournal {
}
/**
+ * @see FileJournal::doGetCurrentPosition()
+ * @return integer|false
+ */
+ protected function doGetCurrentPosition() {
+ $dbw = $this->getMasterDB();
+
+ return $dbw->selectField( 'filejournal', 'MAX(fj_id)',
+ array( 'fj_backend' => $this->backend ),
+ __METHOD__
+ );
+ }
+
+ /**
+ * @see FileJournal::doGetPositionAtTime()
+ * @param $time integer|string timestamp
+ * @return integer|false
+ */
+ protected function doGetPositionAtTime( $time ) {
+ $dbw = $this->getMasterDB();
+
+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $time ) );
+ return $dbw->selectField( 'filejournal', 'fj_id',
+ array( 'fj_backend' => $this->backend, "fj_timestamp <= $encTimestamp" ),
+ __METHOD__,
+ array( 'ORDER BY' => 'fj_timestamp DESC' )
+ );
+ }
+
+ /**
* @see FileJournal::doGetChangeEntries()
* @return Array
* @throws DBError
diff --git a/includes/filebackend/filejournal/FileJournal.php b/includes/filebackend/filejournal/FileJournal.php
index ce029bbe..a1b7a459 100644
--- a/includes/filebackend/filejournal/FileJournal.php
+++ b/includes/filebackend/filejournal/FileJournal.php
@@ -54,7 +54,7 @@ abstract class FileJournal {
* Create an appropriate FileJournal object from config
*
* @param $config Array
- * @param $backend string A registered file backend name
+ * @param string $backend A registered file backend name
* @throws MWException
* @return FileJournal
*/
@@ -85,13 +85,13 @@ abstract class FileJournal {
/**
* Log changes made by a batch file operation.
* $entries is an array of log entries, each of which contains:
- * op : Basic operation name (create, store, copy, delete)
+ * op : Basic operation name (create, update, delete)
* path : The storage path of the file
* newSha1 : The final base 36 SHA-1 of the file
* Note that 'false' should be used as the SHA-1 for non-existing files.
*
- * @param $entries Array List of file operations (each an array of parameters)
- * @param $batchId string UUID string that identifies the operation batch
+ * @param array $entries List of file operations (each an array of parameters)
+ * @param string $batchId UUID string that identifies the operation batch
* @return Status
*/
final public function logChangeBatch( array $entries, $batchId ) {
@@ -104,13 +104,45 @@ abstract class FileJournal {
/**
* @see FileJournal::logChangeBatch()
*
- * @param $entries Array List of file operations (each an array of parameters)
- * @param $batchId string UUID string that identifies the operation batch
+ * @param array $entries List of file operations (each an array of parameters)
+ * @param string $batchId UUID string that identifies the operation batch
* @return Status
*/
abstract protected function doLogChangeBatch( array $entries, $batchId );
/**
+ * Get the position ID of the latest journal entry
+ *
+ * @return integer|false
+ */
+ final public function getCurrentPosition() {
+ return $this->doGetCurrentPosition();
+ }
+
+ /**
+ * @see FileJournal::getCurrentPosition()
+ * @return integer|false
+ */
+ abstract protected function doGetCurrentPosition();
+
+ /**
+ * Get the position ID of the latest journal entry at some point in time
+ *
+ * @param $time integer|string timestamp
+ * @return integer|false
+ */
+ final public function getPositionAtTime( $time ) {
+ return $this->doGetPositionAtTime( $time );
+ }
+
+ /**
+ * @see FileJournal::getPositionAtTime()
+ * @param $time integer|string timestamp
+ * @return integer|false
+ */
+ abstract protected function doGetPositionAtTime( $time );
+
+ /**
* Get an array of file change log entries.
* A starting change ID and/or limit can be specified.
*
@@ -169,7 +201,7 @@ abstract class FileJournal {
*/
class NullFileJournal extends FileJournal {
/**
- * @see FileJournal::logChangeBatch()
+ * @see FileJournal::doLogChangeBatch()
* @param $entries array
* @param $batchId string
* @return Status
@@ -179,6 +211,23 @@ class NullFileJournal extends FileJournal {
}
/**
+ * @see FileJournal::doGetCurrentPosition()
+ * @return integer|false
+ */
+ protected function doGetCurrentPosition() {
+ return false;
+ }
+
+ /**
+ * @see FileJournal::doGetPositionAtTime()
+ * @param $time integer|string timestamp
+ * @return integer|false
+ */
+ protected function doGetPositionAtTime( $time ) {
+ return false;
+ }
+
+ /**
* @see FileJournal::doGetChangeEntries()
* @return Array
*/
@@ -187,7 +236,7 @@ class NullFileJournal extends FileJournal {
}
/**
- * @see FileJournal::purgeOldLogs()
+ * @see FileJournal::doPurgeOldLogs()
* @return Status
*/
protected function doPurgeOldLogs() {
diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php
index a8fe258b..3e934ba5 100644
--- a/includes/filebackend/lockmanager/DBLockManager.php
+++ b/includes/filebackend/lockmanager/DBLockManager.php
@@ -22,10 +22,9 @@
*/
/**
- * Version of LockManager based on using DB table locks.
+ * Version of LockManager based on using named/row DB locks.
+ *
* This is meant for multi-wiki systems that may share files.
- * All locks are blocking, so it might be useful to set a small
- * lock-wait timeout via server config to curtail deadlocks.
*
* All lock requests for a resource, identified by a hash string, will map
* to one bucket. Each bucket maps to one or several peer DBs, each on their
@@ -37,7 +36,7 @@
* @ingroup LockManager
* @since 1.19
*/
-class DBLockManager extends QuorumLockManager {
+abstract class DBLockManager extends QuorumLockManager {
/** @var Array Map of DB names to server config */
protected $dbServers; // (DB name => server config array)
/** @var BagOStuff */
@@ -67,11 +66,12 @@ class DBLockManager extends QuorumLockManager {
* each having an odd-numbered list of DB names (peers) as values.
* Any DB named 'localDBMaster' will automatically use the DB master
* settings for this wiki (without the need for a dbServers entry).
+ * Only use 'localDBMaster' if the domain is a valid wiki ID.
* - lockExpiry : Lock timeout (seconds) for dropped connections. [optional]
* This tells the DB server how long to wait before assuming
* connection failure and releasing all the locks for a session.
*
- * @param Array $config
+ * @param array $config
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -110,63 +110,17 @@ class DBLockManager extends QuorumLockManager {
$this->session = wfRandomString( 31 );
}
- /**
- * Get a connection to a lock DB and acquire locks on $paths.
- * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
- *
- * @see QuorumLockManager::getLocksOnServer()
- * @return Status
- */
- protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ // @TODO: change this code to work in one batch
+ protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
$status = Status::newGood();
-
- if ( $type == self::LOCK_EX ) { // writer locks
- try {
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
- # Build up values for INSERT clause
- $data = array();
- foreach ( $keys as $key ) {
- $data[] = array( 'fle_key' => $key );
- }
- # Wait on any existing writers and block new ones if we get in
- $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
- $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
- } catch ( DBError $e ) {
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
}
-
return $status;
}
- /**
- * @see QuorumLockManager::freeLocksOnServer()
- * @return Status
- */
- protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
- return Status::newGood(); // not supported
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- $status = Status::newGood();
-
- foreach ( $this->conns as $lockDb => $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback( __METHOD__ ); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
- }
-
- return $status;
+ protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+ return Status::newGood();
}
/**
@@ -197,8 +151,8 @@ class DBLockManager extends QuorumLockManager {
if ( !isset( $this->conns[$lockDb] ) ) {
$db = null;
if ( $lockDb === 'localDBMaster' ) {
- $lb = wfGetLBFactory()->newMainLB();
- $db = $lb->getConnection( DB_MASTER );
+ $lb = wfGetLBFactory()->getMainLB( $this->domain );
+ $db = $lb->getConnection( DB_MASTER, array(), $this->domain );
} elseif ( isset( $this->dbServers[$lockDb] ) ) {
$config = $this->dbServers[$lockDb];
$db = DatabaseBase::factory( $config['type'], $config );
@@ -274,14 +228,8 @@ class DBLockManager extends QuorumLockManager {
* Make sure remaining locks get cleared for sanity
*/
function __destruct() {
+ $this->releaseAllLocks();
foreach ( $this->conns as $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback( __METHOD__ ); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- // oh well
- }
- }
$db->close();
}
}
@@ -317,31 +265,42 @@ class MySqlLockManager extends DBLockManager {
* @see DBLockManager::getLocksOnServer()
* @return Status
*/
- protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
$status = Status::newGood();
$db = $this->getConnection( $lockSrv ); // checked in isServerUp()
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+
+ $keys = array(); // list of hash keys for the paths
+ $data = array(); // list of rows to insert
+ $checkEXKeys = array(); // list of hash keys that this has no EX lock on
# Build up values for INSERT clause
- $data = array();
- foreach ( $keys as $key ) {
+ foreach ( $paths as $path ) {
+ $key = $this->sha1Base36Absolute( $path );
+ $keys[] = $key;
$data[] = array( 'fls_key' => $key, 'fls_session' => $this->session );
+ if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
+ $checkEXKeys[] = $key;
+ }
}
- # Block new writers...
+
+ # Block new writers (both EX and SH locks leave entries here)...
$db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) );
# Actually do the locking queries...
if ( $type == self::LOCK_SH ) { // reader locks
+ $blocked = false;
# Bail if there are any existing writers...
- $blocked = $db->selectField( 'filelocks_exclusive', '1',
- array( 'fle_key' => $keys ),
- __METHOD__
- );
- # Prospective writers that haven't yet updated filelocks_exclusive
- # will recheck filelocks_shared after doing so and bail due to our entry.
+ if ( count( $checkEXKeys ) ) {
+ $blocked = $db->selectField( 'filelocks_exclusive', '1',
+ array( 'fle_key' => $checkEXKeys ),
+ __METHOD__
+ );
+ }
+ # Other prospective writers that haven't yet updated filelocks_exclusive
+ # will recheck filelocks_shared after doing so and bail due to this entry.
} else { // writer locks
$encSession = $db->addQuotes( $this->session );
# Bail if there are any existing writers...
- # The may detect readers, but the safe check for them is below.
+ # This may detect readers, but the safe check for them is below.
# Note: if two writers come at the same time, both bail :)
$blocked = $db->selectField( 'filelocks_shared', '1',
array( 'fls_key' => $keys, "fls_session != $encSession" ),
@@ -371,4 +330,103 @@ class MySqlLockManager extends DBLockManager {
return $status;
}
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockDb => $db ) {
+ if ( $db->trxLevel() ) { // in transaction
+ try {
+ $db->rollback( __METHOD__ ); // finish transaction and kill any rows
+ } catch ( DBError $e ) {
+ $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+ }
+ }
+ }
+
+ return $status;
+ }
+}
+
+/**
+ * PostgreSQL version of DBLockManager that supports shared locks.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class PostgreSqlLockManager extends DBLockManager {
+ /** @var Array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = array(
+ self::LOCK_SH => self::LOCK_SH,
+ self::LOCK_UW => self::LOCK_SH,
+ self::LOCK_EX => self::LOCK_EX
+ );
+
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+ if ( !count( $paths ) ) {
+ return $status; // nothing to lock
+ }
+
+ $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+ $bigints = array_unique( array_map(
+ function( $key ) {
+ return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 );
+ },
+ array_map( array( $this, 'sha1Base16Absolute' ), $paths )
+ ) );
+
+ // Try to acquire all the locks...
+ $fields = array();
+ foreach ( $bigints as $bigint ) {
+ $fields[] = ( $type == self::LOCK_SH )
+ ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
+ : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
+ }
+ $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+ $row = (array)$res->fetchObject();
+
+ if ( in_array( 'f', $row ) ) {
+ // Release any acquired locks if some could not be acquired...
+ $fields = array();
+ foreach ( $row as $kbigint => $ok ) {
+ if ( $ok === 't' ) { // locked
+ $bigint = substr( $kbigint, 1 ); // strip off the "K"
+ $fields[] = ( $type == self::LOCK_SH )
+ ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
+ : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
+ }
+ }
+ if ( count( $fields ) ) {
+ $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+ }
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockDb => $db ) {
+ try {
+ $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
+ } catch ( DBError $e ) {
+ $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+ }
+ }
+
+ return $status;
+ }
}
diff --git a/includes/filebackend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php
index 9a6206fd..eacba704 100644
--- a/includes/filebackend/lockmanager/FSLockManager.php
+++ b/includes/filebackend/lockmanager/FSLockManager.php
@@ -43,7 +43,7 @@ class FSLockManager extends LockManager {
protected $lockDir; // global dir for all servers
- /** @var Array Map of (locked key => lock type => lock file handle) */
+ /** @var Array Map of (locked key => lock file handle) */
protected $handles = array();
/**
@@ -115,12 +115,16 @@ class FSLockManager extends LockManager {
} elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
$this->locksHeld[$path][$type] = 1;
} else {
- wfSuppressWarnings();
- $handle = fopen( $this->getLockPath( $path ), 'a+' );
- wfRestoreWarnings();
- if ( !$handle ) { // lock dir missing?
- wfMkdirParents( $this->lockDir );
- $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
+ if ( isset( $this->handles[$path] ) ) {
+ $handle = $this->handles[$path];
+ } else {
+ wfSuppressWarnings();
+ $handle = fopen( $this->getLockPath( $path ), 'a+' );
+ wfRestoreWarnings();
+ if ( !$handle ) { // lock dir missing?
+ wfMkdirParents( $this->lockDir );
+ $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
+ }
}
if ( $handle ) {
// Either a shared or exclusive lock
@@ -128,7 +132,7 @@ class FSLockManager extends LockManager {
if ( flock( $handle, $lock | LOCK_NB ) ) {
// Record this lock as active
$this->locksHeld[$path][$type] = 1;
- $this->handles[$path][$type] = $handle;
+ $this->handles[$path] = $handle;
} else {
fclose( $handle );
$status->fatal( 'lockmanager-fail-acquirelock', $path );
@@ -160,24 +164,13 @@ class FSLockManager extends LockManager {
--$this->locksHeld[$path][$type];
if ( $this->locksHeld[$path][$type] <= 0 ) {
unset( $this->locksHeld[$path][$type] );
- // If a LOCK_SH comes in while we have a LOCK_EX, we don't
- // actually add a handler, so check for handler existence.
- if ( isset( $this->handles[$path][$type] ) ) {
- if ( $type === self::LOCK_EX
- && isset( $this->locksHeld[$path][self::LOCK_SH] )
- && !isset( $this->handles[$path][self::LOCK_SH] ) )
- {
- // EX lock came first: move this handle to the SH one
- $this->handles[$path][self::LOCK_SH] = $this->handles[$path][$type];
- } else {
- // Mark this handle to be unlocked and closed
- $handlesToClose[] = $this->handles[$path][$type];
- }
- unset( $this->handles[$path][$type] );
- }
}
if ( !count( $this->locksHeld[$path] ) ) {
unset( $this->locksHeld[$path] ); // no locks on this path
+ if ( isset( $this->handles[$path] ) ) {
+ $handlesToClose[] = $this->handles[$path];
+ unset( $this->handles[$path] );
+ }
}
// Unlock handles to release locks and delete
// any lock files that end up with no locks on them...
@@ -237,8 +230,7 @@ class FSLockManager extends LockManager {
* @return string
*/
protected function getLockPath( $path ) {
- $hash = self::sha1Base36( $path );
- return "{$this->lockDir}/{$hash}.lock";
+ return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
}
/**
diff --git a/includes/filebackend/lockmanager/LSLockManager.php b/includes/filebackend/lockmanager/LSLockManager.php
index 89428182..97de8dca 100644
--- a/includes/filebackend/lockmanager/LSLockManager.php
+++ b/includes/filebackend/lockmanager/LSLockManager.php
@@ -66,7 +66,7 @@ class LSLockManager extends QuorumLockManager {
* each having an odd-numbered list of server names (peers) as values.
* - connTimeout : Lock server connection attempt timeout. [optional]
*
- * @param Array $config
+ * @param array $config
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -94,7 +94,7 @@ class LSLockManager extends QuorumLockManager {
// Send out the command and get the response...
$type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX';
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ $keys = array_unique( array_map( array( $this, 'sha1Base36Absolute' ), $paths ) );
$response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
if ( $response !== 'ACQUIRED' ) {
@@ -115,7 +115,7 @@ class LSLockManager extends QuorumLockManager {
// Send out the command and get the response...
$type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX';
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ $keys = array_unique( array_map( array( $this, 'sha1Base36Absolute' ), $paths ) );
$response = $this->sendCommand( $lockSrv, 'RELEASE', $type, $keys );
if ( $response !== 'RELEASED' ) {
@@ -169,7 +169,7 @@ class LSLockManager extends QuorumLockManager {
$authKey = $this->lockServers[$lockSrv]['authKey'];
// Build of the command as a flat string...
$values = implode( '|', $values );
- $key = sha1( $this->session . $action . $type . $values . $authKey );
+ $key = hash_hmac( 'sha1', "{$this->session}\n{$action}\n{$type}\n{$values}", $authKey );
// Send out the command...
if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
return false;
diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php
index 07853f87..dad8a624 100644
--- a/includes/filebackend/lockmanager/LockManager.php
+++ b/includes/filebackend/lockmanager/LockManager.php
@@ -53,7 +53,10 @@ abstract class LockManager {
/** @var Array Map of (resource path => lock type => count) */
protected $locksHeld = array();
- /* Lock types; stronger locks have higher values */
+ protected $domain; // string; domain (usually wiki ID)
+ protected $lockTTL; // integer; maximum time locks can be held
+
+ /** Lock types; stronger locks have higher values */
const LOCK_SH = 1; // shared lock (for reads)
const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
const LOCK_EX = 3; // exclusive lock (for writes)
@@ -61,341 +64,185 @@ abstract class LockManager {
/**
* Construct a new instance from configuration
*
+ * $config paramaters include:
+ * - domain : Domain (usually wiki ID) that all resources are relative to [optional]
+ * - lockTTL : Age (in seconds) at which resource locks should expire.
+ * This only applies if locks are not tied to a connection/process.
+ *
* @param $config Array
*/
- public function __construct( array $config ) {}
+ public function __construct( array $config ) {
+ $this->domain = isset( $config['domain'] ) ? $config['domain'] : wfWikiID();
+ if ( isset( $config['lockTTL'] ) ) {
+ $this->lockTTL = max( 1, $config['lockTTL'] );
+ } elseif ( PHP_SAPI === 'cli' ) {
+ $this->lockTTL = 2 * 3600;
+ } else {
+ $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
+ $this->lockTTL = max( 5 * 60, 2 * (int)$met );
+ }
+ }
/**
* Lock the resources at the given abstract paths
*
- * @param $paths Array List of resource names
+ * @param array $paths List of resource names
* @param $type integer LockManager::LOCK_* constant
+ * @param integer $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
* @return Status
*/
- final public function lock( array $paths, $type = self::LOCK_EX ) {
- wfProfileIn( __METHOD__ );
- $status = $this->doLock( array_unique( $paths ), $this->lockTypeMap[$type] );
- wfProfileOut( __METHOD__ );
- return $status;
+ final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
+ return $this->lockByType( array( $type => $paths ), $timeout );
}
/**
- * Unlock the resources at the given abstract paths
+ * Lock the resources at the given abstract paths
*
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
+ * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+ * @param integer $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
* @return Status
+ * @since 1.22
*/
- final public function unlock( array $paths, $type = self::LOCK_EX ) {
+ final public function lockByType( array $pathsByType, $timeout = 0 ) {
wfProfileIn( __METHOD__ );
- $status = $this->doUnlock( array_unique( $paths ), $this->lockTypeMap[$type] );
+ $status = Status::newGood();
+ $pathsByType = $this->normalizePathsByType( $pathsByType );
+ $msleep = array( 0, 50, 100, 300, 500 ); // retry backoff times
+ $start = microtime( true );
+ do {
+ $status = $this->doLockByType( $pathsByType );
+ $elapsed = microtime( true ) - $start;
+ if ( $status->isOK() || $elapsed >= $timeout || $elapsed < 0 ) {
+ break; // success, timeout, or clock set back
+ }
+ usleep( 1e3 * ( next( $msleep ) ?: 1000 ) ); // use 1 sec after enough times
+ $elapsed = microtime( true ) - $start;
+ } while ( $elapsed < $timeout && $elapsed >= 0 );
wfProfileOut( __METHOD__ );
return $status;
}
/**
- * Get the base 36 SHA-1 of a string, padded to 31 digits
+ * Unlock the resources at the given abstract paths
*
- * @param $path string
- * @return string
+ * @param array $paths List of paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @return Status
*/
- final protected static function sha1Base36( $path ) {
- return wfBaseConvert( sha1( $path ), 16, 36, 31 );
+ final public function unlock( array $paths, $type = self::LOCK_EX ) {
+ return $this->unlockByType( array( $type => $paths ) );
}
/**
- * Lock resources with the given keys and lock type
+ * Unlock the resources at the given abstract paths
*
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @return string
+ * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+ * @return Status
+ * @since 1.22
*/
- abstract protected function doLock( array $paths, $type );
+ final public function unlockByType( array $pathsByType ) {
+ wfProfileIn( __METHOD__ );
+ $pathsByType = $this->normalizePathsByType( $pathsByType );
+ $status = $this->doUnlockByType( $pathsByType );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
/**
- * Unlock resources with the given keys and lock type
+ * Get the base 36 SHA-1 of a string, padded to 31 digits.
+ * Before hashing, the path will be prefixed with the domain ID.
+ * This should be used interally for lock key or file names.
*
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
+ * @param $path string
* @return string
*/
- abstract protected function doUnlock( array $paths, $type );
-}
-
-/**
- * Self-releasing locks
- *
- * LockManager helper class to handle scoped locks, which
- * release when an object is destroyed or goes out of scope.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class ScopedLock {
- /** @var LockManager */
- protected $manager;
- /** @var Status */
- protected $status;
- /** @var Array List of resource paths*/
- protected $paths;
-
- protected $type; // integer lock type
-
- /**
- * @param $manager LockManager
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @param $status Status
- */
- protected function __construct(
- LockManager $manager, array $paths, $type, Status $status
- ) {
- $this->manager = $manager;
- $this->paths = $paths;
- $this->status = $status;
- $this->type = $type;
+ final protected function sha1Base36Absolute( $path ) {
+ return wfBaseConvert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
}
/**
- * Get a ScopedLock object representing a lock on resource paths.
- * Any locks are released once this object goes out of scope.
- * The status object is updated with any errors or warnings.
+ * Get the base 16 SHA-1 of a string, padded to 31 digits.
+ * Before hashing, the path will be prefixed with the domain ID.
+ * This should be used interally for lock key or file names.
*
- * @param $manager LockManager
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @param $status Status
- * @return ScopedLock|null Returns null on failure
+ * @param $path string
+ * @return string
*/
- public static function factory(
- LockManager $manager, array $paths, $type, Status $status
- ) {
- $lockStatus = $manager->lock( $paths, $type );
- $status->merge( $lockStatus );
- if ( $lockStatus->isOK() ) {
- return new self( $manager, $paths, $type, $status );
- }
- return null;
- }
-
- function __destruct() {
- $wasOk = $this->status->isOK();
- $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) );
- if ( $wasOk ) {
- // Make sure status is OK, despite any unlockFiles() fatals
- $this->status->setResult( true, $this->status->value );
- }
+ final protected function sha1Base16Absolute( $path ) {
+ return sha1( "{$this->domain}:{$path}" );
}
-}
-
-/**
- * Version of LockManager that uses a quorum from peer servers for locks.
- * The resource space can also be sharded into separate peer groups.
- *
- * @ingroup LockManager
- * @since 1.20
- */
-abstract class QuorumLockManager extends LockManager {
- /** @var Array Map of bucket indexes to peer server lists */
- protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...))
/**
- * @see LockManager::doLock()
- * @param $paths array
- * @param $type int
- * @return Status
+ * Normalize the $paths array by converting LOCK_UW locks into the
+ * appropriate type and removing any duplicated paths for each lock type.
+ *
+ * @param array $paths Map of LockManager::LOCK_* constants to lists of paths
+ * @return Array
+ * @since 1.22
*/
- final protected function doLock( array $paths, $type ) {
- $status = Status::newGood();
-
- $pathsToLock = array(); // (bucket => paths)
- // Get locks that need to be acquired (buckets => locks)...
- foreach ( $paths as $path ) {
- if ( isset( $this->locksHeld[$path][$type] ) ) {
- ++$this->locksHeld[$path][$type];
- } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
- $this->locksHeld[$path][$type] = 1;
- } else {
- $bucket = $this->getBucketFromKey( $path );
- $pathsToLock[$bucket][] = $path;
- }
- }
-
- $lockedPaths = array(); // files locked in this attempt
- // Attempt to acquire these locks...
- foreach ( $pathsToLock as $bucket => $paths ) {
- // Try to acquire the locks for this bucket
- $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) );
- if ( !$status->isOK() ) {
- $status->merge( $this->doUnlock( $lockedPaths, $type ) );
- return $status;
- }
- // Record these locks as active
- foreach ( $paths as $path ) {
- $this->locksHeld[$path][$type] = 1; // locked
- }
- // Keep track of what locks were made in this attempt
- $lockedPaths = array_merge( $lockedPaths, $paths );
+ final protected function normalizePathsByType( array $pathsByType ) {
+ $res = array();
+ foreach ( $pathsByType as $type => $paths ) {
+ $res[$this->lockTypeMap[$type]] = array_unique( $paths );
}
-
- return $status;
+ return $res;
}
/**
- * @see LockManager::doUnlock()
- * @param $paths array
- * @param $type int
+ * @see LockManager::lockByType()
+ * @param array $paths Map of LockManager::LOCK_* constants to lists of paths
* @return Status
+ * @since 1.22
*/
- final protected function doUnlock( array $paths, $type ) {
+ protected function doLockByType( array $pathsByType ) {
$status = Status::newGood();
-
- $pathsToUnlock = array();
- foreach ( $paths as $path ) {
- if ( !isset( $this->locksHeld[$path][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
+ $lockedByType = array(); // map of (type => paths)
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doLock( $paths, $type ) );
+ if ( $status->isOK() ) {
+ $lockedByType[$type] = $paths;
} else {
- --$this->locksHeld[$path][$type];
- // Reference count the locks held and release locks when zero
- if ( $this->locksHeld[$path][$type] <= 0 ) {
- unset( $this->locksHeld[$path][$type] );
- $bucket = $this->getBucketFromKey( $path );
- $pathsToUnlock[$bucket][] = $path;
- }
- if ( !count( $this->locksHeld[$path] ) ) {
- unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
+ // Release the subset of locks that were acquired
+ foreach ( $lockedByType as $type => $paths ) {
+ $status->merge( $this->doUnlock( $paths, $type ) );
}
+ break;
}
}
-
- // Remove these specific locks if possible, or at least release
- // all locks once this process is currently not holding any locks.
- foreach ( $pathsToUnlock as $bucket => $paths ) {
- $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) );
- }
- if ( !count( $this->locksHeld ) ) {
- $status->merge( $this->releaseAllLocks() );
- }
-
return $status;
}
/**
- * Attempt to acquire locks with the peers for a bucket.
- * This is all or nothing; if any key is locked then this totally fails.
+ * Lock resources with the given keys and lock type
*
- * @param $bucket integer
- * @param $paths Array List of resource keys to lock
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
+ * @param array $paths List of paths
+ * @param $type integer LockManager::LOCK_* constant
* @return Status
*/
- final protected function doLockingRequestBucket( $bucket, array $paths, $type ) {
- $status = Status::newGood();
-
- $yesVotes = 0; // locks made on trustable servers
- $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
- $quorum = floor( $votesLeft/2 + 1 ); // simple majority
- // Get votes for each peer, in order, until we have enough...
- foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
- if ( !$this->isServerUp( $lockSrv ) ) {
- --$votesLeft;
- $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
- continue; // server down?
- }
- // Attempt to acquire the lock on this peer
- $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) );
- if ( !$status->isOK() ) {
- return $status; // vetoed; resource locked
- }
- ++$yesVotes; // success for this peer
- if ( $yesVotes >= $quorum ) {
- return $status; // lock obtained
- }
- --$votesLeft;
- $votesNeeded = $quorum - $yesVotes;
- if ( $votesNeeded > $votesLeft ) {
- break; // short-circuit
- }
- }
- // At this point, we must not have met the quorum
- $status->setResult( false );
-
- return $status;
- }
+ abstract protected function doLock( array $paths, $type );
/**
- * Attempt to release locks with the peers for a bucket
- *
- * @param $bucket integer
- * @param $paths Array List of resource keys to lock
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
+ * @see LockManager::unlockByType()
+ * @param array $paths Map of LockManager::LOCK_* constants to lists of paths
* @return Status
+ * @since 1.22
*/
- final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
+ protected function doUnlockByType( array $pathsByType ) {
$status = Status::newGood();
-
- foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
- if ( !$this->isServerUp( $lockSrv ) ) {
- $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
- // Attempt to release the lock on this peer
- } else {
- $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) );
- }
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doUnlock( $paths, $type ) );
}
-
return $status;
}
/**
- * Get the bucket for resource path.
- * This should avoid throwing any exceptions.
- *
- * @param $path string
- * @return integer
- */
- protected function getBucketFromKey( $path ) {
- $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
- return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
- }
-
- /**
- * Check if a lock server is up
- *
- * @param $lockSrv string
- * @return bool
- */
- abstract protected function isServerUp( $lockSrv );
-
- /**
- * Get a connection to a lock server and acquire locks on $paths
- *
- * @param $lockSrv string
- * @param $paths array
- * @param $type integer
- * @return Status
- */
- abstract protected function getLocksOnServer( $lockSrv, array $paths, $type );
-
- /**
- * Get a connection to a lock server and release locks on $paths.
- *
- * Subclasses must effectively implement this or releaseAllLocks().
- *
- * @param $lockSrv string
- * @param $paths array
- * @param $type integer
- * @return Status
- */
- abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type );
-
- /**
- * Release all locks that this session is holding.
- *
- * Subclasses must effectively implement this or freeLocksOnServer().
+ * Unlock resources with the given keys and lock type
*
+ * @param array $paths List of paths
+ * @param $type integer LockManager::LOCK_* constant
* @return Status
*/
- abstract protected function releaseAllLocks();
+ abstract protected function doUnlock( array $paths, $type );
}
/**
@@ -403,22 +250,10 @@ abstract class QuorumLockManager extends LockManager {
* @since 1.19
*/
class NullLockManager extends LockManager {
- /**
- * @see LockManager::doLock()
- * @param $paths array
- * @param $type int
- * @return Status
- */
protected function doLock( array $paths, $type ) {
return Status::newGood();
}
- /**
- * @see LockManager::doUnlock()
- * @param $paths array
- * @param $type int
- * @return Status
- */
protected function doUnlock( array $paths, $type ) {
return Status::newGood();
}
diff --git a/includes/filebackend/lockmanager/LockManagerGroup.php b/includes/filebackend/lockmanager/LockManagerGroup.php
index 8c8c940a..9aff2415 100644
--- a/includes/filebackend/lockmanager/LockManagerGroup.php
+++ b/includes/filebackend/lockmanager/LockManagerGroup.php
@@ -29,33 +29,41 @@
* @since 1.19
*/
class LockManagerGroup {
- /**
- * @var LockManagerGroup
- */
- protected static $instance = null;
+ /** @var Array (domain => LockManager) */
+ protected static $instances = array();
+
+ protected $domain; // string; domain (usually wiki ID)
- /** @var Array of (name => ('class' =>, 'config' =>, 'instance' =>)) */
+ /** @var Array of (name => ('class' => ..., 'config' => ..., 'instance' => ...)) */
protected $managers = array();
- protected function __construct() {}
+ /**
+ * @param string $domain Domain (usually wiki ID)
+ */
+ protected function __construct( $domain ) {
+ $this->domain = $domain;
+ }
/**
+ * @param string $domain Domain (usually wiki ID)
* @return LockManagerGroup
*/
- public static function singleton() {
- if ( self::$instance == null ) {
- self::$instance = new self();
- self::$instance->initFromGlobals();
+ public static function singleton( $domain = false ) {
+ $domain = ( $domain === false ) ? wfWikiID() : $domain;
+ if ( !isset( self::$instances[$domain] ) ) {
+ self::$instances[$domain] = new self( $domain );
+ self::$instances[$domain]->initFromGlobals();
}
- return self::$instance;
+ return self::$instances[$domain];
}
/**
- * Destroy the singleton instance, so that a new one will be created next
- * time singleton() is called.
+ * Destroy the singleton instances
+ *
+ * @return void
*/
- public static function destroySingleton() {
- self::$instance = null;
+ public static function destroySingletons() {
+ self::$instances = array();
}
/**
@@ -78,6 +86,7 @@ class LockManagerGroup {
*/
protected function register( array $configs ) {
foreach ( $configs as $config ) {
+ $config['domain'] = $this->domain;
if ( !isset( $config['name'] ) ) {
throw new MWException( "Cannot register a lock manager with no name." );
}
@@ -88,8 +97,8 @@ class LockManagerGroup {
$class = $config['class'];
unset( $config['class'] ); // lock manager won't need this
$this->managers[$name] = array(
- 'class' => $class,
- 'config' => $config,
+ 'class' => $class,
+ 'config' => $config,
'instance' => null
);
}
@@ -116,6 +125,21 @@ class LockManagerGroup {
}
/**
+ * Get the config array for a lock manager object with a given name
+ *
+ * @param $name string
+ * @return Array
+ * @throws MWException
+ */
+ public function config( $name ) {
+ if ( !isset( $this->managers[$name] ) ) {
+ throw new MWException( "No lock manager defined with the name `$name`." );
+ }
+ $class = $this->managers[$name]['class'];
+ return array( 'class' => $class ) + $this->managers[$name]['config'];
+ }
+
+ /**
* Get the default lock manager configured for the site.
* Returns NullLockManager if no lock manager could be found.
*
diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php
index 57c0463d..5eab03ee 100644
--- a/includes/filebackend/lockmanager/MemcLockManager.php
+++ b/includes/filebackend/lockmanager/MemcLockManager.php
@@ -28,8 +28,8 @@
* This is meant for multi-wiki systems that may share files.
* All locks are non-blocking, which avoids deadlocks.
*
- * All lock requests for a resource, identified by a hash string, will map
- * to one bucket. Each bucket maps to one or several peer servers, each running memcached.
+ * All lock requests for a resource, identified by a hash string, will map to one
+ * bucket. Each bucket maps to one or several peer servers, each running memcached.
* A majority of peers must agree for a lock to be acquired.
*
* @ingroup LockManager
@@ -48,9 +48,7 @@ class MemcLockManager extends QuorumLockManager {
/** @var Array */
protected $serversUp = array(); // (server name => bool)
- protected $lockExpiry; // integer; maximum time locks can be held
- protected $session = ''; // string; random SHA-1 UUID
- protected $wikiId = ''; // string
+ protected $session = ''; // string; random UUID
/**
* Construct a new instance from configuration.
@@ -61,9 +59,9 @@ class MemcLockManager extends QuorumLockManager {
* each having an odd-numbered list of server names (peers) as values.
* - memcConfig : Configuration array for ObjectCache::newFromParams. [optional]
* If set, this must use one of the memcached classes.
- * - wikiId : Wiki ID string that all resources are relative to. [optional]
*
- * @param Array $config
+ * @param array $config
+ * @throws MWException
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -87,19 +85,47 @@ class MemcLockManager extends QuorumLockManager {
}
}
- $this->wikiId = isset( $config['wikiId'] ) ? $config['wikiId'] : wfWikiID();
+ $this->session = wfRandomString( 32 );
+ }
- $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
- $this->lockExpiry = $met ? 2*(int)$met : 2*3600;
+ // @TODO: change this code to work in one batch
+ protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
+ $status = Status::newGood();
- $this->session = wfRandomString( 32 );
+ $lockedPaths = array();
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
+ if ( $status->isOK() ) {
+ $lockedPaths[$type] = isset( $lockedPaths[$type] )
+ ? array_merge( $lockedPaths[$type], $paths )
+ : $paths;
+ } else {
+ foreach ( $lockedPaths as $type => $paths ) {
+ $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
+ }
+ break;
+ }
+ }
+
+ return $status;
+ }
+
+ // @TODO: change this code to work in one batch
+ protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+ $status = Status::newGood();
+
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
+ }
+
+ return $status;
}
/**
* @see QuorumLockManager::getLocksOnServer()
* @return Status
*/
- protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
$status = Status::newGood();
$memc = $this->getCache( $lockSrv );
@@ -110,7 +136,7 @@ class MemcLockManager extends QuorumLockManager {
foreach ( $paths as $path ) {
$status->fatal( 'lockmanager-fail-acquirelock', $path );
}
- return;
+ return $status;
}
// Fetch all the existing lock records...
@@ -121,8 +147,8 @@ class MemcLockManager extends QuorumLockManager {
foreach ( $paths as $path ) {
$locksKey = $this->recordKeyForPath( $path );
$locksHeld = isset( $lockRecords[$locksKey] )
- ? $lockRecords[$locksKey]
- : array( self::LOCK_SH => array(), self::LOCK_EX => array() ); // init
+ ? self::sanitizeLockArray( $lockRecords[$locksKey] )
+ : self::newLockArray(); // init
foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
if ( $expiry < $now ) { // stale?
unset( $locksHeld[self::LOCK_EX][$session] );
@@ -141,7 +167,7 @@ class MemcLockManager extends QuorumLockManager {
}
if ( $status->isOK() ) {
// Register the session in the lock record array
- $locksHeld[$type][$this->session] = $now + $this->lockExpiry;
+ $locksHeld[$type][$this->session] = $now + $this->lockTTL;
// We will update this record if none of the other locks conflict
$lockRecords[$locksKey] = $locksHeld;
}
@@ -149,9 +175,15 @@ class MemcLockManager extends QuorumLockManager {
// If there were no lock conflicts, update all the lock records...
if ( $status->isOK() ) {
- foreach ( $lockRecords as $locksKey => $locksHeld ) {
- $memc->set( $locksKey, $locksHeld );
- wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path );
+ $locksHeld = $lockRecords[$locksKey];
+ $ok = $memc->set( $locksKey, $locksHeld, 7 * 86400 );
+ if ( !$ok ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ } else {
+ wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
+ }
}
}
@@ -165,7 +197,7 @@ class MemcLockManager extends QuorumLockManager {
* @see QuorumLockManager::freeLocksOnServer()
* @return Status
*/
- protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+ protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) {
$status = Status::newGood();
$memc = $this->getCache( $lockSrv );
@@ -186,17 +218,22 @@ class MemcLockManager extends QuorumLockManager {
foreach ( $paths as $path ) {
$locksKey = $this->recordKeyForPath( $path ); // lock record
if ( !isset( $lockRecords[$locksKey] ) ) {
+ $status->warning( 'lockmanager-fail-releaselock', $path );
continue; // nothing to do
}
- $locksHeld = $lockRecords[$locksKey];
- if ( is_array( $locksHeld ) && isset( $locksHeld[$type] ) ) {
- unset( $locksHeld[$type][$this->session] );
- $ok = $memc->set( $locksKey, $locksHeld );
+ $locksHeld = self::sanitizeLockArray( $lockRecords[$locksKey] );
+ if ( isset( $locksHeld[$type][$this->session] ) ) {
+ unset( $locksHeld[$type][$this->session] ); // unregister this session
+ if ( $locksHeld === self::newLockArray() ) {
+ $ok = $memc->delete( $locksKey );
+ } else {
+ $ok = $memc->set( $locksKey, $locksHeld );
+ }
+ if ( !$ok ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
} else {
- $ok = true;
- }
- if ( !$ok ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
+ $status->warning( 'lockmanager-fail-releaselock', $path );
}
wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" );
}
@@ -226,7 +263,7 @@ class MemcLockManager extends QuorumLockManager {
/**
* Get the MemcachedBagOStuff object for a $lockSrv
*
- * @param $lockSrv string Server name
+ * @param string $lockSrv Server name
* @return MemcachedBagOStuff|null
*/
protected function getCache( $lockSrv ) {
@@ -234,7 +271,7 @@ class MemcLockManager extends QuorumLockManager {
if ( isset( $this->bagOStuffs[$lockSrv] ) ) {
$memc = $this->bagOStuffs[$lockSrv];
if ( !isset( $this->serversUp[$lockSrv] ) ) {
- $this->serversUp[$lockSrv] = $memc->set( 'MemcLockManager:ping', 1, 1 );
+ $this->serversUp[$lockSrv] = $memc->set( __CLASS__ . ':ping', 1, 1 );
if ( !$this->serversUp[$lockSrv] ) {
trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING );
}
@@ -251,14 +288,32 @@ class MemcLockManager extends QuorumLockManager {
* @return string
*/
protected function recordKeyForPath( $path ) {
- $hash = LockManager::sha1Base36( $path );
- list( $db, $prefix ) = wfSplitWikiID( $this->wikiId );
- return wfForeignMemcKey( $db, $prefix, __CLASS__, 'locks', $hash );
+ return implode( ':', array( __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ) );
+ }
+
+ /**
+ * @return Array An empty lock structure for a key
+ */
+ protected static function newLockArray() {
+ return array( self::LOCK_SH => array(), self::LOCK_EX => array() );
+ }
+
+ /**
+ * @param $a array
+ * @return Array An empty lock structure for a key
+ */
+ protected static function sanitizeLockArray( $a ) {
+ if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
+ return $a;
+ } else {
+ trigger_error( __METHOD__ . ": reset invalid lock array.", E_USER_WARNING );
+ return self::newLockArray();
+ }
}
/**
* @param $memc MemcachedBagOStuff
- * @param $keys Array List of keys to acquire
+ * @param array $keys List of keys to acquire
* @return bool
*/
protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
@@ -275,7 +330,7 @@ class MemcLockManager extends QuorumLockManager {
$start = microtime( true );
do {
if ( ( ++$rounds % 4 ) == 0 ) {
- usleep( 1000*50 ); // 50 ms
+ usleep( 1000 * 50 ); // 50 ms
}
foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
@@ -284,10 +339,10 @@ class MemcLockManager extends QuorumLockManager {
continue; // acquire in order
}
}
- } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 6 );
+ } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 3 );
if ( count( $lockedKeys ) != count( $keys ) ) {
- $this->releaseMutexes( $lockedKeys ); // failed; release what was locked
+ $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
return false;
}
@@ -296,7 +351,7 @@ class MemcLockManager extends QuorumLockManager {
/**
* @param $memc MemcachedBagOStuff
- * @param $keys Array List of acquired keys
+ * @param array $keys List of acquired keys
* @return void
*/
protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
diff --git a/includes/filebackend/lockmanager/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php
new file mode 100644
index 00000000..8356d32a
--- /dev/null
+++ b/includes/filebackend/lockmanager/QuorumLockManager.php
@@ -0,0 +1,246 @@
+<?php
+/**
+ * Version of LockManager that uses a quorum from peer servers for locks.
+ *
+ * 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 LockManager
+ */
+
+/**
+ * Version of LockManager that uses a quorum from peer servers for locks.
+ * The resource space can also be sharded into separate peer groups.
+ *
+ * @ingroup LockManager
+ * @since 1.20
+ */
+abstract class QuorumLockManager extends LockManager {
+ /** @var Array Map of bucket indexes to peer server lists */
+ protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...))
+ /** @var Array Map of degraded buckets */
+ protected $degradedBuckets = array(); // (buckey index => UNIX timestamp)
+
+ final protected function doLock( array $paths, $type ) {
+ return $this->doLockByType( array( $type => $paths ) );
+ }
+
+ final protected function doUnlock( array $paths, $type ) {
+ return $this->doUnlockByType( array( $type => $paths ) );
+ }
+
+ protected function doLockByType( array $pathsByType ) {
+ $status = Status::newGood();
+
+ $pathsToLock = array(); // (bucket => type => paths)
+ // Get locks that need to be acquired (buckets => locks)...
+ foreach ( $pathsByType as $type => $paths ) {
+ foreach ( $paths as $path ) {
+ if ( isset( $this->locksHeld[$path][$type] ) ) {
+ ++$this->locksHeld[$path][$type];
+ } else {
+ $bucket = $this->getBucketFromPath( $path );
+ $pathsToLock[$bucket][$type][] = $path;
+ }
+ }
+ }
+
+ $lockedPaths = array(); // files locked in this attempt (type => paths)
+ // Attempt to acquire these locks...
+ foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
+ // Try to acquire the locks for this bucket
+ $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
+ if ( !$status->isOK() ) {
+ $status->merge( $this->doUnlockByType( $lockedPaths ) );
+ return $status;
+ }
+ // Record these locks as active
+ foreach ( $pathsToLockByType as $type => $paths ) {
+ foreach ( $paths as $path ) {
+ $this->locksHeld[$path][$type] = 1; // locked
+ // Keep track of what locks were made in this attempt
+ $lockedPaths[$type][] = $path;
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ protected function doUnlockByType( array $pathsByType ) {
+ $status = Status::newGood();
+
+ $pathsToUnlock = array(); // (bucket => type => paths)
+ foreach ( $pathsByType as $type => $paths ) {
+ foreach ( $paths as $path ) {
+ if ( !isset( $this->locksHeld[$path][$type] ) ) {
+ $status->warning( 'lockmanager-notlocked', $path );
+ } else {
+ --$this->locksHeld[$path][$type];
+ // Reference count the locks held and release locks when zero
+ if ( $this->locksHeld[$path][$type] <= 0 ) {
+ unset( $this->locksHeld[$path][$type] );
+ $bucket = $this->getBucketFromPath( $path );
+ $pathsToUnlock[$bucket][$type][] = $path;
+ }
+ if ( !count( $this->locksHeld[$path] ) ) {
+ unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
+ }
+ }
+ }
+ }
+
+ // Remove these specific locks if possible, or at least release
+ // all locks once this process is currently not holding any locks.
+ foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
+ $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
+ }
+ if ( !count( $this->locksHeld ) ) {
+ $status->merge( $this->releaseAllLocks() );
+ $this->degradedBuckets = array(); // safe to retry the normal quorum
+ }
+
+ return $status;
+ }
+
+ /**
+ * Attempt to acquire locks with the peers for a bucket.
+ * This is all or nothing; if any key is locked then this totally fails.
+ *
+ * @param $bucket integer
+ * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+ * @return Status
+ */
+ final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
+ $status = Status::newGood();
+
+ $yesVotes = 0; // locks made on trustable servers
+ $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
+ $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
+ // Get votes for each peer, in order, until we have enough...
+ foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
+ if ( !$this->isServerUp( $lockSrv ) ) {
+ --$votesLeft;
+ $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
+ $this->degradedBuckets[$bucket] = time();
+ continue; // server down?
+ }
+ // Attempt to acquire the lock on this peer
+ $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) );
+ if ( !$status->isOK() ) {
+ return $status; // vetoed; resource locked
+ }
+ ++$yesVotes; // success for this peer
+ if ( $yesVotes >= $quorum ) {
+ return $status; // lock obtained
+ }
+ --$votesLeft;
+ $votesNeeded = $quorum - $yesVotes;
+ if ( $votesNeeded > $votesLeft ) {
+ break; // short-circuit
+ }
+ }
+ // At this point, we must not have met the quorum
+ $status->setResult( false );
+
+ return $status;
+ }
+
+ /**
+ * Attempt to release locks with the peers for a bucket
+ *
+ * @param $bucket integer
+ * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+ * @return Status
+ */
+ final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
+ $status = Status::newGood();
+
+ $yesVotes = 0; // locks freed on trustable servers
+ $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
+ $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
+ $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
+ foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
+ if ( !$this->isServerUp( $lockSrv ) ) {
+ $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
+ // Attempt to release the lock on this peer
+ } else {
+ $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) );
+ ++$yesVotes; // success for this peer
+ // Normally the first peers form the quorum, and the others are ignored.
+ // Ignore them in this case, but not when an alternative quorum was used.
+ if ( $yesVotes >= $quorum && !$isDegraded ) {
+ break; // lock released
+ }
+ }
+ }
+ // Set a bad status if the quorum was not met.
+ // Assumes the same "up" servers as during the acquire step.
+ $status->setResult( $yesVotes >= $quorum );
+
+ return $status;
+ }
+
+ /**
+ * Get the bucket for resource path.
+ * This should avoid throwing any exceptions.
+ *
+ * @param $path string
+ * @return integer
+ */
+ protected function getBucketFromPath( $path ) {
+ $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
+ return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
+ }
+
+ /**
+ * Check if a lock server is up.
+ * This should process cache results to reduce RTT.
+ *
+ * @param $lockSrv string
+ * @return bool
+ */
+ abstract protected function isServerUp( $lockSrv );
+
+ /**
+ * Get a connection to a lock server and acquire locks
+ *
+ * @param $lockSrv string
+ * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+ * @return Status
+ */
+ abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
+
+ /**
+ * Get a connection to a lock server and release locks on $paths.
+ *
+ * Subclasses must effectively implement this or releaseAllLocks().
+ *
+ * @param $lockSrv string
+ * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+ * @return Status
+ */
+ abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
+
+ /**
+ * Release all locks that this session is holding.
+ *
+ * Subclasses must effectively implement this or freeLocksOnServer().
+ *
+ * @return Status
+ */
+ abstract protected function releaseAllLocks();
+}
diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php
new file mode 100644
index 00000000..43b0198a
--- /dev/null
+++ b/includes/filebackend/lockmanager/RedisLockManager.php
@@ -0,0 +1,288 @@
+<?php
+/**
+ * Version of LockManager based on using redis servers.
+ *
+ * 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 LockManager
+ */
+
+/**
+ * Manage locks using redis servers.
+ *
+ * Version of LockManager based on using redis servers.
+ * This is meant for multi-wiki systems that may share files.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * All lock requests for a resource, identified by a hash string, will map to one
+ * bucket. Each bucket maps to one or several peer servers, each running redis.
+ * A majority of peers must agree for a lock to be acquired.
+ *
+ * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
+ *
+ * @ingroup LockManager
+ * @since 1.22
+ */
+class RedisLockManager extends QuorumLockManager {
+ /** @var Array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = array(
+ self::LOCK_SH => self::LOCK_SH,
+ self::LOCK_UW => self::LOCK_SH,
+ self::LOCK_EX => self::LOCK_EX
+ );
+
+ /** @var RedisConnectionPool */
+ protected $redisPool;
+ /** @var Array Map server names to hostname/IP and port numbers */
+ protected $lockServers = array();
+
+ protected $session = ''; // string; random UUID
+
+ /**
+ * Construct a new instance from configuration.
+ *
+ * $config paramaters include:
+ * - lockServers : Associative array of server names to "<IP>:<port>" strings.
+ * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
+ * each having an odd-numbered list of server names (peers) as values.
+ * - redisConfig : Configuration for RedisConnectionPool::__construct().
+ *
+ * @param Array $config
+ * @throws MWException
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+
+ $this->lockServers = $config['lockServers'];
+ // Sanitize srvsByBucket config to prevent PHP errors
+ $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
+ $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+ $config['redisConfig']['serializer'] = 'none';
+ $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
+
+ $this->session = wfRandomString( 32 );
+ }
+
+ // @TODO: change this code to work in one batch
+ protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
+ $status = Status::newGood();
+
+ $lockedPaths = array();
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
+ if ( $status->isOK() ) {
+ $lockedPaths[$type] = isset( $lockedPaths[$type] )
+ ? array_merge( $lockedPaths[$type], $paths )
+ : $paths;
+ } else {
+ foreach ( $lockedPaths as $type => $paths ) {
+ $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
+ }
+ break;
+ }
+ }
+
+ return $status;
+ }
+
+ // @TODO: change this code to work in one batch
+ protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+ $status = Status::newGood();
+
+ foreach ( $pathsByType as $type => $paths ) {
+ $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
+ }
+
+ return $status;
+ }
+
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $server = $this->lockServers[$lockSrv];
+ $conn = $this->redisPool->getConnection( $server );
+ if ( !$conn ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ return $status;
+ }
+
+ $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records
+
+ try {
+ static $script =
+<<<LUA
+ if ARGV[1] ~= 'EX' and ARGV[1] ~= 'SH' then
+ return redis.error_reply('Unrecognized lock type given (must be EX or SH)')
+ end
+ local failed = {}
+ -- Check that all the locks can be acquired
+ for i,resourceKey in ipairs(KEYS) do
+ local keyIsFree = true
+ local currentLocks = redis.call('hKeys',resourceKey)
+ for i,lockKey in ipairs(currentLocks) do
+ local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
+ -- Check any locks that are not owned by this session
+ if session ~= ARGV[2] then
+ local lockTimestamp = redis.call('hGet',resourceKey,lockKey)
+ if 1*lockTimestamp < ( ARGV[4] - ARGV[3] ) then
+ -- Lock is stale, so just prune it out
+ redis.call('hDel',resourceKey,lockKey)
+ elseif ARGV[1] == 'EX' or type == 'EX' then
+ keyIsFree = false
+ break
+ end
+ end
+ end
+ if not keyIsFree then
+ failed[#failed+1] = resourceKey
+ end
+ end
+ -- If all locks could be acquired, then do so
+ if #failed == 0 then
+ for i,resourceKey in ipairs(KEYS) do
+ redis.call('hSet',resourceKey,ARGV[1] .. ':' .. ARGV[2],ARGV[4])
+ -- In addition to invalidation logic, be sure to garbage collect
+ redis.call('expire',resourceKey,ARGV[3])
+ end
+ end
+ return failed
+LUA;
+ $res = $conn->luaEval( $script,
+ array_merge(
+ $keys, // KEYS[0], KEYS[1],...KEYS[N]
+ array(
+ $type === self::LOCK_SH ? 'SH' : 'EX', // ARGV[1]
+ $this->session, // ARGV[2]
+ $this->lockTTL, // ARGV[3]
+ time() // ARGV[4]
+ )
+ ),
+ count( $keys ) # number of first argument(s) that are keys
+ );
+ } catch ( RedisException $e ) {
+ $res = false;
+ $this->redisPool->handleException( $server, $conn, $e );
+ }
+
+ if ( $res === false ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ } else {
+ $pathsByKey = array_combine( $keys, $paths );
+ foreach ( $res as $key ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
+ }
+ }
+
+ return $status;
+ }
+
+ protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $server = $this->lockServers[$lockSrv];
+ $conn = $this->redisPool->getConnection( $server );
+ if ( !$conn ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
+ return $status;
+ }
+
+ $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records
+
+ try {
+ static $script =
+<<<LUA
+ if ARGV[1] ~= 'EX' and ARGV[1] ~= 'SH' then
+ return redis.error_reply('Unrecognized lock type given (must be EX or SH)')
+ end
+ local failed = {}
+ for i,resourceKey in ipairs(KEYS) do
+ local released = redis.call('hDel',resourceKey,ARGV[1] .. ':' .. ARGV[2])
+ if released > 0 then
+ -- Remove the whole structure if it is now empty
+ if redis.call('hLen',resourceKey) == 0 then
+ redis.call('del',resourceKey)
+ end
+ else
+ failed[#failed+1] = resourceKey
+ end
+ end
+ return failed
+LUA;
+ $res = $conn->luaEval( $script,
+ array_merge(
+ $keys, // KEYS[0], KEYS[1],...KEYS[N]
+ array(
+ $type === self::LOCK_SH ? 'SH' : 'EX', // ARGV[1]
+ $this->session // ARGV[2]
+ )
+ ),
+ count( $keys ) # number of first argument(s) that are keys
+ );
+ } catch ( RedisException $e ) {
+ $res = false;
+ $this->redisPool->handleException( $server, $conn, $e );
+ }
+
+ if ( $res === false ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
+ } else {
+ $pathsByKey = array_combine( $keys, $paths );
+ foreach ( $res as $key ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
+ }
+ }
+
+ return $status;
+ }
+
+ protected function releaseAllLocks() {
+ return Status::newGood(); // not supported
+ }
+
+ protected function isServerUp( $lockSrv ) {
+ return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] );
+ }
+
+ /**
+ * @param $path string
+ * @return string
+ */
+ protected function recordKeyForPath( $path ) {
+ return implode( ':', array( __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ) );
+ }
+
+ /**
+ * Make sure remaining locks get cleared for sanity
+ */
+ function __destruct() {
+ while ( count( $this->locksHeld ) ) {
+ foreach ( $this->locksHeld as $path => $locks ) {
+ $this->doUnlock( array( $path ), self::LOCK_EX );
+ $this->doUnlock( array( $path ), self::LOCK_SH );
+ }
+ }
+ }
+}
diff --git a/includes/filebackend/lockmanager/ScopedLock.php b/includes/filebackend/lockmanager/ScopedLock.php
new file mode 100644
index 00000000..5faad4a6
--- /dev/null
+++ b/includes/filebackend/lockmanager/ScopedLock.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Resource locking handling.
+ *
+ * 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 LockManager
+ * @author Aaron Schulz
+ */
+
+/**
+ * Self-releasing locks
+ *
+ * LockManager helper class to handle scoped locks, which
+ * release when an object is destroyed or goes out of scope.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class ScopedLock {
+ /** @var LockManager */
+ protected $manager;
+ /** @var Status */
+ protected $status;
+ /** @var Array Map of lock types to resource paths */
+ protected $pathsByType;
+
+ /**
+ * @param LockManager $manager
+ * @param array $pathsByType Map of lock types to path lists
+ * @param Status $status
+ */
+ protected function __construct( LockManager $manager, array $pathsByType, Status $status ) {
+ $this->manager = $manager;
+ $this->pathsByType = $pathsByType;
+ $this->status = $status;
+ }
+
+ /**
+ * Get a ScopedLock object representing a lock on resource paths.
+ * Any locks are released once this object goes out of scope.
+ * The status object is updated with any errors or warnings.
+ *
+ * $type can be "mixed" and $paths can be a map of types to paths (since 1.22).
+ * Otherwise $type should be an integer and $paths should be a list of paths.
+ *
+ * @param LockManager $manager
+ * @param array $paths List of storage paths or map of lock types to path lists
+ * @param integer|string $type LockManager::LOCK_* constant or "mixed"
+ * @param Status $status
+ * @param integer $timeout Timeout in seconds (0 means non-blocking) (since 1.22)
+ * @return ScopedLock|null Returns null on failure
+ */
+ public static function factory(
+ LockManager $manager, array $paths, $type, Status $status, $timeout = 0
+ ) {
+ $pathsByType = is_integer( $type ) ? array( $type => $paths ) : $paths;
+ $lockStatus = $manager->lockByType( $pathsByType, $timeout );
+ $status->merge( $lockStatus );
+ if ( $lockStatus->isOK() ) {
+ return new self( $manager, $pathsByType, $status );
+ }
+ return null;
+ }
+
+ /**
+ * Release a scoped lock and set any errors in the attatched Status object.
+ * This is useful for early release of locks before function scope is destroyed.
+ * This is the same as setting the lock object to null.
+ *
+ * @param ScopedLock $lock
+ * @return void
+ * @since 1.21
+ */
+ public static function release( ScopedLock &$lock = null ) {
+ $lock = null;
+ }
+
+ /**
+ * Release the locks when this goes out of scope
+ */
+ function __destruct() {
+ $wasOk = $this->status->isOK();
+ $this->status->merge( $this->manager->unlockByType( $this->pathsByType ) );
+ if ( $wasOk ) {
+ // Make sure status is OK, despite any unlockFiles() fatals
+ $this->status->setResult( true, $this->status->value );
+ }
+ }
+}
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
index 9c8d85dc..42c9c945 100644
--- a/includes/filerepo/FSRepo.php
+++ b/includes/filerepo/FSRepo.php
@@ -24,9 +24,9 @@
/**
* A repository for files accessible via the local filesystem.
* Does not support database access or registration.
- *
+ *
* This is a mostly a legacy class. New uses should not be added.
- *
+ *
* @ingroup FileRepo
* @deprecated since 1.19
*/
@@ -46,6 +46,9 @@ class FSRepo extends FileRepo {
$thumbDir = isset( $info['thumbDir'] )
? $info['thumbDir']
: "{$directory}/thumb";
+ $transcodedDir = isset( $info['transcodedDir'] )
+ ? $info['transcodedDir']
+ : "{$directory}/transcoded";
$fileMode = isset( $info['fileMode'] )
? $info['fileMode']
: 0644;
@@ -53,15 +56,16 @@ class FSRepo extends FileRepo {
$repoName = $info['name'];
// Get the FS backend configuration
$backend = new FSFileBackend( array(
- 'name' => $info['name'] . '-backend',
- 'lockManager' => 'fsLockManager',
+ 'name' => $info['name'] . '-backend',
+ 'lockManager' => 'fsLockManager',
'containerPaths' => array(
- "{$repoName}-public" => "{$directory}",
- "{$repoName}-temp" => "{$directory}/temp",
- "{$repoName}-thumb" => $thumbDir,
+ "{$repoName}-public" => "{$directory}",
+ "{$repoName}-temp" => "{$directory}/temp",
+ "{$repoName}-thumb" => $thumbDir,
+ "{$repoName}-transcoded" => $transcodedDir,
"{$repoName}-deleted" => $deletedDir
),
- 'fileMode' => $fileMode,
+ 'fileMode' => $fileMode,
) );
// Update repo config to use this backend
$info['backend'] = $backend;
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index a31b148a..1195d5f8 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -67,7 +67,7 @@ class FileRepo {
*/
public function __construct( array $info = null ) {
// Verify required settings presence
- if(
+ if (
$info === null
|| !array_key_exists( 'name', $info )
|| !array_key_exists( 'backend', $info )
@@ -120,13 +120,16 @@ class FileRepo {
$this->isPrivate = !empty( $info['isPrivate'] );
// Give defaults for the basic zones...
$this->zones = isset( $info['zones'] ) ? $info['zones'] : array();
- foreach ( array( 'public', 'thumb', 'temp', 'deleted' ) as $zone ) {
+ foreach ( array( 'public', 'thumb', 'transcoded', 'temp', 'deleted' ) as $zone ) {
if ( !isset( $this->zones[$zone]['container'] ) ) {
$this->zones[$zone]['container'] = "{$this->name}-{$zone}";
}
if ( !isset( $this->zones[$zone]['directory'] ) ) {
$this->zones[$zone]['directory'] = '';
}
+ if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) {
+ $this->zones[$zone]['urlsByExt'] = array();
+ }
}
}
@@ -152,7 +155,7 @@ class FileRepo {
/**
* Check if a single zone or list of zones is defined for usage
*
- * @param $doZones Array Only do a particular zones
+ * @param array $doZones Only do a particular zones
* @throws MWException
* @return Status
*/
@@ -196,14 +199,17 @@ class FileRepo {
/**
* Get the URL corresponding to one of the four basic zones
*
- * @param $zone String: one of: public, deleted, temp, thumb
+ * @param string $zone One of: public, deleted, temp, thumb
+ * @param string|null $ext Optional file extension
* @return String or false
*/
- public function getZoneUrl( $zone ) {
- if ( isset( $this->zones[$zone]['url'] )
- && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) )
- {
- return $this->zones[$zone]['url']; // custom URL
+ public function getZoneUrl( $zone, $ext = null ) {
+ if ( in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) { // standard public zones
+ if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) {
+ return $this->zones[$zone]['urlsByExt'][$ext]; // custom URL for extension/zone
+ } elseif ( isset( $this->zones[$zone]['url'] ) ) {
+ return $this->zones[$zone]['url']; // custom URL for zone
+ }
}
switch ( $zone ) {
case 'public':
@@ -214,6 +220,8 @@ class FileRepo {
return false; // no public URL
case 'thumb':
return $this->thumbUrl;
+ case 'transcoded':
+ return "{$this->url}/transcoded";
default:
return false;
}
@@ -229,12 +237,12 @@ class FileRepo {
* from the URL path, one can configure thumb_handler.php to recognize a special path on the
* same host name as the wiki that is used for viewing thumbnails.
*
- * @param $zone String: one of: public, deleted, temp, thumb
+ * @param string $zone one of: public, deleted, temp, thumb
* @return String or false
*/
public function getZoneHandlerUrl( $zone ) {
if ( isset( $this->zones[$zone]['handlerUrl'] )
- && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) )
+ && in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) )
{
return $this->zones[$zone]['handlerUrl'];
}
@@ -251,19 +259,19 @@ class FileRepo {
*/
public function resolveVirtualUrl( $url ) {
if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
- throw new MWException( __METHOD__.': unknown protocol' );
+ throw new MWException( __METHOD__ . ': unknown protocol' );
}
$bits = explode( '/', substr( $url, 9 ), 3 );
if ( count( $bits ) != 3 ) {
- throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
+ throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
}
list( $repo, $zone, $rel ) = $bits;
if ( $repo !== $this->name ) {
- throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
+ throw new MWException( __METHOD__ . ": fetching from a foreign repo is not supported" );
}
$base = $this->getZonePath( $zone );
if ( !$base ) {
- throw new MWException( __METHOD__.": invalid zone: $zone" );
+ throw new MWException( __METHOD__ . ": invalid zone: $zone" );
}
return $base . '/' . rawurldecode( $rel );
}
@@ -332,7 +340,7 @@ class FileRepo {
* version control should return false if the time is specified.
*
* @param $title Mixed: Title object or string
- * @param $options array Associative array of options:
+ * @param array $options Associative array of options:
* time: requested time for a specific file version, or false for the
* current version. An image object will be returned which was
* created at the specified time (which may be archived or current).
@@ -375,7 +383,7 @@ class FileRepo {
return false;
}
$redir = $this->checkRedirect( $title );
- if ( $redir && $title->getNamespace() == NS_FILE) {
+ if ( $redir && $title->getNamespace() == NS_FILE ) {
$img = $this->newFile( $redir );
if ( !$img ) {
return false;
@@ -391,7 +399,7 @@ class FileRepo {
/**
* Find many files at once.
*
- * @param $items array An array of titles, or an array of findFile() options with
+ * @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 );
@@ -423,8 +431,8 @@ class FileRepo {
* Returns false if the file does not exist. Repositories not supporting
* version control should return false if the time is specified.
*
- * @param $sha1 String base 36 SHA-1 hash
- * @param $options array Option array, same as findFile().
+ * @param string $sha1 base 36 SHA-1 hash
+ * @param array $options Option array, same as findFile().
* @return File|bool False on failure
*/
public function findFileFromKey( $sha1, $options = array() ) {
@@ -468,7 +476,7 @@ class FileRepo {
* Get an array of arrays or iterators of file objects for files that
* have the given SHA-1 content hashes.
*
- * @param $hashes array An array of hashes
+ * @param array $hashes An array of hashes
* @return array An Array of arrays or iterators of file objects and the hash as key
*/
public function findBySha1s( array $hashes ) {
@@ -483,6 +491,18 @@ class FileRepo {
}
/**
+ * Return an array of files where the name starts with $prefix.
+ *
+ * STUB
+ * @param string $prefix The prefix to search for
+ * @param int $limit The maximum amount of files to return
+ * @return array
+ */
+ public function findFilesByPrefix( $prefix, $limit ) {
+ return array();
+ }
+
+ /**
* Get the public root URL of the repository
*
* @deprecated since 1.20
@@ -542,7 +562,7 @@ class FileRepo {
* Get a relative path including trailing slash, e.g. f/fa/
* If the repo is not hashed, returns an empty string
*
- * @param $name string Name of file
+ * @param string $name Name of file
* @return string
*/
public function getHashPath( $name ) {
@@ -553,7 +573,7 @@ class FileRepo {
* Get a relative path including trailing slash, e.g. f/fa/
* If the repo is not hashed, returns an empty string
*
- * @param $suffix string Basename of file from FileRepo::storeTemp()
+ * @param string $suffix Basename of file from FileRepo::storeTemp()
* @return string
*/
public function getTempHashPath( $suffix ) {
@@ -602,7 +622,7 @@ class FileRepo {
* Make an url to this repo
*
* @param $query mixed Query string to append
- * @param $entry string Entry point; defaults to index
+ * @param string $entry Entry point; defaults to index
* @return string|bool False on failure
*/
public function makeUrl( $query = '', $entry = 'index' ) {
@@ -656,8 +676,8 @@ class FileRepo {
* repository's file class, since it may return invalid results. User code
* should use File::getDescriptionText().
*
- * @param $name String: name of image to fetch
- * @param $lang String: language to fetch it in, if any.
+ * @param string $name name of image to fetch
+ * @param string $lang language to fetch it in, if any.
* @return string
*/
public function getDescriptionRenderUrl( $name, $lang = null ) {
@@ -688,7 +708,7 @@ class FileRepo {
public function getDescriptionStylesheetUrl() {
if ( isset( $this->scriptDirUrl ) ) {
return $this->makeUrl( 'title=MediaWiki:Filepage.css&' .
- wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) );
+ wfArrayToCgi( Skin::getDynamicStylesheetQuery() ) );
}
return false;
}
@@ -696,9 +716,9 @@ class FileRepo {
/**
* Store a file to a given destination.
*
- * @param $srcPath String: source file system path, storage path, or virtual URL
- * @param $dstZone String: destination zone
- * @param $dstRel String: destination relative path
+ * @param string $srcPath source file system path, storage path, or virtual URL
+ * @param string $dstZone destination zone
+ * @param string $dstRel destination relative path
* @param $flags Integer: bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source file after upload
* self::OVERWRITE Overwrite an existing destination file instead of failing
@@ -721,7 +741,7 @@ class FileRepo {
/**
* Store a batch of files
*
- * @param $triplets Array: (src, dest zone, dest rel) triplets as per store()
+ * @param array $triplets (src, dest zone, dest rel) triplets as per store()
* @param $flags Integer: bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source file after upload
* self::OVERWRITE Overwrite an existing destination file instead of failing
@@ -755,7 +775,7 @@ class FileRepo {
throw new MWException( 'Validation error in $dstRel' );
}
$dstPath = "$root/$dstRel";
- $dstDir = dirname( $dstPath );
+ $dstDir = dirname( $dstPath );
// Create destination directories for this triplet
if ( !$this->initDirectory( $dstDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $dstDir );
@@ -774,10 +794,10 @@ class FileRepo {
}
}
$operations[] = array(
- 'op' => $opName,
- 'src' => $srcPath,
- 'dst' => $dstPath,
- 'overwrite' => $flags & self::OVERWRITE,
+ 'op' => $opName,
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => $flags & self::OVERWRITE,
'overwriteSame' => $flags & self::OVERWRITE_SAME,
);
}
@@ -803,7 +823,7 @@ class FileRepo {
* Each file can be a (zone, rel) pair, virtual url, storage path.
* It will try to delete each file, but ignores any errors that may occur.
*
- * @param $files array List of files to delete
+ * @param array $files List of files to delete
* @param $flags Integer: bitwise combination of the following flags:
* self::SKIP_LOCKING Skip any file locking when doing the deletions
* @return FileRepoStatus
@@ -841,9 +861,9 @@ class FileRepo {
* This function can be used to write to otherwise read-only foreign repos.
* This is intended for copying generated thumbnails into the repo.
*
- * @param $src string Source file system path, storage path, or virtual URL
- * @param $dst string Virtual URL or storage path
- * @param $disposition string|null Content-Disposition if given and supported
+ * @param string $src Source file system path, storage path, or virtual URL
+ * @param string $dst Virtual URL or storage path
+ * @param string|null $disposition Content-Disposition if given and supported
* @return FileRepoStatus
*/
final public function quickImport( $src, $dst, $disposition = null ) {
@@ -855,7 +875,7 @@ class FileRepo {
* This function can be used to write to otherwise read-only foreign repos.
* This is intended for purging thumbnails.
*
- * @param $path string Virtual URL or storage path
+ * @param string $path Virtual URL or storage path
* @return FileRepoStatus
*/
final public function quickPurge( $path ) {
@@ -866,7 +886,7 @@ class FileRepo {
* Deletes a directory if empty.
* This function can be used to write to otherwise read-only foreign repos.
*
- * @param $dir string Virtual URL (or storage path) of directory to clean
+ * @param string $dir Virtual URL (or storage path) of directory to clean
* @return Status
*/
public function quickCleanDir( $dir ) {
@@ -886,7 +906,7 @@ class FileRepo {
* All path parameters may be a file system path, storage path, or virtual URL.
* When "dispositions" are given they are used as Content-Disposition if supported.
*
- * @param $triples Array List of (source path, destination path, disposition)
+ * @param array $triples List of (source path, destination path, disposition)
* @return FileRepoStatus
*/
public function quickImportBatch( array $triples ) {
@@ -897,9 +917,9 @@ class FileRepo {
$src = $this->resolveToStoragePath( $src );
$dst = $this->resolveToStoragePath( $dst );
$operations[] = array(
- 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store',
- 'src' => $src,
- 'dst' => $dst,
+ 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store',
+ 'src' => $src,
+ 'dst' => $dst,
'disposition' => isset( $triple[2] ) ? $triple[2] : null
);
$status->merge( $this->initDirectory( dirname( $dst ) ) );
@@ -914,7 +934,7 @@ class FileRepo {
* This function can be used to write to otherwise read-only foreign repos.
* This does no locking nor journaling and is intended for purging thumbnails.
*
- * @param $paths Array List of virtual URLs or storage paths
+ * @param array $paths List of virtual URLs or storage paths
* @return FileRepoStatus
*/
public function quickPurgeBatch( array $paths ) {
@@ -922,8 +942,8 @@ class FileRepo {
$operations = array();
foreach ( $paths as $path ) {
$operations[] = array(
- 'op' => 'delete',
- 'src' => $this->resolveToStoragePath( $path ),
+ 'op' => 'delete',
+ 'src' => $this->resolveToStoragePath( $path ),
'ignoreMissingSource' => true
);
}
@@ -937,19 +957,18 @@ class FileRepo {
* Returns a FileRepoStatus object with the file Virtual URL in the value,
* file can later be disposed using FileRepo::freeTemp().
*
- * @param $originalName String: the base name of the file as specified
+ * @param string $originalName the base name of the file as specified
* by the user. The file extension will be maintained.
- * @param $srcPath String: the current location of the file.
+ * @param string $srcPath the current location of the file.
* @return FileRepoStatus object with the URL in the value.
*/
public function storeTemp( $originalName, $srcPath ) {
$this->assertWritableRepo(); // fail out if read-only
- $date = gmdate( "YmdHis" );
- $hashPath = $this->getHashPath( $originalName );
- $dstRel = "{$hashPath}{$date}!{$originalName}";
- $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
- $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
+ $date = MWTimestamp::getInstance()->format( 'YmdHis' );
+ $hashPath = $this->getHashPath( $originalName );
+ $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
+ $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
$result = $this->quickImport( $srcPath, $virtualUrl );
$result->value = $virtualUrl;
@@ -960,7 +979,7 @@ class FileRepo {
/**
* Remove a temporary file or mark it for garbage collection
*
- * @param $virtualUrl String: the virtual URL returned by FileRepo::storeTemp()
+ * @param string $virtualUrl the virtual URL returned by FileRepo::storeTemp()
* @return Boolean: true on success, false on failure
*/
public function freeTemp( $virtualUrl ) {
@@ -968,7 +987,7 @@ class FileRepo {
$temp = $this->getVirtualUrl( 'temp' );
if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
- wfDebug( __METHOD__.": Invalid temp virtual URL\n" );
+ wfDebug( __METHOD__ . ": Invalid temp virtual URL\n" );
return false;
}
@@ -978,8 +997,8 @@ class FileRepo {
/**
* Concatenate a list of temporary files into a target file location.
*
- * @param $srcPaths Array Ordered list of source virtual URLs/storage paths
- * @param $dstPath String Target file system path
+ * @param array $srcPaths Ordered list of source virtual URLs/storage paths
+ * @param string $dstPath Target file system path
* @param $flags Integer: bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source files
* @return FileRepoStatus
@@ -1021,18 +1040,25 @@ class FileRepo {
* Returns a FileRepoStatus object. On success, the value contains "new" or
* "archived", to indicate whether the file was new with that name.
*
- * @param $srcPath String: the source file system path, storage path, or URL
- * @param $dstRel String: the destination relative path
- * @param $archiveRel String: the relative path where the existing file is to
+ * Options to $options include:
+ * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
+ *
+ * @param string $srcPath the source file system path, storage path, or URL
+ * @param string $dstRel the destination relative path
+ * @param string $archiveRel the relative path where the existing file is to
* be archived, if there is one. Relative to the public zone root.
* @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
* that the source file should be deleted if possible
+ * @param array $options Optional additional parameters
* @return FileRepoStatus
*/
- public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ public function publish(
+ $srcPath, $dstRel, $archiveRel, $flags = 0, array $options = array()
+ ) {
$this->assertWritableRepo(); // fail out if read-only
- $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
+ $status = $this->publishBatch(
+ array( array( $srcPath, $dstRel, $archiveRel, $options ) ), $flags );
if ( $status->successCount == 0 ) {
$status->ok = false;
}
@@ -1048,13 +1074,14 @@ class FileRepo {
/**
* Publish a batch of files
*
- * @param $triplets Array: (source, dest, archive) triplets as per publish()
+ * @param array $ntuples (source, dest, archive) triplets or
+ * (source, dest, archive, options) 4-tuples as per publish().
* @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
* that the source files should be deleted if possible
* @throws MWException
* @return FileRepoStatus
*/
- public function publishBatch( array $triplets, $flags = 0 ) {
+ public function publishBatch( array $ntuples, $flags = 0 ) {
$this->assertWritableRepo(); // fail out if read-only
$backend = $this->backend; // convenience
@@ -1069,8 +1096,9 @@ class FileRepo {
$operations = array();
$sourceFSFilesToDelete = array(); // cleanup for disk source files
// Validate each triplet and get the store operation...
- foreach ( $triplets as $i => $triplet ) {
- list( $srcPath, $dstRel, $archiveRel ) = $triplet;
+ foreach ( $ntuples as $ntuple ) {
+ list( $srcPath, $dstRel, $archiveRel ) = $ntuple;
+ $options = isset( $ntuple[3] ) ? $ntuple[3] : array();
// Resolve source to a storage path if virtual
$srcPath = $this->resolveToStoragePath( $srcPath );
if ( !$this->validateFilename( $dstRel ) ) {
@@ -1090,51 +1118,52 @@ class FileRepo {
if ( !$this->initDirectory( $dstDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $dstDir );
}
- if ( !$this->initDirectory($archiveDir )->isOK() ) {
+ if ( !$this->initDirectory( $archiveDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $archiveDir );
}
- // Archive destination file if it exists
- if ( $backend->fileExists( array( 'src' => $dstPath ) ) ) {
- // Check if the archive file exists
- // This is a sanity check to avoid data loss. In UNIX, the rename primitive
- // unlinks the destination file if it exists. DB-based synchronisation in
- // publishBatch's caller should prevent races. In Windows there's no
- // problem because the rename primitive fails if the destination exists.
- if ( $backend->fileExists( array( 'src' => $archivePath ) ) ) {
- $operations[] = array( 'op' => 'null' );
- continue;
- } else {
- $operations[] = array(
- 'op' => 'move',
- 'src' => $dstPath,
- 'dst' => $archivePath
- );
- }
- $status->value[$i] = 'archived';
- } else {
- $status->value[$i] = 'new';
- }
+ // Set any desired headers to be use in GET/HEAD responses
+ $headers = isset( $options['headers'] ) ? $options['headers'] : array();
+
+ // Archive destination file if it exists.
+ // This will check if the archive file also exists and fail if does.
+ // This is a sanity check to avoid data loss. On Windows and Linux,
+ // copy() will overwrite, so the existence check is vulnerable to
+ // race conditions unless an functioning LockManager is used.
+ // LocalFile also uses SELECT FOR UPDATE for synchronization.
+ $operations[] = array(
+ 'op' => 'copy',
+ 'src' => $dstPath,
+ 'dst' => $archivePath,
+ 'ignoreMissingSource' => true
+ );
+
// Copy (or move) the source file to the destination
if ( FileBackend::isStoragePath( $srcPath ) ) {
if ( $flags & self::DELETE_SOURCE ) {
$operations[] = array(
- 'op' => 'move',
- 'src' => $srcPath,
- 'dst' => $dstPath
+ 'op' => 'move',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
);
} else {
$operations[] = array(
- 'op' => 'copy',
- 'src' => $srcPath,
- 'dst' => $dstPath
+ 'op' => 'copy',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
);
}
} else { // FS source path
$operations[] = array(
- 'op' => 'store',
- 'src' => $srcPath,
- 'dst' => $dstPath
+ 'op' => 'store',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
);
if ( $flags & self::DELETE_SOURCE ) {
$sourceFSFilesToDelete[] = $srcPath;
@@ -1143,8 +1172,17 @@ class FileRepo {
}
// Execute the operations for each triplet
- $opts = array( 'force' => true );
- $status->merge( $backend->doOperations( $operations, $opts ) );
+ $status->merge( $backend->doOperations( $operations ) );
+ // Find out which files were archived...
+ foreach ( $ntuples as $i => $ntuple ) {
+ list( , , $archiveRel ) = $ntuple;
+ $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
+ if ( $this->fileExists( $archivePath ) ) {
+ $status->value[$i] = 'archived';
+ } else {
+ $status->value[$i] = 'new';
+ }
+ }
// Cleanup for disk source files...
foreach ( $sourceFSFilesToDelete as $file ) {
wfSuppressWarnings();
@@ -1159,12 +1197,12 @@ class FileRepo {
* Creates a directory with the appropriate zone permissions.
* Callers are responsible for doing read-only and "writable repo" checks.
*
- * @param $dir string Virtual URL (or storage path) of directory to clean
+ * @param string $dir Virtual URL (or storage path) of directory to clean
* @return Status
*/
protected function initDirectory( $dir ) {
$path = $this->resolveToStoragePath( $dir );
- list( $b, $container, $r ) = FileBackend::splitStoragePath( $path );
+ list( , $container, ) = FileBackend::splitStoragePath( $path );
$params = array( 'dir' => $path );
if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) {
@@ -1179,7 +1217,7 @@ class FileRepo {
/**
* Deletes a directory if empty.
*
- * @param $dir string Virtual URL (or storage path) of directory to clean
+ * @param string $dir Virtual URL (or storage path) of directory to clean
* @return Status
*/
public function cleanDir( $dir ) {
@@ -1195,7 +1233,7 @@ class FileRepo {
/**
* Checks existence of a a file
*
- * @param $file string Virtual URL (or storage path) of file to check
+ * @param string $file Virtual URL (or storage path) of file to check
* @return bool
*/
public function fileExists( $file ) {
@@ -1206,7 +1244,7 @@ class FileRepo {
/**
* Checks existence of an array of files.
*
- * @param $files Array: Virtual URLs (or storage paths) of files to check
+ * @param array $files Virtual URLs (or storage paths) of files to check
* @return array|bool Either array of files and existence flags, or false
*/
public function fileExistsBatch( array $files ) {
@@ -1244,7 +1282,7 @@ class FileRepo {
* assumes a naming scheme in the deleted zone based on content hash, as
* opposed to the public zone which is assumed to be unique.
*
- * @param $sourceDestPairs Array of source/destination pairs. Each element
+ * @param array $sourceDestPairs of source/destination pairs. Each element
* is a two-element array containing the source file path relative to the
* public root in the first element, and the archive file path relative
* to the deleted zone root in the second element.
@@ -1268,9 +1306,9 @@ class FileRepo {
foreach ( $sourceDestPairs as $pair ) {
list( $srcRel, $archiveRel ) = $pair;
if ( !$this->validateFilename( $srcRel ) ) {
- throw new MWException( __METHOD__.':Validation error in $srcRel' );
+ throw new MWException( __METHOD__ . ':Validation error in $srcRel' );
} elseif ( !$this->validateFilename( $archiveRel ) ) {
- throw new MWException( __METHOD__.':Validation error in $archiveRel' );
+ throw new MWException( __METHOD__ . ':Validation error in $archiveRel' );
}
$publicRoot = $this->getZonePath( 'public' );
@@ -1286,9 +1324,9 @@ class FileRepo {
}
$operations[] = array(
- 'op' => 'move',
- 'src' => $srcPath,
- 'dst' => $archivePath,
+ 'op' => 'move',
+ 'src' => $srcPath,
+ 'dst' => $archivePath,
// We may have 2+ identical files being deleted,
// all of which will map to the same destination file
'overwriteSame' => true // also see bug 31792
@@ -1318,9 +1356,13 @@ class FileRepo {
* e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
*
* @param $key string
+ * @throws MWException
* @return string
*/
public function getDeletedHashPath( $key ) {
+ if ( strlen( $key ) < 31 ) {
+ throw new MWException( "Invalid storage key '$key'." );
+ }
$path = '';
for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
$path .= $key[$i] . '/';
@@ -1392,6 +1434,17 @@ class FileRepo {
}
/**
+ * Get the size of a file with a given virtual URL/storage path
+ *
+ * @param $virtualUrl string
+ * @return integer|bool False on failure
+ */
+ public function getFileSize( $virtualUrl ) {
+ $path = $this->resolveToStoragePath( $virtualUrl );
+ return $this->backend->getFileSize( array( 'src' => $path ) );
+ }
+
+ /**
* Get the sha1 (base 36) of a file with a given virtual URL/storage path
*
* @param $virtualUrl string
@@ -1406,7 +1459,7 @@ class FileRepo {
* Attempt to stream a file with the given virtual URL/storage path
*
* @param $virtualUrl string
- * @param $headers Array Additional HTTP headers to send on success
+ * @param array $headers Additional HTTP headers to send on success
* @return bool Success
*/
public function streamFile( $virtualUrl, $headers = array() ) {
@@ -1511,7 +1564,7 @@ class FileRepo {
public function newFatal( $message /*, parameters...*/ ) {
$params = func_get_args();
array_unshift( $params, $this );
- return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params );
+ return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
}
/**
@@ -1568,7 +1621,7 @@ class FileRepo {
*/
public function nameForThumb( $name ) {
if ( strlen( $name ) > $this->abbrvThreshold ) {
- $ext = FileBackend::extensionFromPath( $name );
+ $ext = FileBackend::extensionFromPath( $name );
$name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
}
return $name;
@@ -1618,22 +1671,29 @@ class FileRepo {
*/
public function getTempRepo() {
return new TempFileRepo( array(
- 'name' => "{$this->name}-temp",
- 'backend' => $this->backend,
- 'zones' => array(
+ 'name' => "{$this->name}-temp",
+ 'backend' => $this->backend,
+ 'zones' => array(
'public' => array(
'container' => $this->zones['temp']['container'],
'directory' => $this->zones['temp']['directory']
),
- 'thumb' => array(
+ 'thumb' => array(
'container' => $this->zones['thumb']['container'],
'directory' => ( $this->zones['thumb']['directory'] == '' )
? 'temp'
: $this->zones['thumb']['directory'] . '/temp'
+ ),
+ 'transcoded' => array(
+ 'container' => $this->zones['transcoded']['container'],
+ 'directory' => ( $this->zones['transcoded']['directory'] == '' )
+ ? 'temp'
+ : $this->zones['transcoded']['directory'] . '/temp'
)
),
- 'url' => $this->getZoneUrl( 'temp' ),
- 'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp',
+ 'url' => $this->getZoneUrl( 'temp' ),
+ 'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp',
+ 'transcodedUrl' => $this->getZoneUrl( 'transcoded' ) . '/temp',
'hashLevels' => $this->hashLevels // performance
) );
}
@@ -1641,10 +1701,11 @@ class FileRepo {
/**
* Get an UploadStash associated with this repo.
*
+ * @param $user User
* @return UploadStash
*/
- public function getUploadStash() {
- return new UploadStash( $this );
+ public function getUploadStash( User $user = null ) {
+ return new UploadStash( $this, $user );
}
/**
@@ -1655,6 +1716,22 @@ class FileRepo {
* @throws MWException
*/
protected function assertWritableRepo() {}
+
+
+ /**
+ * Return information about the repository.
+ *
+ * @return array
+ * @since 1.22
+ */
+ public function getInfo() {
+ return array(
+ 'name' => $this->getName(),
+ 'displayname' => $this->getDisplayName(),
+ 'rootUrl' => $this->getRootUrl(),
+ 'local' => $this->isLocal(),
+ );
+ }
}
/**
diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php
index 13de9e6b..5eec9a50 100644
--- a/includes/filerepo/ForeignAPIRepo.php
+++ b/includes/filerepo/ForeignAPIRepo.php
@@ -61,26 +61,34 @@ class ForeignAPIRepo extends FileRepo {
// http://commons.wikimedia.org/w/api.php
$this->mApiBase = isset( $info['apibase'] ) ? $info['apibase'] : null;
- if( isset( $info['apiThumbCacheExpiry'] ) ) {
+ if ( isset( $info['apiThumbCacheExpiry'] ) ) {
$this->apiThumbCacheExpiry = $info['apiThumbCacheExpiry'];
}
- if( isset( $info['fileCacheExpiry'] ) ) {
+ if ( isset( $info['fileCacheExpiry'] ) ) {
$this->fileCacheExpiry = $info['fileCacheExpiry'];
}
- if( !$this->scriptDirUrl ) {
+ 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 ) {
+ if ( $this->canCacheThumbs() && !$this->url ) {
$this->url = $wgLocalFileRepo['url'];
}
- if( $this->canCacheThumbs() && !$this->thumbUrl ) {
+ if ( $this->canCacheThumbs() && !$this->thumbUrl ) {
$this->thumbUrl = $this->url . '/thumb';
}
}
/**
+ * @return string
+ * @since 1.22
+ */
+ function getApiUrl() {
+ return $this->mApiBase;
+ }
+
+ /**
* Per docs in FileRepo, this needs to return false if we don't support versioned
* files. Well, we don't.
*
@@ -102,10 +110,10 @@ class ForeignAPIRepo extends FileRepo {
function fileExistsBatch( array $files ) {
$results = array();
foreach ( $files as $k => $f ) {
- if ( isset( $this->mFileExists[$k] ) ) {
- $results[$k] = true;
+ if ( isset( $this->mFileExists[$f] ) ) {
+ $results[$k] = $this->mFileExists[$f];
unset( $files[$k] );
- } elseif( self::isVirtualUrl( $f ) ) {
+ } 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.
@@ -120,11 +128,27 @@ class ForeignAPIRepo extends FileRepo {
$data = $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++;
+ if ( isset( $data['query']['pages'] ) ) {
+ # First, get results from the query. Note we only care whether the image exists,
+ # not whether it has a description page.
+ foreach ( $data['query']['pages'] as $p ) {
+ $this->mFileExists[$p['title']] = ( $p['imagerepository'] !== '' );
+ }
+ # Second, copy the results to any redirects that were queried
+ if ( isset( $data['query']['redirects'] ) ) {
+ foreach ( $data['query']['redirects'] as $r ) {
+ $this->mFileExists[$r['from']] = $this->mFileExists[$r['to']];
+ }
+ }
+ # Third, copy the results to any non-normalized titles that were queried
+ if ( isset( $data['query']['normalized'] ) ) {
+ foreach ( $data['query']['normalized'] as $n ) {
+ $this->mFileExists[$n['from']] = $this->mFileExists[$n['to']];
+ }
+ }
+ # Finally, copy the results to the output
+ foreach ( $files as $key => $file ) {
+ $results[$key] = $this->mFileExists[$file];
}
}
return $results;
@@ -143,38 +167,26 @@ class ForeignAPIRepo extends FileRepo {
* @return string
*/
function fetchImageQuery( $query ) {
- global $wgMemc;
+ global $wgMemc, $wgLanguageCode;
$query = array_merge( $query,
array(
- 'format' => 'json',
- 'action' => 'query',
+ 'format' => 'json',
+ 'action' => 'query',
'redirects' => 'true'
) );
- if ( $this->mApiBase ) {
- $url = wfAppendQuery( $this->mApiBase, $query );
- } else {
- $url = $this->makeUrl( $query, 'api' );
+
+ if ( !isset( $query['uselang'] ) ) { // uselang is unset or null
+ $query['uselang'] = $wgLanguageCode;
}
- if( !isset( $this->mQueryCache[$url] ) ) {
- $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) );
- $data = $wgMemc->get( $key );
- if( !$data ) {
- $data = self::httpGet( $url );
- if ( !$data ) {
- return null;
- }
- $wgMemc->set( $key, $data, 3600 );
- }
+ $data = $this->httpGetCached( 'Metadata', $query );
- if( count( $this->mQueryCache ) > 100 ) {
- // Keep the cache from growing infinitely
- $this->mQueryCache = array();
- }
- $this->mQueryCache[$url] = $data;
+ if ( $data ) {
+ return FormatJson::decode( $data, true );
+ } else {
+ return null;
}
- return FormatJson::decode( $this->mQueryCache[$url], true );
}
/**
@@ -182,9 +194,9 @@ class ForeignAPIRepo extends FileRepo {
* @return bool|array
*/
function getImageInfo( $data ) {
- if( $data && isset( $data['query']['pages'] ) ) {
- foreach( $data['query']['pages'] as $info ) {
- if( isset( $info['imageinfo'][0] ) ) {
+ if ( $data && isset( $data['query']['pages'] ) ) {
+ foreach ( $data['query']['pages'] as $info ) {
+ if ( isset( $info['imageinfo'][0] ) ) {
return $info['imageinfo'][0];
}
}
@@ -198,14 +210,15 @@ class ForeignAPIRepo extends FileRepo {
*/
function findBySha1( $hash ) {
$results = $this->fetchImageQuery( array(
- 'aisha1base36' => $hash,
- 'aiprop' => ForeignAPIFile::getProps(),
- 'list' => 'allimages', ) );
+ 'aisha1base36' => $hash,
+ 'aiprop' => ForeignAPIFile::getProps(),
+ 'list' => 'allimages',
+ ) );
$ret = array();
if ( isset( $results['query']['allimages'] ) ) {
foreach ( $results['query']['allimages'] as $img ) {
// 1.14 was broken, doesn't return name attribute
- if( !isset( $img['name'] ) ) {
+ if ( !isset( $img['name'] ) ) {
continue;
}
$ret[] = new ForeignAPIFile( Title::makeTitle( NS_FILE, $img['name'] ), $this, $img );
@@ -228,11 +241,11 @@ class ForeignAPIRepo extends FileRepo {
'iiprop' => 'url|timestamp',
'iiurlwidth' => $width,
'iiurlheight' => $height,
- 'iiurlparam' => $otherParams,
+ 'iiurlparam' => $otherParams,
'prop' => 'imageinfo' ) );
$info = $this->getImageInfo( $data );
- if( $data && $info && isset( $info['thumburl'] ) ) {
+ if ( $data && $info && isset( $info['thumburl'] ) ) {
wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" );
$result = $info;
return $info['thumburl'];
@@ -242,15 +255,49 @@ class ForeignAPIRepo extends FileRepo {
}
/**
+ * @param $name string
+ * @param $width int
+ * @param $height int
+ * @param $otherParams string
+ * @return bool|MediaTransformError
+ * @since 1.22
+ */
+ function getThumbError( $name, $width = -1, $height = -1, $otherParams = '', $lang = null ) {
+ $data = $this->fetchImageQuery( array(
+ 'titles' => 'File:' . $name,
+ 'iiprop' => 'url|timestamp',
+ 'iiurlwidth' => $width,
+ 'iiurlheight' => $height,
+ 'iiurlparam' => $otherParams,
+ 'prop' => 'imageinfo',
+ 'uselang' => $lang,
+ ) );
+ $info = $this->getImageInfo( $data );
+
+ if ( $data && $info && isset( $info['thumberror'] ) ) {
+ wfDebug( __METHOD__ . " got remote thumb error " . $info['thumberror'] . "\n" );
+ return new MediaTransformError(
+ 'thumbnail_error_remote',
+ $width,
+ $height,
+ $this->getDisplayName(),
+ $info['thumberror'] // already parsed message from foreign repo
+ );
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Return the imageurl from cache if possible
*
* If the url has been requested today, get it from cache
* Otherwise retrieve remote thumb url, check for local file.
*
- * @param $name String is a dbkey form of a title
+ * @param string $name is a dbkey form of a title
* @param $width
* @param $height
- * @param String $params Other rendering parameters (page number, etc) from handler's makeParamString.
+ * @param string $params Other rendering parameters (page number, etc) from handler's makeParamString.
* @return bool|string
*/
function getThumbUrlFromCache( $name, $width, $height, $params = "" ) {
@@ -267,14 +314,14 @@ class ForeignAPIRepo extends FileRepo {
$sizekey = "$width:$height:$params";
/* Get the array of urls that we already know */
- $knownThumbUrls = $wgMemc->get($key);
- if( !$knownThumbUrls ) {
+ $knownThumbUrls = $wgMemc->get( $key );
+ if ( !$knownThumbUrls ) {
/* No knownThumbUrls for this file */
$knownThumbUrls = array();
} else {
- if( isset( $knownThumbUrls[$sizekey] ) ) {
+ if ( isset( $knownThumbUrls[$sizekey] ) ) {
wfDebug( __METHOD__ . ': Got thumburl from local cache: ' .
- "{$knownThumbUrls[$sizekey]} \n");
+ "{$knownThumbUrls[$sizekey]} \n" );
return $knownThumbUrls[$sizekey];
}
/* This size is not yet known */
@@ -283,30 +330,29 @@ class ForeignAPIRepo extends FileRepo {
$metadata = null;
$foreignUrl = $this->getThumbUrl( $name, $width, $height, $metadata, $params );
- if( !$foreignUrl ) {
+ if ( !$foreignUrl ) {
wfDebug( __METHOD__ . " Could not find thumburl\n" );
return false;
}
// We need the same filename as the remote one :)
$fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) );
- if( !$this->validateFilename( $fileName ) ) {
+ if ( !$this->validateFilename( $fileName ) ) {
wfDebug( __METHOD__ . " The deduced filename $fileName is not safe\n" );
return false;
}
- $localPath = $this->getZonePath( 'thumb' ) . "/" . $this->getHashPath( $name ) . $name;
+ $localPath = $this->getZonePath( 'thumb' ) . "/" . $this->getHashPath( $name ) . $name;
$localFilename = $localPath . "/" . $fileName;
- $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . rawurlencode( $name ) . "/" . rawurlencode( $fileName );
+ $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . rawurlencode( $name ) . "/" . rawurlencode( $fileName );
- if( $backend->fileExists( array( 'src' => $localFilename ) )
- && isset( $metadata['timestamp'] ) )
- {
+ if ( $backend->fileExists( array( 'src' => $localFilename ) )
+ && isset( $metadata['timestamp'] ) ) {
wfDebug( __METHOD__ . " Thumbnail was already downloaded before\n" );
$modified = $backend->getFileTimestamp( array( 'src' => $localFilename ) );
$remoteModified = strtotime( $metadata['timestamp'] );
$current = time();
$diff = abs( $modified - $current );
- if( $remoteModified < $modified && $diff < $this->fileCacheExpiry ) {
+ if ( $remoteModified < $modified && $diff < $this->fileCacheExpiry ) {
/* Use our current and already downloaded thumbnail */
$knownThumbUrls[$sizekey] = $localUrl;
$wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry );
@@ -315,16 +361,15 @@ class ForeignAPIRepo extends FileRepo {
/* There is a new Commons file, or existing thumbnail older than a month */
}
$thumb = self::httpGet( $foreignUrl );
- if( !$thumb ) {
+ if ( !$thumb ) {
wfDebug( __METHOD__ . " Could not download thumb\n" );
return false;
}
-
# @todo FIXME: Delete old thumbs that aren't being used. Maintenance script?
$backend->prepare( array( 'dir' => dirname( $localFilename ) ) );
$params = array( 'dst' => $localFilename, 'content' => $thumb );
- if( !$backend->quickCreate( $params )->isOK() ) {
+ if ( !$backend->quickCreate( $params )->isOK() ) {
wfDebug( __METHOD__ . " could not write to thumb path '$localFilename'\n" );
return $foreignUrl;
}
@@ -337,16 +382,17 @@ class ForeignAPIRepo extends FileRepo {
/**
* @see FileRepo::getZoneUrl()
* @param $zone String
+ * @param string|null $ext Optional file extension
* @return String
*/
- function getZoneUrl( $zone ) {
+ function getZoneUrl( $zone, $ext = null ) {
switch ( $zone ) {
case 'public':
return $this->url;
case 'thumb':
return $this->thumbUrl;
default:
- return parent::getZoneUrl( $zone );
+ return parent::getZoneUrl( $zone, $ext );
}
}
@@ -380,6 +426,36 @@ class ForeignAPIRepo extends FileRepo {
}
/**
+ * Get information about the repo - overrides/extends the parent
+ * class's information.
+ * @return array
+ * @since 1.22
+ */
+ function getInfo() {
+ $info = parent::getInfo();
+ $info['apiurl'] = $this->getApiUrl();
+
+ $query = array(
+ 'format' => 'json',
+ 'action' => 'query',
+ 'meta' => 'siteinfo',
+ 'siprop' => 'general',
+ );
+
+ $data = $this->httpGetCached( 'SiteInfo', $query, 7200 );
+
+ if ( $data ) {
+ $siteInfo = FormatJson::decode( $data, true );
+ $general = $siteInfo['query']['general'];
+
+ $info['articlepath'] = $general['articlepath'];
+ $info['server'] = $general['server'];
+ }
+
+ return $info;
+ }
+
+ /**
* Like a Http:get request, but with custom User-Agent.
* @see Http:get
* @param $url string
@@ -410,6 +486,46 @@ class ForeignAPIRepo extends FileRepo {
}
/**
+ * HTTP GET request to a mediawiki API (with caching)
+ * @param $target string Used in cache key creation, mostly
+ * @param $query array The query parameters for the API request
+ * @param $cacheTTL int Time to live for the memcached caching
+ */
+ public function httpGetCached( $target, $query, $cacheTTL = 3600 ) {
+ if ( $this->mApiBase ) {
+ $url = wfAppendQuery( $this->mApiBase, $query );
+ } else {
+ $url = $this->makeUrl( $query, 'api' );
+ }
+
+ if ( !isset( $this->mQueryCache[$url] ) ) {
+ global $wgMemc;
+
+ $key = $this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) );
+ $data = $wgMemc->get( $key );
+
+ if ( !$data ) {
+ $data = self::httpGet( $url );
+
+ if ( !$data ) {
+ return null;
+ }
+
+ $wgMemc->set( $key, $data, $cacheTTL );
+ }
+
+ if ( count( $this->mQueryCache ) > 100 ) {
+ // Keep the cache from growing infinitely
+ $this->mQueryCache = array();
+ }
+
+ $this->mQueryCache[$url] = $data;
+ }
+
+ return $this->mQueryCache[$url];
+ }
+
+ /**
* @param $callback Array|string
* @throws MWException
*/
diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php
index 4b206c3d..37c65723 100644
--- a/includes/filerepo/ForeignDBRepo.php
+++ b/includes/filerepo/ForeignDBRepo.php
@@ -59,11 +59,12 @@ class ForeignDBRepo extends LocalRepo {
$this->dbConn = DatabaseBase::factory( $this->dbType,
array(
'host' => $this->dbServer,
- 'user' => $this->dbUser,
+ 'user' => $this->dbUser,
'password' => $this->dbPassword,
'dbname' => $this->dbName,
'flags' => $this->dbFlags,
- 'tablePrefix' => $this->tablePrefix
+ 'tablePrefix' => $this->tablePrefix,
+ 'foreign' => true,
)
);
}
@@ -86,7 +87,7 @@ class ForeignDBRepo extends LocalRepo {
/**
* Get a key on the primary cache for this repository.
- * Returns false if the repository's cache is not accessible at this site.
+ * Returns false if the repository's cache is not accessible at this site.
* The parameters are the parts of the key, as for wfMemcKey().
* @return bool|mixed
*/
diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php
index bd76fce7..7951fb13 100644
--- a/includes/filerepo/ForeignDBViaLBRepo.php
+++ b/includes/filerepo/ForeignDBViaLBRepo.php
@@ -61,7 +61,7 @@ class ForeignDBViaLBRepo extends LocalRepo {
/**
* Get a key on the primary cache for this repository.
- * Returns false if the repository's cache is not accessible at this site.
+ * Returns false if the repository's cache is not accessible at this site.
* The parameters are the parts of the key, as for wfMemcKey().
* @return bool|string
*/
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
index 0954422d..9b62243b 100644
--- a/includes/filerepo/LocalRepo.php
+++ b/includes/filerepo/LocalRepo.php
@@ -47,7 +47,7 @@ class LocalRepo extends FileRepo {
} elseif ( isset( $row->oi_name ) ) {
return call_user_func( $this->oldFileFromRowFactory, $row, $this );
} else {
- throw new MWException( __METHOD__.': invalid row' );
+ throw new MWException( __METHOD__ . ': invalid row' );
}
}
@@ -103,8 +103,8 @@ class LocalRepo extends FileRepo {
/**
* Check if a deleted (filearchive) file has this sha1 key
*
- * @param $key String File storage key (base-36 sha1 key with file extension)
- * @param $lock String|null Use "lock" to lock the row via FOR UPDATE
+ * @param string $key File storage key (base-36 sha1 key with file extension)
+ * @param string|null $lock Use "lock" to lock the row via FOR UPDATE
* @return bool File with this key is in use
*/
protected function deletedFileHasKey( $key, $lock = null ) {
@@ -120,8 +120,8 @@ class LocalRepo extends FileRepo {
/**
* Check if a hidden (revision delete) file has this sha1 key
*
- * @param $key String File storage key (base-36 sha1 key with file extension)
- * @param $lock String|null Use "lock" to lock the row via FOR UPDATE
+ * @param string $key File storage key (base-36 sha1 key with file extension)
+ * @param string|null $lock Use "lock" to lock the row via FOR UPDATE
* @return bool File with this key is in use
*/
protected function hiddenFileHasKey( $key, $lock = null ) {
@@ -168,16 +168,16 @@ class LocalRepo extends FileRepo {
$expiry = 86400; // has invalidation, 1 day
}
$cachedValue = $wgMemc->get( $memcKey );
- if ( $cachedValue === ' ' || $cachedValue === '' ) {
+ if ( $cachedValue === ' ' || $cachedValue === '' ) {
// Does not exist
return false;
- } elseif ( strval( $cachedValue ) !== '' ) {
+ } elseif ( strval( $cachedValue ) !== '' && $cachedValue !== ' PURGED' ) {
return Title::newFromText( $cachedValue, NS_FILE );
} // else $cachedValue is false or null: cache miss
$id = $this->getArticleID( $title );
- if( !$id ) {
- $wgMemc->set( $memcKey, " ", $expiry );
+ if ( !$id ) {
+ $wgMemc->add( $memcKey, " ", $expiry );
return false;
}
$dbr = $this->getSlaveDB();
@@ -188,12 +188,12 @@ class LocalRepo extends FileRepo {
__METHOD__
);
- if( $row && $row->rd_namespace == NS_FILE ) {
+ if ( $row && $row->rd_namespace == NS_FILE ) {
$targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title );
- $wgMemc->set( $memcKey, $targetTitle->getDBkey(), $expiry );
+ $wgMemc->add( $memcKey, $targetTitle->getDBkey(), $expiry );
return $targetTitle;
} else {
- $wgMemc->set( $memcKey, '', $expiry );
+ $wgMemc->add( $memcKey, '', $expiry );
return false;
}
}
@@ -206,18 +206,18 @@ class LocalRepo extends FileRepo {
* @return bool|int|mixed
*/
protected function getArticleID( $title ) {
- if( !$title instanceof Title ) {
+ if ( !$title instanceof Title ) {
return 0;
}
$dbr = $this->getSlaveDB();
$id = $dbr->selectField(
'page', // Table
- 'page_id', //Field
- array( //Conditions
+ 'page_id', //Field
+ array( //Conditions
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey(),
),
- __METHOD__ //Function name
+ __METHOD__ //Function name
);
return $id;
}
@@ -226,7 +226,7 @@ class LocalRepo extends FileRepo {
* Get an array or iterator of file objects for files that have a given
* SHA-1 content hash.
*
- * @param $hash String a sha1 hash to look for
+ * @param string $hash a sha1 hash to look for
* @return Array
*/
function findBySha1( $hash ) {
@@ -238,7 +238,7 @@ class LocalRepo extends FileRepo {
__METHOD__,
array( 'ORDER BY' => 'img_name' )
);
-
+
$result = array();
foreach ( $res as $row ) {
$result[] = $this->newFileFromRow( $row );
@@ -254,11 +254,11 @@ class LocalRepo extends FileRepo {
*
* Overrides generic implementation in FileRepo for performance reason
*
- * @param $hashes array An array of hashes
+ * @param array $hashes An array of hashes
* @return array An Array of arrays or iterators of file objects and the hash as key
*/
function findBySha1s( array $hashes ) {
- if( !count( $hashes ) ) {
+ if ( !count( $hashes ) ) {
return array(); //empty parameter
}
@@ -282,6 +282,34 @@ class LocalRepo extends FileRepo {
}
/**
+ * Return an array of files where the name starts with $prefix.
+ *
+ * @param string $prefix The prefix to search for
+ * @param int $limit The maximum amount of files to return
+ * @return array
+ */
+ public function findFilesByPrefix( $prefix, $limit ) {
+ $selectOptions = array( 'ORDER BY' => 'img_name', 'LIMIT' => intval( $limit ) );
+
+ // Query database
+ $dbr = $this->getSlaveDB();
+ $res = $dbr->select(
+ 'image',
+ LocalFile::selectFields(),
+ 'img_name ' . $dbr->buildLike( $prefix, $dbr->anyString() ),
+ __METHOD__,
+ $selectOptions
+ );
+
+ // Build file objects
+ $files = array();
+ foreach ( $res as $row ) {
+ $files[] = $this->newFileFromRow( $row );
+ }
+ return $files;
+ }
+
+ /**
* Get a connection to the slave DB
* @return DatabaseBase
*/
@@ -299,7 +327,7 @@ class LocalRepo extends FileRepo {
/**
* Get a key on the primary cache for this repository.
- * Returns false if the repository's cache is not accessible at this site.
+ * Returns false if the repository's cache is not accessible at this site.
* The parameters are the parts of the key, as for wfMemcKey().
*
* @return string
@@ -319,8 +347,11 @@ class LocalRepo extends FileRepo {
global $wgMemc;
$memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) );
if ( $memcKey ) {
- $wgMemc->delete( $memcKey );
+ // Set a temporary value for the cache key, to ensure
+ // that this value stays purged long enough so that
+ // it isn't refreshed with a stale value due to a
+ // lagged slave.
+ $wgMemc->set( $memcKey, ' PURGED', 12 );
}
}
}
-
diff --git a/includes/filerepo/README b/includes/filerepo/README
index 885a1ded..1423d359 100644
--- a/includes/filerepo/README
+++ b/includes/filerepo/README
@@ -18,10 +18,10 @@ repository-specific configuration is needed, or in static members of File or
FileRepo, where no such configuration is needed.
File objects are generated by a factory function from the repository. The
-repository thus has full control over the behaviour of its subsidiary file
+repository thus has full control over the behavior of its subsidiary file
class, since it can subclass the file class and override functionality at its
whim. Thus there is no need for the File subclass to query its parent repository
-for information about repository-class-dependent behaviour -- the file subclass
+for information about repository-class-dependent behavior -- the file subclass
is generally fully aware of the static preferences of its repository. Limited
exceptions can be made to this rule to permit sharing of functions, or perhaps
even entire classes, between repositories.
@@ -39,22 +39,3 @@ LocalRepo.php. LocalRepo provides only file access, and LocalFile provides
database access and higher-level functions such as cache management.
Tim Starling, June 2007
-
-Structure:
-
-File defines an abstract class File.
- ForeignAPIFile extends File.
- LocalFile extends File.
- ForeignDBFile extends LocalFile
- Image extends LocalFile
- UnregisteredLocalFile extends File.
- UploadStashFile extends UnregisteredLocalFile.
-FileRepo defines an abstract class FileRepo.
- ForeignAPIRepo extends FileRepo
- FSRepo extends FileRepo
- LocalRepo extends FSRepo
- ForeignDBRepo extends LocalRepo
- ForeignDBViaLBRepo extends LocalRepo
- NullRepo extends FileRepo
-
-Russ Nelson, March 2011
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
index f9e57599..b2b9477a 100644
--- a/includes/filerepo/RepoGroup.php
+++ b/includes/filerepo/RepoGroup.php
@@ -79,8 +79,8 @@ class RepoGroup {
/**
* Construct a group of file repositories.
*
- * @param $localInfo array Associative array for local repo's info
- * @param $foreignInfo Array of repository info arrays.
+ * @param array $localInfo Associative array for local repo's info
+ * @param array $foreignInfo of repository info arrays.
* Each info array is an associative array with the 'class' member
* giving the class name. The entire array is passed to the repository
* constructor as the first parameter.
@@ -96,7 +96,7 @@ class RepoGroup {
* You can also use wfFindFile() to do this.
*
* @param $title Title|string Title object or string
- * @param $options array Associative array of options:
+ * @param array $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.
@@ -131,7 +131,7 @@ class RepoGroup {
$time = isset( $options['time'] ) ? $options['time'] : '';
$dbkey = $title->getDBkey();
if ( isset( $this->cache[$dbkey][$time] ) ) {
- wfDebug( __METHOD__.": got File:$dbkey from process cache\n" );
+ 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
$this->pingCache( $dbkey );
# Return the entry
@@ -209,7 +209,7 @@ class RepoGroup {
}
$redir = $this->localRepo->checkRedirect( $title );
- if( $redir ) {
+ if ( $redir ) {
return $redir;
}
foreach ( $this->foreignRepos as $repo ) {
@@ -225,8 +225,8 @@ class RepoGroup {
* Find an instance of the file with this key, created at the specified time
* Returns false if the file does not exist.
*
- * @param $hash String base 36 SHA-1 hash
- * @param $options array Option array, same as findFile()
+ * @param string $hash base 36 SHA-1 hash
+ * @param array $options Option array, same as findFile()
* @return File object or false if it is not found
*/
function findFileFromKey( $hash, $options = array() ) {
@@ -238,7 +238,9 @@ class RepoGroup {
if ( !$file ) {
foreach ( $this->foreignRepos as $repo ) {
$file = $repo->findFileFromKey( $hash, $options );
- if ( $file ) break;
+ if ( $file ) {
+ break;
+ }
}
}
return $file;
@@ -247,7 +249,7 @@ class RepoGroup {
/**
* Find all instances of files with this key
*
- * @param $hash String base 36 SHA-1 hash
+ * @param string $hash base 36 SHA-1 hash
* @return Array of File objects
*/
function findBySha1( $hash ) {
@@ -266,7 +268,7 @@ class RepoGroup {
/**
* Find all instances of files with this keys
*
- * @param $hashes Array base 36 SHA-1 hashes
+ * @param array $hashes base 36 SHA-1 hashes
* @return Array of array of File objects
*/
function findBySha1s( array $hashes ) {
@@ -279,7 +281,7 @@ class RepoGroup {
$result = array_merge_recursive( $result, $repo->findBySha1s( $hashes ) );
}
//sort the merged (and presorted) sublist of each hash
- foreach( $result as $hash => $files ) {
+ foreach ( $result as $hash => $files ) {
usort( $result[$hash], 'File::compare' );
}
return $result;
@@ -335,13 +337,13 @@ class RepoGroup {
* first parameter.
*
* @param $callback Callback: the function to call
- * @param $params Array: optional additional parameters to pass to the function
+ * @param array $params optional additional parameters to pass to the function
* @return bool
*/
function forEachForeignRepo( $callback, $params = array() ) {
- foreach( $this->foreignRepos as $repo ) {
+ foreach ( $this->foreignRepos as $repo ) {
$args = array_merge( array( $repo ), $params );
- if( call_user_func_array( $callback, $args ) ) {
+ if ( call_user_func_array( $callback, $args ) ) {
return true;
}
}
@@ -388,12 +390,12 @@ class RepoGroup {
*/
function splitVirtualUrl( $url ) {
if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
- throw new MWException( __METHOD__.': unknown protocol' );
+ throw new MWException( __METHOD__ . ': unknown protocol' );
}
$bits = explode( '/', substr( $url, 9 ), 3 );
if ( count( $bits ) != 3 ) {
- throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
+ throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
}
return $bits;
}
@@ -433,7 +435,7 @@ class RepoGroup {
while ( count( $this->cache ) >= self::MAX_CACHE_SIZE ) {
reset( $this->cache );
$key = key( $this->cache );
- wfDebug( __METHOD__.": evicting $key\n" );
+ wfDebug( __METHOD__ . ": evicting $key\n" );
unset( $this->cache[$key] );
}
}
diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php
index c5a0bd1b..749f11a5 100644
--- a/includes/filerepo/file/ArchivedFile.php
+++ b/includes/filerepo/file/ArchivedFile.php
@@ -47,6 +47,7 @@ class ArchivedFile {
$timestamp, # time of upload
$dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
$deleted, # Bitfield akin to rev_deleted
+ $sha1, # sha1 hash of file content
$pageCount,
$archive_name;
@@ -67,7 +68,7 @@ class ArchivedFile {
* @param int $id
* @param string $key
*/
- function __construct( $title, $id=0, $key='' ) {
+ function __construct( $title, $id = 0, $key = '' ) {
$this->id = -1;
$this->title = false;
$this->name = false;
@@ -87,17 +88,18 @@ class ArchivedFile {
$this->deleted = 0;
$this->dataLoaded = false;
$this->exists = false;
+ $this->sha1 = '';
- if( $title instanceof Title ) {
+ if ( $title instanceof Title ) {
$this->title = File::normalizeTitle( $title, 'exception' );
$this->name = $title->getDBkey();
}
- if ($id) {
+ if ( $id ) {
$this->id = $id;
}
- if ($key) {
+ if ( $key ) {
$this->key = $key;
}
@@ -108,6 +110,7 @@ class ArchivedFile {
/**
* Loads a file object from the filearchive table
+ * @throws MWException
* @return bool|null True on success or null
*/
public function load() {
@@ -116,75 +119,41 @@ class ArchivedFile {
}
$conds = array();
- if( $this->id > 0 ) {
+ if ( $this->id > 0 ) {
$conds['fa_id'] = $this->id;
}
- if( $this->key ) {
+ if ( $this->key ) {
$conds['fa_storage_group'] = $this->group;
$conds['fa_storage_key'] = $this->key;
}
- if( $this->title ) {
+ if ( $this->title ) {
$conds['fa_name'] = $this->title->getDBkey();
}
- if( !count($conds)) {
+ if ( !count( $conds ) ) {
throw new MWException( "No specific information for retrieving archived file" );
}
- if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
+ if ( !$this->title || $this->title->getNamespace() == NS_FILE ) {
+ $this->dataLoaded = true; // set it here, to have also true on miss
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'filearchive',
- array(
- 'fa_id',
- 'fa_name',
- 'fa_archive_name',
- 'fa_storage_key',
- 'fa_storage_group',
- 'fa_size',
- 'fa_bits',
- 'fa_width',
- 'fa_height',
- 'fa_metadata',
- 'fa_media_type',
- 'fa_major_mime',
- 'fa_minor_mime',
- 'fa_description',
- 'fa_user',
- 'fa_user_text',
- 'fa_timestamp',
- 'fa_deleted' ),
+ $row = $dbr->selectRow(
+ 'filearchive',
+ self::selectFields(),
$conds,
__METHOD__,
- array( 'ORDER BY' => 'fa_timestamp DESC' ) );
- if ( $res == false || $dbr->numRows( $res ) == 0 ) {
- // this revision does not exist?
+ array( 'ORDER BY' => 'fa_timestamp DESC' )
+ );
+ if ( !$row ) {
+ // this revision does not exist?
return null;
}
- $ret = $dbr->resultObject( $res );
- $row = $ret->fetchObject();
// initialize fields for filestore image object
- $this->id = intval($row->fa_id);
- $this->name = $row->fa_name;
- $this->archive_name = $row->fa_archive_name;
- $this->group = $row->fa_storage_group;
- $this->key = $row->fa_storage_key;
- $this->size = $row->fa_size;
- $this->bits = $row->fa_bits;
- $this->width = $row->fa_width;
- $this->height = $row->fa_height;
- $this->metadata = $row->fa_metadata;
- $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
- $this->media_type = $row->fa_media_type;
- $this->description = $row->fa_description;
- $this->user = $row->fa_user;
- $this->user_text = $row->fa_user_text;
- $this->timestamp = $row->fa_timestamp;
- $this->deleted = $row->fa_deleted;
+ $this->loadFromRow( $row );
} else {
throw new MWException( 'This title does not correspond to an image page.' );
}
- $this->dataLoaded = true;
$this->exists = true;
return true;
@@ -199,26 +168,69 @@ class ArchivedFile {
*/
public static function newFromRow( $row ) {
$file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
+ $file->loadFromRow( $row );
+ return $file;
+ }
- $file->id = intval($row->fa_id);
- $file->name = $row->fa_name;
- $file->archive_name = $row->fa_archive_name;
- $file->group = $row->fa_storage_group;
- $file->key = $row->fa_storage_key;
- $file->size = $row->fa_size;
- $file->bits = $row->fa_bits;
- $file->width = $row->fa_width;
- $file->height = $row->fa_height;
- $file->metadata = $row->fa_metadata;
- $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
- $file->media_type = $row->fa_media_type;
- $file->description = $row->fa_description;
- $file->user = $row->fa_user;
- $file->user_text = $row->fa_user_text;
- $file->timestamp = $row->fa_timestamp;
- $file->deleted = $row->fa_deleted;
+ /**
+ * Fields in the filearchive table
+ * @return array
+ */
+ static function selectFields() {
+ return array(
+ 'fa_id',
+ 'fa_name',
+ 'fa_archive_name',
+ 'fa_storage_key',
+ 'fa_storage_group',
+ 'fa_size',
+ 'fa_bits',
+ 'fa_width',
+ 'fa_height',
+ 'fa_metadata',
+ 'fa_media_type',
+ 'fa_major_mime',
+ 'fa_minor_mime',
+ 'fa_description',
+ 'fa_user',
+ 'fa_user_text',
+ 'fa_timestamp',
+ 'fa_deleted',
+ 'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
+ 'fa_sha1',
+ );
+ }
- return $file;
+ /**
+ * Load ArchivedFile object fields from a DB row.
+ *
+ * @param $row Object database row
+ * @since 1.21
+ */
+ public function loadFromRow( $row ) {
+ $this->id = intval( $row->fa_id );
+ $this->name = $row->fa_name;
+ $this->archive_name = $row->fa_archive_name;
+ $this->group = $row->fa_storage_group;
+ $this->key = $row->fa_storage_key;
+ $this->size = $row->fa_size;
+ $this->bits = $row->fa_bits;
+ $this->width = $row->fa_width;
+ $this->height = $row->fa_height;
+ $this->metadata = $row->fa_metadata;
+ $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+ $this->media_type = $row->fa_media_type;
+ $this->description = $row->fa_description;
+ $this->user = $row->fa_user;
+ $this->user_text = $row->fa_user_text;
+ $this->timestamp = $row->fa_timestamp;
+ $this->deleted = $row->fa_deleted;
+ if ( isset( $row->fa_sha1 ) ) {
+ $this->sha1 = $row->fa_sha1;
+ } else {
+ // old row, populate from key
+ $this->sha1 = LocalRepo::getHashFromKey( $this->key );
+ }
}
/**
@@ -381,13 +393,24 @@ class ArchivedFile {
}
/**
+ * Get the SHA-1 base 36 hash of the file
+ *
+ * @return string
+ * @since 1.21
+ */
+ function getSha1() {
+ $this->load();
+ return $this->sha1;
+ }
+
+ /**
* Return the user ID of the uploader.
*
* @return int
*/
public function getUser() {
$this->load();
- if( $this->isDeleted( File::DELETED_USER ) ) {
+ if ( $this->isDeleted( File::DELETED_USER ) ) {
return 0;
} else {
return $this->user;
@@ -401,7 +424,7 @@ class ArchivedFile {
*/
public function getUserText() {
$this->load();
- if( $this->isDeleted( File::DELETED_USER ) ) {
+ if ( $this->isDeleted( File::DELETED_USER ) ) {
return 0;
} else {
return $this->user_text;
@@ -415,7 +438,7 @@ class ArchivedFile {
*/
public function getDescription() {
$this->load();
- if( $this->isDeleted( File::DELETED_COMMENT ) ) {
+ if ( $this->isDeleted( File::DELETED_COMMENT ) ) {
return 0;
} else {
return $this->description;
@@ -469,7 +492,7 @@ class ArchivedFile {
*/
public function isDeleted( $field ) {
$this->load();
- return ($this->deleted & $field) == $field;
+ return ( $this->deleted & $field ) == $field;
}
/**
diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php
index 557609d4..ec5f927b 100644
--- a/includes/filerepo/file/File.php
+++ b/includes/filerepo/file/File.php
@@ -40,7 +40,7 @@
* never name a file class explictly outside of the repo class. Instead use the
* repo's factory functions to generate file objects, for example:
*
- * RepoGroup::singleton()->getLocalRepo()->newFile($title);
+ * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
*
* The convenience functions wfLocalFile() and wfFindFile() should be sufficient
* in most cases.
@@ -48,13 +48,14 @@
* @ingroup FileAbstraction
*/
abstract class File {
+ // Bitfield values akin to the Revision deletion constants
const DELETED_FILE = 1;
const DELETED_COMMENT = 2;
const DELETED_USER = 4;
const DELETED_RESTRICTED = 8;
/** Force rendering in the current process */
- const RENDER_NOW = 1;
+ const RENDER_NOW = 1;
/**
* Force rendering even if thumbnail already exist and using RENDER_NOW
* I.e. you have to pass both flags: File::RENDER_NOW | File::RENDER_FORCE
@@ -152,7 +153,7 @@ abstract class File {
* valid Title object with namespace NS_FILE or null
*
* @param $title Title|string
- * @param $exception string|bool Use 'exception' to throw an error on bad titles
+ * @param string|bool $exception Use 'exception' to throw an error on bad titles
* @throws MWException
* @return Title|null
*/
@@ -190,7 +191,7 @@ abstract class File {
* Normalize a file extension to the common form, and ensure it's clean.
* Extensions with non-alphanumeric characters will be discarded.
*
- * @param $ext string (without the .)
+ * @param string $ext (without the .)
* @return string
*/
static function normalizeExtension( $ext ) {
@@ -201,9 +202,9 @@ abstract class File {
'mpeg' => 'mpg',
'tiff' => 'tif',
'ogv' => 'ogg' );
- if( isset( $squish[$lower] ) ) {
+ if ( isset( $squish[$lower] ) ) {
return $squish[$lower];
- } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
+ } elseif ( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
return $lower;
} else {
return '';
@@ -214,7 +215,7 @@ abstract class File {
* Checks if file extensions are compatible
*
* @param $old File Old file
- * @param $new string New name
+ * @param string $new New name
*
* @return bool|null
*/
@@ -241,7 +242,7 @@ abstract class File {
* @return array ("text", "html") etc
*/
public static function splitMime( $mime ) {
- if( strpos( $mime, '/' ) !== false ) {
+ if ( strpos( $mime, '/' ) !== false ) {
return explode( '/', $mime, 2 );
} else {
return array( $mime, 'unknown' );
@@ -316,7 +317,8 @@ abstract class File {
public function getUrl() {
if ( !isset( $this->url ) ) {
$this->assertRepoDefined();
- $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
+ $ext = $this->getExtension();
+ $this->url = $this->repo->getZoneUrl( 'public', $ext ) . '/' . $this->getUrlRel();
}
return $this->url;
}
@@ -347,7 +349,7 @@ abstract class File {
if ( $this->canRender() ) {
return $this->createThumb( $this->getWidth() );
} else {
- wfDebug( __METHOD__.': supposed to render ' . $this->getName() .
+ wfDebug( __METHOD__ . ': supposed to render ' . $this->getName() .
' (' . $this->getMimeType() . "), but can't!\n" );
return $this->getURL(); #hm... return NULL?
}
@@ -368,7 +370,7 @@ abstract class File {
* returns false.
*
* @return string|bool ForeignAPIFile::getPath can return false
- */
+ */
public function getPath() {
if ( !isset( $this->path ) ) {
$this->assertRepoDefined();
@@ -431,7 +433,7 @@ abstract class File {
* Returns ID or name of user who uploaded the file
* STUB
*
- * @param $type string 'text' or 'id'
+ * @param string $type 'text' or 'id'
*
* @return string|int
*/
@@ -511,13 +513,13 @@ abstract class File {
}
/**
- * get versioned metadata
- *
- * @param $metadata Mixed Array or String of (serialized) metadata
- * @param $version integer version number.
- * @return Array containing metadata, or what was passed to it on fail (unserializing if not array)
- */
- public function convertMetadataVersion($metadata, $version) {
+ * get versioned metadata
+ *
+ * @param $metadata Mixed Array or String of (serialized) metadata
+ * @param $version integer version number.
+ * @return Array containing metadata, or what was passed to it on fail (unserializing if not array)
+ */
+ public function convertMetadataVersion( $metadata, $version ) {
$handler = $this->getHandler();
if ( !is_array( $metadata ) ) {
// Just to make the return type consistent
@@ -662,13 +664,13 @@ abstract class File {
if ( $this->allowInlineDisplay() ) {
return true;
}
- if ($this->isTrustedFile()) {
+ if ( $this->isTrustedFile() ) {
return true;
}
$type = $this->getMediaType();
$mime = $this->getMimeType();
- #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n");
+ #wfDebug( "LocalFile::isSafeFile: type= $type, mime= $mime\n" );
if ( !$type || $type === MEDIATYPE_UNKNOWN ) {
return false; #unknown type, not trusted
@@ -680,7 +682,7 @@ abstract class File {
if ( $mime === "unknown/unknown" ) {
return false; #unknown type, not trusted
}
- if ( in_array( $mime, $wgTrustedMediaFormats) ) {
+ if ( in_array( $mime, $wgTrustedMediaFormats ) ) {
return true;
}
@@ -736,7 +738,7 @@ abstract class File {
if ( $this->repo ) {
$script = $this->repo->getThumbScriptUrl();
if ( $script ) {
- $this->transformScript = "$script?f=" . urlencode( $this->getName() );
+ $this->transformScript = wfAppendQuery( $script, array( 'f' => $this->getName() ) );
}
}
}
@@ -766,7 +768,7 @@ abstract class File {
* Use File::THUMB_FULL_NAME to always get a name like "<params>-<source>".
* Otherwise, the format may be "<params>-<source>" or "<params>-thumbnail.<ext>".
*
- * @param $params Array: handler-specific parameters
+ * @param array $params handler-specific parameters
* @param $flags integer Bitfield that supports THUMB_* constants
* @return string
*/
@@ -831,8 +833,8 @@ abstract class File {
/**
* Return either a MediaTransformError or placeholder thumbnail (if $wgIgnoreImageErrors)
*
- * @param $thumbPath string Thumbnail storage path
- * @param $thumbUrl string Thumbnail URL
+ * @param string $thumbPath Thumbnail storage path
+ * @param string $thumbUrl Thumbnail URL
* @param $params Array
* @param $flags integer
* @return MediaTransformOutput
@@ -840,8 +842,9 @@ abstract class File {
protected function transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ) {
global $wgIgnoreImageErrors;
- if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
- return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ $handler = $this->getHandler();
+ if ( $handler && $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
+ return $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
} else {
return new MediaTransformError( 'thumbnail_error',
$params['width'], 0, wfMessage( 'thumbnail-dest-create' )->text() );
@@ -851,7 +854,7 @@ abstract class File {
/**
* Transform a media file
*
- * @param $params Array: an associative array of handler-specific parameters.
+ * @param array $params an associative array of handler-specific parameters.
* Typical keys are width, height and page.
* @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
* @return MediaTransformOutput|bool False on failure
@@ -872,17 +875,18 @@ abstract class File {
$params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
}
+ $handler = $this->getHandler();
$script = $this->getTransformScript();
if ( $script && !( $flags & self::RENDER_NOW ) ) {
// Use a script to transform on client request, if possible
- $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
+ $thumb = $handler->getScriptedTransform( $this, $script, $params );
if ( $thumb ) {
break;
}
}
$normalisedParams = $params;
- $this->handler->normaliseParams( $this, $normalisedParams );
+ $handler->normaliseParams( $this, $normalisedParams );
$thumbName = $this->thumbName( $normalisedParams );
$thumbUrl = $this->getThumbUrl( $thumbName );
@@ -895,20 +899,20 @@ abstract class File {
// XXX: Pass in the storage path even though we are not rendering anything
// and the path is supposed to be an FS path. This is due to getScalerType()
// getting called on the path and clobbering $thumb->getUrl() if it's false.
- $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
break;
}
// Clean up broken thumbnails as needed
$this->migrateThumbFile( $thumbName );
// Check if an up-to-date thumbnail already exists...
- wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
- if ( $this->repo->fileExists( $thumbPath ) && !( $flags & self::RENDER_FORCE ) ) {
+ wfDebug( __METHOD__ . ": Doing stat for $thumbPath\n" );
+ if ( !( $flags & self::RENDER_FORCE ) && $this->repo->fileExists( $thumbPath ) ) {
$timestamp = $this->repo->getFileTimestamp( $thumbPath );
if ( $timestamp !== false && $timestamp >= $wgThumbnailEpoch ) {
// XXX: Pass in the storage path even though we are not rendering anything
// and the path is supposed to be an FS path. This is due to getScalerType()
// getting called on the path and clobbering $thumb->getUrl() if it's false.
- $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
$thumb->setStoragePath( $thumbPath );
break;
}
@@ -935,7 +939,7 @@ abstract class File {
// Actually render the thumbnail...
wfProfileIn( __METHOD__ . '-doTransform' );
- $thumb = $this->handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params );
+ $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params );
wfProfileOut( __METHOD__ . '-doTransform' );
$tmpFile->bind( $thumb ); // keep alive with $thumb
@@ -945,7 +949,7 @@ abstract class File {
$this->lastError = $thumb->toText();
// Ignore errors if requested
if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
- $thumb = $this->handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params );
+ $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params );
}
} elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
// Copy the thumbnail from the file system into storage...
@@ -975,7 +979,7 @@ abstract class File {
}
/**
- * @param $thumbName string Thumbnail name
+ * @param string $thumbName Thumbnail name
* @return string Content-Disposition header value
*/
function getThumbDisposition( $thumbName ) {
@@ -997,7 +1001,7 @@ abstract class File {
/**
* Get a MediaHandler instance for this file
*
- * @return MediaHandler
+ * @return MediaHandler|boolean Registered MediaHandler for file's mime type or false if none found
*/
function getHandler() {
if ( !isset( $this->handler ) ) {
@@ -1048,7 +1052,7 @@ abstract class File {
* Purge shared caches such as thumbnails and DB data caching
* STUB
* Overridden by LocalFile
- * @param $options Array Options, which include:
+ * @param array $options Options, which include:
* 'forThumbRefresh' : The purging is only to refresh thumbnails
*/
function purgeCache( $options = array() ) {}
@@ -1088,13 +1092,13 @@ abstract class File {
*
* STUB
* @param $limit integer Limit of rows to return
- * @param $start string timestamp Only revisions older than $start will be returned
- * @param $end string timestamp Only revisions newer than $end will be returned
- * @param $inc bool Include the endpoints of the time range
+ * @param string $start timestamp Only revisions older than $start will be returned
+ * @param string $end timestamp Only revisions newer than $end will be returned
+ * @param bool $inc Include the endpoints of the time range
*
* @return array
*/
- function getHistory( $limit = null, $start = null, $end = null, $inc=true ) {
+ function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
return array();
}
@@ -1147,7 +1151,7 @@ abstract class File {
/**
* Get the path of an archived file relative to the public zone root
*
- * @param $suffix bool|string if not false, the name of an archived thumbnail file
+ * @param bool|string $suffix if not false, the name of an archived thumbnail file
*
* @return string
*/
@@ -1165,7 +1169,7 @@ abstract class File {
* Get the path, relative to the thumbnail zone root, of the
* thumbnail directory or a particular file if $suffix is specified
*
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1191,8 +1195,8 @@ abstract class File {
* Get the path, relative to the thumbnail zone root, for an archived file's thumbs directory
* or a specific thumb if the $suffix is given.
*
- * @param $archiveName string the timestamped name of an archived image
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param string $archiveName the timestamped name of an archived image
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1209,7 +1213,7 @@ abstract class File {
/**
* Get the path of the archived file.
*
- * @param $suffix bool|string if not false, the name of an archived file.
+ * @param bool|string $suffix if not false, the name of an archived file.
*
* @return string
*/
@@ -1221,8 +1225,8 @@ abstract class File {
/**
* Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified
*
- * @param $archiveName string the timestamped name of an archived image
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param string $archiveName the timestamped name of an archived image
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1235,7 +1239,7 @@ abstract class File {
/**
* Get the path of the thumbnail directory, or a particular file if $suffix is specified
*
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1245,15 +1249,28 @@ abstract class File {
}
/**
+ * Get the path of the transcoded directory, or a particular file if $suffix is specified
+ *
+ * @param bool|string $suffix if not false, the name of a media file
+ *
+ * @return string
+ */
+ function getTranscodedPath( $suffix = false ) {
+ $this->assertRepoDefined();
+ return $this->repo->getZonePath( 'transcoded' ) . '/' . $this->getThumbRel( $suffix );
+ }
+
+ /**
* Get the URL of the archive directory, or a particular file if $suffix is specified
*
- * @param $suffix bool|string if not false, the name of an archived file
+ * @param bool|string $suffix if not false, the name of an archived file
*
* @return string
*/
function getArchiveUrl( $suffix = false ) {
$this->assertRepoDefined();
- $path = $this->repo->getZoneUrl( 'public' ) . '/archive/' . $this->getHashPath();
+ $ext = $this->getExtension();
+ $path = $this->repo->getZoneUrl( 'public', $ext ) . '/archive/' . $this->getHashPath();
if ( $suffix === false ) {
$path = substr( $path, 0, -1 );
} else {
@@ -1265,14 +1282,15 @@ abstract class File {
/**
* Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified
*
- * @param $archiveName string the timestamped name of an archived image
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param string $archiveName the timestamped name of an archived image
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
function getArchiveThumbUrl( $archiveName, $suffix = false ) {
$this->assertRepoDefined();
- $path = $this->repo->getZoneUrl( 'thumb' ) . '/archive/' .
+ $ext = $this->getExtension();
+ $path = $this->repo->getZoneUrl( 'thumb', $ext ) . '/archive/' .
$this->getHashPath() . rawurlencode( $archiveName ) . "/";
if ( $suffix === false ) {
$path = substr( $path, 0, -1 );
@@ -1283,15 +1301,17 @@ abstract class File {
}
/**
- * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
+ * Get the URL of the zone directory, or a particular file if $suffix is specified
*
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param string $zone name of requested zone
+ * @param bool|string $suffix if not false, the name of a file in zone
*
* @return string path
*/
- function getThumbUrl( $suffix = false ) {
+ function getZoneUrl( $zone, $suffix = false ) {
$this->assertRepoDefined();
- $path = $this->repo->getZoneUrl( 'thumb' ) . '/' . $this->getUrlRel();
+ $ext = $this->getExtension();
+ $path = $this->repo->getZoneUrl( $zone, $ext ) . '/' . $this->getUrlRel();
if ( $suffix !== false ) {
$path .= '/' . rawurlencode( $suffix );
}
@@ -1299,9 +1319,31 @@ abstract class File {
}
/**
+ * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
+ *
+ * @param bool|string $suffix if not false, the name of a thumbnail file
+ *
+ * @return string path
+ */
+ function getThumbUrl( $suffix = false ) {
+ return $this->getZoneUrl( 'thumb', $suffix );
+ }
+
+ /**
+ * Get the URL of the transcoded directory, or a particular file if $suffix is specified
+ *
+ * @param bool|string $suffix if not false, the name of a media file
+ *
+ * @return string path
+ */
+ function getTranscodedUrl( $suffix = false ) {
+ return $this->getZoneUrl( 'transcoded', $suffix );
+ }
+
+ /**
* Get the public zone virtual URL for a current version source file
*
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1317,7 +1359,7 @@ abstract class File {
/**
* Get the public zone virtual URL for an archived version source file
*
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1335,7 +1377,7 @@ abstract class File {
/**
* Get the virtual URL for a thumbnail file or directory
*
- * @param $suffix bool|string if not false, the name of a thumbnail file
+ * @param bool|string $suffix if not false, the name of a thumbnail file
*
* @return string
*/
@@ -1360,7 +1402,7 @@ abstract class File {
* @throws MWException
*/
function readOnlyError() {
- throw new MWException( get_class($this) . ': write operations are not supported' );
+ throw new MWException( get_class( $this ) . ': write operations are not supported' );
}
/**
@@ -1373,8 +1415,12 @@ abstract class File {
* @param $copyStatus string
* @param $source string
* @param $watch bool
+ * @param $timestamp string|bool
+ * @param $user User object or null to use $wgUser
+ * @return bool
+ * @throws MWException
*/
- function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false, User $user = null ) {
$this->readOnlyError();
}
@@ -1386,17 +1432,21 @@ abstract class File {
* The archive name should be passed through to recordUpload for database
* registration.
*
- * @param $srcPath String: local filesystem path to the source image
+ * Options to $options include:
+ * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
+ *
+ * @param string $srcPath local filesystem path to the source image
* @param $flags Integer: a bitwise combination of:
* File::DELETE_SOURCE Delete the source file, i.e. move
* rather than copy
+ * @param array $options Optional additional parameters
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*
* STUB
* Overridden by LocalFile
*/
- function publish( $srcPath, $flags = 0 ) {
+ function publish( $srcPath, $flags = 0, array $options = array() ) {
$this->readOnlyError();
}
@@ -1451,7 +1501,7 @@ abstract class File {
* Is this file a "deleted" file in a private archive?
* STUB
*
- * @param $field
+ * @param integer $field one of DELETED_* bitfield constants
*
* @return bool
*/
@@ -1490,9 +1540,9 @@ abstract class File {
* @param $target Title New file name
* @return FileRepoStatus object.
*/
- function move( $target ) {
+ function move( $target ) {
$this->readOnlyError();
- }
+ }
/**
* Delete all versions of the file.
@@ -1518,9 +1568,9 @@ abstract class File {
*
* May throw database exceptions on error.
*
- * @param $versions array set of record ids of deleted items to restore,
+ * @param array $versions set of record ids of deleted items to restore,
* or empty to restore all revisions.
- * @param $unsuppress bool remove restrictions on content upon restoration?
+ * @param bool $unsuppress remove restrictions on content upon restoration?
* @return int|bool the number of file revisions restored if successful,
* or false on failure
* STUB
@@ -1580,7 +1630,7 @@ abstract class File {
* Get an image size array like that returned by getImageSize(), or false if it
* can't be determined.
*
- * @param $fileName String: The filename
+ * @param string $fileName The filename
* @return Array
*/
function getImageSize( $fileName ) {
@@ -1607,25 +1657,29 @@ abstract class File {
/**
* Get the HTML text of the description page, if available
*
+ * @param $lang Language Optional language to fetch description in
* @return string
*/
- function getDescriptionText() {
+ function getDescriptionText( $lang = false ) {
global $wgMemc, $wgLang;
if ( !$this->repo || !$this->repo->fetchDescription ) {
return false;
}
- $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgLang->getCode() );
+ if ( !$lang ) {
+ $lang = $wgLang;
+ }
+ $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $lang->getCode() );
if ( $renderUrl ) {
if ( $this->repo->descriptionCacheExpiry > 0 ) {
- wfDebug("Attempting to get the description from cache...");
- $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(),
+ wfDebug( "Attempting to get the description from cache..." );
+ $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $lang->getCode(),
$this->getName() );
- $obj = $wgMemc->get($key);
- if ($obj) {
- wfDebug("success!\n");
+ $obj = $wgMemc->get( $key );
+ if ( $obj ) {
+ wfDebug( "success!\n" );
return $obj;
}
- wfDebug("miss\n");
+ wfDebug( "miss\n" );
}
wfDebug( "Fetching shared description from $renderUrl\n" );
$res = Http::get( $renderUrl );
@@ -1704,14 +1758,15 @@ abstract class File {
/**
* Get an associative array containing information about a file in the local filesystem.
*
- * @param $path String: absolute local filesystem path
+ * @param string $path absolute local filesystem path
* @param $ext Mixed: the file extension, or true to extract it from the filename.
* Set it to false to ignore the extension.
*
* @return array
+ * @deprecated since 1.19
*/
static function getPropsFromPath( $path, $ext = true ) {
- wfDebug( __METHOD__.": Getting file info for $path\n" );
+ wfDebug( __METHOD__ . ": Getting file info for $path\n" );
wfDeprecated( __METHOD__, '1.19' );
$fsFile = new FSFile( $path );
@@ -1728,6 +1783,7 @@ abstract class File {
* @param $path string
*
* @return bool|string False on failure
+ * @deprecated since 1.19
*/
static function sha1Base36( $path ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -1737,6 +1793,18 @@ abstract class File {
}
/**
+ * @return Array HTTP header name/value map to use for HEAD/GET request responses
+ */
+ function getStreamHeaders() {
+ $handler = $this->getHandler();
+ if ( $handler ) {
+ return $handler->getStreamHeaders( $this->getMetadata() );
+ } else {
+ return array();
+ }
+ }
+
+ /**
* @return string
*/
function getLongDesc() {
@@ -1780,7 +1848,7 @@ abstract class File {
}
/**
- * @return Title
+ * @return Title|null
*/
function getRedirectedTitle() {
if ( $this->redirected ) {
@@ -1789,6 +1857,7 @@ abstract class File {
}
return $this->redirectTitle;
}
+ return null;
}
/**
diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php
index 56482611..ed96d446 100644
--- a/includes/filerepo/file/ForeignAPIFile.php
+++ b/includes/filerepo/file/ForeignAPIFile.php
@@ -54,22 +54,22 @@ class ForeignAPIFile extends File {
*/
static function newFromTitle( Title $title, $repo ) {
$data = $repo->fetchImageQuery( array(
- 'titles' => 'File:' . $title->getDBKey(),
- 'iiprop' => self::getProps(),
- 'prop' => 'imageinfo',
+ 'titles' => 'File:' . $title->getDBkey(),
+ 'iiprop' => self::getProps(),
+ 'prop' => 'imageinfo',
'iimetadataversion' => MediaHandler::getMetadataVersion()
) );
$info = $repo->getImageInfo( $data );
- if( $info ) {
+ if ( $info ) {
$lastRedirect = isset( $data['query']['redirects'] )
? count( $data['query']['redirects'] ) - 1
: -1;
- if( $lastRedirect >= 0 ) {
- $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to']);
+ if ( $lastRedirect >= 0 ) {
+ $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to'] );
$img = new self( $newtitle, $repo, $info, true );
- if( $img ) {
+ if ( $img ) {
$img->redirectedFrom( $title->getDBkey() );
}
} else {
@@ -86,7 +86,7 @@ class ForeignAPIFile extends File {
* @return string
*/
static function getProps() {
- return 'timestamp|user|comment|url|size|sha1|metadata|mime';
+ return 'timestamp|user|comment|url|size|sha1|metadata|mime|mediatype';
}
// Dummy functions...
@@ -106,12 +106,12 @@ class ForeignAPIFile extends File {
}
/**
- * @param Array $params
+ * @param array $params
* @param int $flags
* @return bool|MediaTransformOutput
*/
function transform( $params, $flags = 0 ) {
- if( !$this->canRender() ) {
+ if ( !$this->canRender() ) {
// show icon
return parent::transform( $params, $flags );
}
@@ -119,12 +119,25 @@ class ForeignAPIFile extends File {
// Note, the this->canRender() check above implies
// that we have a handler, and it can do makeParamString.
$otherParams = $this->handler->makeParamString( $params );
+ $width = isset( $params['width'] ) ? $params['width'] : -1;
+ $height = isset( $params['height'] ) ? $params['height'] : -1;
$thumbUrl = $this->repo->getThumbUrlFromCache(
$this->getName(),
- isset( $params['width'] ) ? $params['width'] : -1,
- isset( $params['height'] ) ? $params['height'] : -1,
- $otherParams );
+ $width,
+ $height,
+ $otherParams
+ );
+ if ( $thumbUrl === false ) {
+ global $wgLang;
+ return $this->repo->getThumbError(
+ $this->getName(),
+ $width,
+ $height,
+ $otherParams,
+ $wgLang->getCode()
+ );
+ }
return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
}
@@ -161,12 +174,12 @@ class ForeignAPIFile extends File {
* @return array
*/
public static function parseMetadata( $metadata ) {
- if( !is_array( $metadata ) ) {
+ if ( !is_array( $metadata ) ) {
return $metadata;
}
$ret = array();
- foreach( $metadata as $meta ) {
- $ret[ $meta['name'] ] = self::parseMetadata( $meta['value'] );
+ foreach ( $metadata as $meta ) {
+ $ret[$meta['name']] = self::parseMetadata( $meta['value'] );
}
return $ret;
}
@@ -189,7 +202,7 @@ class ForeignAPIFile extends File {
* @param string $method
* @return int|null|string
*/
- public function getUser( $method='text' ) {
+ public function getUser( $method = 'text' ) {
return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
}
@@ -224,7 +237,7 @@ class ForeignAPIFile extends File {
* @return string
*/
function getMimeType() {
- if( !isset( $this->mInfo['mime'] ) ) {
+ if ( !isset( $this->mInfo['mime'] ) ) {
$magic = MimeMagic::singleton();
$this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
}
@@ -232,10 +245,12 @@ class ForeignAPIFile extends File {
}
/**
- * @todo FIXME: May guess wrong on file types that can be eg audio or video
* @return int|string
*/
function getMediaType() {
+ if ( isset( $this->mInfo['mediatype'] ) ) {
+ return $this->mInfo['mediatype'];
+ }
$magic = MimeMagic::singleton();
return $magic->getMediaType( null, $this->getMimeType() );
}
@@ -256,7 +271,7 @@ class ForeignAPIFile extends File {
*/
function getThumbPath( $suffix = '' ) {
if ( $this->repo->canCacheThumbs() ) {
- $path = $this->repo->getZonePath('thumb') . '/' . $this->getHashPath( $this->getName() );
+ $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getHashPath( $this->getName() );
if ( $suffix ) {
$path = $path . $suffix . '/';
}
@@ -293,7 +308,7 @@ class ForeignAPIFile extends File {
global $wgMemc, $wgContLang;
$url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
- $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) );
+ $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5( $url ) );
$wgMemc->delete( $key );
}
diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php
index 91f6cb62..01d6b0f5 100644
--- a/includes/filerepo/file/ForeignDBFile.php
+++ b/includes/filerepo/file/ForeignDBFile.php
@@ -57,9 +57,11 @@ class ForeignDBFile extends LocalFile {
/**
* @param $srcPath String
* @param $flags int
+ * @param $options Array
+ * @return \FileRepoStatus
* @throws MWException
*/
- function publish( $srcPath, $flags = 0 ) {
+ function publish( $srcPath, $flags = 0, array $options = array() ) {
$this->readOnlyError();
}
@@ -71,16 +73,19 @@ class ForeignDBFile extends LocalFile {
* @param $source string
* @param $watch bool
* @param $timestamp bool|string
+ * @param $user User object or null to use $wgUser
+ * @return bool
* @throws MWException
*/
function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
- $watch = false, $timestamp = false ) {
+ $watch = false, $timestamp = false, User $user = null ) {
$this->readOnlyError();
}
/**
* @param $versions array
* @param $unsuppress bool
+ * @return \FileRepoStatus
* @throws MWException
*/
function restore( $versions = array(), $unsuppress = false ) {
@@ -90,6 +95,7 @@ class ForeignDBFile extends LocalFile {
/**
* @param $reason string
* @param $suppress bool
+ * @return \FileRepoStatus
* @throws MWException
*/
function delete( $reason, $suppress = false ) {
@@ -98,6 +104,7 @@ class ForeignDBFile extends LocalFile {
/**
* @param $target Title
+ * @return \FileRepoStatus
* @throws MWException
*/
function move( $target ) {
@@ -108,15 +115,16 @@ class ForeignDBFile extends LocalFile {
* @return string
*/
function getDescriptionUrl() {
- // Restore remote behaviour
+ // Restore remote behavior
return File::getDescriptionUrl();
}
/**
+ * @param $lang Language Optional language to fetch description in.
* @return string
*/
- function getDescriptionText() {
- // Restore remote behaviour
- return File::getDescriptionText();
+ function getDescriptionText( $lang = false ) {
+ // Restore remote behavior
+ return File::getDescriptionText( $lang );
}
}
diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php
index 695c4e9e..fe769be2 100644
--- a/includes/filerepo/file/LocalFile.php
+++ b/includes/filerepo/file/LocalFile.php
@@ -24,7 +24,7 @@
/**
* Bump this number when serialized cache records may be incompatible.
*/
-define( 'MW_FILE_VERSION', 8 );
+define( 'MW_FILE_VERSION', 9 );
/**
* Class to represent a local file in the wiki's own database
@@ -36,7 +36,7 @@ define( 'MW_FILE_VERSION', 8 );
* never name a file class explictly outside of the repo class. Instead use the
* repo's factory functions to generate file objects, for example:
*
- * RepoGroup::singleton()->getLocalRepo()->newFile($title);
+ * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
*
* The convenience functions wfLocalFile() and wfFindFile() should be sufficient
* in most cases.
@@ -67,9 +67,11 @@ class LocalFile extends File {
$sha1, # SHA-1 base 36 content hash
$user, $user_text, # User, who uploaded the file
$description, # Description of current revision of the file
- $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
+ $dataLoaded, # Whether or not core data has been loaded from the database (loadFromXxx)
+ $extraDataLoaded, # Whether or not lazy-loaded data has been loaded from the database
$upgraded, # Whether the row was upgraded on load
$locked, # True if the image row is locked
+ $lockedOwnTrx, # True if the image row is locked with a lock initiated transaction
$missing, # True if file is not present in file system. Not to be cached in memcached
$deleted; # Bitfield akin to rev_deleted
@@ -82,6 +84,8 @@ class LocalFile extends File {
protected $repoClass = 'LocalRepo';
+ const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata)
+
/**
* Create a LocalFile from a title
* Do not call this except from inside a repo class.
@@ -119,7 +123,7 @@ class LocalFile extends File {
* Create a LocalFile from a SHA-1 key
* Do not call this except from inside a repo class.
*
- * @param $sha1 string base-36 SHA-1
+ * @param string $sha1 base-36 SHA-1
* @param $repo LocalRepo
* @param string|bool $timestamp MW_timestamp (optional)
*
@@ -175,6 +179,7 @@ class LocalFile extends File {
$this->historyLine = 0;
$this->historyRes = null;
$this->dataLoaded = false;
+ $this->extraDataLoaded = false;
$this->assertRepoDefined();
$this->assertTitleDefined();
@@ -200,6 +205,7 @@ class LocalFile extends File {
wfProfileIn( __METHOD__ );
$this->dataLoaded = false;
+ $this->extraDataLoaded = false;
$key = $this->getCacheKey();
if ( !$key ) {
@@ -210,13 +216,17 @@ class LocalFile extends File {
$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 ) {
$this->setProps( $cachedValues );
}
$this->dataLoaded = true;
+ $this->extraDataLoaded = true;
+ foreach ( $this->getLazyCacheFields( '' ) as $field ) {
+ $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
+ }
}
if ( $this->dataLoaded ) {
@@ -252,7 +262,17 @@ class LocalFile extends File {
}
}
- $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
+ // Strip off excessive entries from the subset of fields that can become large.
+ // If the cache value gets to large it will not fit in memcached and nothing will
+ // get cached at all, causing master queries for any file access.
+ foreach ( $this->getLazyCacheFields( '' ) as $field ) {
+ if ( isset( $cache[$field] ) && strlen( $cache[$field] ) > 100 * 1024 ) {
+ unset( $cache[$field] ); // don't let the value get too big
+ }
+ }
+
+ // Cache presence for 1 week and negatives for 1 day
+ $wgMemc->set( $key, $cache, $this->fileExists ? 86400 * 7 : 86400 );
}
/**
@@ -288,6 +308,28 @@ class LocalFile extends File {
}
/**
+ * @return array
+ */
+ function getLazyCacheFields( $prefix = 'img_' ) {
+ static $fields = array( 'metadata' );
+ static $results = array();
+
+ if ( $prefix == '' ) {
+ return $fields;
+ }
+
+ if ( !isset( $results[$prefix] ) ) {
+ $prefixedFields = array();
+ foreach ( $fields as $field ) {
+ $prefixedFields[] = $prefix . $field;
+ }
+ $results[$prefix] = $prefixedFields;
+ }
+
+ return $results[$prefix];
+ }
+
+ /**
* Load file metadata from the DB
*/
function loadFromDB() {
@@ -297,9 +339,9 @@ class LocalFile extends File {
# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
$this->dataLoaded = true;
+ $this->extraDataLoaded = true;
$dbr = $this->repo->getMasterDB();
-
$row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
array( 'img_name' => $this->getName() ), $fname );
@@ -313,27 +355,71 @@ class LocalFile extends File {
}
/**
- * Decode a row from the database (either object or array) to an array
- * with timestamps and MIME types decoded, and the field prefix removed.
- * @param $row
+ * Load lazy file metadata from the DB.
+ * This covers fields that are sometimes not cached.
+ */
+ protected function loadExtraFromDB() {
+ # Polymorphic function name to distinguish foreign and local fetches
+ $fname = get_class( $this ) . '::' . __FUNCTION__;
+ wfProfileIn( $fname );
+
+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+ $this->extraDataLoaded = true;
+
+ $dbr = $this->repo->getSlaveDB();
+ // In theory the file could have just been renamed/deleted...oh well
+ $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
+ array( 'img_name' => $this->getName() ), $fname );
+
+ if ( !$row ) { // fallback to master
+ $dbr = $this->repo->getMasterDB();
+ $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
+ array( 'img_name' => $this->getName() ), $fname );
+ }
+
+ if ( $row ) {
+ foreach ( $this->unprefixRow( $row, 'img_' ) as $name => $value ) {
+ $this->$name = $value;
+ }
+ } else {
+ wfProfileOut( $fname );
+ throw new MWException( "Could not find data for image '{$this->getName()}'." );
+ }
+
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * @param Row $row
* @param $prefix string
- * @throws MWException
- * @return array
+ * @return Array
*/
- function decodeRow( $row, $prefix = 'img_' ) {
+ protected function unprefixRow( $row, $prefix = 'img_' ) {
$array = (array)$row;
$prefixLength = strlen( $prefix );
// Sanity check prefix once
if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
- throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
+ throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
}
$decoded = array();
-
foreach ( $array as $name => $value ) {
$decoded[substr( $name, $prefixLength )] = $value;
}
+ return $decoded;
+ }
+
+ /**
+ * Decode a row from the database (either object or array) to an array
+ * with timestamps and MIME types decoded, and the field prefix removed.
+ * @param $row
+ * @param $prefix string
+ * @throws MWException
+ * @return array
+ */
+ function decodeRow( $row, $prefix = 'img_' ) {
+ $decoded = $this->unprefixRow( $row, $prefix );
$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
@@ -357,6 +443,8 @@ class LocalFile extends File {
*/
function loadFromRow( $row, $prefix = 'img_' ) {
$this->dataLoaded = true;
+ $this->extraDataLoaded = true;
+
$array = $this->decodeRow( $row, $prefix );
foreach ( $array as $name => $value ) {
@@ -369,8 +457,9 @@ class LocalFile extends File {
/**
* Load file metadata from cache or DB, unless already loaded
+ * @param integer $flags
*/
- function load() {
+ function load( $flags = 0 ) {
if ( !$this->dataLoaded ) {
if ( !$this->loadFromCache() ) {
$this->loadFromDB();
@@ -378,6 +467,9 @@ class LocalFile extends File {
}
$this->dataLoaded = true;
}
+ if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
+ $this->loadExtraFromDB();
+ }
}
/**
@@ -397,7 +489,7 @@ class LocalFile extends File {
} else {
$handler = $this->getHandler();
if ( $handler ) {
- $validity = $handler->isMetadataValid( $this, $this->metadata );
+ $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
if ( $validity === MediaHandler::METADATA_BAD
|| ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
) {
@@ -440,15 +532,15 @@ class LocalFile extends File {
$dbw->update( 'image',
array(
- 'img_size' => $this->size, // sanity
- 'img_width' => $this->width,
- 'img_height' => $this->height,
- 'img_bits' => $this->bits,
+ 'img_size' => $this->size, // sanity
+ 'img_width' => $this->width,
+ 'img_height' => $this->height,
+ 'img_bits' => $this->bits,
'img_media_type' => $this->media_type,
'img_major_mime' => $major,
'img_minor_mime' => $minor,
- 'img_metadata' => $this->metadata,
- 'img_sha1' => $this->sha1,
+ 'img_metadata' => $dbw->encodeBlob($this->metadata),
+ 'img_sha1' => $this->sha1,
),
array( 'img_name' => $this->getName() ),
__METHOD__
@@ -464,6 +556,7 @@ class LocalFile extends File {
/**
* Set properties in this object to be equal to those given in the
* associative array $info. Only cacheable fields can be set.
+ * All fields *must* be set in $info except for getLazyCacheFields().
*
* If 'mime' is given, it will be split into major_mime/minor_mime.
* If major_mime/minor_mime are given, $this->mime will also be set.
@@ -511,17 +604,23 @@ class LocalFile extends File {
* Return the width of the image
*
* @param $page int
- * @return bool|int Returns false on error
+ * @return int
*/
public function getWidth( $page = 1 ) {
$this->load();
if ( $this->isMultipage() ) {
- $dim = $this->getHandler()->getPageDimensions( $this, $page );
+ $handler = $this->getHandler();
+ if ( !$handler ) {
+ return 0;
+ }
+ $dim = $handler->getPageDimensions( $this, $page );
if ( $dim ) {
return $dim['width'];
} else {
- return false;
+ // For non-paged media, the false goes through an
+ // intval, turning failure into 0, so do same here.
+ return 0;
}
} else {
return $this->width;
@@ -532,17 +631,23 @@ class LocalFile extends File {
* Return the height of the image
*
* @param $page int
- * @return bool|int Returns false on error
+ * @return int
*/
public function getHeight( $page = 1 ) {
$this->load();
if ( $this->isMultipage() ) {
- $dim = $this->getHandler()->getPageDimensions( $this, $page );
+ $handler = $this->getHandler();
+ if ( !$handler ) {
+ return 0;
+ }
+ $dim = $handler->getPageDimensions( $this, $page );
if ( $dim ) {
return $dim['height'];
} else {
- return false;
+ // For non-paged media, the false goes through an
+ // intval, turning failure into 0, so do same here.
+ return 0;
}
} else {
return $this->height;
@@ -552,7 +657,7 @@ class LocalFile extends File {
/**
* Returns ID or name of user who uploaded the file
*
- * @param $type string 'text' or 'id'
+ * @param string $type 'text' or 'id'
* @return int|string
*/
function getUser( $type = 'text' ) {
@@ -570,7 +675,7 @@ class LocalFile extends File {
* @return string
*/
function getMetadata() {
- $this->load();
+ $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
return $this->metadata;
}
@@ -638,15 +743,14 @@ class LocalFile extends File {
* RTT regression for wikis without 404 handling.
*/
function migrateThumbFile( $thumbName ) {
- $thumbDir = $this->getThumbPath();
-
/* Old code for bug 2532
+ $thumbDir = $this->getThumbPath();
$thumbPath = "$thumbDir/$thumbName";
if ( is_dir( $thumbPath ) ) {
// Directory where file should be
// This happened occasionally due to broken migration code in 1.5
// Rename to broken-*
- for ( $i = 0; $i < 100 ; $i++ ) {
+ for ( $i = 0; $i < 100; $i++ ) {
$broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
if ( !file_exists( $broken ) ) {
rename( $thumbPath, $broken );
@@ -672,7 +776,7 @@ class LocalFile extends File {
/**
* Get all thumbnail names previously generated for this file
- * @param $archiveName string|bool Name of an archive file, default false
+ * @param string|bool $archiveName Name of an archive file, default false
* @return array first element is the base dir, then files in that base dir.
*/
function getThumbnails( $archiveName = false ) {
@@ -684,10 +788,12 @@ class LocalFile extends File {
$backend = $this->repo->getBackend();
$files = array( $dir );
- $iterator = $backend->getFileList( array( 'dir' => $dir ) );
- foreach ( $iterator as $file ) {
- $files[] = $file;
- }
+ try {
+ $iterator = $backend->getFileList( array( 'dir' => $dir ) );
+ foreach ( $iterator as $file ) {
+ $files[] = $file;
+ }
+ } catch ( FileBackendError $e ) {} // suppress (bug 54674)
return $files;
}
@@ -702,7 +808,9 @@ class LocalFile extends File {
}
/**
- * Purge the shared history (OldLocalFile) cache
+ * Purge the shared history (OldLocalFile) cache.
+ *
+ * @note This used to purge old thumbnails as well.
*/
function purgeHistory() {
global $wgMemc;
@@ -710,20 +818,20 @@ class LocalFile extends File {
$hashedName = md5( $this->getName() );
$oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
- // Must purge thumbnails for old versions too! bug 30192
- foreach( $this->getHistory() as $oldFile ) {
- $oldFile->purgeThumbnails();
- }
-
if ( $oldKey ) {
$wgMemc->delete( $oldKey );
}
}
/**
- * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
+ * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid.
+ *
+ * @param Array $options An array potentially with the key forThumbRefresh.
+ *
+ * @note This used to purge old thumbnails by default as well, but doesn't anymore.
*/
function purgeCache( $options = array() ) {
+ wfProfileIn( __METHOD__ );
// Refresh metadata cache
$this->purgeMetadataCache();
@@ -732,11 +840,12 @@ class LocalFile extends File {
// Purge squid cache for this file
SquidUpdate::purge( array( $this->getURL() ) );
+ wfProfileOut( __METHOD__ );
}
/**
* Delete cached transformed files for an archived version only.
- * @param $archiveName string name of the archived file
+ * @param string $archiveName name of the archived file
*/
function purgeOldThumbnails( $archiveName ) {
global $wgUseSquid;
@@ -753,7 +862,7 @@ class LocalFile extends File {
// Purge the squid
if ( $wgUseSquid ) {
$urls = array();
- foreach( $files as $file ) {
+ foreach ( $files as $file ) {
$urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
}
SquidUpdate::purge( $urls );
@@ -771,8 +880,16 @@ class LocalFile extends File {
// Delete thumbnails
$files = $this->getThumbnails();
+ // Always purge all files from squid regardless of handler filters
+ if ( $wgUseSquid ) {
+ $urls = array();
+ foreach ( $files as $file ) {
+ $urls[] = $this->getThumbUrl( $file );
+ }
+ array_shift( $urls ); // don't purge directory
+ }
- // Give media handler a chance to filter the purge list
+ // Give media handler a chance to filter the file purge list
if ( !empty( $options['forThumbRefresh'] ) ) {
$handler = $this->getHandler();
if ( $handler ) {
@@ -788,10 +905,6 @@ class LocalFile extends File {
// Purge the squid
if ( $wgUseSquid ) {
- $urls = array();
- foreach( $files as $file ) {
- $urls[] = $this->getThumbUrl( $file );
- }
SquidUpdate::purge( $urls );
}
@@ -800,13 +913,13 @@ class LocalFile extends File {
/**
* Delete a list of thumbnails visible at urls
- * @param $dir string base dir of the files.
- * @param $files array of strings: relative filenames (to $dir)
+ * @param string $dir base dir of the files.
+ * @param array $files of strings: relative filenames (to $dir)
*/
protected function purgeThumbList( $dir, $files ) {
$fileListDebug = strtr(
var_export( $files, true ),
- array("\n"=>'')
+ array( "\n" => '' )
);
wfDebug( __METHOD__ . ": $fileListDebug\n" );
@@ -814,7 +927,9 @@ class LocalFile extends File {
foreach ( $files as $file ) {
# Check that the base file name is part of the thumb name
# This is a basic sanity check to avoid erasing unrelated directories
- if ( strpos( $file, $this->getName() ) !== false ) {
+ if ( strpos( $file, $this->getName() ) !== false
+ || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
+ ) {
$purgeList[] = "{$dir}/{$file}";
}
}
@@ -949,15 +1064,15 @@ class LocalFile extends File {
/**
* Upload a file and record it in the DB
- * @param $srcPath String: source storage path or virtual URL
- * @param $comment String: upload description
- * @param $pageText String: text to use for the new description page,
+ * @param string $srcPath source storage path, virtual URL, or filesystem path
+ * @param string $comment upload description
+ * @param string $pageText text to use for the new description page,
* if a new description page is created
* @param $flags Integer|bool: flags for publish()
- * @param $props Array|bool: File properties, if known. This can be used to reduce the
+ * @param array|bool $props File properties, if known. This can be used to reduce the
* upload time when uploading virtual URLs for which the file info
* is already known
- * @param $timestamp String|bool: timestamp for img_timestamp, or false to use the current time
+ * @param string|bool $timestamp timestamp for img_timestamp, or false to use the current time
* @param $user User|null: User object or null to use $wgUser
*
* @return FileRepoStatus object. On success, the value member contains the
@@ -970,11 +1085,34 @@ class LocalFile extends File {
return $this->readOnlyFatalStatus();
}
+ if ( !$props ) {
+ wfProfileIn( __METHOD__ . '-getProps' );
+ if ( $this->repo->isVirtualUrl( $srcPath )
+ || FileBackend::isStoragePath( $srcPath ) )
+ {
+ $props = $this->repo->getFileProps( $srcPath );
+ } else {
+ $props = FSFile::getPropsFromPath( $srcPath );
+ }
+ wfProfileOut( __METHOD__ . '-getProps' );
+ }
+
+ $options = array();
+ $handler = MediaHandler::getHandler( $props['mime'] );
+ if ( $handler ) {
+ $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
+ } else {
+ $options['headers'] = array();
+ }
+
+ // Trim spaces on user supplied text
+ $comment = trim( $comment );
+
// truncate nicely or the DB will do it for us
// non-nicely (dangling multi-byte chars, non-truncated version in cache).
$comment = $wgContLang->truncate( $comment, 255 );
$this->lock(); // begin
- $status = $this->publish( $srcPath, $flags );
+ $status = $this->publish( $srcPath, $flags, $options );
if ( $status->successCount > 0 ) {
# Essentially we are displacing any existing current file and saving
@@ -999,20 +1137,25 @@ class LocalFile extends File {
* @param $source string
* @param $watch bool
* @param $timestamp string|bool
+ * @param $user User object or null to use $wgUser
* @return bool
*/
function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
- $watch = false, $timestamp = false )
+ $watch = false, $timestamp = false, User $user = null )
{
+ if ( !$user ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+
$pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
- if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
+ if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
return false;
}
if ( $watch ) {
- global $wgUser;
- $wgUser->addWatch( $this->getTitle() );
+ $user->addWatch( $this->getTitle() );
}
return true;
}
@@ -1070,20 +1213,20 @@ class LocalFile extends File {
# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
$dbw->insert( 'image',
array(
- 'img_name' => $this->getName(),
- 'img_size' => $this->size,
- 'img_width' => intval( $this->width ),
- 'img_height' => intval( $this->height ),
- 'img_bits' => $this->bits,
- 'img_media_type' => $this->media_type,
- 'img_major_mime' => $this->major_mime,
- 'img_minor_mime' => $this->minor_mime,
- 'img_timestamp' => $timestamp,
+ 'img_name' => $this->getName(),
+ 'img_size' => $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->media_type,
+ 'img_major_mime' => $this->major_mime,
+ 'img_minor_mime' => $this->minor_mime,
+ 'img_timestamp' => $timestamp,
'img_description' => $comment,
- 'img_user' => $user->getId(),
- 'img_user_text' => $user->getName(),
- 'img_metadata' => $this->metadata,
- 'img_sha1' => $this->sha1
+ 'img_user' => $user->getId(),
+ 'img_user_text' => $user->getName(),
+ 'img_metadata' => $dbw->encodeBlob($this->metadata),
+ 'img_sha1' => $this->sha1
),
__METHOD__,
'IGNORE'
@@ -1133,7 +1276,7 @@ class LocalFile extends File {
'img_description' => $comment,
'img_user' => $user->getId(),
'img_user_text' => $user->getName(),
- 'img_metadata' => $this->metadata,
+ 'img_metadata' => $dbw->encodeBlob($this->metadata),
'img_sha1' => $this->sha1
),
array( 'img_name' => $this->getName() ),
@@ -1149,23 +1292,48 @@ class LocalFile extends File {
$wikiPage->setFile( $this );
# Add the log entry
- $log = new LogPage( 'upload' );
$action = $reupload ? 'overwrite' : 'upload';
- $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user );
- wfProfileIn( __METHOD__ . '-edit' );
+ $logEntry = new ManualLogEntry( 'upload', $action );
+ $logEntry->setPerformer( $user );
+ $logEntry->setComment( $comment );
+ $logEntry->setTarget( $descTitle );
+
+ // Allow people using the api to associate log entries with the upload.
+ // Log has a timestamp, but sometimes different from upload timestamp.
+ $logEntry->setParameters(
+ array(
+ 'img_sha1' => $this->sha1,
+ 'img_timestamp' => $timestamp,
+ )
+ );
+ // Note we keep $logId around since during new image
+ // creation, page doesn't exist yet, so log_page = 0
+ // but we want it to point to the page we're making,
+ // so we later modify the log entry.
+ // For a similar reason, we avoid making an RC entry
+ // now and wait until the page exists.
+ $logId = $logEntry->insert();
+
$exists = $descTitle->exists();
+ if ( $exists ) {
+ // Page exists, do RC entry now (otherwise we wait for later).
+ $logEntry->publish( $logId );
+ }
+ wfProfileIn( __METHOD__ . '-edit' );
if ( $exists ) {
# Create a null revision
$latest = $descTitle->getLatestRevID();
+ $editSummary = LogFormatter::newFromEntry( $logEntry )->getPlainActionText();
+
$nullRevision = Revision::newNullRevision(
$dbw,
$descTitle->getArticleID(),
- $log->getRcComment(),
+ $editSummary,
false
);
- if (!is_null($nullRevision)) {
+ if ( !is_null( $nullRevision ) ) {
$nullRevision->insertOn( $dbw );
wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
@@ -1186,19 +1354,24 @@ class LocalFile extends File {
} else {
# New file; create the description page.
# There's already a log entry, so don't make a second RC entry
- # Squid and file cache for the description page are purged by doEdit.
- $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
-
- if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
- $dbw->begin();
+ # Squid and file cache for the description page are purged by doEditContent.
+ $content = ContentHandler::makeContent( $pageText, $descTitle );
+ $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
+
+ $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction
+ // Now that the page exists, make an RC entry.
+ $logEntry->publish( $logId );
+ if ( isset( $status->value['revision'] ) ) {
$dbw->update( 'logging',
array( 'log_page' => $status->value['revision']->getPage() ),
array( 'log_id' => $logId ),
__METHOD__
);
- $dbw->commit(); // commit before anything bad can happen
}
+ $dbw->commit( __METHOD__ ); // commit before anything bad can happen
}
+
+
wfProfileOut( __METHOD__ . '-edit' );
# Save to cache and purge the squid
@@ -1225,11 +1398,17 @@ class LocalFile extends File {
# Invalidate cache for all pages using this file
$update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
$update->doUpdate();
+ if ( !$reupload ) {
+ LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
+ }
# Invalidate cache for all pages that redirects on this page
$redirs = $this->getTitle()->getRedirectsHere();
foreach ( $redirs as $redir ) {
+ if ( !$reupload && $redir->getNamespace() === NS_FILE ) {
+ LinksUpdate::queueRecursiveJobsForTable( $redir, 'imagelinks' );
+ }
$update = new HTMLCacheUpdate( $redir, 'imagelinks' );
$update->doUpdate();
}
@@ -1246,14 +1425,15 @@ class LocalFile extends File {
* The archive name should be passed through to recordUpload for database
* registration.
*
- * @param $srcPath String: local filesystem path to the source image
+ * @param string $srcPath local filesystem path to the source image
* @param $flags Integer: a bitwise combination of:
* File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
+ * @param array $options Optional additional parameters
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*/
- function publish( $srcPath, $flags = 0 ) {
- return $this->publishTo( $srcPath, $this->getRel(), $flags );
+ function publish( $srcPath, $flags = 0, array $options = array() ) {
+ return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
}
/**
@@ -1263,24 +1443,25 @@ class LocalFile extends File {
* The archive name should be passed through to recordUpload for database
* registration.
*
- * @param $srcPath String: local filesystem path to the source image
- * @param $dstRel String: target relative path
+ * @param string $srcPath local filesystem path to the source image
+ * @param string $dstRel target relative path
* @param $flags Integer: a bitwise combination of:
* File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
+ * @param array $options Optional additional parameters
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*/
- function publishTo( $srcPath, $dstRel, $flags = 0 ) {
+ function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
if ( $this->getRepo()->getReadOnlyReason() !== false ) {
return $this->readOnlyFatalStatus();
}
$this->lock(); // begin
- $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
+ $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
$archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
- $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
+ $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
if ( $status->value == 'new' ) {
$status->value = '';
@@ -1326,18 +1507,27 @@ class LocalFile extends File {
wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
- $this->purgeEverything();
- foreach ( $archiveNames as $archiveName ) {
- $this->purgeOldThumbnails( $archiveName );
- }
+ // Purge the source and target files...
+ $oldTitleFile = wfLocalFile( $this->title );
+ $newTitleFile = wfLocalFile( $target );
+ // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
+ // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
+ $this->getRepo()->getMasterDB()->onTransactionIdle(
+ function() use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
+ $oldTitleFile->purgeEverything();
+ foreach ( $archiveNames as $archiveName ) {
+ $oldTitleFile->purgeOldThumbnails( $archiveName );
+ }
+ $newTitleFile->purgeEverything();
+ }
+ );
+
if ( $status->isOK() ) {
// Now switch the object
$this->title = $target;
// Force regeneration of the name and hashpath
unset( $this->name );
unset( $this->hashPath );
- // Purge the new image
- $this->purgeEverything();
}
return $status;
@@ -1373,10 +1563,28 @@ class LocalFile extends File {
DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
}
- $this->purgeEverything();
- foreach ( $archiveNames as $archiveName ) {
- $this->purgeOldThumbnails( $archiveName );
- }
+ // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
+ // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
+ $file = $this;
+ $this->getRepo()->getMasterDB()->onTransactionIdle(
+ function() use ( $file, $archiveNames ) {
+ global $wgUseSquid;
+
+ $file->purgeEverything();
+ foreach ( $archiveNames as $archiveName ) {
+ $file->purgeOldThumbnails( $archiveName );
+ }
+
+ if ( $wgUseSquid ) {
+ // Purge the squid
+ $purgeUrls = array();
+ foreach ( $archiveNames as $archiveName ) {
+ $purgeUrls[] = $file->getArchiveUrl( $archiveName );
+ }
+ SquidUpdate::purge( $purgeUrls );
+ }
+ }
+ );
return $status;
}
@@ -1396,6 +1604,7 @@ class LocalFile extends File {
* @return FileRepoStatus object.
*/
function deleteOld( $archiveName, $reason, $suppress = false ) {
+ global $wgUseSquid;
if ( $this->getRepo()->getReadOnlyReason() !== false ) {
return $this->readOnlyFatalStatus();
}
@@ -1413,6 +1622,11 @@ class LocalFile extends File {
$this->purgeHistory();
}
+ if ( $wgUseSquid ) {
+ // Purge the squid
+ SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
+ }
+
return $status;
}
@@ -1422,7 +1636,7 @@ class LocalFile extends File {
*
* May throw database exceptions on error.
*
- * @param $versions array set of record ids of deleted items to restore,
+ * @param array $versions set of record ids of deleted items to restore,
* or empty to restore all revisions.
* @param $unsuppress Boolean
* @return FileRepoStatus
@@ -1462,22 +1676,27 @@ class LocalFile extends File {
* @return String
*/
function getDescriptionUrl() {
- return $this->title->getLocalUrl();
+ return $this->title->getLocalURL();
}
/**
* Get the HTML text of the description page
* This is not used by ImagePage for local files, since (among other things)
* it skips the parser cache.
+ *
+ * @param $lang Language What language to get description in (Optional)
* @return bool|mixed
*/
- function getDescriptionText() {
- global $wgParser;
+ function getDescriptionText( $lang = null ) {
$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
- if ( !$revision ) return false;
- $text = $revision->getText();
- if ( !$text ) return false;
- $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
+ if ( !$revision ) {
+ return false;
+ }
+ $content = $revision->getContent();
+ if ( !$content ) {
+ return false;
+ }
+ $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
return $pout->getText();
}
@@ -1531,11 +1750,13 @@ class LocalFile extends File {
}
/**
- * @return bool
+ * @return bool Whether to cache in RepoGroup (this avoids OOMs)
*/
function isCacheable() {
$this->load();
- return strlen( $this->metadata ) <= self::CACHE_FIELD_MAX_LEN; // avoid OOMs
+ // If extra data (metadata) was not loaded then it must have been large
+ return $this->extraDataLoaded
+ && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
}
/**
@@ -1547,8 +1768,21 @@ class LocalFile extends File {
$dbw = $this->repo->getMasterDB();
if ( !$this->locked ) {
- $dbw->begin( __METHOD__ );
+ if ( !$dbw->trxLevel() ) {
+ $dbw->begin( __METHOD__ );
+ $this->lockedOwnTrx = true;
+ }
$this->locked++;
+ // Bug 54736: use simple lock to handle when the file does not exist.
+ // SELECT FOR UPDATE only locks records not the gaps where there are none.
+ $cache = wfGetMainCache();
+ $key = $this->getCacheKey();
+ if ( !$cache->lock( $key, 60 ) ) {
+ throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
+ }
+ $dbw->onTransactionIdle( function() use ( $cache, $key ) {
+ $cache->unlock( $key ); // release on commit
+ } );
}
return $dbw->selectField( 'image', '1',
@@ -1562,9 +1796,10 @@ class LocalFile extends File {
function unlock() {
if ( $this->locked ) {
--$this->locked;
- if ( !$this->locked ) {
+ if ( !$this->locked && $this->lockedOwnTrx ) {
$dbw = $this->repo->getMasterDB();
$dbw->commit( __METHOD__ );
+ $this->lockedOwnTrx = false;
}
}
}
@@ -1576,6 +1811,7 @@ class LocalFile extends File {
$this->locked = false;
$dbw = $this->repo->getMasterDB();
$dbw->rollback( __METHOD__ );
+ $this->lockedOwnTrx = false;
}
/**
@@ -1758,7 +1994,7 @@ class LocalFileDeleteBatch {
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_deleted_reason' => $encReason,
- 'fa_deleted' => $this->suppress ? $bitfield : 0,
+ 'fa_deleted' => $this->suppress ? $bitfield : 0,
'fa_name' => 'img_name',
'fa_archive_name' => 'NULL',
@@ -1773,7 +2009,8 @@ class LocalFileDeleteBatch {
'fa_description' => 'img_description',
'fa_user' => 'img_user',
'fa_user_text' => 'img_user_text',
- 'fa_timestamp' => 'img_timestamp'
+ 'fa_timestamp' => 'img_timestamp',
+ 'fa_sha1' => 'img_sha1',
), $where, __METHOD__ );
}
@@ -1805,6 +2042,7 @@ class LocalFileDeleteBatch {
'fa_user' => 'oi_user',
'fa_user_text' => 'oi_user_text',
'fa_timestamp' => 'oi_timestamp',
+ 'fa_sha1' => 'oi_sha1',
), $where, __METHOD__ );
}
}
@@ -1836,7 +2074,7 @@ class LocalFileDeleteBatch {
$this->file->lock();
// Leave private files alone
$privateFiles = array();
- list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+ list( $oldRels, ) = $this->getOldRels();
$dbw = $this->file->repo->getMasterDB();
if ( !empty( $oldRels ) ) {
@@ -1914,7 +2152,7 @@ class LocalFileDeleteBatch {
$files = $newBatch = array();
foreach ( $batch as $batchItem ) {
- list( $src, $dest ) = $batchItem;
+ list( $src, ) = $batchItem;
$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
}
@@ -2004,7 +2242,9 @@ class LocalFileRestoreBatch {
$conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
}
- $result = $dbw->select( 'filearchive', '*',
+ $result = $dbw->select(
+ 'filearchive',
+ ArchivedFile::selectFields(),
$conditions,
__METHOD__,
array( 'ORDER BY' => 'fa_timestamp DESC' )
@@ -2037,7 +2277,12 @@ class LocalFileRestoreBatch {
$deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
$deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
- $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
+ if ( isset( $row->fa_sha1 ) ) {
+ $sha1 = $row->fa_sha1;
+ } else {
+ // old row, populate from key
+ $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
+ }
# Fix leading zero
if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
@@ -2251,7 +2496,7 @@ class LocalFileRestoreBatch {
/**
* Delete unused files in the deleted zone.
* This should be called from outside the transaction in which execute() was called.
- * @return FileRepoStatus|void
+ * @return FileRepoStatus
*/
function cleanup() {
if ( !$this->cleanupBatch ) {
@@ -2492,7 +2737,7 @@ class LocalFileMoveBatch {
*/
function getMoveTriplets() {
$moves = array_merge( array( $this->cur ), $this->olds );
- $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
+ $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
foreach ( $moves as $move ) {
// $move: (oldRelativePath, newRelativePath)
diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php
index 40d7dca7..2c545963 100644
--- a/includes/filerepo/file/OldLocalFile.php
+++ b/includes/filerepo/file/OldLocalFile.php
@@ -42,7 +42,7 @@ class OldLocalFile extends LocalFile {
static function newFromTitle( $title, $repo, $time = null ) {
# The null default value is only here to avoid an E_STRICT
if ( $time === null ) {
- throw new MWException( __METHOD__.' got null for $time parameter' );
+ throw new MWException( __METHOD__ . ' got null for $time parameter' );
}
return new self( $title, $repo, $time, null );
}
@@ -73,7 +73,7 @@ class OldLocalFile extends LocalFile {
* Create a OldLocalFile from a SHA-1 key
* Do not call this except from inside a repo class.
*
- * @param $sha1 string base-36 SHA-1
+ * @param string $sha1 base-36 SHA-1
* @param $repo LocalRepo
* @param string|bool $timestamp MW_timestamp (optional)
*
@@ -123,8 +123,8 @@ class OldLocalFile extends LocalFile {
/**
* @param $title Title
* @param $repo FileRepo
- * @param $time String: timestamp or null to load by archive name
- * @param $archiveName String: archive name or null to load by timestamp
+ * @param string $time timestamp or null to load by archive name
+ * @param string $archiveName archive name or null to load by timestamp
* @throws MWException
*/
function __construct( $title, $repo, $time, $archiveName ) {
@@ -132,7 +132,7 @@ class OldLocalFile extends LocalFile {
$this->requestedTime = $time;
$this->archive_name = $archiveName;
if ( is_null( $time ) && is_null( $archiveName ) ) {
- throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' );
+ throw new MWException( __METHOD__ . ': must specify at least one of $time or $archiveName' );
}
}
@@ -164,18 +164,19 @@ class OldLocalFile extends LocalFile {
* @return bool
*/
function isVisible() {
- return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
+ return $this->exists() && !$this->isDeleted( File::DELETED_FILE );
}
function loadFromDB() {
wfProfileIn( __METHOD__ );
+
$this->dataLoaded = true;
$dbr = $this->repo->getSlaveDB();
$conds = array( 'oi_name' => $this->getName() );
if ( is_null( $this->requestedTime ) ) {
$conds['oi_archive_name'] = $this->archive_name;
} else {
- $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
+ $conds['oi_timestamp'] = $dbr->timestamp( $this->requestedTime );
}
$row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
$conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
@@ -184,6 +185,43 @@ class OldLocalFile extends LocalFile {
} else {
$this->fileExists = false;
}
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Load lazy file metadata from the DB
+ */
+ protected function loadExtraFromDB() {
+ wfProfileIn( __METHOD__ );
+
+ $this->extraDataLoaded = true;
+ $dbr = $this->repo->getSlaveDB();
+ $conds = array( 'oi_name' => $this->getName() );
+ if ( is_null( $this->requestedTime ) ) {
+ $conds['oi_archive_name'] = $this->archive_name;
+ } else {
+ $conds['oi_timestamp'] = $dbr->timestamp( $this->requestedTime );
+ }
+ // In theory the file could have just been renamed/deleted...oh well
+ $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
+ $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
+
+ if ( !$row ) { // fallback to master
+ $dbr = $this->repo->getMasterDB();
+ $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
+ $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
+ }
+
+ if ( $row ) {
+ foreach ( $this->unprefixRow( $row, 'oi_' ) as $name => $value ) {
+ $this->$name = $value;
+ }
+ } else {
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Could not find data for image '{$this->archive_name}'." );
+ }
+
wfProfileOut( __METHOD__ );
}
@@ -218,7 +256,7 @@ class OldLocalFile extends LocalFile {
# 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;
}
@@ -226,7 +264,7 @@ class OldLocalFile extends LocalFile {
$dbw = $this->repo->getMasterDB();
list( $major, $minor ) = self::splitMime( $this->mime );
- wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
+ wfDebug( __METHOD__ . ': upgrading ' . $this->archive_name . " to the current schema\n" );
$dbw->update( 'oldimage',
array(
'oi_size' => $this->size, // sanity
@@ -253,7 +291,7 @@ class OldLocalFile extends LocalFile {
*/
function isDeleted( $field ) {
$this->load();
- return ($this->deleted & $field) == $field;
+ return ( $this->deleted & $field ) == $field;
}
/**
@@ -281,8 +319,8 @@ class OldLocalFile extends LocalFile {
/**
* Upload a file directly into archive. Generally for Special:Import.
*
- * @param $srcPath string File system path of the source file
- * @param $archiveName string Full archive name of the file, in the form
+ * @param string $srcPath File system path of the source file
+ * @param string $archiveName Full archive name of the file, in the form
* $timestamp!$filename, where $filename must match $this->getName()
*
* @param $timestamp string
@@ -313,10 +351,10 @@ class OldLocalFile extends LocalFile {
/**
* Record a file upload in the oldimage table, without adding log entries.
*
- * @param $srcPath string File system path to the source file
- * @param $archiveName string The archive name of the file
+ * @param string $srcPath File system path to the source file
+ * @param string $archiveName The archive name of the file
* @param $timestamp string
- * @param $comment string Upload comment
+ * @param string $comment Upload comment
* @param $user User User who did this upload
* @return bool
*/
diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php
index 8d4a3f88..47ba6d6b 100644
--- a/includes/filerepo/file/UnregisteredLocalFile.php
+++ b/includes/filerepo/file/UnregisteredLocalFile.php
@@ -42,7 +42,7 @@ class UnregisteredLocalFile extends File {
var $handler;
/**
- * @param $path string Storage path
+ * @param string $path Storage path
* @param $mime string
* @return UnregisteredLocalFile
*/
@@ -71,7 +71,7 @@ class UnregisteredLocalFile extends File {
*/
function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
if ( !( $title && $repo ) && !$path ) {
- throw new MWException( __METHOD__.': not enough parameters, must specify title and repo, or a full path' );
+ throw new MWException( __METHOD__ . ': not enough parameters, must specify title and repo, or a full path' );
}
if ( $title instanceof Title ) {
$this->title = File::normalizeTitle( $title, 'exception' );
@@ -179,10 +179,18 @@ class UnregisteredLocalFile extends File {
*/
function getSize() {
$this->assertRepoDefined();
- $props = $this->repo->getFileProps( $this->path );
- if ( isset( $props['size'] ) ) {
- return $props['size'];
- }
- return false; // doesn't exist
+ return $this->repo->getFileSize( $this->path );
+ }
+
+ /**
+ * Optimize getLocalRefPath() by using an existing local reference.
+ * The file at the path of $fsFile should not be deleted (or at least
+ * not until the end of the request). This is mostly a performance hack.
+ *
+ * @param $fsFile FSFile
+ * @return void
+ */
+ public function setLocalReference( FSFile $fsFile ) {
+ $this->fsFile = $fsFile;
}
}
diff --git a/includes/gallery/ImageGalleryBase.php b/includes/gallery/ImageGalleryBase.php
new file mode 100644
index 00000000..f8b8c505
--- /dev/null
+++ b/includes/gallery/ImageGalleryBase.php
@@ -0,0 +1,331 @@
+<?php
+/**
+ * Image gallery.
+ *
+ * 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
+ */
+
+/**
+ * Image gallery
+ *
+ * Add images to the gallery using add(), then render that list to HTML using toHTML().
+ *
+ * @ingroup Media
+ */
+abstract class ImageGalleryBase extends ContextSource {
+ var $mImages, $mShowBytes, $mShowFilename, $mMode;
+ var $mCaption = false;
+
+ /**
+ * Hide blacklisted images?
+ */
+ var $mHideBadImages;
+
+ /**
+ * Registered parser object for output callbacks
+ * @var Parser
+ */
+ var $mParser;
+
+ /**
+ * Contextual title, used when images are being screened
+ * against the bad image list
+ */
+ protected $contextTitle = false;
+
+ protected $mAttribs = array();
+
+ static private $modeMapping = false;
+
+ /**
+ * Get a new image gallery. This is the method other callers
+ * should use to get a gallery.
+ *
+ * @param String|bool $mode Mode to use. False to use the default.
+ */
+ static function factory( $mode = false ) {
+ global $wgGalleryOptions, $wgContLang;
+ self::loadModes();
+ if ( !$mode ) {
+ $mode = $wgGalleryOptions['mode'];
+ }
+
+ $mode = $wgContLang->lc( $mode );
+
+ if ( isset( self::$modeMapping[$mode] ) ) {
+ return new self::$modeMapping[$mode]( $mode );
+ } else {
+ throw new MWException( "No gallery class registered for mode $mode" );
+ }
+ }
+
+ static private function loadModes() {
+ if ( self::$modeMapping === false ) {
+ self::$modeMapping = array(
+ 'traditional' => 'TraditionalImageGallery',
+ 'nolines' => 'NolinesImageGallery',
+ 'packed' => 'PackedImageGallery',
+ 'packed-hover' => 'PackedHoverImageGallery',
+ 'packed-overlay' => 'PackedOverlayImageGallery',
+ );
+ // Allow extensions to make a new gallery format.
+ wfRunHooks( 'GalleryGetModes', self::$modeMapping );
+ }
+ }
+
+ /**
+ * Create a new image gallery object.
+ *
+ * You should not call this directly, but instead use
+ * ImageGalleryBase::factory().
+ */
+ function __construct( $mode = 'traditional' ) {
+ global $wgGalleryOptions;
+ $this->mImages = array();
+ $this->mShowBytes = $wgGalleryOptions['showBytes'];
+ $this->mShowFilename = true;
+ $this->mParser = false;
+ $this->mHideBadImages = false;
+ $this->mPerRow = $wgGalleryOptions['imagesPerRow'];
+ $this->mWidths = $wgGalleryOptions['imageWidth'];
+ $this->mHeights = $wgGalleryOptions['imageHeight'];
+ $this->mCaptionLength = $wgGalleryOptions['captionLength'];
+ $this->mMode = $mode;
+ }
+
+ /**
+ * Register a parser object. If you do not set this
+ * and the output of this gallery ends up in parser
+ * cache, the javascript will break!
+ *
+ * @note This also triggers using the page's target
+ * language instead of the user language.
+ *
+ * @param $parser Parser
+ */
+ function setParser( $parser ) {
+ $this->mParser = $parser;
+ }
+
+ /**
+ * Set bad image flag
+ */
+ function setHideBadImages( $flag = true ) {
+ $this->mHideBadImages = $flag;
+ }
+
+ /**
+ * Set the caption (as plain text)
+ *
+ * @param string $caption Caption
+ */
+ function setCaption( $caption ) {
+ $this->mCaption = htmlspecialchars( $caption );
+ }
+
+ /**
+ * Set the caption (as HTML)
+ *
+ * @param string $caption Caption
+ */
+ public function setCaptionHtml( $caption ) {
+ $this->mCaption = $caption;
+ }
+
+ /**
+ * Set how many images will be displayed per row.
+ *
+ * @param $num Integer >= 0; If perrow=0 the gallery layout will adapt to screensize
+ * invalid numbers will be rejected
+ */
+ public function setPerRow( $num ) {
+ if ( $num >= 0 ) {
+ $this->mPerRow = (int)$num;
+ }
+ }
+
+ /**
+ * Set how wide each image will be, in pixels.
+ *
+ * @param $num Integer > 0; invalid numbers will be ignored
+ */
+ public function setWidths( $num ) {
+ if ( $num > 0 ) {
+ $this->mWidths = (int)$num;
+ }
+ }
+
+ /**
+ * Set how high each image will be, in pixels.
+ *
+ * @param $num Integer > 0; invalid numbers will be ignored
+ */
+ public function setHeights( $num ) {
+ if ( $num > 0 ) {
+ $this->mHeights = (int)$num;
+ }
+ }
+
+ /**
+ * Allow setting additional options. This is meant
+ * to allow extensions to add additional parameters to
+ * <gallery> parser tag.
+ *
+ * @param Array $options Attributes of gallery tag.
+ */
+ public function setAdditionalOptions( $options ) { }
+
+ /**
+ * Instruct the class to use a specific skin for rendering
+ *
+ * @param $skin Skin object
+ * @deprecated since 1.18 Not used anymore
+ */
+ function useSkin( $skin ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ /* no op */
+ }
+
+ /**
+ * Add an image to the gallery.
+ *
+ * @param $title Title object of the image that is added to the gallery
+ * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown.
+ * @param $alt String: Alt text for the image
+ * @param $link String: Override image link (optional)
+ * @param $handlerOpts Array: Array of options for image handler (aka page number)
+ */
+ function add( $title, $html = '', $alt = '', $link = '', $handlerOpts = array() ) {
+ if ( $title instanceof File ) {
+ // Old calling convention
+ $title = $title->getTitle();
+ }
+ $this->mImages[] = array( $title, $html, $alt, $link, $handlerOpts );
+ wfDebug( 'ImageGallery::add ' . $title->getText() . "\n" );
+ }
+
+ /**
+ * Add an image at the beginning of the gallery.
+ *
+ * @param $title Title object of the image that is added to the gallery
+ * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown.
+ * @param $alt String: Alt text for the image
+ * @param $link String: Override image link (optional)
+ * @param $handlerOpts Array: Array of options for image handler (aka page number)
+ */
+ function insert( $title, $html = '', $alt = '', $link = '', $handlerOpts = array() ) {
+ if ( $title instanceof File ) {
+ // Old calling convention
+ $title = $title->getTitle();
+ }
+ array_unshift( $this->mImages, array( &$title, $html, $alt, $link, $handlerOpts ) );
+ }
+
+ /**
+ * isEmpty() returns true if the gallery contains no images
+ * @return bool
+ */
+ function isEmpty() {
+ return empty( $this->mImages );
+ }
+
+ /**
+ * Enable/Disable showing of the file size of an image in the gallery.
+ * Enabled by default.
+ *
+ * @param $f Boolean: set to false to disable.
+ */
+ function setShowBytes( $f ) {
+ $this->mShowBytes = (bool)$f;
+ }
+
+ /**
+ * Enable/Disable showing of the filename of an image in the gallery.
+ * Enabled by default.
+ *
+ * @param $f Boolean: set to false to disable.
+ */
+ function setShowFilename( $f ) {
+ $this->mShowFilename = (bool)$f;
+ }
+
+ /**
+ * Set arbitrary attributes to go on the HTML gallery output element.
+ * Should be suitable for a <ul> element.
+ *
+ * Note -- if taking from user input, you should probably run through
+ * Sanitizer::validateAttributes() first.
+ *
+ * @param array $attribs of HTML attribute pairs
+ */
+ function setAttributes( $attribs ) {
+ $this->mAttribs = $attribs;
+ }
+
+ /**
+ * Display an html representation of the gallery
+ *
+ * @return String The html
+ */
+ abstract public function toHTML();
+
+ /**
+ * @return Integer: number of images in the gallery
+ */
+ public function count() {
+ return count( $this->mImages );
+ }
+
+ /**
+ * Set the contextual title
+ *
+ * @param $title Title: contextual title
+ */
+ public function setContextTitle( $title ) {
+ $this->contextTitle = $title;
+ }
+
+ /**
+ * Get the contextual title, if applicable
+ *
+ * @return mixed Title or false
+ */
+ public function getContextTitle() {
+ return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title
+ ? $this->contextTitle
+ : false;
+ }
+
+ /**
+ * Determines the correct language to be used for this image gallery
+ * @return Language object
+ */
+ protected function getRenderLang() {
+ return $this->mParser
+ ? $this->mParser->getTargetLanguage()
+ : $this->getLanguage();
+ }
+
+ /* Old constants no longer used.
+ const THUMB_PADDING = 30;
+ const GB_PADDING = 5;
+ const GB_BORDERS = 8;
+ */
+
+}
+
diff --git a/includes/gallery/NolinesImageGallery.php b/includes/gallery/NolinesImageGallery.php
new file mode 100644
index 00000000..6b0d0fa6
--- /dev/null
+++ b/includes/gallery/NolinesImageGallery.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Nolines image gallery. Like "traditional" but without borders and
+ * less padding.
+ *
+ * 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
+ */
+
+class NolinesImageGallery extends TraditionalImageGallery {
+
+ protected function getThumbPadding() {
+ return 0;
+ }
+
+ protected function getGBBorders() {
+ // This accounts for extra space between <li> elements.
+ return 4;
+ }
+
+ protected function getVPad( $boxHeight, $thumbHeight ) {
+ return 0;
+ }
+}
diff --git a/includes/gallery/PackedImageGallery.php b/includes/gallery/PackedImageGallery.php
new file mode 100644
index 00000000..963ee6b9
--- /dev/null
+++ b/includes/gallery/PackedImageGallery.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Packed image gallery. All images adjusted to be same height.
+ *
+ * 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
+ */
+
+class PackedImageGallery extends TraditionalImageGallery {
+
+ function __construct( $mode = 'traditional' ) {
+ parent::__construct( $mode );
+ // Does not support per row option.
+ $this->mPerRow = 0;
+ }
+
+ /**
+ * We artificially have 1.5 the resolution neccessary so that
+ * we can scale it up by that much on the client side, without
+ * worrying about requesting a new image.
+ */
+ const SCALE_FACTOR = 1.5;
+
+ protected function getVPad( $boxHeight, $thumbHeight ) {
+ return ( $this->getThumbPadding() + $boxHeight - $thumbHeight / self::SCALE_FACTOR ) / 2;
+ }
+
+ protected function getThumbPadding() {
+ return 0;
+ }
+
+ protected function getGBPadding() {
+ return 2;
+ }
+
+ /**
+ * @param File $img The file being transformed. May be false
+ */
+ protected function getThumbParams( $img ) {
+ if ( $img && $img->getMediaType() === MEDIATYPE_AUDIO ) {
+ $width = $this->mWidths;
+ } else {
+ // We want the width not to be the constraining
+ // factor, so use random big number.
+ $width = $this->mHeights * 10 + 100;
+ }
+ // self::SCALE_FACTOR so the js has some room to manipulate sizes.
+ return array(
+ 'width' => $width * self::SCALE_FACTOR,
+ 'height' => $this->mHeights * self::SCALE_FACTOR,
+ );
+ }
+
+ protected function getThumbDivWidth( $thumbWidth ) {
+ // Require at least 60px wide, so caption is wide enough to work.
+ if ( $thumbWidth < 60 * self::SCALE_FACTOR ) {
+ $thumbWidth = 60 * self::SCALE_FACTOR;
+ }
+ return $thumbWidth / self::SCALE_FACTOR + $this->getThumbPadding();
+ }
+
+ /**
+ * @param MediaTransformOutput|bool $thumb the thumbnail, or false if no thumb (which can happen)
+ */
+ protected function getGBWidth( $thumb ) {
+ $thumbWidth = $thumb ? $thumb->getWidth() : $this->mWidths * self::SCALE_FACTOR;
+ return $this->getThumbDivWidth( $thumbWidth ) + $this->getGBPadding();
+ }
+
+ protected function adjustImageParameters( $thumb, &$imageParameters ) {
+ // Re-adjust back to normal size.
+ $imageParameters['override-width'] = ceil( $thumb->getWidth() / self::SCALE_FACTOR );
+ $imageParameters['override-height'] = ceil( $thumb->getHeight() / self::SCALE_FACTOR );
+ }
+
+ /**
+ * Add javascript which auto-justifies the rows by manipulating the image sizes.
+ * Also ensures that the hover version of this degrades gracefully.
+ */
+ protected function getModules() {
+ return array( 'mediawiki.page.gallery' );
+ }
+
+ /**
+ * Do not support per-row on packed. It really doesn't work
+ * since the images have varying widths.
+ */
+ public function setPerRow( $num ) {
+ return;
+ }
+}
diff --git a/includes/gallery/PackedOverlayImageGallery.php b/includes/gallery/PackedOverlayImageGallery.php
new file mode 100644
index 00000000..bba06fcf
--- /dev/null
+++ b/includes/gallery/PackedOverlayImageGallery.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Packed overlay image gallery. All images adjusted to be same height and
+ * image caption being placed over top of image.
+ *
+ * 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
+ */
+
+class PackedOverlayImageGallery extends PackedImageGallery {
+
+ /**
+ * Add the wrapper html around the thumb's caption
+ *
+ * @param String $galleryText The caption
+ * @param MediaTransformOutput|boolean $thumb The thumb this caption is for or false for bad image.
+ */
+ protected function wrapGalleryText( $galleryText, $thumb ) {
+
+ // If we have no text, do not output anything to avoid
+ // ugly white overlay.
+ if ( trim( $galleryText ) === '' ) {
+ return '';
+ }
+
+ # ATTENTION: The newline after <div class="gallerytext"> is needed to accommodate htmltidy which
+ # in version 4.8.6 generated crackpot html in its absence, see:
+ # http://bugzilla.wikimedia.org/show_bug.cgi?id=1765 -Ævar
+
+ $thumbWidth = $this->getGBWidth( $thumb ) - $this->getThumbPadding() - $this->getGBPadding();
+ $captionWidth = ceil( $thumbWidth - 20 );
+
+ $outerWrapper = '<div class="gallerytextwrapper" style="width: ' . $captionWidth . 'px">';
+ return "\n\t\t\t" . $outerWrapper . '<div class="gallerytext">' . "\n"
+ . $galleryText
+ . "\n\t\t\t</div>";
+ }
+}
+
+/**
+ * Same as Packed except different CSS is applied to make the
+ * caption only show up on hover. If a touch screen is detected,
+ * falls back to PackedHoverGallery. Degrades gracefully for
+ * screen readers.
+ */
+class PackedHoverImageGallery extends PackedOverlayImageGallery { }
diff --git a/includes/gallery/TraditionalImageGallery.php b/includes/gallery/TraditionalImageGallery.php
new file mode 100644
index 00000000..1f60fa69
--- /dev/null
+++ b/includes/gallery/TraditionalImageGallery.php
@@ -0,0 +1,328 @@
+<?php
+/**
+ * Image gallery.
+ *
+ * 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
+ */
+
+class TraditionalImageGallery extends ImageGalleryBase {
+
+
+ /**
+ * Return a HTML representation of the image gallery
+ *
+ * For each image in the gallery, display
+ * - a thumbnail
+ * - the image name
+ * - the additional text provided when adding the image
+ * - the size of the image
+ *
+ * @return string
+ */
+ function toHTML() {
+ if ( $this->mPerRow > 0 ) {
+ $maxwidth = $this->mPerRow * ( $this->mWidths + $this->getAllPadding() );
+ $oldStyle = isset( $this->mAttribs['style'] ) ? $this->mAttribs['style'] : '';
+ # _width is ignored by any sane browser. IE6 doesn't know max-width so it uses _width instead
+ $this->mAttribs['style'] = "max-width: {$maxwidth}px;_width: {$maxwidth}px;" . $oldStyle;
+ }
+
+ $attribs = Sanitizer::mergeAttributes(
+ array( 'class' => 'gallery mw-gallery-' . $this->mMode ), $this->mAttribs );
+
+ $modules = $this->getModules();
+
+ if ( $this->mParser ) {
+ $this->mParser->getOutput()->addModules( $modules );
+ } else {
+ $this->getOutput()->addModules( $modules );
+ }
+ $output = Xml::openElement( 'ul', $attribs );
+ if ( $this->mCaption ) {
+ $output .= "\n\t<li class='gallerycaption'>{$this->mCaption}</li>";
+ }
+
+ $lang = $this->getRenderLang();
+ # Output each image...
+ foreach ( $this->mImages as $pair ) {
+ $nt = $pair[0];
+ $text = $pair[1]; # "text" means "caption" here
+ $alt = $pair[2];
+ $link = $pair[3];
+
+ $descQuery = false;
+ if ( $nt->getNamespace() === NS_FILE ) {
+ # Get the file...
+ if ( $this->mParser instanceof Parser ) {
+ # Give extensions a chance to select the file revision for us
+ $options = array();
+ wfRunHooks( 'BeforeParserFetchFileAndTitle',
+ array( $this->mParser, $nt, &$options, &$descQuery ) );
+ # Fetch and register the file (file title may be different via hooks)
+ list( $img, $nt ) = $this->mParser->fetchFileAndTitle( $nt, $options );
+ } else {
+ $img = wfFindFile( $nt );
+ }
+ } else {
+ $img = false;
+ }
+
+ $params = $this->getThumbParams( $img );
+ // $pair[4] is per image handler options
+ $transformOptions = $params + $pair[4];
+
+ $thumb = false;
+
+ if ( !$img ) {
+ # We're dealing with a non-image, spit out the name and be done with it.
+ $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' . ( $this->getThumbPadding() + $this->mHeights ) . 'px;">'
+ . htmlspecialchars( $nt->getText() ) . '</div>';
+
+ if ( $this->mParser instanceof Parser ) {
+ $this->mParser->addTrackingCategory( 'broken-file-category' );
+ }
+ } elseif ( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) {
+ # The image is blacklisted, just show it as a text link.
+ $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' . ( $this->getThumbPadding() + $this->mHeights ) . 'px;">' .
+ Linker::link(
+ $nt,
+ htmlspecialchars( $nt->getText() ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) .
+ '</div>';
+ } elseif ( !( $thumb = $img->transform( $transformOptions ) ) ) {
+ # Error generating thumbnail.
+ $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' . ( $this->getThumbPadding() + $this->mHeights ) . 'px;">'
+ . htmlspecialchars( $img->getLastError() ) . '</div>';
+ } else {
+ $vpad = $this->getVPad( $this->mHeights, $thumb->getHeight() );
+
+ $imageParameters = array(
+ 'desc-link' => true,
+ 'desc-query' => $descQuery,
+ 'alt' => $alt,
+ 'custom-url-link' => $link
+ );
+ # In the absence of both alt text and caption, fall back on providing screen readers with the filename as alt text
+ if ( $alt == '' && $text == '' ) {
+ $imageParameters['alt'] = $nt->getText();
+ }
+
+ $this->adjustImageParameters( $thumb, $imageParameters );
+
+ # Set both fixed width and min-height.
+ $thumbhtml = "\n\t\t\t" .
+ '<div class="thumb" style="width: ' . $this->getThumbDivWidth( $thumb->getWidth() ) . 'px;">'
+ # Auto-margin centering for block-level elements. Needed now that we have video
+ # handlers since they may emit block-level elements as opposed to simple <img> tags.
+ # ref http://css-discuss.incutio.com/?page=CenteringBlockElement
+ . '<div style="margin:' . $vpad . 'px auto;">'
+ . $thumb->toHtml( $imageParameters ) . '</div></div>';
+
+ // Call parser transform hook
+ if ( $this->mParser && $img->getHandler() ) {
+ $img->getHandler()->parserTransformHook( $this->mParser, $img );
+ }
+ }
+
+ //TODO
+ // $linkTarget = Title::newFromText( $wgContLang->getNsText( MWNamespace::getUser() ) . ":{$ut}" );
+ // $ul = Linker::link( $linkTarget, $ut );
+
+ if ( $this->mShowBytes ) {
+ if ( $img ) {
+ $fileSize = htmlspecialchars( $lang->formatSize( $img->getSize() ) );
+ } else {
+ $fileSize = $this->msg( 'filemissing' )->escaped();
+ }
+ $fileSize = "$fileSize<br />\n";
+ } else {
+ $fileSize = '';
+ }
+
+ $textlink = $this->mShowFilename ?
+ Linker::link(
+ $nt,
+ htmlspecialchars( $lang->truncate( $nt->getText(), $this->mCaptionLength ) ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) . "<br />\n" :
+ '';
+
+
+ $galleryText = $textlink . $text . $fileSize;
+ $galleryText = $this->wrapGalleryText( $galleryText, $thumb );
+
+ # Weird double wrapping (the extra div inside the li) needed due to FF2 bug
+ # Can be safely removed if FF2 falls completely out of existence
+ $output .=
+ "\n\t\t" . '<li class="gallerybox" style="width: ' . $this->getGBWidth( $thumb ) . 'px">'
+ . '<div style="width: ' . $this->getGBWidth( $thumb ) . 'px">'
+ . $thumbhtml
+ . $galleryText
+ . "\n\t\t</div></li>";
+ }
+ $output .= "\n</ul>";
+
+ return $output;
+ }
+
+
+ /**
+ * Add the wrapper html around the thumb's caption
+ *
+ * @param String $galleryText The caption
+ * @param MediaTransformOutput|boolean $thumb The thumb this caption is for or false for bad image.
+ */
+ protected function wrapGalleryText( $galleryText, $thumb ) {
+ # ATTENTION: The newline after <div class="gallerytext"> is needed to accommodate htmltidy which
+ # in version 4.8.6 generated crackpot html in its absence, see:
+ # http://bugzilla.wikimedia.org/show_bug.cgi?id=1765 -Ævar
+
+ return "\n\t\t\t" . '<div class="gallerytext">' . "\n"
+ . $galleryText
+ . "\n\t\t\t</div>";
+ }
+
+ /**
+ * How much padding such the thumb have between image and inner div that
+ * that contains the border. This is both for verical and horizontal
+ * padding. (However, it is cut in half in the vertical direction).
+ * @return int
+ */
+ protected function getThumbPadding() {
+ return 30;
+ }
+
+ /**
+ *
+ * @note GB stands for gallerybox (as in the <li class="gallerybox"> element)
+ *
+ * @return int
+ */
+ protected function getGBPadding() {
+ return 5;
+ }
+
+ /**
+ * Get how much extra space the borders around the image takes up.
+ *
+ * For this mode, it is 2px borders on each side + 2px implied padding on
+ * each side from the stylesheet, giving us 2*2+2*2 = 8.
+ * @return int
+ */
+ protected function getGBBorders() {
+ return 8;
+ }
+
+ /**
+ * Get total padding.
+ *
+ * @return int How many pixels of whitespace surround the thumbnail.
+ */
+ protected function getAllPadding() {
+ return $this->getThumbPadding() + $this->getGBPadding() + $this->getGBBorders();
+ }
+
+ /**
+ * Get vertical padding for a thumbnail
+ *
+ * Generally this is the total height minus how high the thumb is.
+ *
+ * @param int $boxHeight How high we want the box to be.
+ * @param int $thumbHeight How high the thumbnail is.
+ * @return int How many vertical padding to add on each side.
+ */
+ protected function getVPad( $boxHeight, $thumbHeight ) {
+ return ( $this->getThumbPadding() + $boxHeight - $thumbHeight ) / 2;
+ }
+
+ /**
+ * Get the transform parameters for a thumbnail.
+ *
+ * @param File $img The file in question. May be false for invalid image
+ */
+ protected function getThumbParams( $img ) {
+ return array(
+ 'width' => $this->mWidths,
+ 'height' => $this->mHeights
+ );
+ }
+
+ /**
+ * Get the width of the inner div that contains the thumbnail in
+ * question. This is the div with the class of "thumb".
+ *
+ * @param int $thumbWidth The width of the thumbnail.
+ * @return int Width of inner thumb div.
+ */
+ protected function getThumbDivWidth( $thumbWidth ) {
+ return $this->mWidths + $this->getThumbPadding();
+ }
+
+ /**
+ * Width of gallerybox <li>.
+ *
+ * Generally is the width of the image, plus padding on image
+ * plus padding on gallerybox.
+ *
+ * @note Important: parameter will be false if no thumb used.
+ * @param Mixed $thumb MediaTransformObject object or false.
+ * @return int width of gallerybox element
+ */
+ protected function getGBWidth( $thumb ) {
+ return $this->mWidths + $this->getThumbPadding() + $this->getGBPadding();
+ }
+
+ /**
+ * Get a list of modules to include in the page.
+ *
+ * Primarily intended for subclasses.
+ *
+ * @return Array modules to include
+ */
+ protected function getModules() {
+ return array();
+ }
+
+ /**
+ * Adjust the image parameters for a thumbnail.
+ *
+ * Used by a subclass to insert extra high resolution images.
+ * @param MediaTransformOutput $thumb The thumbnail
+ * @param Array $imageParameters Array of options
+ */
+ protected function adjustImageParameters( $thumb, &$imageParameters ) { }
+}
+
+/**
+ * Backwards compatibility. This always uses traditional mode
+ * if called the old way, for extensions that may expect traditional
+ * mode.
+ *
+ * @deprecated 1.22 Use ImageGalleryBase::factory instead.
+ */
+class ImageGallery extends TraditionalImageGallery {
+ function __construct( $mode = 'traditional' ) {
+ wfDeprecated( __METHOD__, '1.22' );
+ parent::__construct( $mode );
+ }
+}
diff --git a/includes/installer/CliInstaller.php b/includes/installer/CliInstaller.php
index 38b4a824..f944fbed 100644
--- a/includes/installer/CliInstaller.php
+++ b/includes/installer/CliInstaller.php
@@ -114,7 +114,7 @@ class CliInstaller extends Installer {
*/
public function execute() {
$vars = Installer::getExistingLocalSettings();
- if( $vars ) {
+ if ( $vars ) {
$this->showStatusMessage(
Status::newFatal( "config-localsettings-cli-upgrade" )
);
@@ -129,7 +129,7 @@ class CliInstaller extends Installer {
/**
* Write LocalSettings.php to a given path
*
- * @param $path String Full path to write LocalSettings.php to
+ * @param string $path Full path to write LocalSettings.php to
*/
public function writeConfigurationFile( $path ) {
$ls = InstallerOverrides::getLocalSettingsGenerator( $this );
@@ -137,6 +137,8 @@ class CliInstaller extends Installer {
}
public function startStage( $step ) {
+ // Messages: config-install-database, config-install-tables, config-install-interwiki,
+ // config-install-stats, config-install-keys, config-install-sysop, config-install-mainpage
$this->showMessage( "config-install-$step" );
}
@@ -166,6 +168,7 @@ class CliInstaller extends Installer {
$text = wfMessage( $msg, $params )->parse();
$text = preg_replace( '/<a href="(.*?)".*?>(.*?)<\/a>/', '$2 &lt;$1&gt;', $text );
+
return html_entity_decode( strip_tags( $text ), ENT_QUOTES );
}
@@ -191,19 +194,21 @@ class CliInstaller extends Installer {
}
}
- public function envCheckPath( ) {
+ public function envCheckPath() {
if ( !$this->specifiedScriptPath ) {
- $this->showMessage( 'config-no-cli-uri', $this->getVar("wgScriptPath") );
+ $this->showMessage( 'config-no-cli-uri', $this->getVar( "wgScriptPath" ) );
}
+
return parent::envCheckPath();
}
protected function envGetDefaultServer() {
- return $this->getVar( 'wgServer' );
+ return null; // Do not guess if installing from CLI
}
public function dirIsExecutable( $dir, $url ) {
$this->showMessage( 'config-no-cli-uploads-check', $dir );
+
return false;
}
}
diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php
index de59b2d6..0110ac52 100644
--- a/includes/installer/DatabaseInstaller.php
+++ b/includes/installer/DatabaseInstaller.php
@@ -32,7 +32,7 @@ abstract class DatabaseInstaller {
/**
* The Installer object.
*
- * TODO: naming this parent is confusing, 'installer' would be clearer.
+ * @todo Naming this parent is confusing, 'installer' would be clearer.
*
* @var WebInstaller
*/
@@ -62,12 +62,12 @@ abstract class DatabaseInstaller {
/**
* Return the internal name, e.g. 'mysql', or 'sqlite'.
*/
- public abstract function getName();
+ abstract public function getName();
/**
* @return bool Returns true if the client library is compiled in.
*/
- public abstract function isCompiled();
+ abstract public function isCompiled();
/**
* Checks for installation prerequisites other than those checked by isCompiled()
@@ -85,7 +85,7 @@ abstract class DatabaseInstaller {
*
* If this is called, $this->parent can be assumed to be a WebInstaller.
*/
- public abstract function getConnectForm();
+ abstract public function getConnectForm();
/**
* Set variables based on the request array, assuming it was submitted
@@ -96,7 +96,7 @@ abstract class DatabaseInstaller {
*
* @return Status
*/
- public abstract function submitConnectForm();
+ abstract public function submitConnectForm();
/**
* Get HTML for a web form that retrieves settings used for installation.
@@ -127,7 +127,7 @@ abstract class DatabaseInstaller {
*
* @return Status
*/
- public abstract function openConnection();
+ abstract public function openConnection();
/**
* Create the database and return a Status object indicating success or
@@ -135,7 +135,7 @@ abstract class DatabaseInstaller {
*
* @return Status
*/
- public abstract function setupDatabase();
+ abstract public function setupDatabase();
/**
* Connect to the database using the administrative user/password currently
@@ -158,6 +158,7 @@ abstract class DatabaseInstaller {
$this->db->clearFlag( DBO_TRX );
$this->db->commit( __METHOD__ );
}
+
return $status;
}
@@ -173,9 +174,10 @@ abstract class DatabaseInstaller {
}
$this->db->selectDB( $this->getVar( 'wgDBname' ) );
- if( $this->db->tableExists( 'archive', __METHOD__ ) ) {
+ if ( $this->db->tableExists( 'archive', __METHOD__ ) ) {
$status->warning( 'config-install-tables-exist' );
$this->enableLB();
+
return $status;
}
@@ -183,7 +185,7 @@ abstract class DatabaseInstaller {
$this->db->begin( __METHOD__ );
$error = $this->db->sourceFile( $this->db->getSchemaPath() );
- if( $error !== true ) {
+ if ( $error !== true ) {
$this->db->reportQueryError( $error, 0, '', __METHOD__ );
$this->db->rollback( __METHOD__ );
$status->fatal( 'config-install-tables-failed', $error );
@@ -191,9 +193,10 @@ abstract class DatabaseInstaller {
$this->db->commit( __METHOD__ );
}
// Resume normal operations
- if( $status->isOk() ) {
+ if ( $status->isOk() ) {
$this->enableLB();
}
+
return $status;
}
@@ -218,7 +221,7 @@ abstract class DatabaseInstaller {
*
* @return String
*/
- public abstract function getLocalSettings();
+ abstract public function getLocalSettings();
/**
* Override this to provide DBMS-specific schema variables, to be
@@ -240,7 +243,7 @@ abstract class DatabaseInstaller {
if ( $status->isOK() ) {
$status->value->setSchemaVars( $this->getSchemaVars() );
} else {
- throw new MWException( __METHOD__.': unexpected DB connection error' );
+ throw new MWException( __METHOD__ . ': unexpected DB connection error' );
}
}
@@ -252,7 +255,7 @@ abstract class DatabaseInstaller {
public function enableLB() {
$status = $this->getConnection();
if ( !$status->isOK() ) {
- throw new MWException( __METHOD__.': unexpected DB connection error' );
+ throw new MWException( __METHOD__ . ': unexpected DB connection error' );
}
LBFactory::setInstance( new LBFactory_Single( array(
'connection' => $status->value ) ) );
@@ -269,15 +272,17 @@ abstract class DatabaseInstaller {
$ret = true;
ob_start( array( $this, 'outputHandler' ) );
+ $up = DatabaseUpdater::newForDB( $this->db );
try {
- $up = DatabaseUpdater::newForDB( $this->db );
$up->doUpdates();
} catch ( MWException $e ) {
echo "\nAn error occurred:\n";
echo $e->getText();
$ret = false;
}
+ $up->purgeCache();
ob_end_flush();
+
return $ret;
}
@@ -287,14 +292,12 @@ abstract class DatabaseInstaller {
* long after the constructor. Helpful for things like modifying setup steps :)
*/
public function preInstall() {
-
}
/**
* Allow DB installers a chance to make checks before upgrade.
*/
public function preUpgrade() {
-
}
/**
@@ -318,15 +321,11 @@ abstract class DatabaseInstaller {
* Convenience function.
* Check if a named extension is present.
*
- * @see wfDl
* @param $name
* @return bool
*/
protected static function checkExtension( $name ) {
- wfSuppressWarnings();
- $compiled = wfDl( $name );
- wfRestoreWarnings();
- return $compiled;
+ return extension_loaded( $name );
}
/**
@@ -334,6 +333,8 @@ abstract class DatabaseInstaller {
* @return String
*/
public function getReadableName() {
+ // Messages: config-type-mysql, config-type-postgres, config-type-sqlite,
+ // config-type-oracle
return wfMessage( 'config-type-' . $this->getName() )->text();
}
@@ -368,6 +369,7 @@ abstract class DatabaseInstaller {
} elseif ( isset( $internal[$var] ) ) {
$default = $internal[$var];
}
+
return $this->parent->getVar( $var, $default );
}
@@ -395,6 +397,7 @@ abstract class DatabaseInstaller {
if ( !isset( $attribs ) ) {
$attribs = array();
}
+
return $this->parent->getTextBox( array(
'var' => $var,
'label' => $label,
@@ -421,6 +424,7 @@ abstract class DatabaseInstaller {
if ( !isset( $attribs ) ) {
$attribs = array();
}
+
return $this->parent->getPasswordBox( array(
'var' => $var,
'label' => $label,
@@ -439,6 +443,7 @@ abstract class DatabaseInstaller {
public function getCheckBox( $var, $label, $attribs = array(), $helpData = "" ) {
$name = $this->getName() . '_' . $var;
$value = $this->getVar( $var );
+
return $this->parent->getCheckBox( array(
'var' => $var,
'label' => $label,
@@ -446,7 +451,7 @@ abstract class DatabaseInstaller {
'controlName' => $name,
'value' => $value,
'help' => $helpData
- ));
+ ) );
}
/**
@@ -465,6 +470,7 @@ abstract class DatabaseInstaller {
public function getRadioSet( $params ) {
$params['controlName'] = $this->getName() . '_' . $params['var'];
$params['value'] = $this->getVar( $params['var'] );
+
return $this->parent->getRadioSet( $params );
}
@@ -498,7 +504,9 @@ abstract class DatabaseInstaller {
if ( !$this->db->selectDB( $this->getVar( 'wgDBname' ) ) ) {
return false;
}
- return $this->db->tableExists( 'cur', __METHOD__ ) || $this->db->tableExists( 'revision', __METHOD__ );
+
+ return $this->db->tableExists( 'cur', __METHOD__ ) ||
+ $this->db->tableExists( 'revision', __METHOD__ );
}
/**
@@ -507,11 +515,20 @@ abstract class DatabaseInstaller {
* @return String
*/
public function getInstallUserBox() {
- return
- Html::openElement( 'fieldset' ) .
+ return Html::openElement( 'fieldset' ) .
Html::element( 'legend', array(), wfMessage( 'config-db-install-account' )->text() ) .
- $this->getTextBox( '_InstallUser', 'config-db-username', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-install-username' ) ) .
- $this->getPasswordBox( '_InstallPassword', 'config-db-password', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-install-password' ) ) .
+ $this->getTextBox(
+ '_InstallUser',
+ 'config-db-username',
+ array( 'dir' => 'ltr' ),
+ $this->parent->getHelpBox( 'config-db-install-username' )
+ ) .
+ $this->getPasswordBox(
+ '_InstallPassword',
+ 'config-db-password',
+ array( 'dir' => 'ltr' ),
+ $this->parent->getHelpBox( 'config-db-install-password' )
+ ) .
Html::closeElement( 'fieldset' );
}
@@ -521,12 +538,13 @@ abstract class DatabaseInstaller {
*/
public function submitInstallUserBox() {
$this->setVarsFromRequest( array( '_InstallUser', '_InstallPassword' ) );
+
return Status::newGood();
}
/**
* Get a standard web-user fieldset
- * @param $noCreateMsg String: Message to display instead of the creation checkbox.
+ * @param string $noCreateMsg Message to display instead of the creation checkbox.
* Set this to false to show a creation checkbox.
*
* @return String
@@ -549,6 +567,7 @@ abstract class DatabaseInstaller {
$s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
}
$s .= Html::closeElement( 'div' ) . Html::closeElement( 'fieldset' );
+
return $s;
}
@@ -567,7 +586,7 @@ abstract class DatabaseInstaller {
$this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
}
- if( $this->getVar( '_CreateDBAccount' ) && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
+ if ( $this->getVar( '_CreateDBAccount' ) && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
return Status::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
}
@@ -586,8 +605,9 @@ abstract class DatabaseInstaller {
}
$this->db->selectDB( $this->getVar( 'wgDBname' ) );
- if( $this->db->selectRow( 'interwiki', '*', array(), __METHOD__ ) ) {
+ if ( $this->db->selectRow( 'interwiki', '*', array(), __METHOD__ ) ) {
$status->warning( 'config-install-interwiki-exists' );
+
return $status;
}
global $IP;
@@ -599,9 +619,11 @@ abstract class DatabaseInstaller {
if ( !$rows ) {
return Status::newFatal( 'config-install-interwiki-list' );
}
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
$row = preg_replace( '/^\s*([^#]*?)\s*(#.*)?$/', '\\1', $row ); // strip comments - whee
- if ( $row == "" ) continue;
+ if ( $row == "" ) {
+ continue;
+ }
$row .= "||";
$interwikis[] = array_combine(
array( 'iw_prefix', 'iw_url', 'iw_local', 'iw_api', 'iw_wikiid' ),
@@ -609,6 +631,7 @@ abstract class DatabaseInstaller {
);
}
$this->db->insert( 'interwiki', $interwikis, __METHOD__ );
+
return Status::newGood();
}
diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php
index ff0a99e9..267b6c5a 100644
--- a/includes/installer/DatabaseUpdater.php
+++ b/includes/installer/DatabaseUpdater.php
@@ -21,7 +21,7 @@
* @ingroup Deployment
*/
-require_once( __DIR__ . '/../../maintenance/Maintenance.php' );
+require_once __DIR__ . '/../../maintenance/Maintenance.php';
/**
* Class for handling database updates. Roughly based off of updaters.inc, with
@@ -40,6 +40,13 @@ abstract class DatabaseUpdater {
protected $updates = array();
/**
+ * Array of updates that were skipped
+ *
+ * @var array
+ */
+ protected $updatesSkipped = array();
+
+ /**
* List of extension-provided database updates
* @var array
*/
@@ -54,19 +61,38 @@ abstract class DatabaseUpdater {
protected $shared = false;
+ /**
+ * Scripts to run after database update
+ * Should be a subclass of LoggedUpdateMaintenance
+ */
protected $postDatabaseUpdateMaintenance = array(
'DeleteDefaultMessages',
'PopulateRevisionLength',
'PopulateRevisionSha1',
'PopulateImageSha1',
'FixExtLinksProtocolRelative',
+ 'PopulateFilearchiveSha1',
);
/**
+ * File handle for SQL output.
+ *
+ * @var Filehandle
+ */
+ protected $fileHandle = null;
+
+ /**
+ * Flag specifying whether or not to skip schema (e.g. SQL-only) updates.
+ *
+ * @var bool
+ */
+ protected $skipSchema = false;
+
+ /**
* Constructor
*
* @param $db DatabaseBase object to perform updates on
- * @param $shared bool Whether to perform updates on shared tables
+ * @param bool $shared Whether to perform updates on shared tables
* @param $maintenance Maintenance Maintenance object which created us
*/
protected function __construct( DatabaseBase &$db, $shared, Maintenance $maintenance = null ) {
@@ -75,6 +101,7 @@ abstract class DatabaseUpdater {
$this->shared = $shared;
if ( $maintenance ) {
$this->maintenance = $maintenance;
+ $this->fileHandle = $maintenance->fileHandle;
} else {
$this->maintenance = new FakeMaintenance;
}
@@ -103,7 +130,8 @@ abstract class DatabaseUpdater {
}
/**
- * Loads LocalSettings.php, if needed, and initialises everything needed for LoadExtensionSchemaUpdates hook
+ * Loads LocalSettings.php, if needed, and initialises everything needed for
+ * LoadExtensionSchemaUpdates hook.
*/
private function loadExtensions() {
if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
@@ -130,8 +158,9 @@ abstract class DatabaseUpdater {
*/
public static function newForDB( &$db, $shared = false, $maintenance = null ) {
$type = $db->getType();
- if( in_array( $type, Installer::getDBTypes() ) ) {
+ if ( in_array( $type, Installer::getDBTypes() ) ) {
$class = ucfirst( $type ) . 'Updater';
+
return new $class( $db, $shared, $maintenance );
} else {
throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
@@ -150,14 +179,14 @@ abstract class DatabaseUpdater {
/**
* Output some text. If we're running from web, escape the text first.
*
- * @param $str String: Text to output
+ * @param string $str Text to output
*/
public function output( $str ) {
if ( $this->maintenance->isQuiet() ) {
return;
}
global $wgCommandLineMode;
- if( !$wgCommandLineMode ) {
+ if ( !$wgCommandLineMode ) {
$str = htmlspecialchars( $str );
}
echo $str;
@@ -170,14 +199,14 @@ abstract class DatabaseUpdater {
*
* @since 1.17
*
- * @param $update Array: the update to run. Format is the following:
+ * @param array $update the update to run. Format is the following:
* first item is the callback function, it also can be a
* simple string with the name of a function in this class,
* following elements are parameters to the function.
* Note that callback functions will receive this object as
* first parameter.
*/
- public function addExtensionUpdate( Array $update ) {
+ public function addExtensionUpdate( array $update ) {
$this->extensionUpdates[] = $update;
}
@@ -187,8 +216,8 @@ abstract class DatabaseUpdater {
*
* @since 1.18
*
- * @param $tableName String Name of table to create
- * @param $sqlPath String Full path to the schema file
+ * @param string $tableName Name of table to create
+ * @param string $sqlPath Full path to the schema file
*/
public function addExtensionTable( $tableName, $sqlPath ) {
$this->extensionUpdates[] = array( 'addTable', $tableName, $sqlPath, true );
@@ -230,6 +259,19 @@ abstract class DatabaseUpdater {
}
/**
+ * Drop an index from an extension table
+ *
+ * @since 1.21
+ *
+ * @param string $tableName The table name
+ * @param string $indexName The index name
+ * @param string $sqlPath The path to the SQL change path
+ */
+ public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
+ $this->extensionUpdates[] = array( 'dropIndex', $tableName, $indexName, $sqlPath, true );
+ }
+
+ /**
*
* @since 1.20
*
@@ -241,6 +283,43 @@ abstract class DatabaseUpdater {
}
/**
+ * Rename an index on an extension table
+ *
+ * @since 1.21
+ *
+ * @param string $tableName The table name
+ * @param string $oldIndexName The old index name
+ * @param string $newIndexName The new index name
+ * @param $skipBothIndexExistWarning Boolean: Whether to warn if both the old
+ * and the new indexes exist. [facultative; by default, false]
+ * @param string $sqlPath The path to the SQL change path
+ */
+ public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
+ $sqlPath, $skipBothIndexExistWarning = false
+ ) {
+ $this->extensionUpdates[] = array(
+ 'renameIndex',
+ $tableName,
+ $oldIndexName,
+ $newIndexName,
+ $skipBothIndexExistWarning,
+ $sqlPath,
+ true
+ );
+ }
+
+ /**
+ * @since 1.21
+ *
+ * @param string $tableName The table name
+ * @param string $fieldName The field to be modified
+ * @param string $sqlPath The path to the SQL change path
+ */
+ public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
+ $this->extensionUpdates[] = array( 'modifyField', $tableName, $fieldName, $sqlPath, true );
+ }
+
+ /**
*
* @since 1.20
*
@@ -254,9 +333,11 @@ abstract class DatabaseUpdater {
/**
* Add a maintenance script to be run after the database updates are complete.
*
+ * Script should subclass LoggedUpdateMaintenance
+ *
* @since 1.19
*
- * @param $class string Name of a Maintenance subclass
+ * @param string $class Name of a Maintenance subclass
*/
public function addPostDatabaseUpdateMaintenance( $class ) {
$this->postDatabaseUpdateMaintenance[] = $class;
@@ -281,15 +362,35 @@ abstract class DatabaseUpdater {
}
/**
+ * @since 1.21
+ *
+ * Writes the schema updates desired to a file for the DB Admin to run.
+ */
+ private function writeSchemaUpdateFile( $schemaUpdate = array() ) {
+ $updates = $this->updatesSkipped;
+ $this->updatesSkipped = array();
+
+ foreach ( $updates as $funcList ) {
+ $func = $funcList[0];
+ $arg = $funcList[1];
+ $origParams = $funcList[2];
+ call_user_func_array( $func, $arg );
+ flush();
+ $this->updatesSkipped[] = $origParams;
+ }
+ }
+
+ /**
* Do all the updates
*
- * @param $what Array: what updates to perform
+ * @param array $what what updates to perform
*/
- public function doUpdates( $what = array( 'core', 'extensions', 'purge', 'stats' ) ) {
- global $wgLocalisationCacheConf, $wgVersion;
+ public function doUpdates( $what = array( 'core', 'extensions', 'stats' ) ) {
+ global $wgVersion;
$this->db->begin( __METHOD__ );
$what = array_flip( $what );
+ $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
if ( isset( $what['core'] ) ) {
$this->runUpdates( $this->getCoreUpdateList(), false );
}
@@ -298,41 +399,49 @@ abstract class DatabaseUpdater {
$this->runUpdates( $this->getExtensionUpdates(), true );
}
- $this->setAppliedUpdates( $wgVersion, $this->updates );
-
if ( isset( $what['stats'] ) ) {
$this->checkStats();
}
- if ( isset( $what['purge'] ) ) {
- $this->purgeCache();
+ $this->setAppliedUpdates( $wgVersion, $this->updates );
- if ( $wgLocalisationCacheConf['manualRecache'] ) {
- $this->rebuildLocalisationCache();
- }
+ if ( $this->fileHandle ) {
+ $this->skipSchema = false;
+ $this->writeSchemaUpdateFile();
+ $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped );
}
+
$this->db->commit( __METHOD__ );
}
/**
* Helper function for doUpdates()
*
- * @param $updates Array of updates to run
+ * @param array $updates of updates to run
* @param $passSelf Boolean: whether to pass this object we calling external
* functions
*/
private function runUpdates( array $updates, $passSelf ) {
+ $updatesDone = array();
+ $updatesSkipped = array();
foreach ( $updates as $params ) {
+ $origParams = $params;
$func = array_shift( $params );
- if( !is_array( $func ) && method_exists( $this, $func ) ) {
+ if ( !is_array( $func ) && method_exists( $this, $func ) ) {
$func = array( $this, $func );
} elseif ( $passSelf ) {
array_unshift( $params, $this );
}
- call_user_func_array( $func, $params );
+ $ret = call_user_func_array( $func, $params );
flush();
+ if ( $ret !== false ) {
+ $updatesDone[] = $origParams;
+ } else {
+ $updatesSkipped[] = array( $func, $params, $origParams );
+ }
}
- $this->updates = array_merge( $this->updates, $updates );
+ $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
+ $this->updates = array_merge( $this->updates, $updatesDone );
}
/**
@@ -341,13 +450,13 @@ abstract class DatabaseUpdater {
*/
protected function setAppliedUpdates( $version, $updates = array() ) {
$this->db->clearFlag( DBO_DDLMODE );
- if( !$this->canUseNewUpdatelog() ) {
+ if ( !$this->canUseNewUpdatelog() ) {
return;
}
$key = "updatelist-$version-" . time();
$this->db->insert( 'updatelog',
array( 'ul_key' => $key, 'ul_value' => serialize( $updates ) ),
- __METHOD__ );
+ __METHOD__ );
$this->db->setFlag( DBO_DDLMODE );
}
@@ -355,7 +464,7 @@ abstract class DatabaseUpdater {
* Helper function: check if the given key is present in the updatelog table.
* Obviously, only use this for updates that occur after the updatelog table was
* created!
- * @param $key String Name of the key to check for
+ * @param string $key Name of the key to check for
*
* @return bool
*/
@@ -366,6 +475,7 @@ abstract class DatabaseUpdater {
array( 'ul_key' => $key ),
__METHOD__
);
+
return (bool)$row;
}
@@ -373,13 +483,13 @@ abstract class DatabaseUpdater {
* Helper function: Add a key to the updatelog table
* Obviously, only use this for updates that occur after the updatelog table was
* created!
- * @param $key String Name of key to insert
- * @param $val String [optional] value to insert along with the key
+ * @param string $key Name of key to insert
+ * @param string $val [optional] value to insert along with the key
*/
public function insertUpdateRow( $key, $val = null ) {
$this->db->clearFlag( DBO_DDLMODE );
$values = array( 'ul_key' => $key );
- if( $val && $this->canUseNewUpdatelog() ) {
+ if ( $val && $this->canUseNewUpdatelog() ) {
$values['ul_value'] = $val;
}
$this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
@@ -400,6 +510,26 @@ abstract class DatabaseUpdater {
}
/**
+ * Returns whether updates should be executed on the database table $name.
+ * Updates will be prevented if the table is a shared table and it is not
+ * specified to run updates on shared tables.
+ *
+ * @param string $name table name
+ * @return bool
+ */
+ protected function doTable( $name ) {
+ global $wgSharedDB, $wgSharedTables;
+
+ // Don't bother to check $wgSharedTables if there isn't a shared database
+ // or the user actually also wants to do updates on the shared database.
+ if ( $wgSharedDB === null || $this->shared ) {
+ return true;
+ }
+
+ return !in_array( $name, $wgSharedTables );
+ }
+
+ /**
* Before 1.17, we used to handle updates via stuff like
* $wgExtNewTables/Fields/Indexes. This is nasty :) We refactored a lot
* of this in 1.17 but we want to remain back-compatible for a while. So
@@ -409,11 +539,7 @@ abstract class DatabaseUpdater {
*/
protected function getOldGlobalUpdates() {
global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields,
- $wgExtNewIndexes, $wgSharedDB, $wgSharedTables;
-
- $doUser = $this->shared ?
- $wgSharedDB && in_array( 'user', $wgSharedTables ) :
- !$wgSharedDB || !in_array( 'user', $wgSharedTables );
+ $wgExtNewIndexes;
$updates = array();
@@ -424,25 +550,23 @@ abstract class DatabaseUpdater {
}
foreach ( $wgExtNewFields as $fieldRecord ) {
- if ( $fieldRecord[0] != 'user' || $doUser ) {
- $updates[] = array(
- 'addField', $fieldRecord[0], $fieldRecord[1],
- $fieldRecord[2], true
- );
- }
+ $updates[] = array(
+ 'addField', $fieldRecord[0], $fieldRecord[1],
+ $fieldRecord[2], true
+ );
}
foreach ( $wgExtNewIndexes as $fieldRecord ) {
$updates[] = array(
'addIndex', $fieldRecord[0], $fieldRecord[1],
- $fieldRecord[2], true
+ $fieldRecord[2], true
);
}
foreach ( $wgExtModifiedFields as $fieldRecord ) {
$updates[] = array(
'modifyField', $fieldRecord[0], $fieldRecord[1],
- $fieldRecord[2], true
+ $fieldRecord[2], true
);
}
@@ -457,104 +581,242 @@ abstract class DatabaseUpdater {
*
* @return Array
*/
- protected abstract function getCoreUpdateList();
+ abstract protected function getCoreUpdateList();
+
+ /**
+ * Append an SQL fragment to the open file handle.
+ *
+ * @param string $filename File name to open
+ */
+ public function copyFile( $filename ) {
+ $this->db->sourceFile( $filename, false, false, false,
+ array( $this, 'appendLine' )
+ );
+ }
+
+ /**
+ * Append a line to the open filehandle. The line is assumed to
+ * be a complete SQL statement.
+ *
+ * This is used as a callback for for sourceLine().
+ *
+ * @param string $line text to append to the file
+ * @return Boolean false to skip actually executing the file
+ * @throws MWException
+ */
+ public function appendLine( $line ) {
+ $line = rtrim( $line ) . ";\n";
+ if ( fwrite( $this->fileHandle, $line ) === false ) {
+ throw new MWException( "trouble writing file" );
+ }
+
+ return false;
+ }
/**
* Applies a SQL patch
- * @param $path String Path to the patch file
+ *
+ * @param string $path Path to the patch file
* @param $isFullPath Boolean Whether to treat $path as a relative or not
- * @param $msg String Description of the patch
+ * @param string $msg Description of the patch
+ * @return boolean false if patch is skipped.
*/
protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
if ( $msg === null ) {
$msg = "Applying $path patch";
}
+ if ( $this->skipSchema ) {
+ $this->output( "...skipping schema change ($msg).\n" );
- if ( !$isFullPath ) {
- $path = $this->db->patchPath( $path );
+ return false;
}
$this->output( "$msg ..." );
- $this->db->sourceFile( $path );
+
+ if ( !$isFullPath ) {
+ $path = $this->db->patchPath( $path );
+ }
+ if ( $this->fileHandle !== null ) {
+ $this->copyFile( $path );
+ } else {
+ $this->db->sourceFile( $path );
+ }
$this->output( "done.\n" );
+
+ return true;
}
/**
* Add a new table to the database
- * @param $name String Name of the new table
- * @param $patch String Path to the patch file
+ *
+ * @param string $name Name of the new table
+ * @param string $patch Path to the patch file
* @param $fullpath Boolean Whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
protected function addTable( $name, $patch, $fullpath = false ) {
+ if ( !$this->doTable( $name ) ) {
+ return true;
+ }
+
if ( $this->db->tableExists( $name, __METHOD__ ) ) {
$this->output( "...$name table already exists.\n" );
} else {
- $this->applyPatch( $patch, $fullpath, "Creating $name table" );
+ return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
}
+
+ return true;
}
/**
* Add a new field to an existing table
- * @param $table String Name of the table to modify
- * @param $field String Name of the new field
- * @param $patch String Path to the patch file
+ *
+ * @param string $table Name of the table to modify
+ * @param string $field Name of the new field
+ * @param string $patch Path to the patch file
* @param $fullpath Boolean Whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
protected function addField( $table, $field, $patch, $fullpath = false ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
$this->output( "...$table table does not exist, skipping new field patch.\n" );
} elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
$this->output( "...have $field field in $table table.\n" );
} else {
- $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
+ return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
}
+
+ return true;
}
/**
* Add a new index to an existing table
- * @param $table String Name of the table to modify
- * @param $index String Name of the new index
- * @param $patch String Path to the patch file
+ *
+ * @param string $table Name of the table to modify
+ * @param string $index Name of the new index
+ * @param string $patch Path to the patch file
* @param $fullpath Boolean Whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
protected function addIndex( $table, $index, $patch, $fullpath = false ) {
- if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
+ if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+ $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
+ } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
$this->output( "...index $index already set on $table table.\n" );
} else {
- $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
+ return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
}
+
+ return true;
}
/**
* Drop a field from an existing table
*
- * @param $table String Name of the table to modify
- * @param $field String Name of the old field
- * @param $patch String Path to the patch file
+ * @param string $table Name of the table to modify
+ * @param string $field Name of the old field
+ * @param string $patch Path to the patch file
* @param $fullpath Boolean Whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
protected function dropField( $table, $field, $patch, $fullpath = false ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
- $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
+ return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
} else {
$this->output( "...$table table does not contain $field field.\n" );
}
+
+ return true;
}
/**
* Drop an index from an existing table
*
- * @param $table String: Name of the table to modify
- * @param $index String: Name of the old index
- * @param $patch String: Path to the patch file
+ * @param string $table Name of the table to modify
+ * @param string $index Name of the index
+ * @param string $patch Path to the patch file
* @param $fullpath Boolean: Whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
- $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
+ return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
} else {
$this->output( "...$index key doesn't exist.\n" );
}
+
+ return true;
+ }
+
+ /**
+ * Rename an index from an existing table
+ *
+ * @param string $table Name of the table to modify
+ * @param string $oldIndex Old name of the index
+ * @param string $newIndex New name of the index
+ * @param $skipBothIndexExistWarning Boolean: Whether to warn if both the
+ * old and the new indexes exist.
+ * @param string $patch Path to the patch file
+ * @param $fullpath Boolean: Whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
+ */
+ protected function renameIndex( $table, $oldIndex, $newIndex,
+ $skipBothIndexExistWarning, $patch, $fullpath = false
+ ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
+ // First requirement: the table must exist
+ if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+ $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
+
+ return true;
+ }
+
+ // Second requirement: the new index must be missing
+ if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
+ $this->output( "...index $newIndex already set on $table table.\n" );
+ if ( !$skipBothIndexExistWarning &&
+ $this->db->indexExists( $table, $oldIndex, __METHOD__ )
+ ) {
+ $this->output( "...WARNING: $oldIndex still exists, despite it has " .
+ "been renamed into $newIndex (which also exists).\n" .
+ " $oldIndex should be manually removed if not needed anymore.\n" );
+ }
+
+ return true;
+ }
+
+ // Third requirement: the old index must exist
+ if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
+ $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
+
+ return true;
+ }
+
+ // Requirements have been satisfied, patch can be applied
+ return $this->applyPatch(
+ $patch,
+ $fullpath,
+ "Renaming index $oldIndex into $newIndex to table $table"
+ );
}
/**
@@ -566,8 +828,13 @@ abstract class DatabaseUpdater {
* @param $table string
* @param $patch string|false
* @param $fullpath bool
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
public function dropTable( $table, $patch = false, $fullpath = false ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
if ( $this->db->tableExists( $table, __METHOD__ ) ) {
$msg = "Dropping table $table";
@@ -575,46 +842,60 @@ abstract class DatabaseUpdater {
$this->output( "$msg ..." );
$this->db->dropTable( $table, __METHOD__ );
$this->output( "done.\n" );
+ } else {
+ return $this->applyPatch( $patch, $fullpath, $msg );
}
- else {
- $this->applyPatch( $patch, $fullpath, $msg );
- }
-
} else {
$this->output( "...$table doesn't exist.\n" );
}
+
+ return true;
}
/**
* Modify an existing field
*
- * @param $table String: name of the table to which the field belongs
- * @param $field String: name of the field to modify
- * @param $patch String: path to the patch file
+ * @param string $table name of the table to which the field belongs
+ * @param string $field name of the field to modify
+ * @param string $patch path to the patch file
* @param $fullpath Boolean: whether to treat $patch path as a relative or not
+ * @return Boolean false if this was skipped because schema changes are skipped
*/
public function modifyField( $table, $field, $patch, $fullpath = false ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
$updateKey = "$table-$field-$patch";
if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
$this->output( "...$table table does not exist, skipping modify field patch.\n" );
} elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
- $this->output( "...$field field does not exist in $table table, skipping modify field patch.\n" );
- } elseif( $this->updateRowExists( $updateKey ) ) {
+ $this->output( "...$field field does not exist in $table table, " .
+ "skipping modify field patch.\n" );
+ } elseif ( $this->updateRowExists( $updateKey ) ) {
$this->output( "...$field in table $table already modified by patch $patch.\n" );
} else {
- $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
$this->insertUpdateRow( $updateKey );
+
+ return $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
}
+
+ return true;
}
/**
* Purge the objectcache table
*/
- protected function purgeCache() {
+ public function purgeCache() {
+ global $wgLocalisationCacheConf;
# We can't guarantee that the user will be able to use TRUNCATE,
# but we know that DELETE is available to us
$this->output( "Purging caches..." );
$this->db->delete( 'objectcache', '*', __METHOD__ );
+ if ( $wgLocalisationCacheConf['manualRecache'] ) {
+ $this->rebuildLocalisationCache();
+ }
+ MessageBlobStore::clear();
$this->output( "done.\n" );
}
@@ -630,6 +911,7 @@ abstract class DatabaseUpdater {
$this->output( "missing ss_total_pages, rebuilding...\n" );
} else {
$this->output( "done.\n" );
+
return;
}
SiteStatsInit::doAllAndCommit( $this->db );
@@ -661,9 +943,10 @@ abstract class DatabaseUpdater {
protected function doLogUsertextPopulation() {
if ( !$this->updateRowExists( 'populate log_usertext' ) ) {
$this->output(
- "Populating log_user_text field, printing progress markers. For large\n" .
- "databases, you may want to hit Ctrl-C and do this manually with\n" .
- "maintenance/populateLogUsertext.php.\n" );
+ "Populating log_user_text field, printing progress markers. For large\n" .
+ "databases, you may want to hit Ctrl-C and do this manually with\n" .
+ "maintenance/populateLogUsertext.php.\n"
+ );
$task = $this->maintenance->runChild( 'PopulateLogUsertext' );
$task->execute();
@@ -693,10 +976,12 @@ abstract class DatabaseUpdater {
protected function doUpdateTranscacheField() {
if ( $this->updateRowExists( 'convert transcache field' ) ) {
$this->output( "...transcache tc_time already converted.\n" );
- return;
+
+ return true;
}
- $this->applyPatch( 'patch-tc-timestamp.sql', false, "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
+ return $this->applyPatch( 'patch-tc-timestamp.sql', false,
+ "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
}
/**
@@ -704,29 +989,35 @@ abstract class DatabaseUpdater {
*/
protected function doCollationUpdate() {
global $wgCategoryCollation;
- if ( $this->db->selectField(
- 'categorylinks',
- 'COUNT(*)',
- 'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
- __METHOD__
- ) == 0 ) {
- $this->output( "...collations up-to-date.\n" );
- return;
- }
+ if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
+ if ( $this->db->selectField(
+ 'categorylinks',
+ 'COUNT(*)',
+ 'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
+ __METHOD__
+ ) == 0
+ ) {
+ $this->output( "...collations up-to-date.\n" );
+
+ return;
+ }
- $this->output( "Updating category collations..." );
- $task = $this->maintenance->runChild( 'UpdateCollation' );
- $task->execute();
- $this->output( "...done.\n" );
+ $this->output( "Updating category collations..." );
+ $task = $this->maintenance->runChild( 'UpdateCollation' );
+ $task->execute();
+ $this->output( "...done.\n" );
+ }
}
/**
* Migrates user options from the user table blob to user_properties
*/
protected function doMigrateUserOptions() {
- $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
- $cl->execute();
- $this->output( "done.\n" );
+ if ( $this->db->tableExists( 'user_properties' ) ) {
+ $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
+ $cl->execute();
+ $this->output( "done.\n" );
+ }
}
/**
diff --git a/includes/installer/Ibm_db2Installer.php b/includes/installer/Ibm_db2Installer.php
deleted file mode 100644
index ca9bdf4b..00000000
--- a/includes/installer/Ibm_db2Installer.php
+++ /dev/null
@@ -1,270 +0,0 @@
-<?php
-/**
- * IBM_DB2-specific installer.
- *
- * 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 Deployment
- */
-
-/**
- * Class for setting up the MediaWiki database using IBM_DB2.
- *
- * @ingroup Deployment
- * @since 1.17
- */
-class Ibm_db2Installer extends DatabaseInstaller {
-
-
- protected $globalNames = array(
- 'wgDBserver',
- 'wgDBport',
- 'wgDBname',
- 'wgDBuser',
- 'wgDBpassword',
- 'wgDBmwschema',
- );
-
- protected $internalDefaults = array(
- '_InstallUser' => 'db2admin'
- );
-
- /**
- * Get the DB2 database extension name
- * @return string
- */
- public function getName(){
- return 'ibm_db2';
- }
-
- /**
- * Determine whether the DB2 database extension is currently available in PHP
- * @return boolean
- */
- public function isCompiled() {
- return self::checkExtension( 'ibm_db2' );
- }
-
- /**
- * Generate a connection form for a DB2 database
- * @return string
- */
- public function getConnectForm() {
- return
- $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
- $this->getTextBox( 'wgDBport', 'config-db-port', array(), $this->parent->getHelpBox( 'config-db-port' ) ) .
- Html::openElement( 'fieldset' ) .
- Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
- $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-db-name-help' ) ) .
- $this->getTextBox( 'wgDBmwschema', 'config-db-schema', array(), $this->parent->getHelpBox( 'config-db-schema-help' ) ) .
- Html::closeElement( 'fieldset' ) .
- $this->getInstallUserBox();
- }
-
- /**
- * Validate and then execute the connection form for a DB2 database
- * @return Status
- */
- public function submitConnectForm() {
- // Get variables from the request
- $newValues = $this->setVarsFromRequest(
- array( 'wgDBserver', 'wgDBport', 'wgDBname',
- 'wgDBmwschema', 'wgDBuser', 'wgDBpassword' ) );
-
- // Validate them
- $status = Status::newGood();
- if ( !strlen( $newValues['wgDBname'] ) ) {
- $status->fatal( 'config-missing-db-name' );
- } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
- $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
- }
- if ( !strlen( $newValues['wgDBmwschema'] ) ) {
- $status->fatal( 'config-invalid-schema' );
- }
- elseif ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
- $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
- }
- if ( !strlen( $newValues['wgDBport'] ) ) {
- $status->fatal( 'config-invalid-port' );
- }
- elseif ( !preg_match( '/^[0-9_]*$/', $newValues['wgDBport'] ) ) {
- $status->fatal( 'config-invalid-port', $newValues['wgDBport'] );
- }
-
- // Submit user box
- if ( $status->isOK() ) {
- $status->merge( $this->submitInstallUserBox() );
- }
- if ( !$status->isOK() ) {
- return $status;
- }
-
- global $wgDBport;
- $wgDBport = $newValues['wgDBport'];
-
- // Try to connect
- $status->merge( $this->getConnection() );
- if ( !$status->isOK() ) {
- return $status;
- }
-
- $this->parent->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
- $this->parent->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
-
- return $status;
- }
-
- /**
- * Open a DB2 database connection
- * @return Status
- */
- public function openConnection() {
- $status = Status::newGood();
- try {
- $db = new DatabaseIbm_db2(
- $this->getVar( 'wgDBserver' ),
- $this->getVar( '_InstallUser' ),
- $this->getVar( '_InstallPassword' ),
- $this->getVar( 'wgDBname' ),
- 0,
- $this->getVar( 'wgDBmwschema' )
- );
- $status->value = $db;
- } catch ( DBConnectionError $e ) {
- $status->fatal( 'config-connection-error', $e->getMessage() );
- }
- return $status;
- }
-
- /**
- * Create a DB2 database for MediaWiki
- * @return Status
- */
- public function setupDatabase() {
- $status = $this->getConnection();
- if ( !$status->isOK() ) {
- return $status;
- }
- /**
- * @var $conn DatabaseBase
- */
- $conn = $status->value;
- $dbName = $this->getVar( 'wgDBname' );
- if( !$conn->selectDB( $dbName ) ) {
- $conn->query( "CREATE DATABASE "
- . $conn->addIdentifierQuotes( $dbName )
- . " AUTOMATIC STORAGE YES"
- . " USING CODESET UTF-8 TERRITORY US COLLATE USING SYSTEM"
- . " PAGESIZE 32768", __METHOD__ );
- $conn->selectDB( $dbName );
- }
- $this->setupSchemaVars();
- return $status;
- }
-
- /**
- * Create tables from scratch.
- * First check if pagesize >= 32k.
- *
- * @return Status
- */
- public function createTables() {
- $status = $this->getConnection();
- if ( !$status->isOK() ) {
- return $status;
- }
- $this->db->selectDB( $this->getVar( 'wgDBname' ) );
-
- if( $this->db->tableExists( 'user' ) ) {
- $status->warning( 'config-install-tables-exist' );
- return $status;
- }
-
- /* Check for pagesize */
- $status = $this->checkPageSize();
- if ( !$status->isOK() ) {
- return $status;
- }
-
- $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
- $this->db->begin( __METHOD__ );
-
- $error = $this->db->sourceFile( $this->db->getSchemaPath() );
- if( $error !== true ) {
- $this->db->reportQueryError( $error, 0, '', __METHOD__ );
- $this->db->rollback( __METHOD__ );
- $status->fatal( 'config-install-tables-failed', $error );
- } else {
- $this->db->commit( __METHOD__ );
- }
- // Resume normal operations
- if( $status->isOk() ) {
- $this->enableLB();
- }
- return $status;
- }
-
- /**
- * Check if database has a tablspace with pagesize >= 32k.
- *
- * @return Status
- */
- public function checkPageSize() {
- $status = $this->getConnection();
- if ( !$status->isOK() ) {
- return $status;
- }
- $this->db->selectDB( $this->getVar( 'wgDBname' ) );
-
- try {
- $result = $this->db->query( 'SELECT PAGESIZE FROM SYSCAT.TABLESPACES FOR READ ONLY' );
- if( $result == false ) {
- $status->fatal( 'config-connection-error', '' );
- } else {
- $row = $this->db->fetchRow( $result );
- while ( $row ) {
- if( $row[0] >= 32768 ) {
- return $status;
- }
- $row = $this->db->fetchRow( $result );
- }
- $status->fatal( 'config-ibm_db2-low-db-pagesize', '' );
- }
- } catch ( DBUnexpectedError $e ) {
- $status->fatal( 'config-connection-error', $e->getMessage() );
- }
-
- return $status;
- }
-
- /**
- * Generate the code to store the DB2-specific settings defined by the configuration form
- * @return string
- */
- public function getLocalSettings() {
- $schema = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBmwschema' ) );
- $port = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBport' ) );
- return
-"# IBM_DB2 specific settings
-\$wgDBmwschema = \"{$schema}\";
-\$wgDBport = \"{$port}\";";
- }
-
- public function __construct( $parent ) {
- parent::__construct( $parent );
- }
-}
diff --git a/includes/installer/Ibm_db2Updater.php b/includes/installer/Ibm_db2Updater.php
deleted file mode 100644
index 9daba9c2..00000000
--- a/includes/installer/Ibm_db2Updater.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-/**
- * IBM_DB2-specific updater.
- *
- * 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 Deployment
- */
-
-/**
- * Class for handling updates to IBM_DB2 databases.
- *
- * @ingroup Deployment
- * @since 1.17
- */
-class Ibm_db2Updater extends DatabaseUpdater {
-
- /**
- * Get the changes in the DB2 database scheme since MediaWiki 1.14
- * @return array
- */
- protected function getCoreUpdateList() {
- return array(
- // 1.14
- array( 'addField', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ),
- array( 'addField', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ),
-
- // 1.15
- array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
- array( 'addTable', 'tag_summary', 'patch-change_tag_summary.sql' ),
- array( 'addTable', 'valid_tag', 'patch-change_valid_tag.sql' ),
-
- // 1.16
- array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
- array( 'addTable', 'log_search', 'patch-log_search.sql' ),
- array( 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ),
- array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
- array( 'addTable', 'external_user', 'patch-external_user.sql' ),
- array( 'addIndex', 'log_search', 'ls_field_val', 'patch-log_search-rename-index.sql' ),
- array( 'addIndex', 'change_tag', 'change_tag_rc_tag', 'patch-change_tag-indexes.sql' ),
- array( 'addField', 'redirect', 'rd_interwiki', 'patch-rd_interwiki.sql' ),
-
- // 1.17
- array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
- array( 'addField', 'updatelog', 'ul_value', 'patch-ul_value.sql' ),
- array( 'addField', 'interwiki', 'iw_api', 'patch-iw_api_and_wikiid.sql' ),
- array( 'addField', 'categorylinks', 'cl_collation', 'patch-categorylinks-better-collation.sql' ),
- array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
- array( 'addTable', 'msg_resource_links', 'patch-msg_resource_links.sql' ),
- array( 'addIndex', 'msg_resource_links', 'uq61_msg_resource_links', 'patch-uq_61_msg_resource_links.sql' ),
- array( 'addIndex', 'msg_resource', 'uq81_msg_resource', 'patch-uq_81_msg_resource.sql' ),
- array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
- array( 'addIndex', 'module_deps', 'uq96_module_deps', 'patch-uq_96_module_deps.sql' ),
- array( 'addField', 'interwiki', 'iw_api', 'patch-iw_api-field.sql' ),
- array( 'addField', 'interwiki', 'iw_wikiid', 'patch-iw_wikiid-field.sql' ),
- array( 'addField', 'categorylinks', 'cl_sortkey_prefix', 'patch-cl_sortkey_prefix-field.sql' ),
- array( 'addField', 'categorylinks', 'cl_collation', 'patch-cl_collation-field.sql' ),
- array( 'addField', 'categorylinks', 'cl_type', 'patch-cl_type-field.sql' ),
-
- //1.18
- array( 'doUserNewTalkTimestampNotNull' ),
- array( 'addIndex', 'user', 'user_email', 'patch-user_email_index.sql' ),
- array( 'modifyField', 'user_properties', 'up_property', 'patch-up_property.sql' ),
- array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
- array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql'),
- array( 'doRebuildLocalisationCache' ),
-
- // 1.19
- array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql'),
- array( 'dropField', 'user', 'user_options', 'patch-drop-user_options.sql' ),
- array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ),
- array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ),
-
- // 1.20
- );
- }
-}
diff --git a/includes/installer/InstallDocFormatter.php b/includes/installer/InstallDocFormatter.php
index 9a389dd8..6d3819cd 100644
--- a/includes/installer/InstallDocFormatter.php
+++ b/includes/installer/InstallDocFormatter.php
@@ -1,6 +1,6 @@
<?php
/**
- * Installer-specific wikitext formating.
+ * Installer-specific wikitext formatting.
*
* 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
@@ -23,6 +23,7 @@
class InstallDocFormatter {
static function format( $text ) {
$obj = new self( $text );
+
return $obj->execute();
}
@@ -33,8 +34,8 @@ class InstallDocFormatter {
protected function execute() {
$text = $this->text;
// Use Unix line endings, escape some wikitext stuff
- $text = str_replace( array( '<', '{{', '[[', "\r" ),
- array( '&lt;', '&#123;&#123;', '&#91;&#91;', '' ), $text );
+ $text = str_replace( array( '<', '{{', '[[', '__', "\r" ),
+ array( '&lt;', '&#123;&#123;', '&#91;&#91;', '&#95;&#95;', '' ), $text );
// join word-wrapped lines into one
do {
$prev = $text;
@@ -44,9 +45,14 @@ class InstallDocFormatter {
$text = preg_replace( '/^\t\t/m', '::', $text );
$text = preg_replace( '/^\t/m', ':', $text );
// turn (bug nnnn) into links
- $text = preg_replace_callback('/bug (\d+)/', array( $this, 'replaceBugLinks' ), $text );
+ $text = preg_replace_callback( '/bug (\d+)/', array( $this, 'replaceBugLinks' ), $text );
// add links to manual to every global variable mentioned
- $text = preg_replace_callback('/(\$wg[a-z0-9_]+)/i', array( $this, 'replaceConfigLinks' ), $text );
+ $text = preg_replace_callback(
+ '/(\$wg[a-z0-9_]+)/i',
+ array( $this, 'replaceConfigLinks' ),
+ $text
+ );
+
return $text;
}
diff --git a/includes/installer/Installer.i18n.php b/includes/installer/Installer.i18n.php
index 4f1c4d0c..16e83e4f 100644
--- a/includes/installer/Installer.i18n.php
+++ b/includes/installer/Installer.i18n.php
@@ -17,19 +17,19 @@ $messages['en'] = array(
'config-information' => 'Information',
'config-localsettings-upgrade' => "A <code>LocalSettings.php</code> file has been detected.
To upgrade this installation, please enter the value of <code>\$wgUpgradeKey</code> in the box below.
-You will find it in LocalSettings.php.",
- 'config-localsettings-cli-upgrade' => 'A LocalSettings.php file has been detected.
-To upgrade this installation, please run update.php instead',
+You will find it in <code>LocalSettings.php</code>.",
+ 'config-localsettings-cli-upgrade' => 'A <code>LocalSettings.php</code> file has been detected.
+To upgrade this installation, please run <code>update.php</code> instead',
'config-localsettings-key' => 'Upgrade key:',
'config-localsettings-badkey' => 'The key you provided is incorrect.',
'config-upgrade-key-missing' => 'An existing installation of MediaWiki has been detected.
-To upgrade this installation, please put the following line at the bottom of your LocalSettings.php:
+To upgrade this installation, please put the following line at the bottom of your <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'The existing LocalSettings.php appears to be incomplete.
+ 'config-localsettings-incomplete' => 'The existing <code>LocalSettings.php</code> appears to be incomplete.
The $1 variable is not set.
-Please change LocalSettings.php so that this variable is set, and click "Continue".',
- 'config-localsettings-connection-error' => 'An error was encountered when connecting to the database using the settings specified in LocalSettings.php or AdminSettings.php. Please fix these settings and try again.
+Please change <code>LocalSettings.php</code> so that this variable is set, and click "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'An error was encountered when connecting to the database using the settings specified in <code>LocalSettings.php</code> or <code>AdminSettings.php</code>. Please fix these settings and try again.
$1',
'config-session-error' => 'Error starting session: $1',
@@ -63,8 +63,8 @@ Check your php.ini and make sure <code>session.save_path</code> is set to an app
'config-help-restart' => 'Do you want to clear all saved data that you have entered and restart the installation process?',
'config-restart' => 'Yes, restart it',
'config-welcome' => "=== Environmental checks ===
-Basic checks are performed to see if this environment is suitable for MediaWiki installation.
-You should provide the results of these checks if you need help during installation.",
+Basic checks will now be performed to see if this environment is suitable for MediaWiki installation.
+Remember to include this information if you seek support on how to complete the installation.",
'config-copyright' => "=== Copyright and Terms ===
$1
@@ -93,18 +93,17 @@ You cannot install MediaWiki.',
However, MediaWiki requires PHP $2 or higher.',
'config-unicode-using-utf8' => 'Using Brion Vibber\'s utf8_normalize.so for Unicode normalization.',
'config-unicode-using-intl' => 'Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalization.',
- 'config-unicode-pure-php-warning' => "'''Warning''': The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.
+ 'config-unicode-pure-php-warning' => "'''Warning:''' The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.
If you run a high-traffic site, you should read a little on [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode normalization].",
- 'config-unicode-update-warning' => "'''Warning''': The installed version of the Unicode normalization wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.
+ 'config-unicode-update-warning' => "'''Warning:''' The installed version of the Unicode normalization wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.
You should [//www.mediawiki.org/wiki/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
'config-no-db' => 'Could not find a suitable database driver! You need to install a database driver for PHP.
The following database types are supported: $1.
-If you are on shared hosting, ask your hosting provider to install a suitable database driver.
-If you compiled PHP yourself, reconfigure it with a database client enabled, for example using <code>./configure --with-mysql</code>.
-If you installed PHP from a Debian or Ubuntu package, then you also need install the php5-mysql module.',
- 'config-outdated-sqlite' => "'''Warning''': you have SQLite $1, which is lower than minimum required version $2. SQLite will be unavailable.",
- 'config-no-fts3' => "'''Warning''': SQLite is compiled without the [//sqlite.org/fts3.html FTS3 module], search features will be unavailable on this backend.",
+If you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.
+If you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.',
+ 'config-outdated-sqlite' => "'''Warning:''' you have SQLite $1, which is lower than minimum required version $2. SQLite will be unavailable.",
+ 'config-no-fts3' => "'''Warning:''' SQLite is compiled without the [//sqlite.org/fts3.html FTS3 module], search features will be unavailable on this backend.",
'config-register-globals' => "'''Warning: PHP's <code>[http://php.net/register_globals register_globals]</code> option is enabled.'''
'''Disable it if you can.'''
MediaWiki will work, but your server is exposed to potential security vulnerabilities.",
@@ -127,21 +126,27 @@ MediaWiki requires functions in this module and will not work in this configurat
If you're running Mandrake, install the php-xml package.",
'config-pcre' => 'The PCRE support module appears to be missing.
MediaWiki requires the Perl-compatible regular expression functions to work.',
- 'config-pcre-no-utf8' => "'''Fatal''': PHP's PCRE module seems to be compiled without PCRE_UTF8 support.
+ 'config-pcre-no-utf8' => "'''Fatal:''' PHP's PCRE module seems to be compiled without PCRE_UTF8 support.
MediaWiki requires UTF-8 support to function correctly.",
'config-memory-raised' => "PHP's <code>memory_limit</code> is $1, raised to $2.",
'config-memory-bad' => "'''Warning:''' PHP's <code>memory_limit</code> is $1.
This is probably too low.
The installation may fail!",
- 'config-ctype' => "'''Fatal''': PHP must be compiled with support for the [http://www.php.net/manual/en/ctype.installation.php Ctype extension].",
+ 'config-ctype' => "'''Fatal:''' PHP must be compiled with support for the [http://www.php.net/manual/en/ctype.installation.php Ctype extension].",
+ 'config-json' => "'''Fatal:''' PHP was compiled without JSON support.
+You must install either the PHP JSON extension or the [http://pecl.php.net/package/jsonc PECL jsonc] extension before installing MediaWiki.
+* The PHP extension is included in Red Hat Enterprise Linux (CentOS) 5 and 6, though must be enabled in <code>/etc/php.ini</code> or <code>/etc/php.d/json.ini</code>.
+* Some Linux distributions released after May 2013 omit the PHP extension, instead packaging the PECL extension as <code>php5-json</code> or <code>php-pecl-jsonc</code>.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] is installed',
'config-apc' => '[http://www.php.net/apc APC] is installed',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] is installed',
'config-no-cache' => "'''Warning:''' Could not find [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] or [http://www.iis.net/download/WinCacheForPhp WinCache].
Object caching is not enabled.",
- 'config-mod-security' => "'''Warning''': Your web server has [http://modsecurity.org/ mod_security] enabled. If misconfigured, it can cause problems for MediaWiki or other software that allows users to post arbitrary content.
+ 'config-mod-security' => "'''Warning:''' Your web server has [http://modsecurity.org/ mod_security] enabled. If misconfigured, it can cause problems for MediaWiki or other software that allows users to post arbitrary content.
Refer to [http://modsecurity.org/documentation/ mod_security documentation] or contact your host's support if you encounter random errors.",
'config-diff3-bad' => 'GNU diff3 not found.',
+ 'config-git' => 'Found the Git version control software: <code>$1</code>.',
+ 'config-git-bad' => 'Git version control software not found.',
'config-imagemagick' => 'Found ImageMagick: <code>$1</code>.
Image thumbnailing will be enabled if you enable uploads.',
'config-gd' => 'Found GD graphics library built-in.
@@ -150,7 +155,7 @@ Image thumbnailing will be enabled if you enable uploads.',
Image thumbnailing will be disabled.',
'config-no-uri' => "'''Error:''' Could not determine the current URI.
Installation aborted.",
- 'config-no-cli-uri' => "'''Warning''': No --scriptpath specified, using default: <code>$1</code>.",
+ 'config-no-cli-uri' => "'''Warning:''' No --scriptpath specified, using default: <code>$1</code>.",
'config-using-server' => 'Using server name "<nowiki>$1</nowiki>".',
'config-using-uri' => 'Using server URL "<nowiki>$1$2</nowiki>".',
'config-uploads-not-safe' => "'''Warning:''' Your default directory for uploads <code>$1</code> is vulnerable to arbitrary scripts execution.
@@ -163,7 +168,9 @@ Installation aborted.',
'config-using531' => 'MediaWiki cannot be used with PHP $1 due to a bug involving reference parameters to <code>__call()</code>.
Upgrade to PHP 5.3.2 or higher, or downgrade to PHP 5.3.0 to resolve this.
Installation aborted.',
- 'config-suhosin-max-value-length' => "Suhosin is installed and limits the GET parameter length to $1 bytes. MediaWiki's ResourceLoader component will work around this limit, but that will degrade performance. If at all possible, you should set suhosin.get.max_value_length to 1024 or higher in php.ini , and set \$wgResourceLoaderMaxQueryLength to the same value in LocalSettings.php .",
+ 'config-suhosin-max-value-length' => "Suhosin is installed and limits the GET parameter <code>length</code> to $1 bytes.
+MediaWiki's ResourceLoader component will work around this limit, but that will degrade performance.
+If at all possible, you should set <code>suhosin.get.max_value_length</code> to 1024 or higher in <code>php.ini</code>, and set <code>\$wgResourceLoaderMaxQueryLength</code> to the same value in <code>LocalSettings.php</code>.",
'config-db-type' => 'Database type:',
'config-db-host' => 'Database host:',
'config-db-host-help' => 'If your database server is on different server, enter the host name or IP address here.
@@ -230,37 +237,34 @@ The directory you provide must be writable by the webserver during installation.
It should '''not''' be accessible via the web, this is why we're not putting it where your PHP files are.
The installer will write a <code>.htaccess</code> file along with it, but if that fails someone can gain access to your raw database.
-That includes raw user data (e-mail addresses, hashed passwords) as well as deleted revisions and other restricted data on the wiki.
+That includes raw user data (email addresses, hashed passwords) as well as deleted revisions and other restricted data on the wiki.
Consider putting the database somewhere else altogether, for example in <code>/var/lib/mediawiki/yourwiki</code>.",
'config-oracle-def-ts' => 'Default tablespace:',
'config-oracle-temp-ts' => 'Temporary tablespace:',
- 'config-type-mysql' => 'MySQL',
+ 'config-type-mysql' => 'MySQL (or compatible)',
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki supports the following database systems:
$1
If you do not see the database system you are trying to use listed below, then follow the instructions linked above to enable support.',
- 'config-support-mysql' => '* $1 is the primary target for MediaWiki and is best supported ([http://www.php.net/manual/en/mysql.installation.php how to compile PHP with MySQL support])',
- 'config-support-postgres' => '* $1 is a popular open source database system as an alternative to MySQL ([http://www.php.net/manual/en/pgsql.installation.php how to compile PHP with PostgreSQL support]). There may be some minor outstanding bugs, and it is not recommended for use in a production environment.',
- 'config-support-sqlite' => '* $1 is a lightweight database system which is very well supported. ([http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], uses PDO)',
- 'config-support-oracle' => '* $1 is a commercial enterprise database. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])',
- 'config-support-ibm_db2' => '* $1 is a commercial enterprise database.',
+ 'config-dbsupport-mysql' => '* [{{int:version-db-mysql-url}} MySQL] is the primary target for MediaWiki and is best supported. MediaWiki also works with [{{int:version-db-mariadb-url}} MariaDB] and [{{int:version-db-percona-url}} Percona Server], which are MySQL compatible. ([http://www.php.net/manual/en/mysqli.installation.php How to compile PHP with MySQL support])',
+ 'config-dbsupport-postgres' => '* [{{int:version-db-postgres-url}} PostgreSQL] is a popular open source database system as an alternative to MySQL. There may be some minor outstanding bugs, and it is not recommended for use in a production environment. ([http://www.php.net/manual/en/pgsql.installation.php How to compile PHP with PostgreSQL support])',
+ 'config-dbsupport-sqlite' => '* [{{int:version-db-sqlite-url}} SQLite] is a lightweight database system that is very well supported. ([http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], uses PDO)',
+ 'config-dbsupport-oracle' => '* [{{int:version-db-oracle-url}} Oracle] is a commercial enterprise database. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])',
'config-header-mysql' => 'MySQL settings',
'config-header-postgres' => 'PostgreSQL settings',
'config-header-sqlite' => 'SQLite settings',
'config-header-oracle' => 'Oracle settings',
- 'config-header-ibm_db2' => 'IBM DB2 settings',
'config-invalid-db-type' => 'Invalid database type',
'config-missing-db-name' => 'You must enter a value for "Database name"',
'config-missing-db-host' => 'You must enter a value for "Database host"',
'config-missing-db-server-oracle' => 'You must enter a value for "Database TNS"',
'config-invalid-db-server-oracle' => 'Invalid database TNS "$1".
-Use only ASCII letters (a-z, A-Z), numbers (0-9), underscores (_) and dots (.).',
+Use either "TNS Name" or an "Easy Connect" string ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])',
'config-invalid-db-name' => 'Invalid database name "$1".
Use only ASCII letters (a-z, A-Z), numbers (0-9), underscores (_) and hyphens (-).',
'config-invalid-db-prefix' => 'Invalid database prefix "$1".
@@ -317,8 +321,8 @@ This is '''not recommended''' unless you are having problems with your wiki.",
You can now [$1 start using your wiki].",
'config-regenerate' => 'Regenerate LocalSettings.php →',
- 'config-show-table-status' => 'SHOW TABLE STATUS query failed!',
- 'config-unknown-collation' => "'''Warning:''' Database is using unrecognised collation.",
+ 'config-show-table-status' => '<code>SHOW TABLE STATUS</code> query failed!',
+ 'config-unknown-collation' => "'''Warning:''' Database is using unrecognized collation.",
'config-db-web-account' => 'Database account for web access',
'config-db-web-help' => 'Select the username and password that the web server will use to connect to the database server, during ordinary operation of the wiki.',
'config-db-web-account-same' => 'Use the same account as for installation',
@@ -328,13 +332,19 @@ The account you specify here must already exist.',
'config-mysql-engine' => 'Storage engine:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
- 'config-mysql-myisam-dep' => "'''Warning''': You have selected MyISAM as storage engine for MySQL, which is not recommended for use with MediaWiki, because:
+ 'config-mysql-myisam-dep' => "'''Warning:''' You have selected MyISAM as storage engine for MySQL, which is not recommended for use with MediaWiki, because:
* it barely supports concurrency due to table locking
* it is more prone to corruption than other engines
* the MediaWiki codebase does not always handle MyISAM as it should
If your MySQL installation supports InnoDB, it is highly recommended that you choose that instead.
If your MySQL installation does not support InnoDB, maybe it's time for an upgrade.",
+ 'config-mysql-only-myisam-dep' => "'''Warning:''' MyISAM is the only available storage engine for MySQL, which is not recommended for use with MediaWiki, because:
+* it barely supports concurrency due to table locking
+* it is more prone to corruption than other engines
+* the MediaWiki codebase does not always handle MyISAM as it should
+
+Your MySQL installation does not support InnoDB, maybe it's time for an upgrade.",
'config-mysql-engine-help' => "'''InnoDB''' is almost always the best option, since it has good concurrency support.
'''MyISAM''' may be faster in single-user or read-only installations.
@@ -347,8 +357,6 @@ This is more efficient than MySQL's UTF-8 mode, and allows you to use the full r
In '''UTF-8 mode''', MySQL will know what character set your data is in, and can present and convert it appropriately, but it will not let you store characters above the [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane].",
- 'config-ibm_db2-low-db-pagesize' => "Your DB2 database has a default tablespace with an insufficient pagesize. The pagesize has to be '''32K''' or greater.",
-
'config-site-name' => 'Name of wiki:',
'config-site-name-help' => "This will appear in the title bar of the browser and in various other places.",
'config-site-name-blank' => 'Enter a site name.',
@@ -359,7 +367,7 @@ In '''UTF-8 mode''', MySQL will know what character set your data is in, and can
'config-ns-other-default' => 'MyWiki',
'config-project-namespace-help' => 'Following Wikipedia\'s example, many wikis keep their policy pages separate from their content pages, in a "\'\'\'project namespace\'\'\'".
All page titles in this namespace start with a certain prefix, which you can specify here.
-Traditionally, this prefix is derived from the name of the wiki, but it cannot contain punctuation characters such as "#" or ":".',
+Usually, this prefix is derived from the name of the wiki, but it cannot contain punctuation characters such as "#" or ":".',
'config-ns-invalid' => 'The specified namespace "<nowiki>$1</nowiki>" is invalid.
Specify a different project namespace.',
'config-ns-conflict' => 'The specified namespace "<nowiki>$1</nowiki>" conflicts with a default MediaWiki namespace.
@@ -376,22 +384,22 @@ Specify a different username.',
'config-admin-password-blank' => 'Enter a password for the administrator account.',
'config-admin-password-same' => 'The password must not be the same as the username.',
'config-admin-password-mismatch' => 'The two passwords you entered do not match.',
- 'config-admin-email' => 'E-mail address:',
- 'config-admin-email-help' => 'Enter an e-mail address here to allow you to receive e-mail from other users on the wiki, reset your password, and be notified of changes to pages on your watchlist. You can leave this field empty.',
+ 'config-admin-email' => 'Email address:',
+ 'config-admin-email-help' => 'Enter an email address here to allow you to receive email from other users on the wiki, reset your password, and be notified of changes to pages on your watchlist. You can leave this field empty.',
'config-admin-error-user' => 'Internal error when creating an admin with the name "<nowiki>$1</nowiki>".',
'config-admin-error-password' => 'Internal error when setting a password for the admin "<nowiki>$1</nowiki>": <pre>$2</pre>',
- 'config-admin-error-bademail' => 'You have entered an invalid e-mail address.',
+ 'config-admin-error-bademail' => 'You have entered an invalid email address.',
'config-subscribe' => 'Subscribe to the [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce release announcements mailing list].',
'config-subscribe-help' => 'This is a low-volume mailing list used for release announcements, including important security announcements.
You should subscribe to it and update your MediaWiki installation when new versions come out.',
- 'config-subscribe-noemail' => 'You tried to subscribe to the release announcements mailing list without providing an e-mail address.
-Please provide an e-mail address if you wish to subscribe to the mailing list.',
+ 'config-subscribe-noemail' => 'You tried to subscribe to the release announcements mailing list without providing an email address.
+Please provide an email address if you wish to subscribe to the mailing list.',
'config-almost-done' => 'You are almost done!
You can now skip the remaining configuration and install the wiki right now.',
'config-optional-continue' => 'Ask me more questions.',
'config-optional-skip' => "I'm bored already, just install the wiki.",
'config-profile' => 'User rights profile:',
- 'config-profile-wiki' => 'Traditional wiki',
+ 'config-profile-wiki' => 'Open wiki',
'config-profile-no-anon' => 'Account creation required',
'config-profile-fishbowl' => 'Authorized editors only',
'config-profile-private' => 'Private wiki',
@@ -401,7 +409,7 @@ In MediaWiki, it is easy to review the recent changes, and to revert any damage
However, many have found MediaWiki to be useful in a wide variety of roles, and sometimes it is not easy to convince everyone of the benefits of the wiki way.
So you have the choice.
-A '''{{int:config-profile-wiki}}''' allows anyone to edit, without even logging in.
+The '''{{int:config-profile-wiki}}''' model allows anyone to edit, without even logging in.
A wiki with '''{{int:config-profile-no-anon}}''' provides extra accountability, but may deter casual contributors.
The '''{{int:config-profile-fishbowl}}''' scenario allows approved users to edit, but the public can view the pages, including history.
@@ -426,22 +434,22 @@ If you want to be able to use text from Wikipedia, and you want Wikipedia to be
Wikipedia previously used the GNU Free Documentation License.
The GFDL is a valid license, but it is difficult to understand.
It is also difficult to reuse content licensed under the GFDL.",
- 'config-email-settings' => 'E-mail settings',
- 'config-enable-email' => 'Enable outbound e-mail',
- 'config-enable-email-help' => "If you want e-mail to work, [http://www.php.net/manual/en/mail.configuration.php PHP's mail settings] need to be configured correctly.
-If you do not want any e-mail features, you can disable them here.",
- 'config-email-user' => 'Enable user-to-user e-mail',
- 'config-email-user-help' => 'Allow all users to send each other e-mail if they have enabled it in their preferences.',
+ 'config-email-settings' => 'Email settings',
+ 'config-enable-email' => 'Enable outbound email',
+ 'config-enable-email-help' => "If you want email to work, [http://www.php.net/manual/en/mail.configuration.php PHP's mail settings] need to be configured correctly.
+If you do not want any email features, you can disable them here.",
+ 'config-email-user' => 'Enable user-to-user email',
+ 'config-email-user-help' => 'Allow all users to send each other email if they have enabled it in their preferences.',
'config-email-usertalk' => 'Enable user talk page notification',
'config-email-usertalk-help' => 'Allow users to receive notifications on user talk page changes, if they have enabled it in their preferences.',
'config-email-watchlist' => 'Enable watchlist notification',
'config-email-watchlist-help' => 'Allow users to receive notifications about their watched pages if they have enabled it in their preferences.',
- 'config-email-auth' => 'Enable e-mail authentication',
- 'config-email-auth-help' => "If this option is enabled, users have to confirm their e-mail address using a link sent to them whenever they set or change it.
-Only authenticated e-mail addresses can receive e-mails from other users or change notification e-mails.
-Setting this option is '''recommended''' for public wikis because of potential abuse of the e-mail features.",
- 'config-email-sender' => 'Return e-mail address:',
- 'config-email-sender-help' => 'Enter the e-mail address to use as the return address on outbound e-mail.
+ 'config-email-auth' => 'Enable email authentication',
+ 'config-email-auth-help' => "If this option is enabled, users have to confirm their email address using a link sent to them whenever they set or change it.
+Only authenticated email addresses can receive emails from other users or change notification emails.
+Setting this option is '''recommended''' for public wikis because of potential abuse of the email features.",
+ 'config-email-sender' => 'Return email address:',
+ 'config-email-sender-help' => 'Enter the email address to use as the return address on outbound email.
This is where bounces will be sent.
Many mail servers require at least the domain name part to be valid.',
'config-upload-settings' => 'Images and file uploads',
@@ -458,6 +466,8 @@ Ideally, this should not be accessible from the web.',
'config-logo-help' => "MediaWiki's default skin includes space for a 135x160 pixel logo above the sidebar menu.
Upload an image of the appropriate size, and enter the URL here.
+You can use <code>\$wgStylePath</code> or <code>\$wgScriptPath</code> if your logo is relative to those paths.
+
If you do not want a logo, leave this box blank.",
'config-instantcommons' => 'Enable Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] is a feature that allows wikis to use images, sounds and other media found on the [//commons.wikimedia.org/ Wikimedia Commons] site.
@@ -492,7 +502,7 @@ They may require additional configuration, but you can enable them now',
'config-install-alreadydone' => "'''Warning:''' You seem to have already installed MediaWiki and are trying to install it again.
Please proceed to the next page.",
'config-install-begin' => 'By pressing "{{int:config-continue}}", you will begin the installation of MediaWiki.
-If you still want to make changes, press back.',
+If you still want to make changes, press "{{int:config-back}}".',
'config-install-step-done' => 'done',
'config-install-step-failed' => 'failed',
'config-install-extensions' => 'Including extensions',
@@ -517,12 +527,12 @@ MediaWiki currently requires that the tables be owned by the web user. Please sp
'config-install-user-missing-create' => 'The specified user "$1" does not exist.
Please click the "create account" checkbox below if you want to create it.',
'config-install-tables' => 'Creating tables',
- 'config-install-tables-exist' => "'''Warning''': MediaWiki tables seem to already exist.
+ 'config-install-tables-exist' => "'''Warning:''' MediaWiki tables seem to already exist.
Skipping creation.",
- 'config-install-tables-failed' => "'''Error''': Table creation failed with the following error: $1",
+ 'config-install-tables-failed' => "'''Error:''' Table creation failed with the following error: $1",
'config-install-interwiki' => 'Populating default interwiki table',
'config-install-interwiki-list' => 'Could not read file <code>interwiki.list</code>.',
- 'config-install-interwiki-exists' => "'''Warning''': The interwiki table seems to already have entries.
+ 'config-install-interwiki-exists' => "'''Warning:''' The interwiki table seems to already have entries.
Skipping default list.",
'config-install-stats' => 'Initializing statistics',
'config-install-keys' => 'Generating secret keys',
@@ -545,19 +555,23 @@ If the download was not offered, or if you cancelled it, you can restart the dow
$3
-'''Note''': If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.
+'''Note:''' If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.
When that has been done, you can '''[$2 enter your wiki]'''.",
- 'config-download-localsettings' => 'Download LocalSettings.php',
+ 'config-download-localsettings' => 'Download <code>LocalSettings.php</code>',
'config-help' => 'help',
'config-nofile' => 'File "$1" could not be found. Has it been deleted?',
+ 'config-extension-link' => 'Did you know that your wiki supports [//www.mediawiki.org/wiki/Manual:Extensions extensions]?
+
+You can browse [//www.mediawiki.org/wiki/Category:Extensions_by_category extensions by category] or the [//www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] to see the full list of extensions.',
'mainpagetext' => "'''MediaWiki has been successfully installed.'''",
'mainpagedocfooter' => "Consult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software.
== Getting started ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localise MediaWiki for your language]",
);
/** Message documentation (Message documentation)
@@ -576,65 +590,125 @@ When that has been done, you can '''[$2 enter your wiki]'''.",
* @author Shirayuki
* @author Siebrand
* @author Umherirrender
+ * @author Waldir
*/
$messages['qqq'] = array(
- 'config-desc' => '{{desc}}',
+ 'config-desc' => 'Short description of the installer.',
'config-title' => 'Parameters:
* $1 is the version of MediaWiki that is being installed.',
'config-information' => '{{Identical|Information}}',
- 'config-localsettings-cli-upgrade' => 'Do not translate the <code>LocalSettings.php</code> and the <code>update.php</code> parts.',
+ 'config-localsettings-upgrade' => '{{doc-important|Do not translate <code>LocalSettings.php</code> and <code>$wgUpgradeKey</code>.}}',
+ 'config-localsettings-cli-upgrade' => '{{doc-important|Do not translate the <code>LocalSettings.php</code> and the <code>update.php</code> parts.}}',
+ 'config-upgrade-key-missing' => 'Used in info box. Parameters:
+* $1 - the upgrade key, enclosed in <code><nowiki><pre></nowikI></code> tag.',
+ 'config-localsettings-incomplete' => '{{doc-important|Do not translate <code>LocalSettings.php</code> and <code><nowiki>{{int:Config-continue}}</nowiki><code>.}}
+Parameters:
+* $1 - name of variable (any one of required variables or installer-specific global variables)',
+ 'config-localsettings-connection-error' => '{{doc-important|Do not translate <code>LocalSettings.php</code> and <code>AdminSettings.php</code>.}}
+Used as error message. Parameters:
+* $1 - (probably empty string)',
'config-session-error' => 'Parameters:
* $1 is the error that was encountered with the session.',
'config-session-expired' => 'Parameters:
* $1 is the configured session lifetime.',
+ 'config-no-session' => '{{doc-important|Do not translate <code>php.ini</code> and <code>session.save_path</code>.}}
+Used as error message.',
'config-back' => '{{Identical|Back}}',
'config-continue' => '{{Identical|Continue}}',
'config-page-language' => '{{Identical|Language}}',
'config-page-name' => '{{Identical|Name}}',
'config-page-options' => '{{Identical|Options}}',
'config-page-install' => '{{Identical|Install}}',
+ 'config-page-complete' => '{{Identical|Complete}}',
+ 'config-page-releasenotes' => '{{Identical|Release notes}}',
'config-page-copying' => 'This is a link to the full GPL text',
'config-restart' => 'Button text to confirm the installation procedure has to be restarted.',
+ 'config-copyright' => 'This message follows {{msg-mw|config-env-good}}.
+
+Parameters:
+* $1 - copyright and author list',
'config-sidebar' => 'Maximum width for words is 24 characters. Only visible part of the translation counts to this limit.',
'config-env-php' => 'Parameters:
-* $1 is the version of PHP that has been installed.',
+* $1 - the version of PHP that has been installed
+See also:
+* {{msg-mw|config-env-php-toolow}}',
+ 'config-env-php-toolow' => 'Parameters:
+* $1 - the version of PHP that has been installed
+* $2 - minimum PHP version number
+See also:
+* {{msg-mw|config-env-php}}',
'config-unicode-pure-php-warning' => 'PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.',
'config-unicode-update-warning' => "ICU is a body producing standard software tools for support of Unicode and other internationalization aspects. This message warns the system administrator installing MediaWiki that the server's software is not up-to-date and MediaWiki will have problems handling some characters.",
- 'config-no-db' => 'Do not translate: <code>./configure --with-mysql</code>.
-<br />
-Do not translate: <code>php5-mysql</code>.
-
+ 'config-no-db' => '{{doc-important|Do not translate "<code>./configure --with-mysqli</code>" and "<code>php5-mysql</code>".}}
Parameters:
* $1 is comma separated list of database types supported by MediaWiki.',
+ 'config-outdated-sqlite' => 'Used as warning. Parameters:
+* $1 - the version of SQLite that has been installed
+* $2 - minimum version',
'config-no-fts3' => 'A "[[:wikipedia:Front and back ends|backend]]" is a system or component that ordinary users don\'t interact with directly and don\'t need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are "system" or "service", or (depending on context and language) even leave it untranslated.',
+ 'config-magic-quotes-runtime' => '{{Related|Config-fatal}}',
+ 'config-magic-quotes-sybase' => '{{Related|Config-fatal}}',
+ 'config-mbstring' => '{{Related|Config-fatal}}',
+ 'config-ze1' => '{{Related|Config-fatal}}',
'config-pcre' => 'PCRE is an initialism for "Perl-compatible regular expression". Perl is programming language whose [[:w:regular expression|regular expression]] syntax is popular and used in other languages using a library called PCRE.',
- 'config-pcre-no-utf8' => "PCRE is a name of a programmers' library for supporting regular expressions. It can probably be translated without change.",
+ 'config-pcre-no-utf8' => "PCRE is a name of a programmers' library for supporting regular expressions. It can probably be translated without change.
+{{Related|Config-fatal}}",
'config-memory-raised' => 'Parameters:
* $1 is the configured <code>memory_limit</code>.
* $2 is the value to which <code>memory_limit</code> was raised.',
'config-memory-bad' => 'Parameters:
* $1 is the configured <code>memory_limit</code>.',
'config-ctype' => 'Message if support for [http://www.php.net/manual/en/ctype.installation.php Ctype] is missing from PHP',
+ 'config-json' => 'Message if support for [[wikipedia:JSON|JSON]] is missing from PHP.
+* "[[wikipedia:Red Hat Enterprise Linux|Red Hat Enterprise Linux]]" (RHEL) and "[[wikipedia:CentOS|CentOS]]" refer to two almost-identical Linux distributions. "5 and 6" refers to version 5 or 6 of either distribution. Because RHEL 7 likely will not include the PHP extension, do not translate as "5 or newer".
+* "The [http://www.php.net/json PHP extension]" is the JSON extension included with PHP 5.2 and newer.
+* "The [http://pecl.php.net/package/jsonc PECL extension]" is based on the PHP extension, though excludes code some distributions have found unacceptable (see [[bugzilla:47431]]).',
'config-xcache' => 'Message indicates if this program is available',
'config-apc' => 'Message indicates if this program is available',
'config-wincache' => 'Message indicates if this program is available',
+ 'config-git' => 'Message if Git version control software is available.
+Parameter:
+* $1 is the <code>Git</code> executable file name.',
+ 'config-git-bad' => 'Message if Git version control software is not found.',
'config-imagemagick' => '$1 is ImageMagick\'s <code>convert</code> executable file name.
Add dir="ltr" to the <nowiki><code></nowiki> for right-to-left languages.',
'config-no-cli-uri' => 'Parameters:
* $1 is the default value for scriptpath.',
+ 'config-using-server' => 'Used as a part of environment check result. Parameters:
+* $1 - default server name',
+ 'config-using-uri' => 'Used as a part of environment check result. Parameters:
+* $1 - server name
+* $2 - script path',
+ 'config-uploads-not-safe' => 'Used as a part of environment check result. Parameters:
+* $1 - name of directory for images: <code>$IP/images/</code>',
'config-no-cli-uploads-check' => 'CLI = [[w:Command-line interface|command-line interface]] (i.e. the installer runs as a command-line script, not using HTML interface via an internet browser)',
- 'config-suhosin-max-value-length' => 'Message shown when PHP parameter suhosin.get.max_value_length is between 0 and 1023 (that max value is hard set in MediaWiki software)',
+ 'config-using531' => 'Used as error message. Parameters:
+* $1 - the version of PHP that has been installed',
+ 'config-suhosin-max-value-length' => '{{doc-important|Do not translate "length", "suhosin.get.max_value_length", "php.ini", "$wgResourceLoaderMaxQueryLength" and "LocalSettings.php".}}
+Message shown when PHP parameter <code>suhosin.get.max_value_length</code> is between 0 and 1023 (that max value is hard set in MediaWiki software).',
+ 'config-db-host-help' => '{{doc-singularthey}}',
'config-db-host-oracle' => 'TNS = [[:wikipedia:Transparent Network Substrate|Transparent Network Substrate]] (<== wikipedia link)',
+ 'config-db-host-oracle-help' => 'See also:
+* {{msg-mw|Config-invalid-db-server-oracle}}',
'config-db-wiki-settings' => 'This is more acurate: "Enter identifying or distinguishing data for this wiki" since a MySQL database can host tables of several wikis.',
'config-db-account-oracle-warn' => 'A "[[:wikipedia:Front and back ends|backend]]" is a system or component that ordinary users don\'t interact with directly and don\'t need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are "system" or "service", or (depending on context and language) even leave it untranslated.',
+ 'config-db-password-empty' => 'Used as error message. Parameters:
+* $1 - database username',
'config-db-account-lock' => "It might be easier to translate ''normal operation'' as \"also after the installation process\"",
- 'config-type-mysql' => '{{optional}}',
+ 'config-mysql-old' => 'Used as error message. Parameters:
+* $1 - minimum version
+* $2 - the version of MySQL that has been installed',
+ 'config-pg-test-error' => '* $1 - database name
+* $2 - error message',
+ 'config-sqlite-dir-help' => '{{doc-important|Do not translate <code>.htaccess</code> and <code>/var/lib/mediawiki/yourwiki</code>.}}
+Used in help box.',
+ 'config-type-mysql' => '"Or compatible" refers to several database systems that are compatible with MySQL, as explained in {{msg-mw|config-dbsupport-mysql}}, and thus also work with this choice of database type.',
'config-type-postgres' => '{{optional}}',
'config-type-sqlite' => '{{optional}}',
'config-type-oracle' => '{{optional}}',
'config-support-info' => 'Parameters:
-* $1 - a list of DBMSs that MediaWiki supports, composed with other config-type-* and config-support-* messages.',
+* $1 - a list of DBMSs that MediaWiki supports, composed with config-dbsupport-* messages.',
'config-support-mysql' => 'Parameters:
* $1 - a link to the MySQL home page having the anchor text "MySQL".',
'config-support-postgres' => 'Parameters:
@@ -643,32 +717,150 @@ Add dir="ltr" to the <nowiki><code></nowiki> for right-to-left languages.',
* $1 - a link to the SQLite home page having the anchor text "SQLite".',
'config-support-oracle' => 'Parameters:
* $1 - a link to the Oracle home page, the anchor text of which is "Oracle".',
+ 'config-dbsupport-mysql' => 'Used in {{msg-mw|config-support-info}}.',
+ 'config-dbsupport-postgres' => 'Used in {{msg-mw|config-support-info}}.',
+ 'config-dbsupport-sqlite' => 'Used in {{msg-mw|config-support-info}}.',
+ 'config-dbsupport-oracle' => 'Used in {{msg-mw|config-support-info}}.',
+ 'config-invalid-db-server-oracle' => 'Used as error message. Parameters:
+* $1 - database server name
+See also:
+* {{msg-mw|Config-db-host-oracle-help}}',
+ 'config-invalid-db-name' => 'Used as error message. Parameters:
+* $1 - database name
+See also:
+* {{msg-mw|Config-invalid-db-prefix}}',
+ 'config-invalid-db-prefix' => 'Used as error message. Parameters:
+* $1 - database prefix
+See also:
+* {{msg-mw|Config-invalid-db-name}}',
'config-connection-error' => '$1 is the external error from the database, such as "DB connection error: Access denied for user \'dba\'@\'localhost\' (using password: YES) (localhost)."
If you\'re translating this message to a right-to-left language, consider writing <nowiki><div dir="ltr">$1.</div></nowiki>. (When the bidi features for HTML5 will be implemented in the browsers, it will probably be a good idea to write it as <nowiki><div dir="auto">$1.</div></nowiki>.)',
'config-invalid-schema' => '*$1 - schema name',
+ 'config-db-sys-user-exists-oracle' => 'Used as error message. Parameters:
+* $1 - database username',
+ 'config-postgres-old' => 'Used as error message. Used as warning. Parameters:
+* $1 - minimum version
+* $2 - the version of PostgreSQL that has been installed',
+ 'config-sqlite-parent-unwritable-group' => 'Used as SQLite error message. Parameters:
+* $1 - data directory
+* $2 - "dirname" part of $1
+* $3 - "basename" part of $1
+* $4 - web server\'s primary group name
+See also:
+* {{msg-mw|Config-sqlite-parent-unwritable-nogroup}}',
+ 'config-sqlite-parent-unwritable-nogroup' => 'Used as SQLite error message. Parameters:
+* $1 - data directory
+* $2 - "dirname" part of $1
+* $3 - "basename" part of $1
+See also:
+* {{msg-mw|Config-sqlite-parent-unwritable-group}}',
+ 'config-sqlite-mkdir-error' => 'Used as SQLite error message. Parameters:
+* $1 - data directory name',
'config-sqlite-dir-unwritable' => 'webserver refers to a software like Apache or Lighttpd.',
+ 'config-sqlite-connection-error' => 'Used as SQLite error message. Parameters:
+* $1 - error message which SQLite server returned',
+ 'config-sqlite-readonly' => 'Used as SQLite error message. Parameters:
+* $1 - filename',
+ 'config-sqlite-cant-create-db' => 'Used as SQLite error message. Parameters:
+* $1 - filename',
'config-can-upgrade' => 'Parameters:
* $1 - Version or Revision indicator.',
- 'config-show-table-status' => '{{doc-important|"SHOW TABLE STATUS" is a MySQL command. Do not translate this.}}',
+ 'config-upgrade-done' => 'Used as success message. Parameters:
+* $1 - full URL of index.php
+See also:
+* {{msg-mw|config-upgrade-done-no-regenerate}}',
+ 'config-upgrade-done-no-regenerate' => 'Used as success message. Parameters:
+* $1 - full URL of index.php
+See also:
+* {{msg-mw|config-upgrade-done}}',
+ 'config-regenerate' => 'This message appears in a button after LocalSettings.php is generated and downloaded at the end of the MediaWiki installation process.',
+ 'config-show-table-status' => '{{doc-important|"<code>SHOW TABLE STATUS</code>" is a MySQL command. Do not translate this.}}',
'config-db-web-account-same' => 'checkbox label',
'config-db-web-create' => 'checkbox label',
- 'config-ns-generic' => '{{Identical|Project}}',
+ 'config-mysql-only-myisam-dep' => 'Used as warning message when mysql does not support the minimum suggested feature set.',
+ 'config-mysql-binary' => '{{Identical|Binary}}',
+ 'config-ns-generic' => 'Used as label for "namespace type" radio button.
+
+See also:
+* {{msg-mw|Config-ns-site-name}}
+* {{msg-mw|Config-ns-other}}
+{{Identical|Project}}',
+ 'config-ns-site-name' => 'Used as label for "namespace type" radio button. Parameters:
+* $1 - wiki name
+See also:
+* {{msg-mw|Config-ns-generic}}
+* {{msg-mw|Config-ns-other}}',
+ 'config-ns-other' => "Used as label for \"namespace type\" radio button.
+
+This message is followed by the input box which enables to '''specify''' a namespace name.
+
+See also:
+* {{msg-mw|Config-ns-site-name}}
+* {{msg-mw|Config-ns-generic}}",
+ 'config-ns-invalid' => 'Used as error message. Parameters:
+* $1 - namespace name
+See also:
+* {{msg-mw|Config-ns-conflict}}',
+ 'config-ns-conflict' => 'Used as error message. Parameters:
+* $1 - namespace name
+See also:
+* {{msg-mw|Config-ns-invalid}}',
'config-admin-name' => '{{Identical|Your name}}',
'config-admin-password' => '{{Identical|Password}}',
+ 'config-admin-name-invalid' => 'Used as error message. Parameters:
+* $1 - username of administrator',
'config-admin-email' => '{{Identical|E-mail address}}',
+ 'config-admin-error-user' => 'Used as error message. Parameters:
+* $1 - username of administrator
+See also:
+* {{msg-mw|Config-admin-error-password}}',
+ 'config-admin-error-password' => 'Used as error message. Parameters:
+* $1 - username of administrator
+* $2 - error message
+See also:
+* {{msg-mw|Config-admin-error-user}}',
'config-subscribe' => 'Used as label for the installer checkbox',
+ 'config-subscribe-help' => '"Low-volume" in this context means that there will be few e-mails to that mailing list per time period.',
'config-profile-help' => 'Messages referenced:
* {{msg-mw|config-profile-wiki}}
* {{msg-mw|config-profile-no-anon}}
* {{msg-mw|config-profile-fishbowl}}
* {{msg-mw|config-profile-private}}',
+ 'config-email-settings' => '{{Identical|E-mail setting}}',
+ 'config-email-user' => '{{Identical|Enable user-to-user e-mail}}',
'config-upload-help' => 'The word "mode" here refers to the access rights given to various user groups when attempting to create and store files and/or subdiretories in the said directory on the server. It also refers to the <code>mode</code> command used to maipulate said right mask under Unix, Linux, and similar operating systems. A less operating-system-centric translation is fine.',
'config-logo-help' => '',
- 'config-cc-not-chosen' => 'Do not translate the <code>"proceed".</code> part.
-This message refers to a block of HTML being embedded into the installer page. It comes from the Creative Commons Web site. The block is in the English language. It is a scripted license chooser. When an individual license has been selected, it asks you to klick "proceed" so as to return to the MediaWiki installer page.',
+ 'config-instantcommons' => 'Used as label for the checkbox.
+
+The help message for this checkbox is:
+* {{msg-mw|Config-instantcommons-help}}',
+ 'config-instantcommons-help' => 'Used as help message for the checkbox which is labeled {{msg-mw|config-instantcommons}}.',
+ 'config-cc-not-chosen' => '{{doc-important|Do not translate the "<code>proceed</code>" part.}}
+This message refers to a block of HTML being embedded into the installer page. It comes from the Creative Commons Web site. The block is in the English language. It is a scripted license chooser. When an individual license has been selected, it asks you to click "proceed" so as to return to the MediaWiki installer page.',
+ 'config-memcached-servers' => '{{doc-important|Do not translate "memcached".}}
+{{Identical|Memcached server}}',
+ 'config-memcache-badip' => 'Used as error message. Parameters:
+* $1 - IP address for Memcached
+See also:
+* {{msg-mw|Config-memcache-noport}}
+* {{msg-mw|Config-memcache-badport}}',
+ 'config-memcache-noport' => 'Used as error message. Parameters:
+* $1 - Memcached server name
+See also:
+* {{msg-mw|Config-memcache-badip}}
+* {{msg-mw|Config-memcache-badport}}',
+ 'config-memcache-badport' => 'Used as error message. Parameters:
+* $1 - 1 (hard-coded)
+* $2 - 65535 (hard-coded)
+See also:
+* {{msg-mw|Config-memcache-badip}}
+* {{msg-mw|Config-memcache-noport}}',
'config-extensions' => '{{Identical|Extension}}',
+ 'config-extensions-help' => '{{doc-important|Do not translate <code>./extensions</code>.}}
+Used in help box.',
'config-install-step-done' => '{{Identical|Done}}',
+ 'config-install-step-failed' => '{{Identical|Failed}}',
'config-install-database' => '*{{msg-mw|Config-install-database}}
*{{msg-mw|Config-install-tables}}
*{{msg-mw|Config-install-schema}}
@@ -690,6 +882,8 @@ This message refers to a block of HTML being embedded into the installer page. I
'config-install-pg-schema-failed' => 'Parameters:
* $1 = database user name (usernames in the database are unrelated to wiki user names)
* $2 =',
+ 'config-pg-no-plpgsql' => 'Used as error message. Parameters:
+* $1 - database name',
'config-install-user' => 'Message indicates that the user is being created
See also:
@@ -702,9 +896,22 @@ See also:
*{{msg-mw|Config-install-keys}}
*{{msg-mw|Config-install-sysop}}
*{{msg-mw|Config-install-mainpage}}',
+ 'config-install-user-alreadyexists' => 'Used as warning. Parameters:
+* $1 - database username',
+ 'config-install-user-create-failed' => 'Used as MySQL warning and as PostgreSQL error. Parameters:
+* $1 - database username
+* $2 - detailed warning/error message',
'config-install-user-grant-failed' => 'Parameters:
* $1 is the database username for which granting rights failed
* $2 is the error message',
+ 'config-install-user-missing' => 'Used as PostgreSQL error message. Parameters:
+* $1 - database username
+See also:
+* {{msg-mw|Config-install-user-missing-create}}',
+ 'config-install-user-missing-create' => 'Used as PostgreSQL error message. Parameters:
+* $1 - database username
+See also:
+* {{msg-mw|Config-install-user-missing}}',
'config-install-tables' => 'Message indicates that the tables are being created
See also:
@@ -717,6 +924,8 @@ See also:
*{{msg-mw|Config-install-keys}}
*{{msg-mw|Config-install-sysop}}
*{{msg-mw|Config-install-mainpage}}',
+ 'config-install-tables-failed' => 'Used as PostgreSQL error message. Parameters:
+* $1 - detailed error message',
'config-install-interwiki' => 'Message indicates that the interwikitables are being populated
See also:
@@ -729,6 +938,8 @@ See also:
*{{msg-mw|Config-install-keys}}
*{{msg-mw|Config-install-sysop}}
*{{msg-mw|Config-install-mainpage}}',
+ 'config-install-interwiki-list' => '{{doc-important|Do not translate <code>interwiki.list</code>.}}
+Used as error message.',
'config-install-stats' => '*{{msg-mw|Config-install-database}}
*{{msg-mw|Config-install-tables}}
*{{msg-mw|Config-install-schema}}
@@ -772,6 +983,8 @@ See also:
*{{msg-mw|Config-install-keys}}
*{{msg-mw|Config-install-sysop}}
*{{msg-mw|Config-install-mainpage}}',
+ 'config-install-mainpage-failed' => 'Used as error message. Parameters:
+* $1 - detailed error message',
'config-install-done' => 'Parameters:
* $1 is the URL to LocalSettings download
* $2 is a link to the wiki.
@@ -779,8 +992,11 @@ See also:
'config-download-localsettings' => 'The link text used in the download link in config-install-done.',
'config-help' => 'This is used in help boxes.
{{Identical|Help}}',
- 'mainpagetext' => 'Along with {{msg|mainpagedocfooter}}, the text you will see on the Main Page when your wiki is installed.',
- 'mainpagedocfooter' => 'Along with {{msg|mainpagetext}}, the text you will see on the Main Page when your wiki is installed.
+ 'config-nofile' => 'Used as failure message. Parameters:
+* $1 - filename',
+ 'config-extension-link' => 'Shown on last page of installation to inform about possible extensions.',
+ 'mainpagetext' => 'Along with {{msg-mw|mainpagedocfooter}}, the text you will see on the Main Page when your wiki is installed.',
+ 'mainpagedocfooter' => 'Along with {{msg-mw|mainpagetext}}, the text you will see on the Main Page when your wiki is installed.
This might be a good place to put information about <nowiki>{{GRAMMAR:}}</nowiki>. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/fi]] for an example. For languages having grammatical distinctions and not having an appropriate <nowiki>{{GRAMMAR:}}</nowiki> software available, a suggestion to check and possibly amend the messages having <nowiki>{{SITENAME}}</nowiki> may be valuable. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/ksh]] for an example.',
);
@@ -861,12 +1077,10 @@ U gebruik tans $2.',
'config-sqlite-dir' => 'Gids vir SQLite se data:',
'config-oracle-def-ts' => 'Standaard tabelruimte:',
'config-oracle-temp-ts' => 'Tydelike tabelruimte:',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-header-mysql' => 'MySQL-instellings',
'config-header-postgres' => 'PostgreSQL-instellings',
'config-header-sqlite' => 'SQLite-instellings',
'config-header-oracle' => 'Oracle-instellings',
- 'config-header-ibm_db2' => 'Instellings vir IBM DB2',
'config-invalid-db-type' => 'Ongeldige databasistipe',
'config-missing-db-name' => 'U moet \'n waarde vir "Databasnaam" verskaf',
'config-sqlite-readonly' => 'Die lêer <code>$1</code> kan nie geskryf word nie.',
@@ -875,7 +1089,7 @@ U gebruik tans $2.',
U kan nou [$1 u wiki gebruik].',
'config-regenerate' => 'Herskep LocalSettings.php →',
- 'config-show-table-status' => 'Die uitvoer van SHOW TABLE STATUS het gefaal!',
+ 'config-show-table-status' => 'Die uitvoer van <code>SHOW TABLE STATUS</code> het gefaal!',
'config-db-web-account' => 'Databasisgebruiker vir toegang tot die web',
'config-mysql-engine' => 'Stoor-enjin:',
'config-mysql-innodb' => 'InnoDB',
@@ -900,7 +1114,7 @@ U kan nou [$1 u wiki gebruik].',
'config-admin-email' => 'E-posadres:',
'config-optional-continue' => 'Vra my meer vrae.',
'config-optional-skip' => 'Ek is reeds verveeld, installeer maar net die wiki.',
- 'config-profile-wiki' => 'Tradisionele wiki',
+ 'config-profile-wiki' => 'Tradisionele wiki', # Fuzzy
'config-profile-no-anon' => 'Skep van gebruiker is verpligtend',
'config-profile-fishbowl' => 'Slegs vir gemagtigde redaksie',
'config-profile-private' => 'Privaat wiki',
@@ -957,7 +1171,7 @@ U sal dit moet [$1 aflaai] en dit in die hoofgids van u wiki-installasie plaas;
'''Let wel''': As u dit nie nou doen nie, sal die gegenereerde konfigurasielêer nie later meer beskikbaar wees nadat u die installasie afgesluit het nie.
As dit gedoen is, kan u '''[u $2 wiki besoek]'''.", # Fuzzy
- 'config-download-localsettings' => 'Laai LocalSettings.php af',
+ 'config-download-localsettings' => 'Laai <code>LocalSettings.php</code> af',
'config-help' => 'hulp',
'mainpagetext' => "'''MediaWiki is suksesvol geïnstalleer.'''",
'mainpagedocfooter' => "Konsulteer '''[//meta.wikimedia.org/wiki/Help:Contents User's Guide]''' vir inligting oor hoe om die wikisagteware te gebruik.
@@ -965,7 +1179,7 @@ As dit gedoen is, kan u '''[u $2 wiki besoek]'''.", # Fuzzy
== Hoe om te Begin ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Gheg Albanian (Gegë)
@@ -980,7 +1194,7 @@ $messages['aln'] = array(
* [//www.mediawiki.org/wiki/Help:Configuration_settings Konfigurimi i MediaWikit]
* [//www.mediawiki.org/wiki/Help:FAQ Pyetjet e shpeshta rreth MediaWikit]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Njoftime rreth MediaWikit]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Njoftime rreth MediaWikit]', # Fuzzy
);
/** Amharic (አማርኛ)
@@ -993,7 +1207,7 @@ $messages['am'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Aragonese (aragonés)
@@ -1007,30 +1221,31 @@ $messages['an'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de caracteristicas confegurables]
* [//www.mediawiki.org/wiki/Manual:FAQ Preguntas cutianas sobre MediaWiki (FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correu sobre ta anuncios de MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correu sobre ta anuncios de MediaWiki]", # Fuzzy
);
/** Old English (Ænglisc)
* @author Gott wisst
*/
$messages['ang'] = array(
- 'mainpagetext' => "'''MediaǷiki hafaþ ȝeƿorden spēdiȝe inseted.'''",
+ 'mainpagetext' => "'''MediaWiki hafaþ geworden spēdige inseted.'''",
'mainpagedocfooter' => 'Þeahta þone [//meta.wikimedia.org/wiki/Help:Contents Brūcenda Lǣdend] on helpe mid þǣre nytte of ƿikisōftƿare.
== Beȝinnunȝ ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Onfæstnunȝa ȝesetednessa ȝetæl]
* [//www.mediawiki.org/wiki/Manual:FAQ Ȝetæl oft ascodra ascunȝa ymb MediaǷiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Ǣrendunȝȝetæl nīƿra MediaǷiki forþsendnessa]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Ǣrendunȝȝetæl nīƿra MediaǷiki forþsendnessa]', # Fuzzy
);
/** Arabic (العربية)
* @author Meno25
+ * @author Mido
* @author OsamaK
* @author روخو
*/
$messages['ar'] = array(
'config-desc' => 'مثبت لميدياويكي',
- 'config-title' => 'ميدياويكي 1$ التثبيت', # Fuzzy
+ 'config-title' => 'تثبيت ميدياويكي $1',
'config-information' => 'معلومات',
'config-back' => '→ ارجع',
'config-continue' => 'استمر ←',
@@ -1051,7 +1266,7 @@ $messages['ar'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings قائمة إعدادات الضبط]
* [//www.mediawiki.org/wiki/Manual:FAQ أسئلة متكررة حول ميدياويكي]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce القائمة البريدية الخاصة بإصدار ميدياويكي]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce القائمة البريدية الخاصة بإصدار ميدياويكي]', # Fuzzy
);
/** Aramaic (ܐܪܡܝܐ)
@@ -1085,7 +1300,7 @@ $messages['ary'] = array(
== L-bdaya mĝa MediaWiki ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista dyal l-paramétraṫ dyal l-konfigurasyon]
* [//www.mediawiki.org/wiki/Manual:FAQ/fr FAQ fe MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista dyal l-modakaraṫ ĝla versyonaṫ jdad dyal MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista dyal l-modakaraṫ ĝla versyonaṫ jdad dyal MediaWiki]', # Fuzzy
);
/** Egyptian Spoken Arabic (مصرى)
@@ -1098,10 +1313,10 @@ $messages['arz'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings لستة اعدادات الضبط]
* [//www.mediawiki.org/wiki/Manual:FAQ أسئلة بتكرر حوالين الميدياويكى]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce لستة الايميلات بتاعة اعلانات الميدياويكى]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce لستة الايميلات بتاعة اعلانات الميدياويكى]', # Fuzzy
);
-/** Assamese (অসমীয়া)
+/** Assamese (অসমীয়া)
* @author Chaipau
* @author Gitartha.bordoloi
*/
@@ -1112,20 +1327,67 @@ $messages['as'] = array(
== আৰম্ভণি কৰিবলৈ ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Asturian (asturianu)
+ * @author Xuacu
*/
$messages['ast'] = array(
+ 'config-desc' => "L'instalador pa MediaWiki",
+ 'config-title' => 'Instalación de MediaWiki $1',
+ 'config-information' => 'Información',
+ 'config-localsettings-upgrade' => "Detectose un ficheru <code>LocalSettings.php</code>.
+P'anovar esta instalación, escriba'l valor de
+<code>\$wgUpgradeKey</code> nel cuadru d'abaxo.
+Alcontraralu en <code>LocalSettings.php</code>.",
+ 'config-localsettings-cli-upgrade' => "Deteutose un ficheru <code>LocalSettings.php</code>.
+P'anovar esta instalación, execute <code>update.php</code>",
+ 'config-localsettings-key' => "Clave d'anovamientu:",
+ 'config-localsettings-badkey' => 'La clave que dio ye incorreuta.',
+ 'config-upgrade-key-missing' => "Deteutose una instalación esistente de MediaWiki.
+P'anovar esta instalación, ponga la llinia siguiente al final del ficheru <code>LocalSettings.php</code>:
+
+$1",
+ 'config-localsettings-incomplete' => 'Paez que\'l ficheru <code>LocalSettings.php</code> esistente ta incompletu.
+La variable $1 nun ta definida.
+Camude\'l ficheru <code>LocalSettings.php</code> pa qu\'esta variable quede definida y calque "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Alcontrose un error al conectar cola base de datos usando la configuración especificada en <code>LocalSettings.php</code> o <code>AdminSettings.php</code>. Corrixa esta configuración y vuelva a intentalo.
+
+$1',
+ 'config-your-language' => 'La so llingua:',
+ 'config-your-language-help' => "Seleicione la llingua a emplegar nel procesu d'instalación.",
+ 'config-wiki-language' => 'Llingua de la wiki:',
+ 'config-wiki-language-help' => "Seleicione la llingua que s'usará preferentemente na wiki.",
+ 'config-back' => '← Atrás',
+ 'config-continue' => 'Siguir →',
+ 'config-page-language' => 'Llingua',
+ 'config-page-welcome' => '¡Bienveníu a MediaWiki!',
+ 'config-page-dbconnect' => 'Conectar cola base de datos',
+ 'config-page-upgrade' => 'Anovar instalación esistente',
+ 'config-page-dbsettings' => 'Configuración de la base de datos',
+ 'config-page-name' => 'Nome',
+ 'config-page-options' => 'Opciones',
+ 'config-page-install' => 'Instalar',
+ 'config-page-complete' => '¡Completo!',
+ 'config-page-restart' => 'Reaniciar la instalación',
+ 'config-page-readme' => 'Llei-me',
+ 'config-page-releasenotes' => 'Notes de la versión',
+ 'config-page-copying' => 'Copiar',
+ 'config-page-upgradedoc' => 'Anovando',
+ 'config-page-existingwiki' => 'Wiki esistente',
+ 'config-download-localsettings' => 'Descargar <code>LocalSettings.php</code>',
+ 'config-help' => 'Ayuda',
+ 'config-nofile' => 'Nun pudo atopase\'l ficheru "$1". ¿Desaniciose?',
'mainpagetext' => "'''MediaWiki instalóse correchamente.'''",
- 'mainpagedocfooter' => "Visita la [//meta.wikimedia.org/wiki/Help:Contents Guía d'usuariu] pa saber cómo usar esti software wiki.
+ 'mainpagedocfooter' => 'Visita la [//meta.wikimedia.org/wiki/Help:Contents Guía del usuariu] pa saber cómo usar esti software wiki.
== Empecipiando ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Llista de les opciones de configuración]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ de MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de corréu de les ediciones de MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de corréu de les ediciones de MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Llocaliza MediaWiki na to llingua]',
);
/** Kotava (Kotava)
@@ -1151,7 +1413,6 @@ $messages['az'] = array(
'config-page-install' => 'Nizamlama',
'config-page-complete' => 'Komplektləşdir!',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-mysql-myisam' => 'MyISAM',
'config-mysql-utf8' => 'UTF-8',
'config-ns-generic' => 'Layihə',
@@ -1165,7 +1426,7 @@ $messages['az'] = array(
== Faydalı keçidlər ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Tənzimləmələrin siyahısı]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki haqqında tez-tez soruşulan suallar]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-poçt siyahısı]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-poçt siyahısı]', # Fuzzy
);
/** Bashkir (башҡортса)
@@ -1178,7 +1439,7 @@ $messages['ba'] = array(
== Файҙалы сығанаҡтар ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Көйләүҙәр исемлеге (инг.)];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki тураһында йыш бирелгән һорауҙар һәм яуаптар (инг.)];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-ның яңы версиялары тураһында хәбәрҙәр алып тороу].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-ның яңы версиялары тураһында хәбәрҙәр алып тороу].', # Fuzzy
);
/** Bavarian (Boarisch)
@@ -1192,7 +1453,7 @@ $messages['bar'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Listen voh de Konfigurazionsvariaablen]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglisten voh de neichen MediaWiki-Versionen]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglisten voh de neichen MediaWiki-Versionen]', # Fuzzy
);
/** Southern Balochi (بلوچی مکرانی)
@@ -1204,7 +1465,7 @@ $messages['bcc'] = array(
== شروع بیت ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Bikol Central (Bikol Central)
@@ -1217,7 +1478,7 @@ $messages['bcl'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Belarusian (беларуская)
@@ -1230,7 +1491,7 @@ $messages['be'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Пералік параметраў канфігурацыі (англ.)]
* [//www.mediawiki.org/wiki/Manual:FAQ ЧАПЫ MediaWiki (англ.)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Ліставанне аб выпусках MediaWiki (англ.)]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Ліставанне аб выпусках MediaWiki (англ.)]', # Fuzzy
);
/** Belarusian (Taraškievica orthography) (беларуская (тарашкевіца)‎)
@@ -1238,6 +1499,7 @@ $messages['be'] = array(
* @author Jim-by
* @author Wizardist
* @author Zedlik
+ * @author 아라
*/
$messages['be-tarask'] = array(
'config-desc' => 'Праграма ўсталяваньня MediaWiki',
@@ -1245,19 +1507,19 @@ $messages['be-tarask'] = array(
'config-information' => 'Інфармацыя',
'config-localsettings-upgrade' => 'Выяўлены файл <code>LocalSettings.php</code>.
Каб абнавіць гэтае усталяваньне, калі ласка, увядзіце значэньне <code>$wgUpgradeKey</code> у полі ніжэй.
-Яго можна знайсьці ў LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Быў знойдзены файл LocalSettings.php.
-Каб зьмяніць гэтае ўсталяваньне, калі ласка, запусьціце update.php',
+Яго можна знайсьці ў <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Быў знойдзены файл <code>LocalSettings.php</code>.
+Каб зьмяніць гэтае ўсталяваньне, калі ласка, запусьціце <code>update.php</code>',
'config-localsettings-key' => 'Ключ паляпшэньня:',
'config-localsettings-badkey' => 'Пададзены Вамі ключ зьяўляецца няслушным',
'config-upgrade-key-missing' => 'Выяўленае існуючае ўсталяваньне MediaWiki.
-Каб абнавіць гэтае ўсталяваньне, калі ласка, устаўце наступны радок у канец Вашага LocalSettings.php:
+Каб абнавіць гэтае ўсталяваньне, калі ласка, устаўце наступны радок у канец Вашага <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Выглядае, што існуючы LocalSettings.php зьяўляецца няпоўным.
+ 'config-localsettings-incomplete' => 'Выглядае, што існуючы <code>LocalSettings.php</code> зьяўляецца няпоўным.
Не ўстаноўленая пераменная $1.
-Калі ласка, зьмяніце LocalSettings.php так, каб была ўстаноўленая гэтая пераменная, і націсьніце «Працягваць».',
- 'config-localsettings-connection-error' => 'Адбылася памылка падчас злучэньня з базай зьвестак з выкарыстаньнем наладаў, пазначаных у LocalSettings.php ці AdminSettings.php. Калі ласка, выпраўце гэтыя налады і паспрабуйце зноў.
+Калі ласка, зьмяніце <code>LocalSettings.php</code> так, каб была ўстаноўленая гэтая пераменная, і націсьніце «{{int:Config-continue}}».',
+ 'config-localsettings-connection-error' => 'Адбылася памылка падчас злучэньня з базай зьвестак з выкарыстаньнем наладаў, пазначаных у <code>LocalSettings.php</code> ці <code>AdminSettings.php</code>. Калі ласка, выпраўце гэтыя налады і паспрабуйце зноў.
$1',
'config-session-error' => 'Памылка стварэньня сэсіі: $1',
@@ -1370,6 +1632,8 @@ MediaWiki патрабуе падтрымкі UTF-8 для слушнай пра
'config-mod-security' => "'''Папярэджаньне''': на Вашым ўэб-сэрверы ўключаны [http://modsecurity.org/ mod_security]. У выпадку няслушнай наладцы, ён можа стаць прычынай праблемаў для MediaWiki ці іншага праграмнага забесьпячэньня, якое дазваляе ўдзельнікам дасылаць на сэрвэр любы зьмест.
Глядзіце [http://modsecurity.org/documentation/ дакумэнтацыю mod_security] ці зьвярніцеся ў падтрымку Вашага хосту, калі ў Вас узьнікаюць выпадковыя праблемы.",
'config-diff3-bad' => 'GNU diff3 ня знойдзены.',
+ 'config-git' => 'Знойдзеная сыстэма канстролю вэрсіяў Git: <code>$1</code>',
+ 'config-git-bad' => 'Сыстэма кантролю вэрсіяў Git ня знойдзеная.',
'config-imagemagick' => 'Знойдзены ImageMagick: <code>$1</code>.
Пасьля ўключэньня загрузак будзе ўключанае маштабаваньне выяваў.',
'config-gd' => 'GD падтрымліваецца ўбудавана.
@@ -1391,7 +1655,9 @@ MediaWiki патрабуе падтрымкі UTF-8 для слушнай пра
'config-using531' => 'PHP $1 не сумяшчальнае з MediaWiki з-за памылкі ў перадачы парамэтраў па ўказальніку да <code>__call()</code>.
Абнавіце PHP да вэрсіі 5.3.2 ці болей позьняй, ці адкаціце да вэрсіі 5.3.0 каб гэта выправіць.
Усталяваньне перарванае.',
- 'config-suhosin-max-value-length' => 'Suhosin усталяваны і абмяжоўвае даўжыню парамэтра GET у $1 {{PLURAL:$1|байт|байты|байтаў}}. ResourceLoader для MediaWiki будзе абходзіць гэтае абмежаваньне, што, аднак, адаб’ецца на хуткадзеяньні. Калі магчыма, варта ўстанавіць suhosin.get.max_value_length роўным 1024 ці больш у php.ini, а таксама ўстанавіць тое ж значэньне для $wgResourceLoaderMaxQueryLength у LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Suhosin усталяваны і абмяжоўвае <code>даўжыню</code> парамэтра GET да $1 {{PLURAL:$1|байта|байтаў}}.
+ResourceLoader, складнік MediaWiki, будзе абходзіць гэтае абмежаваньне, што, адаб’ецца на прадукцыйнасьці.
+Калі магчыма, варта ўсталяваць у <code>php.ini</code> <code>suhosin.get.max_value_length</code> роўным 1024 ці больш, а таксама вызначыць тое ж значэньне для <code>$wgResourceLoaderMaxQueryLength</code> у LocalSettings.php.',
'config-db-type' => 'Тып базы зьвестак:',
'config-db-host' => 'Хост базы зьвестак:',
'config-db-host-help' => 'Калі сэрвэр Вашай базы зьвестак знаходзіцца на іншым сэрвэры, увядзіце тут імя хоста ці IP-адрас.
@@ -1465,7 +1731,6 @@ MediaWiki патрабуе падтрымкі UTF-8 для слушнай пра
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki падтрымлівае наступныя сыстэмы базаў зьвестак:
$1
@@ -1475,18 +1740,16 @@ $1
'config-support-postgres' => '* $1 — вядомая сыстэма базы зьвестак з адкрытым кодам, якая зьяўляецца альтэрнатывай MySQL ([http://www.php.net/manual/en/pgsql.installation.php як кампіляваць PHP з падтрымкай PostgreSQL]). Яна можа ўтрымліваць дробныя памылкі, і не рэкамэндуецца выкарыстоўваць яе для працуючых праектаў.',
'config-support-sqlite' => '* $1 — невялікая сыстэма базы зьвестак, якая мае вельмі добрую падтрымку. ([http://www.php.net/manual/en/pdo.installation.php як кампіляваць PHP з падтрымкай SQLite], выкарыстоўвае PDO)',
'config-support-oracle' => '* $1 зьяўляецца камэрцыйнай прафэсійнай базай зьвестак. ([http://www.php.net/manual/en/oci8.installation.php Як скампіляваць PHP з падтрымкай OCI8])',
- 'config-support-ibm_db2' => '* $1 — база зьвестак камэрцыйнага прадпрыемства.',
'config-header-mysql' => 'Налады MySQL',
'config-header-postgres' => 'Налады PostgreSQL',
'config-header-sqlite' => 'Налады SQLite',
'config-header-oracle' => 'Налады Oracle',
- 'config-header-ibm_db2' => 'Налады IBM DB2',
'config-invalid-db-type' => 'Няслушны тып базы зьвестак',
'config-missing-db-name' => 'Вы павінны ўвесьці значэньне парамэтру «Імя базы зьвестак»',
'config-missing-db-host' => 'Вы павінны ўвесьці значэньне парамэтру «Хост базы зьвестак»',
'config-missing-db-server-oracle' => 'Вы павінны ўвесьці значэньне парамэтру «TNS базы зьвестак»',
'config-invalid-db-server-oracle' => 'Няслушнае TNS базы зьвестак «$1».
-Назва можа ўтрымліваць толькі ASCII-літары (a-z, A-Z), лічбы (0-9), сымбалі падкрэсьліваньня(_) і кропкі (.).',
+Назва можа ўтрымліваць толькі ASCII-літары (a-z, A-Z), лічбы (0-9), сымбалі падкрэсьліваньня(_) і кропкі (.).', # Fuzzy
'config-invalid-db-name' => 'Няслушная назва базы зьвестак «$1».
Назва можа ўтрымліваць толькі ASCII-літары (a-z, A-Z), лічбы (0-9), сымбалі падкрэсьліваньня(_) і працяжнікі (-).',
'config-invalid-db-prefix' => 'Няслушны прэфікс базы зьвестак «$1».
@@ -1543,7 +1806,7 @@ chmod a+w $3</pre>',
Цяпер Вы можаце [$1 пачаць працу з вікі].',
'config-regenerate' => 'Рэгенэраваць LocalSettings.php →',
- 'config-show-table-status' => "Запыт 'SHOW TABLE STATUS' не атрымаўся!",
+ 'config-show-table-status' => "Запыт '<code>SHOW TABLE STATUS</code>' не атрымаўся!",
'config-unknown-collation' => "'''Папярэджаньне:''' база зьвестак выкарыстоўвае нераспазнанае супастаўленьне.",
'config-db-web-account' => 'Рахунак базы зьвестак для вэб-доступу',
'config-db-web-help' => 'Выберыце імя карыстальніка і пароль, які выкарыстоўваецца вэб-сэрвэрам для злучэньня з сэрвэрам базы зьвестак, падчас звычайных апэрацыяў вікі.',
@@ -1572,7 +1835,6 @@ chmod a+w $3</pre>',
Гэта болей эфэктыўна за рэжым MySQL UTF-8, і дазваляе Вам выкарыстоўваць увесь дыяпазон сымбаляў Unicode.
У '''рэжыме UTF-8''', MySQL ведае, якая табліцы сымбаляў выкарыстоўваецца ў Вашых зьвестках, і можа адпаведна прадстаўляць і канвэртаваць іх, але гэта не дазволіць Вам захоўваць сымбалі па-за межамі [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Базавага шматмоўнага дыяпазону].",
- 'config-ibm_db2-low-db-pagesize' => "Вашая база зьвестак DB2 мае таблічную прасторну зь недастатковым памерам старонкі. Памер старонкі мусіць быць ня менш за '''32к'''.",
'config-site-name' => 'Назва вікі:',
'config-site-name-help' => 'Назва будзе паказвацца ў загалоўку браўзэра і ў некаторых іншых месцах.',
'config-site-name-blank' => 'Увядзіце назву сайта.',
@@ -1615,7 +1877,7 @@ chmod a+w $3</pre>',
'config-optional-continue' => 'Задаць болей пытаньняў.',
'config-optional-skip' => 'Хопіць, проста ўсталяваць вікі.',
'config-profile' => 'Профіль правоў удзельніка:',
- 'config-profile-wiki' => 'Традыцыйная вікі',
+ 'config-profile-wiki' => 'Адкрытая вікі',
'config-profile-no-anon' => 'Патрэбнае стварэньне рахунку',
'config-profile-fishbowl' => 'Толькі для аўтарызаваных рэдактараў',
'config-profile-private' => 'Прыватная вікі',
@@ -1631,7 +1893,7 @@ chmod a+w $3</pre>',
Сцэнар '''{{int:config-profile-fishbowl}}''' дазваляе рэдагаваць зацьверджаным удзельнікам, але ўсе могуць праглядаць старонкі іх гісторыю.
'''{{int:config-profile-private}}''' дазваляе праглядаць і рэдагаваць старонкі толькі зацьверджаным удзельнікам.
-Больш складаныя правы ўдзельнікаў даступныя пасьля ўсталяваньня, глядзіце [//www.mediawiki.org/wiki/Manual:User_rights адпаведную старонку дакумэнтацыі].",
+Больш складаныя правы ўдзельнікаў даступныя пасьля ўсталяваньня, глядзіце [//www.mediawiki.org/wiki/Manual:User_rights адпаведную старонку дакумэнтацыі].", # Fuzzy
'config-license' => 'Аўтарскія правы і ліцэнзія:',
'config-license-none' => 'Без інфармацыі пра ліцэнзію',
'config-license-cc-by-sa' => 'Creative Commons Attribution Share Alike',
@@ -1682,7 +1944,7 @@ chmod a+w $3</pre>',
'config-logo-help' => 'Афармленьне MediaWiki па змоўчваньні уключае прастору для лягатыпу памерам 135×160 піксэляў у верхнім левым куце.
Загрузіце выяву адпаведнага памеру і увядзіце тут URL-адрас.
-Калі Вы не жадаеце мець ніякага лягатыпу, пакіньце поле пустым.',
+Калі Вы не жадаеце мець ніякага лягатыпу, пакіньце поле пустым.', # Fuzzy
'config-instantcommons' => 'Дазволіць Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] — магчымасьць, якая дазваляе вікі выкарыстоўваць выявы, гукі і іншыя мэдыя, якія знаходзяцца на сайце [//commons.wikimedia.org/ Wikimedia Commons].
Каб гэта зрабіць, MediaWiki патрабуе доступу да Інтэрнэту.
@@ -1716,7 +1978,7 @@ chmod a+w $3</pre>',
'config-install-alreadydone' => "'''Папярэджаньне:''' здаецца, што Вы ўжо ўсталёўвалі MediaWiki і спрабуеце зрабіць гэтай зноў.
Калі ласка, перайдзіце на наступную старонку.",
'config-install-begin' => 'Пасьля націску кнопкі «{{int:config-continue}}» пачнецца ўсталяваньне MediaWiki.
-Калі Вы жадаеце што-небудзь зьмяніць, націсьніце кнопку «Вярнуцца».',
+Калі Вы жадаеце што-небудзь зьмяніць, націсьніце кнопку «Вярнуцца».', # Fuzzy
'config-install-step-done' => 'зроблена',
'config-install-step-failed' => 'не атрымалася',
'config-install-extensions' => 'Уключаючы пашырэньні',
@@ -1772,7 +2034,7 @@ $3
'''Заўвага''': калі Вы гэтага ня зробіце зараз, то створаны файл ня будзе даступны Вам потым, калі Вы выйдзеце з праграмы ўсталяваньня без яго загрузкі.
Калі Вы гэта зробіце, Вы можаце '''[$2 ўвайсьці ў Вашую вікі]'''.",
- 'config-download-localsettings' => 'Загрузіць LocalSettings.php',
+ 'config-download-localsettings' => 'Загрузіць <code>LocalSettings.php</code>',
'config-help' => 'дапамога',
'config-nofile' => 'Файл «$1» ня знойдзены. Ці быў ён выдалены?',
'mainpagetext' => "'''MediaWiki пасьпяхова ўсталяваная.'''",
@@ -1781,11 +2043,12 @@ $3
== З чаго пачаць ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Сьпіс парамэтраў канфігурацыі]
* [//www.mediawiki.org/wiki/Manual:FAQ Частыя пытаньні MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Рассылка паведамленьняў пра зьяўленьне новых вэрсіяў MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Рассылка паведамленьняў пра зьяўленьне новых вэрсіяў MediaWiki]', # Fuzzy
);
/** Bulgarian (български)
* @author DCLXVI
+ * @author 아라
*/
$messages['bg'] = array(
'config-desc' => 'Инсталатор на МедияУики',
@@ -1793,19 +2056,19 @@ $messages['bg'] = array(
'config-information' => 'Информация',
'config-localsettings-upgrade' => 'Беше открит файл <code>LocalSettings.php</code>.
За надграждане на съществуващата инсталация, необходимо е в кутията по-долу да се въведе стойността на <code>$wgUpgradeKey</code>.
-Тази информация е налична в LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Беше открит файл LocalSettings.php.
-За надграждане на наличната инсталация, необходимо е да се стартира update.php',
+Тази информация е налична в <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Беше открит файл <code>LocalSettings.php</code>.
+За надграждане на наличната инсталация, необходимо е да се стартира <code>update.php</code>',
'config-localsettings-key' => 'Ключ за надграждане:',
'config-localsettings-badkey' => 'Предоставеният ключ е неправилен.',
'config-upgrade-key-missing' => 'Беше открита съществуваща инсталация на МедияУики.
-За надграждане на съществуващата инсталация, необходимо е да се постави следният ред в края на файла LocalSettings.php:
+За надграждане на съществуващата инсталация, необходимо е да се постави следният ред в края на файла <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Съществуващият файл LocalSettings.php изглежда непълен.
+ 'config-localsettings-incomplete' => 'Съществуващият файл <code>LocalSettings.php</code> изглежда непълен.
Променливата $1 не е зададена.
-Необходимо е да се редактира файлът LocalSettings.php и да се зададе променливата, след което да се натисне "Продължаване".',
- 'config-localsettings-connection-error' => 'Възникна грешка при свързване с базата от данни чрез данните, посочени в LocalSettings.php или AdminSettings.php. Необходимо е да се коригират тези настройки преди повторен опит за свързване.
+Необходимо е да се редактира файлът <code>LocalSettings.php</code> и да се зададе променливата, след което да се натисне „{{int:Config-continue}}“.',
+ 'config-localsettings-connection-error' => 'Възникна грешка при свързване с базата от данни чрез данните, посочени в <code>LocalSettings.php</code> или <code>AdminSettings.php</code>. Необходимо е да се коригират тези настройки преди повторен опит за свързване.
$1',
'config-session-error' => 'Грешка при създаване на сесия: $1',
@@ -1933,7 +2196,7 @@ $1
'config-using531' => 'МедияУики не може да се използва с PHP $1 заради проблем с референтните параметри за <code>__call()</code>.
За разрешаване на този проблем е необходимо да се обнови до PHP 5.3.2 или по-нова версия или да се инсталира по-стара версия, напр. PHP 5.3.0.
Инсталацията беше прекратена.',
- 'config-suhosin-max-value-length' => 'Suhosin е инсталиран и ограничава дължината на параметъра GET на $1 байта. Компонентът на МедияУики ResourceLoader ще може да пренебрегне частично това ограничение, но това ще намали производителността. По възможност е препоръчително да се настрои suhosin.get.max_value_length на 1024 или по-голяма стойност в php.ini и в LocalSettings.php да се настрои $wgResourceLoaderMaxQueryLength със същата стойност.',
+ 'config-suhosin-max-value-length' => 'Suhosin е инсталиран и ограничава дължината на параметъра GET на $1 байта. Компонентът на МедияУики ResourceLoader ще може да пренебрегне частично това ограничение, но това ще намали производителността. По възможност е препоръчително да се настрои <code>suhosin.get.max_value_length</code> на 1024 или по-голяма стойност в <code>php.ini</code> и в LocalSettings.php да се настрои <code>$wgResourceLoaderMaxQueryLength</code> със същата стойност.', # Fuzzy
'config-db-type' => 'Тип на базата от данни:',
'config-db-host' => 'Хост на базата от данни:',
'config-db-host-help' => 'Ако базата от данни е на друг сървър, в кутията се въвежда името на хоста или IP адреса.
@@ -1990,7 +2253,6 @@ $1
Това включва сурови данни за потребителите (адреси за е-поща, хеширани пароли), както и изтрити версии на страници и друга чувствителна и с ограничен достъп информация от и за уикито.
Базата от данни е препоръчително да се разположи на друго място, например в <code>/var/lib/mediawiki/yourwiki</code>.",
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'МедияУики поддържа следните системи за бази от данни:
$1
@@ -2000,12 +2262,10 @@ $1
'config-support-postgres' => '* $1 е популярна система за бази от данни с отворен изходен код, която е алтернатива на MySQL ([http://www.php.net/manual/en/pgsql.installation.php как се компилира PHP с поддръжка на PostgreSQL]). Възможно е все още да има грешки, затова не се препоръчва да се използва в общодостъпна среда.',
'config-support-sqlite' => '* $1 е лека система за база от данни, която е много добре поддържана. ([http://www.php.net/manual/en/pdo.installation.php Как се компилира PHP с поддръжка на SQLite], използва PDO)',
'config-support-oracle' => '* $1 е комерсиална корпоративна база от данни. ([http://www.php.net/manual/en/oci8.installation.php Как се компилира PHP с поддръжка на OCI8])',
- 'config-support-ibm_db2' => '* $1 е комерсиална фирмена база от данни.',
'config-header-mysql' => 'Настройки за MySQL',
'config-header-postgres' => 'Настройки за PostgreSQL',
'config-header-sqlite' => 'Настройки за SQLite',
'config-header-oracle' => 'Настройки за Oracle',
- 'config-header-ibm_db2' => 'Настройки за IBM DB2',
'config-invalid-db-type' => 'Невалиден тип база от данни',
'config-missing-db-name' => 'Необходимо е да се въведе стойност за "Име на базата от данни"',
'config-missing-db-host' => 'Необходимо е да се въведе стойност за "Хост на базата от данни"',
@@ -2068,7 +2328,7 @@ chmod a+w $3</pre>',
Вече е възможно [$1 да използвате уикито].',
'config-regenerate' => 'Създаване на LocalSettings.php →',
- 'config-show-table-status' => 'Заявката SHOW TABLE STATUS не сполучи!',
+ 'config-show-table-status' => 'Заявката <code>SHOW TABLE STATUS</code> не сполучи!',
'config-unknown-collation' => "'''Предупреждение:''' Базата от данни използва неразпозната колация.",
'config-db-web-account' => 'Сметка за уеб достъп до базата от данни',
'config-db-web-help' => 'Избиране на потребителско име и парола, които уеб сървърът ще използва да се свързва с базата от данни при обичайната работа на уикито.',
@@ -2139,7 +2399,7 @@ chmod a+w $3</pre>',
'config-optional-continue' => 'Задаване на допълнителни въпроси.',
'config-optional-skip' => 'Достатъчно, инсталиране на уикито.',
'config-profile' => 'Профил на потребителските права:',
- 'config-profile-wiki' => 'Традиционно уики',
+ 'config-profile-wiki' => 'Отворено уики',
'config-profile-no-anon' => 'Необходимо е създаване на сметка',
'config-profile-fishbowl' => 'Само одобрени редактори',
'config-profile-private' => 'Затворено уики',
@@ -2155,7 +2415,7 @@ chmod a+w $3</pre>',
Уики, което е '''{{int:config-profile-fishbowl}}''' позволява на всички да преглеждат страниците, но само предварително одобрени редактори могат да редактират съдържанието.
В '''{{int:config-profile-private}}''' само предварително одобрени потребители могат да четат и редактират съдържанието.
-Детайлно обяснение на конфигурациите на потребителските права е достъпно след инсталацията в [//www.mediawiki.org/wiki/Manual:User_rights Наръчника за потребителски права].",
+Детайлно обяснение на конфигурациите на потребителските права е достъпно след инсталацията в [//www.mediawiki.org/wiki/Manual:User_rights Наръчника за потребителски права].", # Fuzzy
'config-license' => 'Авторски права и лиценз:',
'config-license-none' => 'Без лиценз',
'config-license-cc-by-sa' => 'Криейтив Комънс Признание-Споделяне на споделеното',
@@ -2205,7 +2465,7 @@ chmod a+w $3</pre>',
'config-logo-help' => 'Обликът по подразбиране на МедияУики вклчва място с размери 135х160 пиксела за лого над страничното меню.
Ако има наличен файл с подходящ размер, неговият адрес може да бъде посочен тук.
-Ако не е необходимо лого, полето може да се остави празно.',
+Ако не е необходимо лого, полето може да се остави празно.', # Fuzzy
'config-instantcommons' => 'Включване на Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] е функционалност, която позволява на уикитата да използват картинки, звуци и друга медиа от сайта на Уикимедия [//commons.wikimedia.org/ Общомедия].
За да е възможно това, МедияУики изисква достъп до Интернет.
@@ -2238,8 +2498,8 @@ chmod a+w $3</pre>',
Възможно е те да изискват допълнително конфигуриране, но сега могат да бъдат включени.',
'config-install-alreadydone' => "'''Предупреждение:''' Изглежда вече сте инсталирали МедияУики и се опитвате да го инсталирате отново.
Продължете към следващата страница.",
- 'config-install-begin' => 'Инсталацията на МедияУики ще започне след натискане на бутона "{{int:config-continue}}".
-Ако желаете да направите промени, натиснете Връщане.',
+ 'config-install-begin' => 'Инсталацията на МедияУики ще започне след натискане на бутона „{{int:config-continue}}“.
+В случай, че е необходимо да се направят промени, използва се бутона „{{int:config-back}}“.',
'config-install-step-done' => 'готово',
'config-install-step-failed' => 'неуспешно',
'config-install-extensions' => 'Добавяне на разширенията',
@@ -2294,15 +2554,17 @@ $3
'''Забележка''': Ако това не бъде извършено сега, генерираният конфигурационен файл няма да е достъпен на по-късен етап ако не бъде изтеглен сега или инсталацията приключи без изтеглянето му.
Когато файлът вече е в основната директория, '''[$2 уикито ще е достъпно на този адрес]'''.",
- 'config-download-localsettings' => 'Изтегляне на LocalSettings.php',
+ 'config-download-localsettings' => 'Изтегляне на <code>LocalSettings.php</code>',
'config-help' => 'помощ',
+ 'config-nofile' => 'Файлът „$1“ не може да бъде открит. Да не е бил изтрит?',
'mainpagetext' => "'''Уикито беше успешно инсталирано.'''",
- 'mainpagedocfooter' => 'Разгледайте [//meta.wikimedia.org/wiki/Help:Contents ръководството] за подробна информация относно използването на софтуера.
+ 'mainpagedocfooter' => 'Разгледайте [//meta.wikimedia.org/wiki/Help:Contents ръководството] за подробна информация относно използването на уики софтуера.
== Първи стъпки ==
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings Конфигурационни настройки]
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Настройки за конфигуриране]
* [//www.mediawiki.org/wiki/Manual:FAQ ЧЗВ за МедияУики]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Пощенски списък относно нови версии на МедияУики]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Пощенски списък относно нови версии на МедияУики]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Локализиране на МедияУики]',
);
/** Banjar (Bahasa Banjar)
@@ -2316,7 +2578,7 @@ $messages['bjn'] = array(
== Gasan bamula ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Daptar konpigurasi setélan]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki nang rancak ditakunakan]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki rilis milis]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki rilis milis]', # Fuzzy
);
/** Bengali (বাংলা)
@@ -2362,12 +2624,10 @@ $messages['bn'] = array(
'config-sqlite-dir' => 'এসকিউলাইট ডেটা ডিরেক্টরি:',
'config-oracle-def-ts' => 'পূর্বনির্ধারিত টেবিলস্পেস',
'config-oracle-temp-ts' => 'সাময়কি টেবিলস্পেস:',
- 'config-type-ibm_db2' => 'আইবিএম ডিবি২',
'config-header-mysql' => 'মাইএসকিউএল সেটিংস',
'config-header-postgres' => 'পোস্টগ্রেএসকিউএল সেটিংস',
'config-header-sqlite' => 'এসকিউলাইট সেটিংস',
'config-header-oracle' => 'ওরাকল সেটিংস',
- 'config-header-ibm_db2' => 'আইবিএম ডিবি২ সেটিংস',
'config-invalid-db-type' => 'ডেটাবেজের ধরন অগ্রহযোগ্য',
'config-missing-db-name' => 'আপনাকে অবশ্যই "ডেটাবেজ নাম"-এর জন্য একটি মান প্রবেশ করাতে হবে',
'config-missing-db-host' => 'আপনাকে অবশ্যই "ডেটাবেজ হোস্ট"-এর জন্য একটি মান প্রবেশ করাতে হবে',
@@ -2397,7 +2657,7 @@ $messages['bn'] = array(
'config-optional-continue' => 'আরও প্রশ্ন জিজ্ঞেস করুন।',
'config-optional-skip' => 'আমি ইতিমধ্যেই বিরক্ত হয়ে গেছি, উইকিটি ইন্সটল করো।',
'config-profile' => 'ব্যবহারকারী অধিকার প্রোফাইল:',
- 'config-profile-wiki' => 'গতানুগতিক উইকি',
+ 'config-profile-wiki' => 'গতানুগতিক উইকি', # Fuzzy
'config-profile-no-anon' => 'অ্যাকাউন্ট তৈরি করা বাধ্যতামূলক',
'config-profile-fishbowl' => 'শুধুমাত্র নির্ধারিত সম্পাদকদের জন্যই',
'config-profile-private' => 'ব্যক্তিগত উইকি',
@@ -2431,7 +2691,7 @@ $messages['bn'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings কনফিগারেশন সেটিংস তালিকা]
* [//www.mediawiki.org/wiki/Manual:FAQ প্রশ্নোত্তরে মিডিয়াউইকি]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce মিডিয়াউইকি রিলিজের মেইলিং লিস্ট]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce মিডিয়াউইকি রিলিজের মেইলিং লিস্ট]', # Fuzzy
);
/** Bishnupria Manipuri (বিষ্ণুপ্রিয়া মণিপুরী)
@@ -2444,7 +2704,7 @@ $messages['bpy'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings কনফিগারেশন সেটিংর তালিকাহান]
* [//www.mediawiki.org/wiki/Manual:FAQ মিডিয়া উইকি আঙলাক]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce মিডিয়া উইকির ফঙপার বারে মেইলর তালিকাহান]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce মিডিয়া উইকির ফঙপার বারে মেইলর তালিকাহান]', # Fuzzy
);
/** Breton (brezhoneg)
@@ -2452,6 +2712,7 @@ $messages['bpy'] = array(
* @author Fulup
* @author Gwendal
* @author Y-M D
+ * @author 아라
*/
$messages['br'] = array(
'config-desc' => 'Poellad staliañ MediaWIki',
@@ -2459,19 +2720,19 @@ $messages['br'] = array(
'config-information' => 'Titouroù',
'config-localsettings-upgrade' => 'Kavet ez eus bet ur restr <code>LocalSettings.php</code>.
Evit hizivaat ar staliadur-se, merkit an talvoud <code>$wgUpgradeKey</code> er voest dindan.
-E gavout a rit e LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Dinoet ez eus bet ur restr LocalSettings.php.
-Evit lakaat ar staliadur-mañ a-live, implijit update.php e plas',
+E gavout a rit e <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Dinoet ez eus bet ur restr <code>LocalSettings.php</code>.
+Evit lakaat ar staliadur-mañ a-live, implijit <code>update.php</code> e plas',
'config-localsettings-key' => "Alc'hwez hizivaat :",
'config-localsettings-badkey' => "Direizh eo an alc'hwez merket ganeoc'h",
'config-upgrade-key-missing' => 'Kavet ez eus bet ur staliadur kent eus MediaWiki.
-Evit hizivaat ar staliadur-se, ouzhpennit al linenn da-heul e traoñ ho restr LocalSettings.php:
+Evit hizivaat ar staliadur-se, ouzhpennit al linenn da-heul e traoñ ho restr <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => "Diglok e seblant bezañ ar restr LocalSettings.php zo anezhi dija.
+ 'config-localsettings-incomplete' => "Diglok e seblant bezañ ar restr <code>LocalSettings.php</code> zo anezhi dija.
An argemmenn $1 n'eo ket termenet.
-Kemmit LocalSettings.php evit ma vo termenet an argemmenn-se, ha klikit war « Kenderc'hel ».",
- 'config-localsettings-connection-error' => "C'hoarvezet ez eus ur fazi en ur gevreañ ouzh an diaz roadennoù oc'h implijout an arventennoù diferet e LocalSettings.php pe AdminSettings.php. Reizhit an arventennoù-se hag esaeit en-dro.
+Kemmit <code>LocalSettings.php</code> evit ma vo termenet an argemmenn-se, ha klikit war « {{int:Config-continue}} ».",
+ 'config-localsettings-connection-error' => "C'hoarvezet ez eus ur fazi en ur gevreañ ouzh an diaz roadennoù oc'h implijout an arventennoù diferet e <code>LocalSettings.php</code> pe <code>AdminSettings.php</code>. Reizhit an arventennoù-se hag esaeit en-dro.
$1",
'config-session-error' => "Fazi e-ser loc'hañ an dalc'h : $1",
@@ -2640,7 +2901,6 @@ Arabat cheñch anezho ma n'hoc'h eus ket ezhomm d'en ober.",
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => "Skoret eo ar reizhiadoù diaz titouroù da-heul gant MediaWiki :
$1
@@ -2650,12 +2910,10 @@ Ma ne welit ket amañ dindan ar reizhiad diaz titouroù a fell deoc'h ober ganti
'config-support-postgres' => "* Ur reizhiad diaz titouroù brudet ha digor eo $1. Gallout a ra ober evit MySQL ([http://www.php.net/manual/en/pgsql.installation.php Penaos kempunañ PHP gant skor PostgreSQL]). Gallout a ra bezañ un nebeud drein bihan enni ha n'eo ket erbedet he implijout en un endro produiñ.",
'config-support-sqlite' => "* $1 zo ur reizhiad diaz titouroù skañv skoret eus ar c'hentañ. ([http://www.php.net/manual/en/pdo.installation.php Penaos kempunañ PHP gant skor SQLite], implijout a ra PDO)",
'config-support-oracle' => '* $1 zo un diaz titouroù kenwerzhel. ([http://www.php.net/manual/en/oci8.installation.php Penaos kempunañ PHP gant skor OCI8])',
- 'config-support-ibm_db2' => '* Un diaz titouroù evit embregerezhioù kenwerzhel eo $1.',
'config-header-mysql' => 'Arventennoù MySQL',
'config-header-postgres' => 'Arventennoù PostgreSQL',
'config-header-sqlite' => 'Arventennoù SQLite',
'config-header-oracle' => 'Arventennoù Oracle',
- 'config-header-ibm_db2' => 'Arventennoù IBM DB2',
'config-invalid-db-type' => 'Direizh eo ar seurt diaz roadennoù',
'config-missing-db-name' => 'Ret eo deoc\'h merkañ un dalvoudenn evit "Anv an diaz titouroù"',
'config-missing-db-host' => 'Ret eo deoc\'h merkañ un dalvoudenn evit "Ostiz an diaz titouroù"',
@@ -2693,7 +2951,7 @@ Da hizivaat anezho da VediaWiki $1, klikañ war '''Kenderc'hel'''.",
Gallout a rit [$1 kregiñ da implijout ho wiki].',
'config-regenerate' => 'Adgenel LocalSettings.php →',
- 'config-show-table-status' => "C'hwitet ar reked SHOW TABLE STATUS !",
+ 'config-show-table-status' => "C'hwitet ar reked <code>SHOW TABLE STATUS</code> !",
'config-unknown-collation' => "'''Diwallit :''' Emañ an diaz roadennoù o renkañ an traoù diouzh un urzh lizherennek dianav.",
'config-db-web-account' => 'Kont an diaz roadennoù evit ar voned Kenrouedad',
'config-db-web-help' => 'Diuzañ an anv implijer hag ar ger-tremen a vo implijet gant ar servijer web evit kevreañ ouzh ar servijer diaz roadennoù pa vez ar wiki o vont en-dro war ar pemdez.',
@@ -2740,7 +2998,7 @@ Gellout a rit tremen ar c'hefluniadur nevez ha staliañ ar wiki war-eeun.",
'config-optional-continue' => "Sevel muioc'h a goulennoù ouzhin.",
'config-optional-skip' => 'Aet on skuizh, staliañ ar wiki hepken.',
'config-profile' => 'Profil ar gwirioù implijer :',
- 'config-profile-wiki' => 'Wiki hengounel',
+ 'config-profile-wiki' => 'Wiki digor',
'config-profile-no-anon' => 'Krouidigezh ur gont ret',
'config-profile-fishbowl' => 'Embanner aotreet hepken',
'config-profile-private' => 'Wiki prevez',
@@ -2797,7 +3055,7 @@ Marteze e vo ezhomm kefluniañ pelloc'h met gallout a rit o gweredekaat bremañ.
'config-install-alreadydone' => "'''Diwallit''': Staliet hoc'h eus MediaWiki dija war a seblant hag emaoc'h o klask e staliañ c'hoazh.
Kit d'ar bajenn war-lerc'h, mar plij.",
'config-install-begin' => 'Pa vo bet pouezet ganeoc\'h war "{{int:config-continue}}" e krogo staliadur MediaWiki.
-Pouezit war Kent mar fell deoc\'h cheñch tra pe dra.',
+Pouezit war "{{int:config-back}}" mar fell deoc\'h cheñch tra pe dra.',
'config-install-step-done' => 'graet',
'config-install-step-failed' => "c'hwitet",
'config-install-extensions' => 'En ur gontañ an astennoù',
@@ -2825,16 +3083,18 @@ Gwiriit hag-eñ e c'hall an implijer « $1 » skrivañ er brastres « $2 ».",
'config-install-mainpage' => "O krouiñ ar bajenn bennañ gant un endalc'had dre ziouer",
'config-install-extension-tables' => 'O krouiñ taolennoù evit an astennoù gweredekaet',
'config-install-mainpage-failed' => "Ne c'haller ket ensoc'hañ ar bajenn bennañ: $1",
- 'config-download-localsettings' => 'Pellgargañ LocalSettings.php',
+ 'config-download-localsettings' => 'Pellgargañ <code>LocalSettings.php</code>',
'config-help' => 'skoazell',
+ 'config-nofile' => 'N\'eus ket bet gallet kavout ar restr "$1". Daoust ha dilamet eo bet ?',
'mainpagetext' => "'''Meziant MediaWiki staliet.'''",
'mainpagedocfooter' => "Sellit ouzh [//meta.wikimedia.org/wiki/Help:Contents Sturlevr an implijerien] evit gouzout hiroc'h war an doare da implijout ar meziant wiki.
== Kregiñ ganti ==
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
-* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Roll an arventennoù kefluniañ]
+* [//www.mediawiki.org/wiki/Manual:FAQ FAG MediaWiki]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Roll ar c'haozeadennoù diwar-benn dasparzhoù MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Lec'hiañ MediaWiki en ho yezh", # Fuzzy
);
/** Bosnian (bosanski)
@@ -2846,7 +3106,7 @@ $messages['bs'] = array(
'config-information' => 'Informacija',
'config-localsettings-upgrade' => 'Otkrivena je datoteka <code>LocalSettings.php</code>.
Da biste unaprijedili vaš softver, molimo vas upišite vrijednost od <code>$wgUpgradeKey</code> u okvir ispod.
-Naći ćete ga u LocalSettings.php.',
+Naći ćete ga u <code>LocalSettings.php</code>.',
'config-localsettings-key' => 'Ključ za nadgradnju:',
'config-session-error' => 'Greška pri pokretanju sesije: $1',
'config-no-session' => 'Vaši podaci sesije su izgubljeni!
@@ -2919,15 +3179,28 @@ Ovo '''nije preporučeno''' osim ako nemate problema s vašom wiki.",
== Početak ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista postavki]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki najčešće postavljana pitanja]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista E-Mail adresa MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista E-Mail adresa MediaWiki]', # Fuzzy
);
/** Catalan (català)
+ * @author Pitort
* @author පසිඳු කාවින්ද
*/
$messages['ca'] = array(
'config-page-language' => 'Llengua',
'config-page-name' => 'Nom',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-ns-generic' => 'Projecte',
+ 'config-admin-password' => 'Contrasenya:',
+ 'config-profile-wiki' => 'Wiki públic',
+ 'config-profile-private' => 'Wiki privat',
+ 'config-license-pd' => 'Domini públic',
+ 'config-upload-deleted' => 'Directori pels arxius suprimits:',
+ 'config-advanced-settings' => 'Configuració avançada',
+ 'config-extensions' => 'Extensions',
'mainpagetext' => "'''El programari del MediaWiki s'ha instaŀlat correctament.'''",
'mainpagedocfooter' => "Consulteu la [//meta.wikimedia.org/wiki/Help:Contents Guia d'Usuari] per a més informació sobre com utilitzar-lo.
@@ -2935,21 +3208,31 @@ $messages['ca'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Llista de característiques configurables]
* [//www.mediawiki.org/wiki/Manual:FAQ PMF del MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de correu (''listserv'') per a anuncis del MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de correu (''listserv'') per a anuncis del MediaWiki]", # Fuzzy
);
/** Chechen (нохчийн)
* @author Sasan700
+ * @author Умар
*/
$messages['ce'] = array(
+ 'config-your-language' => 'Хьан мотт:',
+ 'config-continue' => 'Кхин дӀа →',
+ 'config-page-language' => 'Мотт',
+ 'config-page-name' => 'ЦӀе',
'config-no-fts3' => "'''Тергам бе''': SQLite гулйина хуттург йоцуш [//sqlite.org/fts3.html FTS3] — лахар болхбеш хир дац оцу бухца.",
+ 'config-site-name' => 'Викин цӀе:',
+ 'config-site-name-blank' => 'Язъе сайтан цӀе.',
+ 'config-license' => 'Авторан бакъонаш а лицензи а:',
+ 'config-license-pd' => 'Юкъараллин хьал',
+ 'config-help' => 'гӀо',
'mainpagetext' => "'''Вики-белха гlирс «MediaWiki» кхочуш дика дlахlоттийна.'''",
'mainpagedocfooter' => 'Викийца болх бан хаамаш карор бу хlокху чохь [//meta.wikimedia.org/wiki/%D0%9F%D0%BE%D0%BC%D0%BE%D1%89%D1%8C:%D0%A1%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D0%BD%D0%B8%D0%B5 нисвохааман куьйгаллица].
== Цхьаболу пайде гlирсаш ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Гlирс нисбан тарлушболу могlам];
* [//www.mediawiki.org/wiki/Manual:FAQ Сих сиха лушдолу хаттарш а жоьпаш оцу MediaWiki];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Хаам бохьуьйту араяларца башхонца керла MediaWiki].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Хаам бохьуьйту араяларца башхонца керла MediaWiki].', # Fuzzy
);
/** Cebuano (Cebuano)
@@ -2961,20 +3244,44 @@ $messages['ceb'] = array(
== Pagsugod ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Listahan sa mga setting sa kompigurasyon]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ sa MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list sa mga release sa MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list sa mga release sa MediaWiki]', # Fuzzy
);
/** Sorani Kurdish (کوردی)
* @author Asoxor
+ * @author Calak
+ * @author Muhammed taha
*/
$messages['ckb'] = array(
+ 'config-wiki-language' => 'زمانی ویکی:',
+ 'config-back' => '→ گەڕانەوە',
+ 'config-continue' => 'بەردەوامبوون ←',
+ 'config-page-language' => 'زمان',
+ 'config-page-welcome' => 'بەخێربێیت بۆ میدیاویکی!',
+ 'config-page-dbconnect' => 'پەیوەندی دەکات بەبنکەی زانیارییەکان',
+ 'config-page-upgrade' => 'نويکردنەوەی دابەزاندنی پێشوو',
+ 'config-page-dbsettings' => 'ڕێکخستنەکانی بنکەی زانیارییەکان',
+ 'config-page-name' => 'ناو',
+ 'config-page-options' => 'ھەڵبژاردەکان',
+ 'config-page-install' => 'دابەزاندن',
+ 'config-page-complete' => 'تەواو!',
+ 'config-page-restart' => 'دەست پێکردنەوە بەدابەزاندن',
+ 'config-page-readme' => 'بمخوێنەوە',
+ 'config-page-copying' => 'لەبەردەگیرێتەوە',
+ 'config-page-upgradedoc' => 'نوێدەکرێتەوە',
+ 'config-page-existingwiki' => 'ویکی پێشوو',
+ 'config-restart' => 'بەڵێ، دەستی پێ بکەرەوە',
+ 'config-env-php' => 'PHP $1 دابەزێندرا.',
+ 'config-env-php-toolow' => 'PHP $1 دابەزێندرا.
+ھەرچۆنێک بێت میدیاویکی پێویستی بە PHP $2 یان بەرزتر ھەیە.',
'mainpagetext' => "'''میدیاویکی بە سەرکەوتوویی دامەزرا.'''",
- 'mainpagedocfooter' => 'پرس بکە بە [//meta.wikimedia.org/wiki/Help:Contents ڕێنوێنیی بەکارھێنەران] بۆ زانیاری سەبارەت بە بەکارھێنانی نەرمامێری ویکی.
+ 'mainpagedocfooter' => 'لە [//meta.wikimedia.org/wiki/Help:Contents ڕێنوێنیی بەکارھێنەران] بۆ زانیاری سەبارەت بە بەکارھێنانی نەرمامێری ویکی کەڵک وەربگرە.
== دەستپێکردن ==
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings لیستی ڕێکخستنەکان شێوەپێدان]
-* [//www.mediawiki.org/wiki/Manual:FAQ پرسیارە دوپاتکراوەکانی میدیاویکی (MediaWiki FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce لیستی ئیمەیلی وەشانەکانی میدیاویکی]',
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings پێرستی ڕێکخستنەکانی شێوەپێدان]
+* [//www.mediawiki.org/wiki/Manual:FAQ پرسیارە دووپاتکراوەکانی میدیاویکی (MediaWiki FAQ)]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce پێرستی ئیمەیلی وەشانەکانی میدیاویکی]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources خۆماڵیکردنی ویکیمیدیا بۆ زمانەکەت]',
);
/** Capiznon (Capiceño)
@@ -2988,7 +3295,7 @@ $messages['cps'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista sang mga setting sang konpigurayon]
* [//www.mediawiki.org/wiki/Manual:FAQ Mga perme napangkot sa MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista sang mga ginapadal-an sang sulat sang MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista sang mga ginapadal-an sang sulat sang MediaWiki]', # Fuzzy
);
/** Crimean Turkish (Cyrillic script) (къырымтатарджа (Кирилл)‎)
@@ -3000,7 +3307,7 @@ $messages['crh-cyrl'] = array(
== Базы файдалы сайтлар ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Олуджы сазламалар джедвели];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki боюнджа сыкъ берильген суаллернен джеваплар];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-нинъ янъы версияларынынъ чыкъувындан хабер йиберюв].",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-нинъ янъы версияларынынъ чыкъувындан хабер йиберюв].", # Fuzzy
);
/** Crimean Turkish (Latin script) (qırımtatarca (Latin)‎)
@@ -3012,31 +3319,33 @@ $messages['crh-latn'] = array(
== Bazı faydalı saytlar ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Olucı sazlamalar cedveli];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki boyunca sıq berilgen suallernen cevaplar];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-niñ yañı versiyalarınıñ çıquvından haber yiberüv].",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-niñ yañı versiyalarınıñ çıquvından haber yiberüv].", # Fuzzy
);
/** Czech (česky)
* @author Danny B.
+ * @author Jezevec
* @author Mormegil
+ * @author 아라
*/
$messages['cs'] = array(
'config-desc' => 'Instalační program pro MediaWiki',
'config-title' => 'Instalace MediaWiki $1',
'config-information' => 'Informace',
'config-localsettings-upgrade' => 'Byl nalezen soubor <code>LocalSettings.php</code>.
-Pokud chcete stávající instalaci aktualizovat, zadejte hodnotu <code>$wgUpgradeKey</code>, kterou naleznete v souboru LocalSettings.php, do následujícího rámečku.',
- 'config-localsettings-cli-upgrade' => 'Byl detekován soubor <code>LocalSettings.php</code>
+Pokud chcete stávající instalaci aktualizovat, zadejte hodnotu <code>$wgUpgradeKey</code>, kterou naleznete v souboru <code>LocalSettings.php</code>, do následujícího rámečku.',
+ 'config-localsettings-cli-upgrade' => 'Byl detekován soubor <code><code>LocalSettings.php</code></code>
Pro aktualizaci spusťte místo instalace skript <code>update.php</code>.',
'config-localsettings-key' => 'Klíč pro aktualizaci:',
'config-localsettings-badkey' => 'Zadaný klíč je nesprávný.',
'config-upgrade-key-missing' => 'Byla detekována existující instalace MediaWiki.
-Pokud ji chcete aktualizovat, přidejte následující řádku na konec souboru LocalSettings.php:
+Pokud ji chcete aktualizovat, přidejte následující řádku na konec souboru <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Existující soubor LocalSettings.php vypadá neúplný.
+ 'config-localsettings-incomplete' => 'Existující soubor <code>LocalSettings.php</code> vypadá neúplný.
Není nastavena proměnná $1.
-Upravte soubor LocalSettings.php tak, aby tuto proměnnou obsahoval, a klikněte na „Pokračovat“.',
- 'config-localsettings-connection-error' => 'Při připojování k databázi s využitím nastavení uvedených v LocalSettings.php nebo AdminSettings.php došlo k chybě. Opravte tato nastavení a zkuste to znovu.
+Upravte soubor <code>LocalSettings.php</code> tak, aby tuto proměnnou obsahoval, a klikněte na „{{int:Config-continue}}“.',
+ 'config-localsettings-connection-error' => 'Při připojování k databázi s využitím nastavení uvedených v <code>LocalSettings.php</code> nebo <code>AdminSettings.php</code> došlo k chybě. Opravte tato nastavení a zkuste to znovu.
$1',
'config-session-error' => 'Nepodařilo se inicializovat relaci: $1',
@@ -3070,8 +3379,8 @@ Zkontrolujte svůj soubor php.ini a ujistěte se, že <code>session.save_path</c
'config-help-restart' => 'Chcete smazat všechny údaje, které jste zadali, a spustit proces instalace znovu od začátku?',
'config-restart' => 'Ano, restartovat',
'config-welcome' => '=== Kontrola prostředí ===
-Provedou se základní kontroly, aby se zjistilo, zda je toto prostředí použitelné k instalaci MediaWiki.
-Pokud budete potřebovat při instalaci pomoc, měli byste sdělit výsledky těchto testů.',
+Nyní se provedou základní kontroly, aby se zjistilo, zda je toto prostředí použitelné k instalaci MediaWiki.
+Pokud budete potřebovat k dokončení instalace pomoc, nezapomeňte sdělit výsledky těchto testů.',
'config-copyright' => "=== Licence a podmínky ===
$1
@@ -3140,6 +3449,10 @@ MediaWiki vyžaduje ke správné funkci podporu UTF-8.",
To je pravděpodobně příliš málo.
Instalace může selhat!",
'config-ctype' => "'''Kritická chyba''': PHP musí být přeloženo s podporou pro [http://www.php.net/manual/en/ctype.installation.php rozšíření Ctype].",
+ 'config-json' => "'''Kritická chyba:''' PHP bylo přeloženo bez podpory JSON.
+Před instalací MediaWiki musíte buď nainstalovat rozšíření PHP JSON nebo rozšíření [http://pecl.php.net/package/jsonc PECL jsonc].
+* Rozšíření PHP je součástí Red Hat Enterprise Linux (CentOS) 5 a 6, avšak musí se povolit v <code>/etc/php.ini</code> nebo <code>/etc/php.d/json.ini</code>.
+* V některých linuxových distribucích vydaných po květnu 2013 může toto rozšíření PHP chybět a místo toho mohou používat rozšíření PECL jako <code>php5-json</code> nebo <code>php-pecl-jsonc</code>.",
'config-xcache' => 'Je nainstalována [http://xcache.lighttpd.net/ XCache]',
'config-apc' => 'Je nainstalováno [http://www.php.net/apc APC]',
'config-wincache' => 'Je nainstalována [http://www.iis.net/download/WinCacheForPhp WinCache]',
@@ -3148,6 +3461,8 @@ Kešování objektů bude vypnuto.",
'config-mod-security' => "'''Upozornění''': váš webový server má zapnuto [http://modsecurity.org/ mod_security]. Při chybné konfiguraci může způsobovat potíže MediaWiki či dalším programům, které umožňují ukládat libovolný obsah.
Pokud narazíte na náhodné chyby, podívejte se do [http://modsecurity.org/documentation/ dokumentace mod_security] nebo kontaktujte technickou podporu vašeho poskytovatele.",
'config-diff3-bad' => 'Nebyl nalezen GNU diff3.',
+ 'config-git' => 'Nalezen software pro správu verzí Git: <code>$1</code>.',
+ 'config-git-bad' => 'Software pro správu verzí Git nebyl nalezen.',
'config-imagemagick' => 'Nalezen ImageMagick: <code>$1</code>.
Pokud povolíte načítání souborů, bude zapnuto vytváření náhledů.',
'config-gd' => 'Nalezena vestavěná grafická knihovna GD.
@@ -3168,7 +3483,9 @@ Instalace přerušena.',
'config-using531' => 'MediaWiki nelze používat na PHP $1 kvůli chybě při předávání parametrů odkazem do <code>__call()</code>.
Pro vyřešení upgradujte na PHP 5.3.2 nebo vyšší nebo downgradujte na PHP 5.3.0.
Instalace přerušena.',
- 'config-suhosin-max-value-length' => 'Je nainstalován Suhosin, který omezuje délku parametrů GET na $1 bajtů. Komponenta ResourceLoader z MediaWiki dokáže s tímto omezením pracovat, ale sníží to výkon. Pokud to je alespoň trochu možné, měli byste v php.ini nastavit suhosin.get.max_value_length na 1024 nebo vyšší a na stejnou hodnotu nastavit v LocalSettings.php proměnnou $wgResourceLoaderMaxQueryLength.',
+ 'config-suhosin-max-value-length' => 'Je nainstalován Suhosin, který omezuje délku parametrů GET na $1 bajtů.
+Komponenta ResourceLoader z MediaWiki dokáže s tímto omezením pracovat, ale sníží to výkon.
+Pokud to je alespoň trochu možné, měli byste v <code>php.ini</code> nastavit <code>suhosin.get.max_value_length</code> na 1024 nebo vyšší a na stejnou hodnotu nastavit v <code>LocalSettings.php</code> proměnnou <code>$wgResourceLoaderMaxQueryLength</code>.',
'config-db-type' => 'Typ databáze:',
'config-db-host' => 'Databázový server:',
'config-db-host-help' => 'Pokud je váš databázový server na jiném počítači, zadejte zde jméno stroje nebo IP adresu.
@@ -3244,7 +3561,6 @@ Zvažte umístění databáze někam zcela jinam, například do <code>/var/lib/
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Věštba',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki podporuje následující databázové systémy:
$1
@@ -3254,18 +3570,16 @@ Pokud v nabídce níže nevidíte databázový systém, který chcete použít,
'config-support-postgres' => '* $1 je populární open-source databázový systém používaný jako alternativa k MySQL ([http://www.php.net/manual/en/pgsql.installation.php jak přeložit PHP s podporou PostgreSQL]). Mohou se vyskytnout ještě nějaké menší chyby, použití v produkčním prostředí se nedoporučuje.',
'config-support-sqlite' => '* $1 je velmi dobře podporovaný lehký databázový systém. ([http://www.php.net/manual/en/pdo.installation.php Jak přeložit PHP s podporou SQLite], používá PDO)',
'config-support-oracle' => '* $1 je komerční podniková databáze. ([http://www.php.net/manual/en/oci8.installation.php Jak přeložit PHP s podporou OCI8])',
- 'config-support-ibm_db2' => '* $1 je komerční podniková databáze.',
'config-header-mysql' => 'Nastavení MySQL',
'config-header-postgres' => 'Nastavení PostgreSQL',
'config-header-sqlite' => 'Nastavení SQLite',
'config-header-oracle' => 'Nastavení Oracle',
- 'config-header-ibm_db2' => 'Nastavení IBM DB2',
'config-invalid-db-type' => 'Chybný typ databáze',
'config-missing-db-name' => 'Musíte zadat hodnotu pro „Jméno databáze“',
'config-missing-db-host' => 'Musíte zadat hodnotu pro „Databázový server“',
'config-missing-db-server-oracle' => 'Musíte zadat hodnotu pro „Databázové TNS“',
'config-invalid-db-server-oracle' => 'Chybné databázové TNS „$1“.
-Používejte pouze ASCII písmena (a-z, A-Z), čísla (0-9), podtržítko (_) a tečku (.).',
+Používejte buď „TNS Name“ nebo „Easy Connect“ (vizte [http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).',
'config-invalid-db-name' => 'Chybné jméno databáze „$1“.
Používejte pouze ASCII písmena (a-z, A-Z), čísla (0-9), podtržítko (_) a spojovník (-).',
'config-invalid-db-prefix' => 'Chybný databázový prefix „$1“.
@@ -3322,7 +3636,7 @@ To se ale '''nedoporučuje''', pokud s wiki nemáte problémy.",
Svou wiki teď můžete [$1 začít používat].',
'config-regenerate' => 'Přegenerovat LocalSettings.php →',
- 'config-show-table-status' => 'Dotaz SHOW TABLE STATUS se nezdařil!',
+ 'config-show-table-status' => 'Dotaz <code>SHOW TABLE STATUS</code> se nezdařil!',
'config-unknown-collation' => "'''Upozornění:''' Databáze používá nerozpoznané řazení.",
'config-db-web-account' => 'Databázový účet pro webový přístup',
'config-db-web-help' => 'Zvolte uživatelské jméno a heslo, které bude webový server používat pro připojení k databázovému serveru při běžném provozu wiki.',
@@ -3340,6 +3654,12 @@ Svou wiki teď můžete [$1 začít používat].',
Pokud vaše instalace MySQL podporuje InnoDB, důrazně doporučujeme použít spíše to.
Pokud vaše instalace MySQL InnoDB nepodporuje, možná je čas na aktualizaci.",
+ 'config-mysql-only-myisam-dep' => "'''Upozornění:''' Jediným dostupným formátem dat pro MySQL je MyISAM, který se k užití pro MediaWiki nedoporučuje, protože:
+* téměř nepodporuje současný přístup kvůli zamykání tabulek,
+* oproti jiným formátům je náchylnější k poškození,
+* kód MediaWiki nepodporuje MyISAM tak dobře, jak by bylo potřeba.
+
+Vaše instalace MySQL nepodporuje InnoDB, možná je na čase upgradovat.",
'config-mysql-engine-help' => "'''InnoDB''' je téměř vždy nejlepší volba, neboť má dobrou podporu současného přístupu.
'''MyISAM''' může být rychlejší u instalací pro jednoho uživatele nebo jen pro čtení.
@@ -3351,7 +3671,6 @@ Databáze MyISAM bývají poškozeny častěji než databáze InnoDB.",
To je výkonnější než UTF-8 režim MySQL a umožňuje využít plný rozsah znaků Unicode.
V '''režimu UTF-8''' bude MySQL znát znakovou sadu vašich dat a může je příslušně zobrazovat a převádět, ale neumožní vám uložit znaky mimo [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane].",
- 'config-ibm_db2-low-db-pagesize' => "Vaše DB2 databáze má implicitní tabulkový prostor s nedostatečnou velikostí stránky. Velikost stránky musí být minimálně '''32K'''.",
'config-site-name' => 'Název wiki:',
'config-site-name-help' => 'Bude se zobrazovat v titulku prohlížeče a na dalších místech.',
'config-site-name-blank' => 'Zadejte název serveru.',
@@ -3394,7 +3713,7 @@ Zbývající konfiguraci už můžete přeskočit a nainstalovat wiki hned teď.
'config-optional-continue' => 'Ptejte se mě dál.',
'config-optional-skip' => 'Už mě to nudí, prostě nainstalujte wiki.',
'config-profile' => 'Profil uživatelských práv:',
- 'config-profile-wiki' => 'Tradiční wiki',
+ 'config-profile-wiki' => 'Otevřená wiki',
'config-profile-no-anon' => 'Vyžadována registrace uživatelů',
'config-profile-fishbowl' => 'Editace jen pro vybrané',
'config-profile-private' => 'Soukromá wiki',
@@ -3404,7 +3723,7 @@ V MediaWiki můžete snadno kontrolovat poslední změny a vracet zpět libovoln
Mnoho lidí však zjistilo, že je MediaWiki užitečné v širokém spektru rolí a někdy není snadné všechny přesvědčit o výhodách wikizvyklostí.
Takže si můžete vybrat.
-'''{{int:config-profile-wiki}}''' dovoluje editovat všem, aniž by se museli přihlašovat.
+Model '''{{int:config-profile-wiki}}''' dovoluje editovat všem, aniž by se museli přihlašovat.
Na wiki, kde je '''{{int:config-profile-no-anon}}''', se lépe řídí zodpovědnost, ale může to odradit náhodné přispěvatele.
Profil '''{{int:config-profile-fishbowl}}''' umožňuje schváleným uživatelům editovat, ale veřejnost si může stránky prohlížet včetně jejich historie.
@@ -3461,6 +3780,8 @@ Tento adresář by ideálně neměl být dostupný z webu.',
'config-logo-help' => 'Základní vzhled MediaWiki zahrnuje místo pro logo o velikosti 135×160 pixelů nad bočním menu.
Načtěte obrázek odpovídající velikosti a zadejte sem jeho URL.
+Pokud je vaše logo umístěno relativně vůči <code>$wgStylePath</code> nebo <code>$wgScriptPath</code>, můžete zde tyto proměnné použít.
+
Pokud logo nechcete, ponechte toto pole prázdné.',
'config-instantcommons' => 'Zapnout Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] je funkce, která umožňuje wiki používat obrázky, zvuky a další mediální soubory ze serveru [//commons.wikimedia.org/wiki/Hlavn%C3%AD_strana Wikimedia Commons].
@@ -3495,7 +3816,7 @@ Mohou vyžadovat dodatečnou konfiguraci, ale teď je můžete povolit.',
'config-install-alreadydone' => "'''Upozornění:''' Vypadá to, že jste MediaWiki již nainstalovali a teď se o to pokoušíte znovu.
Pokračujte na další stránku.",
'config-install-begin' => 'Stisknutím „{{int:config-continue}}“ spustíte instalaci MediaWiki.
-Pokud ještě chcete udělat nějaké změny, stiskněte tlačítko zpět.',
+Pokud ještě chcete udělat nějaké změny, stiskněte „{{int:config-back}}“.',
'config-install-step-done' => 'hotovo',
'config-install-step-failed' => 'selhaly',
'config-install-extensions' => 'Vkládají se rozšíření',
@@ -3551,9 +3872,12 @@ $3
'''Poznámka''': Pokud to neuděláte hned, tento vygenerovaný konfigurační soubor nebude později dostupný, pokud instalaci opustíte, aniž byste si ho stáhli.
Až to dokončíte, můžete '''[$2 vstoupit do své wiki]'''.",
- 'config-download-localsettings' => 'Stáhnout LocalSettings.php',
+ 'config-download-localsettings' => 'Stáhnout <code>LocalSettings.php</code>',
'config-help' => 'nápověda',
'config-nofile' => 'Soubor „$1“ nelze nalézt. Byl smazán?',
+ 'config-extension-link' => 'Věděli jste, že vaše wiki podporuje [//www.mediawiki.org/wiki/Manual:Extensions rozšíření]?
+
+Můžete procházet [//www.mediawiki.org/wiki/Category:Extensions_by_category rozšíření po kategoriích] nebo si prohlédnout [//www.mediawiki.org/wiki/Extension_Matrix Matici rozšíření] obsahující úplný seznam.',
'mainpagetext' => "'''MediaWiki byla úspěšně nainstalována.'''",
'mainpagedocfooter' => '[//meta.wikimedia.org/wiki/Help:Contents Uživatelská příručka] vám napoví, jak MediaWiki používat.
@@ -3561,7 +3885,8 @@ Až to dokončíte, můžete '''[$2 vstoupit do své wiki]'''.",
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Nastavení konfigurace]
* [//www.mediawiki.org/wiki/Manual:FAQ Často kladené otázky o MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-mailová konference oznámení MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-mailová konference oznámení MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Překlad MediaWiki do vašeho jazyka]',
);
/** Kashubian (kaszëbsczi)
@@ -3570,6 +3895,15 @@ $messages['csb'] = array(
'mainpagetext' => "'''MediaWiki òsta zainstalowónô.'''",
);
+/** Church Slavic (словѣ́ньскъ / ⰔⰎⰑⰂⰡⰐⰠⰔⰍⰟ)
+ * @author ОйЛ
+ */
+$messages['cu'] = array(
+ 'config-page-language' => 'ѩꙁꙑкъ',
+ 'config-page-name' => 'имѧ',
+ 'config-help' => 'помощь',
+);
+
/** Chuvash (Чӑвашла)
*/
$messages['cv'] = array(
@@ -3579,21 +3913,23 @@ $messages['cv'] = array(
== Пулăшма пултарĕç ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Ĕнерлевсен списокĕ];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki тăрăх час-часах ыйтакан ыйтусемпе хуравсем];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki çĕнĕ верси тухнине пĕлтерекен рассылка].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki çĕнĕ верси тухнине пĕлтерекен рассылка].', # Fuzzy
);
/** Welsh (Cymraeg)
+ * @author Lloffiwr
* @author Xxglennxx
*/
$messages['cy'] = array(
'mainpagetext' => "'''Wedi llwyddo gosod meddalwedd MediaWiki yma'''",
- 'mainpagedocfooter' => 'Ceir cymorth (yn Saesneg) ar ddefnyddio meddalwedd wici yn y [//meta.wikimedia.org/wiki/Help:Contents Canllaw Defnyddwyr] ar wefan Wikimedia.
+ 'mainpagedocfooter' => "Ceir cymorth (yn Saesneg) ar ddefnyddio meddalwedd wici yn y [//meta.wikimedia.org/wiki/Help:Contents Canllaw Defnyddwyr] ar wefan Wikimedia.
==Cychwyn arni==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Rhestr osodiadau wrth gyflunio]
* [//www.mediawiki.org/wiki/Manual:FAQ Cwestiynau poblogaidd ar MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Rhestr postio datganiadau MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Rhestr postio datganiadau MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Cyfieithu MediaWici i'ch iaith chi]",
);
/** Danish (dansk)
@@ -3606,16 +3942,20 @@ $messages['da'] = array(
== At komme i gang ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Listen over opsætningsmuligheder]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki ofte stillede spørgsmål]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Postliste angående udgivelser af MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Postliste angående udgivelser af MediaWiki]', # Fuzzy
);
/** German (Deutsch)
+ * @author Geitost
* @author Kghbln
* @author LWChris
* @author Metalhead64
* @author Purodha
+ * @author Rillke
* @author The Evil IP address
* @author Umherirrender
+ * @author Wikinaut
+ * @author 아라
*/
$messages['de'] = array(
'config-desc' => 'Das MediaWiki-Installationsprogramm',
@@ -3623,19 +3963,19 @@ $messages['de'] = array(
'config-information' => 'Informationen',
'config-localsettings-upgrade' => 'Eine Datei <code>LocalSettings.php</code> wurde gefunden.
Um die vorhandene Installation aktualisieren zu können, muss der Wert des Parameters <code>$wgUpgradeKey</code> im folgenden Eingabefeld angegeben werden.
-Der Parameterwert befindet sich in der Datei LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Eine Datei <code>LocalSettings.php</code> wurde gefunden.
+Der Parameterwert befindet sich in der Datei <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Eine Datei <code><code>LocalSettings.php</code></code> wurde gefunden.
Um die vorhandene Installation zu aktualisieren, muss die Datei <code>update.php</code> ausgeführt werden.',
'config-localsettings-key' => 'Aktualisierungsschlüssel:',
'config-localsettings-badkey' => 'Der angegebene Aktualisierungsschlüssel ist falsch.',
'config-upgrade-key-missing' => 'Eine MediaWiki-Installation wurde gefunden.
-Um die vorhandene Installation aktualisieren zu können, muss die unten angegebene Codezeile in die Datei LocalSettings.php an deren Ende eingefügt werden:
+Um die vorhandene Installation aktualisieren zu können, muss die unten angegebene Codezeile in die Datei <code>LocalSettings.php</code> an deren Ende eingefügt werden:
$1',
- 'config-localsettings-incomplete' => 'Die vorhandene Datei LocalSettings.php scheint unvollständig zu sein.
+ 'config-localsettings-incomplete' => 'Die vorhandene Datei <code>LocalSettings.php</code> scheint unvollständig zu sein.
Die Variable <code>$1</code> wurde nicht definiert.
-Die Datei LocalSettings.php muss entsprechend geändert werden, so dass sie definiert ist. Klicke danach auf „Weiter“.',
- 'config-localsettings-connection-error' => 'Beim Verbindungsversuch zur Datenbank ist, unter Verwendung der in den Dateien LocalSettings.php oder AdminSettings.php hinterlegten Einstellungen, ein Fehler aufgetreten. Diese Einstellungen müssen korrigiert werden. Danach kann ein erneuter Versuch unternommen werden.
+Die Datei <code>LocalSettings.php</code> muss entsprechend geändert werden, so dass sie definiert ist. Klicke danach auf „{{int:Config-continue}}“.',
+ 'config-localsettings-connection-error' => 'Beim Verbindungsversuch zur Datenbank ist, unter Verwendung der in den Dateien <code>LocalSettings.php</code> oder <code>AdminSettings.php</code> hinterlegten Einstellungen, ein Fehler aufgetreten. Diese Einstellungen müssen korrigiert werden. Danach kann ein erneuter Versuch unternommen werden.
$1',
'config-session-error' => 'Fehler beim Starten der Sitzung: $1',
@@ -3666,11 +4006,11 @@ Die Datei <code>php.ini</code> muss geprüft und es muss dabei sichergestellt we
'config-page-copying' => 'Kopie der Lizenz',
'config-page-upgradedoc' => 'Aktualisiere',
'config-page-existingwiki' => 'Vorhandenes Wiki',
- 'config-help-restart' => 'Sollen alle bereits eingegebene Daten gelöscht und der Installationsvorgang erneut gestartet werden?',
+ 'config-help-restart' => 'Sollen alle bereits eingegebenen Daten gelöscht und der Installationsvorgang erneut gestartet werden?',
'config-restart' => 'Ja, erneut starten',
'config-welcome' => '=== Prüfung der Installationsumgebung ===
-Die Basisprüfungen werden durchgeführt, um festzustellen, ob die Installationsumgebung für die Installation von MediaWiki geeignet ist.
-Die Ergebnisse dieser Prüfung sollten angegeben werden, sofern während des Installationsvorgangs Hilfe benötigt und erfragt wird.',
+Die Basisprüfungen werden jetzt durchgeführt, um festzustellen, ob die Installationsumgebung für die Installation von MediaWiki geeignet ist.
+Nimm diese Information auf, falls du Unterstützung bei der Vervollständigung der Installation benötigst.',
'config-copyright' => "=== Lizenz und Nutzungsbedingungen ===
$1
@@ -3679,7 +4019,7 @@ Dieses Programm ist freie Software, d. h. es kann, gemäß den Bedingungen der v
Dieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, allerdings '''ohne jegliche Garantie''' und sogar ohne die implizierte Garantie einer '''Marktgängigkeit''' oder '''Eignung für einen bestimmten Zweck'''. Hierzu sind weitere Hinweise in der ''GNU General Public License'' enthalten.
-Eine <doclink href=Copying>Kopie der ''GNU General Public License''</doclink> sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [http://www.gnu.org/copyleft/gpl.html online gelesen] werden.",
+Eine <doclink href=Copying>Kopie der GNU General Public License</doclink> sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [http://www.gnu.org/copyleft/gpl.html online gelesen] werden.",
'config-sidebar' => '* [//www.mediawiki.org/wiki/MediaWiki/de Website von MediaWiki]
* [//www.mediawiki.org/wiki/Help:Contents/de Benutzeranleitung]
* [//www.mediawiki.org/wiki/Manual:Contents/de Administratorenanleitung]
@@ -3739,14 +4079,20 @@ MediaWiki benötigt die UTF-8-Unterstützung, um fehlerfrei lauffähig zu sein."
Dieser Wert ist wahrscheinlich zu niedrig.
Der Installationsvorgang könnte daher scheitern!",
'config-ctype' => "'''Fataler Fehler:''' PHP muss mit Unterstützung für das [http://www.php.net/manual/de/ctype.installation.php Modul ctype] kompiliert werden.",
+ 'config-json' => "'''Fatal:''' PHP wurde ohne JSON-Support kompiliert.
+Du musst entweder die PHP-JSON- oder die [http://pecl.php.net/package/jsonc PECL-jsonc]-Erweiterung installieren, bevor du MediaWiki installierst.
+* Die PHP-Erweiterung ist in Red Hat Enterprise Linux (CentOS) 5 und 6 enthalten, muss jedoch in <code>/etc/php.ini</code> oder <code>/etc/php.d/json.ini</code> aktiviert werden.
+* Einige Linux-Distributionen, die nach Mai 2013 veröffentlicht wurden, lassen die PHP-Erweiterung weg, stattdessen wird die PECL-Erweiterung als <code>php5-json</code> oder <code>php-pecl-jsonc</code> mitgeliefert.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] ist installiert',
'config-apc' => '[http://www.php.net/apc APC] ist installiert',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] ist installiert',
- 'config-no-cache' => "'''Warnung:''' [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] oder [http://www.iis.net/download/WinCacheForPhp WinCache] konnten nicht gefunden werden.
+ 'config-no-cache' => "'''Warnung:''' [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] oder [http://www.iis.net/download/WinCacheForPhp WinCache] wurden nicht gefunden.
Das Objektcaching kann daher nicht aktiviert werden.",
'config-mod-security' => "'''Warnung:''' Auf dem Webserver wurde [http://modsecurity.org/ ModSecurity] aktiviert. Sofern falsch konfiguriert, kann dies zu Problemen mit MediaWiki sowie anderer Software auf dem Server führen und es Benutzern ermöglichen beliebige Inhalte im Wiki einzustellen.
Für weitere Informationen empfehlen wir die [http://modsecurity.org/documentation/ Dokumentation zu ModSecurity] oder den Kontakt zum Hoster, sofern Fehler auftreten.",
'config-diff3-bad' => 'GNU diff3 wurde nicht gefunden.',
+ 'config-git' => 'Die Git-Versionsverwaltungssoftware wurde gefunden: <code>$1</code>.',
+ 'config-git-bad' => 'Die Git-Versionsverwaltungssoftware wurde nicht gefunden.',
'config-imagemagick' => 'ImageMagick wurde gefunden: <code>$1</code>.
Miniaturansichten von Bildern werden möglich sein, sobald das Hochladen von Dateien aktiviert wurde.',
'config-gd' => 'Die im System integrierte GD-Grafikbibliothek wurde gefunden.
@@ -3766,7 +4112,10 @@ PHP muss auf Version 5.2.9 oder später sowie libxml2 auf die Version 2.7.3 oder
'config-using531' => 'MediaWiki kann nicht zusammen mit PHP $1 verwendet werden. Grund hierfür ist ein Fehler im Zusammenhang mit den Verweisparametern zu <code>__call()</code>.
PHP muss auf Version 5.3.2 oder höher oder 5.3.0 oder niedriger aktualisiert werden, um das Problem zu beheben.
Die Installation wurde abgebrochen.',
- 'config-suhosin-max-value-length' => 'Suhosin ist installiert und beschränkt die Länge des GET-Parameters auf $1 Bytes. Der ResouceLoader von MediaWiki wird zwar unter diesen Bedingungen funktionieren, allerdings nur mit verminderter Leistungsfähigkeit. Sofern möglich sollte der Parameter <code>suhosin.get.max_value_length</code> in der Datei php.ini auf 1024 oder höher festgelegt werden. Gleichzeitig muss der Parameter <code>$wgResourceLoaderMaxQueryLength</code> in der Datei LocalSettings.php auf den selben Wert eingestellt werden.',
+ 'config-suhosin-max-value-length' => 'Suhosin ist installiert und beschränkt die Länge des GET-Parameters auf $1 Bytes.
+Der ResouceLoader von MediaWiki wird zwar unter diesen Bedingungen funktionieren, allerdings nur mit verminderter Leistungsfähigkeit.
+Sofern möglich sollte der Parameter <code>suhosin.get.max_value_length</code> in der Datei <code>php.ini</code> auf 1024 oder höher festgelegt werden.
+Gleichzeitig muss der Parameter <code>$wgResourceLoaderMaxQueryLength</code> in der Datei <code>LocalSettings.php</code> auf den selben Wert eingestellt werden.',
'config-db-type' => 'Datenbanksystem:',
'config-db-host' => 'Datenbankserver:',
'config-db-host-help' => 'Sofern sich die Datenbank auf einem anderen Server befindet, ist hier der Servername oder die entsprechende IP-Adresse anzugeben.
@@ -3828,19 +4177,18 @@ Nur Änderungen daran vornehmen, sofern es gute Gründe dafür gibt.',
Das für sie vorgesehene Verzeichnis muss während des Installationsvorgangs beschreibbar sein.
-Es sollte '''nicht'' über das Web zugänglich sein, was der Grund ist, warum die Datei nicht dort abgelegt wird, wo sich die PHP-Dateien befinden.
+Es sollte '''nicht''' über das Web zugänglich sein, was der Grund ist, warum die Datei nicht dort abgelegt wird, wo sich die PHP-Dateien befinden.
Das Installationsprogramm wird mit der Datei zusammen eine zusätzliche <code>.htaccess</code>-Datei erstellen. Sofern dies scheitert, können Dritte auf die Datendatei zugreifen.
Dies umfasst die Nutzerdaten (E-Mail-Adressen, Passwörter, etc.) wie auch gelöschte Seitenversionen und andere vertrauliche Daten, die im Wiki gespeichert sind.
-Es ist daher zu erwägen die Datendatei an gänzlich anderer Stelle abzulegen, beispielsweise im Verzeichnis <code>./var/lib/mediawiki/yourwiki</code>.",
+Es ist daher zu erwägen, die Datendatei an gänzlich anderer Stelle abzulegen, beispielsweise im Verzeichnis <code>./var/lib/mediawiki/yourwiki</code>.",
'config-oracle-def-ts' => 'Standardtabellenraum:',
'config-oracle-temp-ts' => 'Temporärer Tabellenraum:',
'config-type-mysql' => 'MySQL',
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki unterstützt die folgenden Datenbanksysteme:
$1
@@ -3850,18 +4198,16 @@ Sofern nicht das Datenbanksystem angezeigt wird, das verwendet werden soll, gibt
'config-support-postgres' => '* $1 ist ein beliebtes Open-Source-Datenbanksystem und eine Alternative zu MySQL ([http://www.php.net/manual/de/pgsql.installation.php Anleitung zur Kompilierung von PHP mit PostgreSQL-Unterstützung]). Es gibt allerdings einige kleinere Implementierungsfehler, so dass von der Nutzung in einer Produktivumgebung abgeraten wird.',
'config-support-sqlite' => '* $1 ist ein verschlanktes Datenbanksystem, das auch gut unterstützt wird ([http://www.php.net/manual/de/pdo.installation.php Anleitung zur Kompilierung von PHP mit SQLite-Unterstützung], verwendet PHP Data Objects (PDO))',
'config-support-oracle' => '* $1 ist eine kommerzielle Unternehmensdatenbank ([http://www.php.net/manual/en/oci8.installation.php Anleitung zur Kompilierung von PHP mit OCI8-Unterstützung (en)])',
- 'config-support-ibm_db2' => '* $1 ist eine kommerzielle Unternehmensdatenbank',
'config-header-mysql' => 'MySQL-Einstellungen',
'config-header-postgres' => 'PostgreSQL-Einstellungen',
'config-header-sqlite' => 'SQLite-Einstellungen',
'config-header-oracle' => 'Oracle-Einstellungen',
- 'config-header-ibm_db2' => 'IBM DB2-Einstellungen',
'config-invalid-db-type' => 'Unzulässiges Datenbanksystem',
'config-missing-db-name' => 'Bei „Datenbankname“ muss ein Wert angegeben werden.',
'config-missing-db-host' => 'Bei „Datenbankhost“ muss ein Wert angegeben werden.',
'config-missing-db-server-oracle' => 'Für das „Datenbank-TNS“ muss ein Wert eingegeben werden',
'config-invalid-db-server-oracle' => 'Ungültiges Datenbank-TNS „$1“.
-Es dürfen nur ASCII-codierte Buchstaben (a-z, A-Z), Zahlen (0-9) und Unterstriche (_) und Punkte (.) verwendet werden.',
+Entweder „TNS Name“ oder eine „Easy Connect“-Zeichenfolge verwenden ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle-Benennungsmethoden])',
'config-invalid-db-name' => 'Ungültiger Datenbankname „$1“.
Es dürfen nur ASCII-codierte Buchstaben (a-z, A-Z), Zahlen (0-9), Unter- (_) sowie Bindestriche (-) verwendet werden.',
'config-invalid-db-prefix' => 'Ungültiger Datenbanktabellenpräfix „$1“.
@@ -3919,7 +4265,7 @@ Dies wird '''nicht empfohlen''', es sei denn, es treten Probleme mit dem Wiki au
Das Wiki kann nun [$1 genutzt werden].',
'config-regenerate' => 'LocalSettings.php neu erstellen →',
- 'config-show-table-status' => 'Die Abfrage SHOW TABLE STATUS ist gescheitert!',
+ 'config-show-table-status' => 'Die Abfrage <code>SHOW TABLE STATUS</code> ist gescheitert!',
'config-unknown-collation' => "'''Warnung:''' Die Datenbank nutzt eine unbekannte Kollation.",
'config-db-web-account' => 'Datenbankkonto für den Webzugriff',
'config-db-web-help' => 'Bitte Benutzernamen und Passwort auswählen, die der Webserver während des Normalbetriebes dazu verwenden soll, eine Verbindung zum Datenbankserver herzustellen.',
@@ -3930,13 +4276,19 @@ Das hier angegebene Datenbankkonto muss daher bereits vorhanden sein.',
'config-mysql-engine' => 'Speicher-Engine:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
- 'config-mysql-myisam-dep' => "'''Warnung:''' Es wurde MyISAM als Speicher-Engine für MySQL ausgewählt, die aus folgenden Gründen nicht die für den Einsatz mit MediaWiki empfohlene ist:
-* sie unterstützt aufgrund von Tabellensperrungen kaum die nebenläufige Ausführung von Aktionen
-* sie ist anfälliger für Datenprobleme
-* sie wird von MediaWiki nicht immer adäquat unterstützt
+ 'config-mysql-myisam-dep' => "'''Warnung:''' Es wurde MyISAM als Speicher-Engine für MySQL ausgewählt, die aus folgenden Gründen nicht für den Einsatz mit MediaWiki empfohlen ist:
+* Sie unterstützt aufgrund von Tabellensperrungen kaum die nebenläufige Ausführung von Aktionen.
+* Sie ist anfälliger für Datenprobleme.
+* Sie wird von MediaWiki nicht immer adäquat unterstützt.
Sofern die vorhandene MySQL-Installation die Speicher-Engine InnoDB unterstützt, wird deren Verwendung eindringlich empfohlen.
Sofern sie sie nicht unterstützt, sollte eine entsprechende Aktualisierung nunmehr Erwägung gezogen werden.",
+ 'config-mysql-only-myisam-dep' => "'''Warnung:''' MyISAM ist die einzige verfügbare Speicher-Engine für MySQL, die nicht für die Verwendung mit MediaWiki empfohlen wird, da sie
+* aufgrund von Tabellensperrungen kaum die nebenläufige Ausführung von Aktionen unterstützt,
+* anfälliger für Datenprobleme ist und
+* von MediaWiki nicht immer adäquat unterstützt wird.
+
+Deine MySQL-Installation unterstützt nicht InnoDB. Eventuell muss eine Aktualisierung durchgeführt werden.",
'config-mysql-engine-help' => "'''InnoDB''' ist fast immer die bessere Wahl, da es gleichzeitige Zugriffe gut unterstützt.
'''MyISAM''' ist in Einzelnutzerumgebungen sowie bei schreibgeschützten Wikis schneller.
@@ -3949,7 +4301,6 @@ Dies ist effizienter als der UTF-8-Modus von MySQL und ermöglicht so die Verwen
Im '''UTF-8-Modus''' wird MySQL den Zeichensatz der Daten erkennen und sie richtig anzeigen und konvertieren,
allerdings können keine Zeichen außerhalb des [//de.wikipedia.org/wiki/Basic_Multilingual_Plane#Gliederung_in_Ebenen_und_Bl.C3.B6cke ''Basic Multilingual Plane'' (BMP)] gespeichert werden.",
- 'config-ibm_db2-low-db-pagesize' => "Die DB2-Datenbank verfügt über einen Standardtabellenraum mit einer unzureichenden Seitengröße. Die Seitengröße muss '''32 000'' oder größer sein.",
'config-site-name' => 'Name des Wikis:',
'config-site-name-help' => 'Er wird in der Titelleiste des Browsers, wie auch verschiedenen anderen Stellen, genutzt.',
'config-site-name-blank' => 'Den Namen des Wikis angeben.',
@@ -4001,7 +4352,7 @@ Mit MediaWiki ist es einfach die letzten Änderungen nachzuvollziehen und unbrau
Allerdings finden etliche Menschen Wikis auch mit anderen Bearbeitungskonzepten sinnvoll. Manchmal ist es zudem nicht einfach alle Beteiligten von den Vorteilen des „Wiki-Prinzips” zu überzeugen. Darum ist diese Auswahl möglich.
-Ein '''{{int:config-profile-wiki}}''' ermöglicht es jedermann, sogar ohne über ein Benutzerkonto zu verfügen, Bearbeitungen vorzunehmen.
+Das Modell „'''{{int:config-profile-wiki}}'''“ ermöglicht es jedermann, sogar ohne über ein Benutzerkonto zu verfügen, Bearbeitungen vorzunehmen.
Ein Wiki bei dem die '''{{int:config-profile-no-anon}}''' ist, fordert von den Benutzern eine höhere Verantwortung für ihre Bearbeitungen ein, könnte allerdings Personen abschrecken, die nur gelegentlich Bearbeitungen vornehmen wollen. Ein Wiki für '''{{int:config-profile-fishbowl}}''' gestattet es nur bestimmten Benutzern, Bearbeitungen vorzunehmen. Allerdings kann dabei die Allgemeinheit die Seiten immer noch betrachten und Änderungen nachvollziehen. Ein '''{{int:config-profile-private}}''' gestattet es nur ausgewählten Benutzern, Seiten zu betrachten sowie zu bearbeiten.
Komplexere Konzepte zur Zugriffssteuerung können erst nach abgeschlossenem Installationsvorgang eingerichtet werden. Hierzu gibt es weitere Informationen auf der Website mit der [//www.mediawiki.org/wiki/Manual:User_rights entsprechenden Anleitung].",
@@ -4036,11 +4387,11 @@ Für den Fall, dass die E-Mail-Funktionen nicht benötigt werden, können sie hi
'config-email-auth' => 'E-Mail-Authentifizierung ermöglichen',
'config-email-auth-help' => "Sofern diese Funktion aktiviert ist, müssen Benutzer ihre E-Mail-Adresse bestätigen, indem sie den Bestätigungslink nutzen, der ihnen immer dann zugesandt wird, wenn sie ihre E-Mail-Adresse angeben oder ändern.
Nur bestätigte E-Mail-Adressen können Nachrichten von anderen Benutzer oder Benachrichtigungsmitteilungen erhalten.
-Die Aktivierung dieser Funktion wird bei offenen Wikis, mit Hinblick auf möglichen Missbrauch der E-Mailfunktionen, '''empfohlen'''.",
+Die Aktivierung dieser Funktion wird bei offenen Wikis, mit Hinblick auf möglichen Missbrauch der E-Mail-Funktionen, '''empfohlen.'''",
'config-email-sender' => 'E-Mail-Adresse für Antworten:',
'config-email-sender-help' => 'Bitte hier die E-Mail-Adresse angeben, die als Absenderadresse bei ausgehenden E-Mails eingesetzt werden soll.
Rücklaufende E-Mails werden an diese E-Mail-Adresse gesandt.
-Bei viele E-Mail-Servern muss der Teil der E-Mail-Adresse mit der Domainangabe korrekt sein.',
+Bei vielen E-Mail-Servern muss der Teil der E-Mail-Adresse mit der Domainangabe korrekt sein.',
'config-upload-settings' => 'Hochladen von Bildern und Dateien',
'config-upload-enable' => 'Das Hochladen von Dateien ermöglichen',
'config-upload-help' => 'Das Hochladen von Dateien macht den Server für potentielle Sicherheitsprobleme anfällig.
@@ -4052,9 +4403,11 @@ Hernach kann diese Option aktiviert werden.',
'config-upload-deleted-help' => 'Bitte ein Verzeichnis auswählen, in dem gelöschte Dateien archiviert werden sollen.
Idealerweise sollte es nicht über das Internet zugänglich sein.',
'config-logo' => 'URL des Logos:',
- 'config-logo-help' => 'Die Standardoberfläche von MediaWiki verfügt links oberhalb der Seitenleiste über Platz für eine Logo mit den Maßen 135x160 Pixel.
+ 'config-logo-help' => 'Die Standardoberfläche von MediaWiki verfügt links oberhalb der Seitenleiste über Platz für ein Logo mit den Maßen 135x160 Pixel.
Bitte ein Logo in entsprechender Größe hochladen und die zugehörige URL an dieser Stelle angeben.
+Du kannst <code>$wgStylePath</code> oder <code>$wgScriptPath</code> verwenden, falls dein Logo relativ zu diesen Pfaden ist.
+
Sofern kein Logo benötigt wird, kann dieses Datenfeld leer bleiben.',
'config-instantcommons' => '„InstantCommons“ aktivieren',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons InstantCommons] ist eine Funktion, die es Wikis ermöglicht, Bild-, Klang- und andere Mediendateien zu nutzen, die auf der Website [//commons.wikimedia.org/ Wikimedia Commons] verfügbar sind.
@@ -4071,7 +4424,7 @@ Die Lizenz ist daher jetzt manuell einzugeben.',
Es wird sehr empfohlen es für mittelgroße bis große Wikis zu nutzen, aber auch für kleine Wikis ergeben sich erkennbare Geschwindigkeitsverbesserungen.',
'config-cache-none' => 'Kein Objektcaching (es wird keine Funktion entfernt, allerdings kann dies die Geschwindigkeit größerer Wikis negativ beeinflussen)',
'config-cache-accel' => 'Objektcaching von PHP (APC, XCache oder WinCache)',
- 'config-cache-memcached' => 'Memchached Cacheserver nutzen (erfordert einen zusätzliche Installationsvorgang mitsamt Konfiguration)',
+ 'config-cache-memcached' => 'Memcached Cacheserver nutzen (erfordert einen zusätzlichen Installationsvorgang mitsamt Konfiguration)',
'config-memcached-servers' => 'Memcached Cacheserver',
'config-memcached-help' => 'Liste der für Memcached nutzbaren IP-Adressen.
Es sollte eine je Zeile mitsamt des vorgesehenen Ports angegeben werden. Beispiele:
@@ -4089,7 +4442,7 @@ Es könnten zusätzliche Konfigurierungen zu einzelnen Erweiterungen erforderlic
'config-install-alreadydone' => "'''Warnung:''' Es wurde eine vorhandene MediaWiki-Installation gefunden.
Es muss daher mit den nächsten Seite weitergemacht werden.",
'config-install-begin' => 'Durch Drücken von „{{int:config-continue}}“ wird die Installation von MediaWiki gestartet.
-Sofern Änderungen vorgenommen werden sollen, kann man auf „← Zurück“ klicken.',
+Sofern Änderungen vorgenommen werden sollen, kann man auf „{{int:config-back}}“ klicken.',
'config-install-step-done' => 'erledigt',
'config-install-step-failed' => 'gescheitert',
'config-install-extensions' => 'Programmerweiterungen',
@@ -4145,9 +4498,12 @@ $3
'''Hinweis:''' Die Konfigurationsdatei sollte jetzt unbedingt heruntergeladen werden. Sie wird nach Beenden des Installationsprogramms, nicht mehr zur Verfügung stehen.
Sobald alles erledigt wurde, kann auf das '''[$2 Wiki zugegriffen werden]'''. Wir wünschen viel Spaß und Erfolg mit dem Wiki.",
- 'config-download-localsettings' => 'LocalSettings.php herunterladen',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> herunterladen',
'config-help' => 'Hilfe',
'config-nofile' => 'Die Datei „$1“ konnte nicht gefunden werden. Wurde sie gelöscht?',
+ 'config-extension-link' => 'Wusstest du, dass dein Wiki [//www.mediawiki.org/wiki/Manual:Extensions Erweiterungen] unterstützt?
+
+Du kannst [//www.mediawiki.org/wiki/Category:Extensions_by_category Erweiterungen nach Kategorie] durchsuchen oder die [//www.mediawiki.org/wiki/Extension_Matrix Erweiterungstabelle] ansehen, um eine volle Erweiterungsliste zu erhalten.',
'mainpagetext' => "'''MediaWiki wurde erfolgreich installiert.'''",
'mainpagedocfooter' => 'Hilfe zur Benutzung und Konfiguration der Wiki-Software findest du im [//meta.wikimedia.org/wiki/Help:Contents Benutzerhandbuch].
@@ -4155,7 +4511,8 @@ Sobald alles erledigt wurde, kann auf das '''[$2 Wiki zugegriffen werden]'''. Wi
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste der Konfigurationsvariablen]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Lokalisiere MediaWiki für deine Sprache]',
);
/** German (formal address) (Deutsch (Sie-Form)‎)
@@ -4167,7 +4524,7 @@ $messages['de-formal'] = array(
== Starthilfen ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste der Konfigurationsvariablen]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]', # Fuzzy
);
/** Zazaki (Zazaki)
@@ -4175,6 +4532,8 @@ $messages['de-formal'] = array(
* @author Mirzali
*/
$messages['diq'] = array(
+ 'config-title' => 'MediaWiki $1 sazkerdış',
+ 'config-information' => 'Melumat',
'config-your-language' => 'Zıwanê şıma:',
'config-wiki-language' => 'Wiki zıwan:',
'config-back' => '← Peyser',
@@ -4185,7 +4544,10 @@ $messages['diq'] = array(
'config-page-name' => 'Name',
'config-page-options' => 'Weçinegi',
'config-page-install' => 'Barine',
+ 'config-page-complete' => 'Temamyayo',
'config-page-readme' => 'Mı bıwane',
+ 'config-page-copying' => 'Kopyayeno',
+ 'config-page-upgradedoc' => 'Berzkerdış',
'config-restart' => 'E, fına dest pekê',
'config-sidebar' => "* [//www.mediawiki.org MediaWiki keye]
* [//www.mediawiki.org/wiki/Help:Contents User's Şınasiye]
@@ -4200,7 +4562,6 @@ $messages['diq'] = array(
'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 dılet',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
'config-db-port' => 'Portê database:',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-header-mysql' => 'Sazkardışê MySQL',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
@@ -4214,11 +4575,14 @@ $messages['diq'] = array(
'config-admin-name' => 'Namey şıma:',
'config-admin-password' => 'Parola:',
'config-admin-password-confirm' => 'Fına parola:',
+ 'config-admin-email' => 'Adresa e-postey:',
+ 'config-profile-private' => 'Bexse wiki',
'config-license-cc-by-sa' => 'Creative Commons Attribution Share Alike',
'config-license-cc-by' => 'Creative Commons Attribution',
'config-license-cc-by-nc-sa' => 'Creative Commons Attribution Non-Commercial Share Alike',
'config-license-pd' => 'Malê Şari',
'config-extensions' => 'Olekeni',
+ 'config-help' => 'peşti',
'mainpagetext' => "'''MediaWiki vıst ra ser, vıraziya.'''",
'mainpagedocfooter' => 'Seba gurenayış u eyarkerdışê Wiki-Softwarey [//meta.wikimedia.org/wiki/Help:Contents İdarê karberi] de mıracaet ke.
@@ -4226,20 +4590,24 @@ $messages['diq'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista eyaranê vıraştışi]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki de ÇZP]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki ra lista serbest-dayışê postey]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki ra lista serbest-dayışê postey]', # Fuzzy
);
/** Lower Sorbian (dolnoserbski)
+ * @author Michawiki
*/
$messages['dsb'] = array(
'mainpagetext' => "'''MediaWiki jo se wuspěšnje instalěrowało.'''",
'mainpagedocfooter' => "Pomoc pśi wužywanju softwary wiki namakajoš pód [//meta.wikimedia.org/wiki/Help:Contents User's Guide].
+Pomoc pśi wužywanju softwary wiki namakajoš pód [//meta.wikimedia.org/wiki/Help:Contents User's Guide].
+
== Na zachopjenje ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Konfiguracija lisćiny połoženjow]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ (pšašanja a wótegrona)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lisćina e-mailowych nakładow MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lisćina e-mailowych nakładow MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources MediaWiki za twóju rěc lokalizěrowaś]",
);
/** Central Dusun (Dusun Bundu-liwan)
@@ -4252,14 +4620,89 @@ $messages['dtp'] = array(
== Kopotimpuunan ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lis papatantu nuludan]
* [//www.mediawiki.org/wiki/Manual:FAQ Ponguhatan Koinsoruan om Simbar ModiaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lis pininsuratan pinolabus do ModiaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lis pininsuratan pinolabus do ModiaWiki]', # Fuzzy
);
/** Greek (Ελληνικά)
+ * @author Glavkos
+ * @author Protnet
+ * @author ZaDiak
*/
$messages['el'] = array(
+ 'config-desc' => 'Το πρόγραμμα εγκατάστασης για το MediaWiki',
+ 'config-title' => 'Εγκατάσταση MediaWiki $1',
+ 'config-information' => 'Πληροφορίες',
+ 'config-your-language' => 'Η γλώσσα σας:',
+ 'config-wiki-language' => 'Γλώσσα του wiki:',
+ 'config-back' => '← Πίσω',
+ 'config-continue' => 'Συνέχεια →',
+ 'config-page-language' => 'Γλώσσα',
+ 'config-page-welcome' => 'Καλώς ήλθατε στο MediaWiki!',
+ 'config-page-name' => 'Όνομα',
+ 'config-page-options' => 'Επιλογές',
+ 'config-page-install' => 'Εγκατάσταση',
+ 'config-page-complete' => 'Ολοκληρώθηκε!',
+ 'config-page-restart' => 'Επανεκκίνηση εγκατάστασης',
+ 'config-page-copying' => 'Αντιγραφή',
+ 'config-page-upgradedoc' => 'Αναβάθμιση',
+ 'config-page-existingwiki' => 'Υπάρχον βίκι',
+ 'config-restart' => 'Ναι, κάντε επανεκκίνηση',
+ 'config-env-php' => 'H PHP $1 είναι εγκατεστημένη.',
+ 'config-db-type' => 'Τύπος βάσης δεδομένων:',
+ 'config-db-host' => 'Φιλοξενία βάσης δεδομένων:',
+ 'config-db-wiki-settings' => 'Αναγνώριση αυτού του βίκι',
+ 'config-db-name' => 'Όνομα βάσης δεδομένων:',
+ 'config-db-install-account' => 'Λογαριασμός χρήστη για την εγκατάσταση',
+ 'config-db-username' => 'Όνομα χρήστη βάσης δεδομένων:',
+ 'config-db-password' => 'Κωδικός πρόσβασης βάσης δεδομένων:',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 δυαδικό',
+ 'config-header-mysql' => 'Ρυθμίσεις MySQL',
+ 'config-header-postgres' => 'Ρυθμίσεις PostgreSQL',
+ 'config-header-sqlite' => 'Ρυθμίσεις SQLite',
+ 'config-header-oracle' => 'Ρυθμίσεις Oracle',
+ 'config-invalid-db-type' => 'Μη έγκυρος τύπος βάσης δεδομένων',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-site-name' => 'Όνομα του βίκι:',
+ 'config-site-name-blank' => 'Εισαγάγετε όνομα ιστοχώρου.',
+ 'config-project-namespace' => 'Ονοματοχώρος εγχειρήματος:',
+ 'config-ns-generic' => 'Εγχείρημα',
+ 'config-ns-site-name' => 'Ίδιο με το όνομα του wiki: $1',
+ 'config-ns-other' => 'Άλλο (προσδιορίστε)',
+ 'config-admin-box' => 'Λογαριασμός διαχειριστή',
+ 'config-admin-name' => 'Το όνομά σας:',
+ 'config-admin-password' => 'Κωδικός πρόσβασης:',
+ 'config-admin-password-confirm' => 'Επανάληψη κωδικού πρόσβασης:',
+ 'config-admin-email' => 'Διεύθυνση ηλεκτρονικού ταχυδρομείου:',
+ 'config-optional-continue' => 'Να ερωτηθώ περισσότερες ερωτήσεις.',
+ 'config-profile-wiki' => 'Παραδοσιακό wiki', # Fuzzy
+ 'config-profile-no-anon' => 'Απαιτείται η δημιουργία λογαριασμού',
+ 'config-profile-private' => 'Ιδιωτικό wiki',
+ 'config-email-settings' => 'Ρυθμίσεις ηλεκτρονικού ταχυδρομείου',
+ 'config-upload-settings' => 'Ανέβασμα εικόνων και άλλων αρχείων',
+ 'config-upload-enable' => 'Ενεργοποιήστε το ανέβασμα αρχείων',
+ 'config-logo' => 'Διεύθυνση URL λογότυπου:',
+ 'config-cc-again' => 'Επιλέξτε ξανά...',
+ 'config-extensions' => 'Επεκτάσεις',
+ 'config-install-step-done' => 'έγινε',
+ 'config-install-step-failed' => 'απέτυχε',
+ 'config-help' => 'βοήθεια',
'mainpagetext' => "'''To λογισμικό MediaWiki εγκαταστάθηκε με επιτυχία.'''",
- 'mainpagedocfooter' => 'Περισσότερες πληροφορίες σχετικά με τη χρήση και με τη ρύθμιση παραμέτρων θα βρείτε στους συνδέσμους: [//meta.wikimedia.org/wiki/MediaWiki_localisation Οδηγίες για τροποποίηση του περιβάλλοντος εργασίας] και [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Εγχειρίδιο χρήστη].',
+ 'mainpagedocfooter' => 'Περισσότερες πληροφορίες σχετικά με τη χρήση και με τη ρύθμιση παραμέτρων θα βρείτε στους συνδέσμους: [//meta.wikimedia.org/wiki/MediaWiki_localisation Οδηγίες για τροποποίηση του περιβάλλοντος εργασίας] και [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Εγχειρίδιο χρήστη].', # Fuzzy
+);
+
+/** British English (British English)
+ * @author Shirayuki
+ */
+$messages['en-gb'] = array(
+ 'config-unicode-using-utf8' => "Using Brion Vibber's utf8_normalize.so for Unicode normalisation.",
+ 'config-unicode-using-intl' => 'Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalisation.',
+ 'config-unicode-pure-php-warning' => "'''Warning:''' The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalisation, falling back to slow pure-PHP implementation.
+If you run a high-traffic site, you should read a little on [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode normalisation].",
+ 'config-unicode-update-warning' => "'''Warning:''' The installed version of the Unicode normalisation wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.
+You should [//www.mediawiki.org/wiki/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
+ 'config-unknown-collation' => "'''Warning:''' Database is using unrecognised collation.",
+ 'config-profile-fishbowl' => 'Authorised editors only',
+ 'config-install-stats' => 'Initialising statistics',
);
/** Esperanto (Esperanto)
@@ -4284,19 +4727,22 @@ $messages['eo'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Listo de konfiguraĵoj] (angla)
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki Oftaj Demandoj] (angla)
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki dissendolisto pri anoncoj] (angla)",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki dissendolisto pri anoncoj] (angla)", # Fuzzy
);
/** Spanish (español)
* @author Armando-Martin
+ * @author Ciencia Al Poder
* @author Crazymadlover
* @author Danke7
+ * @author Fitoschido
* @author Locos epraix
* @author Od1n
* @author Platonides
* @author Sanbec
* @author Translationista
* @author Vivaelcelta
+ * @author 아라
*/
$messages['es'] = array(
'config-desc' => 'El instalador para MediaWiki',
@@ -4304,19 +4750,19 @@ $messages['es'] = array(
'config-information' => 'Información',
'config-localsettings-upgrade' => 'Se ha encontrado un archivo <code>LocalSettings.php</code>.
Para actualizar esta instalación, por favor ingresa el valor de <code>$wgUpgradeKey</code> en el cuadro de abajo.
-Lo encontrarás en LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Se ha detectado un archivo LocalSettings.php.
-Para actualizar la instalación, vuelva a ejecutar update.php',
+Lo encontrarás en <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Se ha detectado un archivo <code>LocalSettings.php</code>.
+Para actualizar la instalación, vuelva a ejecutar <code>update.php</code>',
'config-localsettings-key' => 'Clave de actualización:',
'config-localsettings-badkey' => 'La clave proporcionada es incorrecta.',
'config-upgrade-key-missing' => 'Se ha detectado una instalación existente de MediaWiki.
-Para actualizar la instalación, por favor, ponga la siguiente línea al final de su archivo LocalSettings.php:
+Para actualizar la instalación, por favor, ponga la siguiente línea al final de su archivo <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'El archivo LocalSettings.php existente parece estar incompleto.
+ 'config-localsettings-incomplete' => 'El archivo <code>LocalSettings.php</code> existente parece estar incompleto.
La variable $1 no está definida.
-Cambie el archivo LocalSettings.php para que esta variable quede establecida y haga clic en "Continuar".',
- 'config-localsettings-connection-error' => 'Se detectó un error al conectarse a la base de datos utilizando la configuración especificada en los archivos LocalSettings.php o AdminSettings.php. Corrija estas opciones y vuelva a intentarlo.
+Cambie el archivo <code>LocalSettings.php</code> para que esta variable quede establecida y haga clic en "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Se detectó un error al conectarse a la base de datos utilizando la configuración especificada en los archivos <code>LocalSettings.php</code> o <code>AdminSettings.php</code>. Corrija estas opciones y vuelva a intentarlo.
$1',
'config-session-error' => 'Error comenzando sesión: $1',
@@ -4351,7 +4797,7 @@ Verifica tu php.ini y comprueba que <code>session.save_path</code> está estable
'config-restart' => 'Sí, reiniciarlo',
'config-welcome' => '=== Comprobación del entorno ===
Se realiza comprobaciones básicas para ver si el entorno es adecuado para la instalación de MediaWiki.
-Deberás suministrar los resultados de tales comprobaciones si necesitas ayuda durante la instalación.',
+Deberás suministrar los resultados de tales comprobaciones si necesitas ayuda durante la instalación.', # Fuzzy
'config-copyright' => "=== Derechos de autor y Términos de uso ===
$1
@@ -4428,6 +4874,7 @@ El caché de objetos no está habilitado.",
'config-mod-security' => "''' Advertencia ''': Su servidor web tiene [http://modsecurity.org/ mod_security] habilitado. Si la configuración es incorrecta, puede causar problemas a MediaWiki u otro software que permita a los usuarios publicar contenido arbitrarios.
Consulte la [http://modsecurity.org/documentation/ documentación de mod_security] o contacte con el soporte de su servidor (''host'') si encuentra errores aleatorios.",
'config-diff3-bad' => 'GNU diff3 no se encuentra.',
+ 'config-git-bad' => 'No se encontró el software de control de versiones Git.',
'config-imagemagick' => 'ImageMagick encontrado: <code>$1</code>.
La miniaturización de imágenes se habilitará si habilitas las cargas.',
'config-gd' => 'Se ha encontrado una biblioteca de gráficos GD integrada.
@@ -4449,7 +4896,9 @@ Instalación anulada.',
'config-using531' => 'MediaWiki no puede utilizarse con PHP $1 debido a un error con los parámetros de referencia para <code>__call()</code> .
Actualice el sistema a PHP 5.3.2 o superior, o vuelva a la versión PHP 5.3.0 para resolver este problema.
Instalación anulada.',
- 'config-suhosin-max-value-length' => 'Suhosin está instalado y limita la longitud del parámetro GET a $1 bytes. El componente ResourceLoader de MediaWiki trabajará en este límite, pero eso degradará el rendimiento. Si es posible, debe establecer el valor de suhosin.get.max_value_length en 1024 o superior en el archivo php.ini y establecer $wgResourceLoaderMaxQueryLength en el mismo valor en LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Suhosin está instalado y limita el parámetro <code>length</code> GET a $1 bytes.
+El componente ResourceLoader (gestor de recursos) de MediaWiki trabajará en este límite, pero eso perjudicará el rendimiento.
+Si es posible, deberías establecer <code>suhosin.get.max_value_length</code> en el valor 1024 o superior en <code>php.ini</code> y establecer <code>$wgResourceLoaderMaxQueryLength</code> en el mismo valor en <code>php.ini</code>.',
'config-db-type' => 'Tipo de base de datos',
'config-db-host' => 'Servidor de la base de datos:',
'config-db-host-help' => 'Si su servidor de base de datos está en otro servidor, escriba el nombre del host o su dirección IP aquí.
@@ -4522,7 +4971,6 @@ Considere la posibilidad de poner la base de datos en algún otro sitio, por eje
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki es compatible con los siguientes sistemas de bases de datos:
$1
@@ -4530,19 +4978,17 @@ $1
Si no encuentras en el listado el sistema de base de datos que estás intentando utilizar, sigue las instrucciones vinculadas arriba para habilitar la compatibilidad.',
'config-support-mysql' => '* $1 es la base de datos mayoritaria para MediaWiki y la que goza de mayor compatibilidad ([http://www.php.net/manual/es/mysql.installation.php cómo compilar PHP con compatibilidad MySQL])',
'config-support-postgres' => '$1 es un popular sistema de base de datos de código abierto, alternativa a MySQL. ([http://www.php.net/manual/es/pgsql.installation.php cómo compilar PHP con compatibilidad PostgreSQL]). Puede haber algunos defectos menores destacables, y no es recomendable para uso en un entorno de producción.',
- 'config-support-sqlite' => '* $1 es una base de datos ligera con gran compatibilidad con MediaWiki. ([http://www.php.net/manual/es/pdo.installation.php Cómo compilar PHP con compatibilidad SQLite], usa PDO)',
+ 'config-support-sqlite' => '* $1 es una base de datos ligera con gran compatibilidad con MediaWiki ([http://www.php.net/manual/es/pdo.installation.php cómo compilar PHP con compatibilidad SQLite usando PDO]).',
'config-support-oracle' => '* $1 es una base de datos comercial a nivel empresarial ([http://www.php.net/manual/es/oci8.installation.php cómo compilar PHP con compatibilidad con OCI8])',
- 'config-support-ibm_db2' => ' $1 es una base de datos de empresa comercial.',
'config-header-mysql' => 'Configuración de MySQL',
'config-header-postgres' => 'Configuración de PostgreSQL',
'config-header-sqlite' => 'Configuración de SQLite',
'config-header-oracle' => 'Configuración de Oracle',
- 'config-header-ibm_db2' => 'Configuración de IBM DB2',
'config-invalid-db-type' => 'Tipo de base de datos inválida',
'config-missing-db-name' => 'Debes introducir un valor para "Nombre de la base de datos"',
'config-missing-db-host' => 'Debe introducir un valor para "Servidor (host) de base de datos"',
'config-missing-db-server-oracle' => 'Debe introducir un valor para "TNS de la base de datos"',
- 'config-invalid-db-server-oracle' => 'El TNS de la base de datos, "$1", es inválido.Use sólo carateres ASCII: letras (a-z, A-Z), números (0-9), guiones bajos (_) y guiones (-).Usa sólo caracteres ASCII: letras (a-z, A-Z), dígitos (0-9), guiones bajos (_) y puntos (.).',
+ 'config-invalid-db-server-oracle' => 'El TNS de la base de datos, "$1", es inválido.Use sólo carateres ASCII: letras (a-z, A-Z), números (0-9), guiones bajos (_) y guiones (-).Usa sólo caracteres ASCII: letras (a-z, A-Z), dígitos (0-9), guiones bajos (_) y puntos (.).', # Fuzzy
'config-invalid-db-name' => 'El nombre de la base de datos "$1" es inválido.
Usa sólo caracteres ASCII: letras (a-z, A-Z), números (0-9), guiones bajos (_)y guiones (-).',
'config-invalid-db-prefix' => 'El prefijo de la base de datos "$1" es inválido.
@@ -4599,7 +5045,7 @@ Esto '''no se recomienda''' a menos que esté teniendo problemas con su wiki.",
Usted puede ahora [$1 empezar a usar su wiki].',
'config-regenerate' => 'Regenerar LocalSettings.php →',
- 'config-show-table-status' => 'SHOW TABLE STATUS ha fallado!',
+ 'config-show-table-status' => '<code>SHOW TABLE STATUS</code> ha fallado!',
'config-unknown-collation' => "'''Advertencia:''' La base de datos está utilizando una intercalación no reconocida.",
'config-db-web-account' => 'Cuenta de base de datos para acceso Web',
'config-db-web-help' => 'Elige el usuario y contraseña que el servidor Web usará para conectarse al servidor de la base de datos durante el fincionamiento normal del wiki.',
@@ -4628,7 +5074,6 @@ Las bases de datos MyISAM tienden a corromperse más a menudo que las bases de d
Esto es más eficiente que el modo UTF-8 de MySQL y le permite utilizar la gama completa de caracteres Unicode.
En '''modo UTF-8''', MySQL sabrá qué conjunto de caracteres emplean sus datos y puede presentarlos y convertirlos adecuadamente, pero no le permitirá almacenar caracteres por encima del [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes plano multilingüe básico].",
- 'config-ibm_db2-low-db-pagesize' => "Su base de datos DB2 tiene un espacio de tablas por defecto con un tamaño de página insuficiente. El tamaño de página tiene que ser '''32 K''' o superior.",
'config-site-name' => 'Nombre del wiki:',
'config-site-name-help' => 'Esto aparecerá en la barra de título del navegador y en varios otros lugares.',
'config-site-name-blank' => 'Ingresar un nombre de sitio.',
@@ -4672,7 +5117,7 @@ Ahora puedes saltarte el resto de pasos e instalar el wiki con valores predeterm
'config-optional-continue' => 'Hazme más preguntas.',
'config-optional-skip' => 'Ya estoy aburrido, sólo instala el wiki.',
'config-profile' => 'Perfil de derechos de usuario:',
- 'config-profile-wiki' => 'Wiki tradicional',
+ 'config-profile-wiki' => 'Wiki abierto',
'config-profile-no-anon' => 'Creación de cuenta requerida',
'config-profile-fishbowl' => 'Sólo editores autorizados',
'config-profile-private' => 'Wiki privado',
@@ -4687,7 +5132,7 @@ Un wiki con '''{{int:config-profile-no-anon}}''' ofrece rendición de cuentas ad
El escenario '''{{int:config-profile-fishbowl}}''' permite editar a los usuarios autorizados, pero el público puede ver las páginas, incluyendo el historial.
Un '''{{int:config-profile-private}}''' sólo permite ver páginas a los usuarios autorizados, el mismo grupo al que le está permitido editar.
-Configuraciones más complejas de derechos de usuario están disponibles después de la instalación, consulte [//www.mediawiki.org/wiki/Manual:User_rights esta entrada en el manual].",
+Configuraciones más complejas de derechos de usuario están disponibles después de la instalación, consulte [//www.mediawiki.org/wiki/Manual:User_rights esta entrada en el manual].", # Fuzzy
'config-license' => 'Copyright and licencia:',
'config-license-none' => 'Pie sin licencia',
'config-license-cc-by-sa' => 'Creative Commons Reconocimiento Compartir Igual',
@@ -4731,14 +5176,14 @@ Para obtener más información, lea la [//www.mediawiki.org/wiki/Manual:Security
Para habilitar la carga de archivos, cambie el modo en el subdirectorio <code>images</code> bajo el directorio raíz de MediaWiki para que el servidor web pueda escribir en él.
A continuación, habilite esta opción.',
- 'config-upload-deleted' => '*Directório para los archivos eliminados:',
+ 'config-upload-deleted' => '*Directorio para los archivos eliminados:',
'config-upload-deleted-help' => 'Elige un directorio en el que guardar los archivos eliminados.
Lo ideal es una carpeta no accesible desde la red.',
'config-logo' => 'URL del logo :',
'config-logo-help' => 'La apariencia por defecto de MediaWiki incluye espacio para un logotipo de 135x160 píxeles encima del menú de la barra lateral.
Cargue una imagen de tamaño adecuado e introduzca la dirección URL aquí.
-Si no desea un logotipo, deje esta casilla en blanco.',
+Si no desea un logotipo, deje esta casilla en blanco.', # Fuzzy
'config-instantcommons' => 'Habilitar Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] es una característica que permite que los wikis puedan utilizar imágenes, sonidos y otros archivos multimedia que se encuentran en el sitio [//commons.wikimedia.org/ Wikimedia Commons].
Para ello, MediaWiki requiere acceso a Internet.
@@ -4772,7 +5217,7 @@ Puede que necesiten configuraciones adicionales, pero puedes habilitarlas ahora.
'config-install-alreadydone' => "'''Aviso:''' Parece que ya habías instalado MediaWiki y estás intentando instalarlo nuevamente.
Pasa a la próxima página, por favor.",
'config-install-begin' => 'Pulsando "{{int:config-continue}}", se iniciará la instalación de MediaWiki.
-Si todavía desea realizar algún cambio, pulse atrás.',
+Si todavía desea realizar algún cambio, pulse atrás.', # Fuzzy
'config-install-step-done' => 'hecho',
'config-install-step-failed' => 'falló',
'config-install-extensions' => 'Extensiones inclusive',
@@ -4826,16 +5271,17 @@ $3
'''Nota''': Si no haces esto ahora, este archivo de configuración generado no estará disponible para usted más tarde si sale de la instalación sin descargarlo.
Cuando lo haya hecho, usted puede '''[$2 entrar en su wiki]'''.",
- 'config-download-localsettings' => 'Descargar archivo LocalSettings.php',
+ 'config-download-localsettings' => 'Descargar archivo <code>LocalSettings.php</code>',
'config-help' => 'Ayuda',
'config-nofile' => 'El archivo "$1" no se pudo encontrar. ¿Se ha eliminado?',
'mainpagetext' => "'''MediaWiki ha sido instalado con éxito.'''",
- 'mainpagedocfooter' => 'Consulta la [//meta.wikimedia.org/wiki/Ayuda:Contenido Guía de usuario] para obtener información sobre el uso del software wiki.
+ 'mainpagedocfooter' => 'Consulta la [//meta.wikimedia.org/wiki/Ayuda:Guía del usuario de contenidos] para obtener información sobre el uso del software wiki.
== Empezando ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de ajustes de configuración]
* [//www.mediawiki.org/wiki/Manual:FAQ/es FAQ de MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de distribución de MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de distribución de MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Regionalizar MediaWiki para tu idioma]',
);
/** español (formal) (español (formal))
@@ -4847,31 +5293,79 @@ $messages['es-formal'] = array(
== Empezando ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de ajustes de configuración]
* [//www.mediawiki.org/wiki/Manual:FAQ/es FAQ de MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de distribución de MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de distribución de MediaWiki]', # Fuzzy
);
/** Estonian (eesti)
* @author Avjoska
+ * @author Pikne
*/
$messages['et'] = array(
+ 'config-information' => 'Teave',
+ 'config-session-error' => 'Tõrge seansi alustamisel: $1',
+ 'config-your-language' => 'Oma keel:',
+ 'config-wiki-language' => 'Viki keel:',
'config-back' => '← Tagasi',
'config-continue' => 'Jätka →',
'config-page-language' => 'Keel',
'config-page-welcome' => 'Tere tulemast MediaWikisse!',
+ 'config-page-dbconnect' => 'Andmebaasiga ühendamine',
+ 'config-page-upgrade' => 'Olemasoleva installi uuendus',
+ 'config-page-dbsettings' => 'Andmebaasi sätted',
'config-page-name' => 'Nimi',
'config-page-options' => 'Seaded',
'config-page-install' => 'Paigaldamine',
'config-page-complete' => 'Valmis!',
+ 'config-page-restart' => 'Alusta installimist uuesti',
+ 'config-page-readme' => 'Loe mind',
+ 'config-page-copying' => 'Kopeerimine',
+ 'config-page-upgradedoc' => 'Uuendamine',
+ 'config-page-existingwiki' => 'Olemasolev viki',
+ 'config-restart' => 'Jah, tee taaskäivitus',
'config-db-name' => 'Andmebaasi nimi:',
'config-db-username' => 'Andmebaasi kasutajanimi:',
'config-db-password' => 'Andmebaasi parool:',
+ 'config-db-port' => 'Andmebaasi port:',
+ 'config-invalid-db-type' => 'Vigane andmebaasi tüüp',
+ 'config-site-name' => 'Viki nimi:',
+ 'config-site-name-blank' => 'Sisestage lehekülje nimi.',
+ 'config-project-namespace' => 'Projekti nimeruum:',
+ 'config-ns-generic' => 'Projekt',
+ 'config-admin-box' => 'Administraatorikonto',
+ 'config-admin-name' => 'Sinu nimi:',
+ 'config-admin-password' => 'Parool:',
+ 'config-admin-password-confirm' => 'Parool uuesti:',
+ 'config-admin-name-blank' => 'Sisesta administraatori kasutajanimi.',
+ 'config-admin-password-blank' => 'Sisesta administraatorikonto parool.',
+ 'config-admin-password-same' => 'Parool ei tohi kattuda kasutajanimega.',
+ 'config-admin-password-mismatch' => 'Sisestatud kaks parooli ei lange kokku.',
'config-admin-email' => 'E-posti aadress:',
+ 'config-admin-error-bademail' => 'Sisestasid vigase e-posti aadressi.',
'config-optional-continue' => 'Küsi minult veel küsimusi.',
+ 'config-profile-private' => 'Eraviki',
+ 'config-license' => 'Autoriõigus ja litsents:',
+ 'config-license-none' => 'Litsentsijaluseta',
+ 'config-license-cc-by-sa' => 'Creative Commonsi litsents "Autorile viitamine + jagamine samadel tingimustel"',
+ 'config-license-cc-by' => 'Creative Commonsi litsents "Autorile viitamine"',
+ 'config-license-cc-by-nc-sa' => 'Creative Commonsi litsents "Autorile viitamine + mitteäriline eesmärk + jagamine samadel tingimustel"',
+ 'config-email-settings' => 'E-posti sätted',
+ 'config-email-sender' => 'Saatja e-aadress:',
+ 'config-logo' => 'Logo internetiaadress:',
+ 'config-cc-again' => 'Vali uuesti...',
+ 'config-extensions' => 'Lisad',
'config-install-step-done' => 'valmis',
'config-install-step-failed' => 'ebaõnnestus',
+ 'config-install-user-alreadyexists' => 'Kasutaja "$1" on juba olemas',
+ 'config-install-tables' => 'Tabelite loomine',
+ 'config-help' => 'abi',
'mainpagetext' => "'''MediaWiki tarkvara on edukalt paigaldatud.'''",
- 'mainpagedocfooter' => 'Juhiste saamiseks kasutamise ning konfigureerimise kohta vaata palun inglisekeelset [//meta.wikimedia.org/wiki/MediaWiki_localisation dokumentatsiooni liidese kohaldamisest]
-ning [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide kasutusjuhendit].',
+ 'mainpagedocfooter' => 'Vikitarkvara kasutamise kohta leiad lisateavet [//meta.wikimedia.org/wiki/Help:Contents juhendist].
+
+== Alustamine ==
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Häälestussätete loend]
+* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki KKK]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki versiooniuuenduste postiloend]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources MediaWiki lokaliseerimine]',
);
/** Basque (euskara)
@@ -4944,7 +5438,7 @@ $messages['eu'] = array(
'config-admin-password' => 'Pasahitza:',
'config-admin-password-confirm' => 'Pasahitza berriz:',
'config-admin-email' => 'E-posta helbidea:',
- 'config-profile-wiki' => 'Wiki tradizionala',
+ 'config-profile-wiki' => 'Wiki tradizionala', # Fuzzy
'config-profile-private' => 'Wiki pribatua',
'config-license' => 'Copyright eta lizentzia:',
'config-license-pd' => 'Domeinu Askea',
@@ -4960,7 +5454,7 @@ $messages['eu'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Konfigurazio balioen zerrenda]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ (Maiz egindako galderak)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]', # Fuzzy
);
/** Extremaduran (estremeñu)
@@ -4973,7 +5467,7 @@ $messages['ext'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Persian (فارسی)
@@ -4983,18 +5477,22 @@ $messages['fa'] = array(
'config-desc' => 'نصب کنندهٔ ویکی‌مدیا',
'config-title' => 'نصب ویکی‌مدیا $1',
'config-information' => 'اطلاعات',
+ 'config-localsettings-key' => 'کلید ارتقا:',
'config-your-language' => 'زبان شما:',
'config-wiki-language' => 'زبان ویکی:',
'config-back' => '→ بازگشت',
'config-continue' => 'ادامه ←',
'config-page-language' => 'زبان',
'config-page-welcome' => 'به مدیاویکی خوش آمدید!',
+ 'config-page-dbconnect' => 'اتصال به پایگاه داده',
'config-page-name' => 'نام',
'config-page-options' => 'گزینه‌ها',
'config-page-install' => 'نصب',
'config-page-complete' => 'کامل!',
'config-page-readme' => 'مرا بخوان',
'config-page-releasenotes' => 'یادداشت‌های انتشار',
+ 'config-page-copying' => 'تکثیر',
+ 'config-page-upgradedoc' => 'ارتقا',
'config-page-existingwiki' => 'ویکی موجود',
'config-restart' => 'بله ، آن دوباره راه اندازی کن',
'config-sidebar' => '* [//www.mediawiki.org صفحهٔ اصلی مدیاویکی]
@@ -5013,13 +5511,20 @@ $messages['fa'] = array(
'config-db-host' => 'میزبان پایگاه اطلاعات:',
'config-db-username' => 'نام کاربری پایگاه اطلاعات:',
'config-db-password' => 'کلمه عبور پایگاه اطلاعات:',
+ 'config-mysql-old' => 'مای‌اس‌کیو‌ال نسخهٔ $1 و یا بالاتر نیاز است، شما نسخهٔ $2 را دارید.',
+ 'config-db-port' => 'درگاه پایگاه‌داده:',
'config-header-mysql' => 'تنظیمات مای‌اس‌کیو‌ال',
'config-connection-error' => '$1.
میزبان، نام کاربری و گذرواژه را بررسی کنید و دوباره امتحان کنید.',
+ 'config-mysql-binary' => 'دودویی',
+ 'config-mysql-utf8' => 'UTF-8',
'config-site-name' => 'نام ویکی:',
'config-site-name-blank' => 'نام تارنما را وارد کنید.',
'config-project-namespace' => 'فضای نام پروژه:',
+ 'config-ns-generic' => 'پروژه',
+ 'config-ns-other-default' => 'ویکی‌من',
+ 'config-admin-box' => 'حساب مدیر سیستم',
'config-admin-name' => 'نام شما:',
'config-admin-password' => 'کلمه عبور:',
'config-admin-password-confirm' => 'دوباره کلمه عبور:',
@@ -5032,6 +5537,7 @@ $messages['fa'] = array(
'config-email-settings' => 'تنظیمات پست الکترونیکی',
'config-upload-enable' => 'فعال سازی بارگذاری پرونده',
'config-logo' => 'نشانی نامواره:',
+ 'config-extensions' => 'افزونه‌ها',
'config-install-step-done' => 'انجام شد',
'config-install-step-failed' => 'ناموفق بود',
'config-help' => 'راهنما',
@@ -5043,7 +5549,7 @@ $messages['fa'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings تنظیم پیکربندی]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki پرسش‌های متداول]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce فهرست پست الکترونیکی نسخه‌های مدیاویکی]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce فهرست پست الکترونیکی نسخه‌های مدیاویکی]', # Fuzzy
);
/** Finnish (suomi)
@@ -5052,7 +5558,10 @@ $messages['fa'] = array(
* @author Crt
* @author Nike
* @author Olli
+ * @author Silvonen
* @author Str4nd
+ * @author VezonThunder
+ * @author 아라
*/
$messages['fi'] = array(
'config-desc' => 'MediaWiki-asennin',
@@ -5060,19 +5569,28 @@ $messages['fi'] = array(
'config-information' => 'Tiedot',
'config-localsettings-upgrade' => '<code>LocalSettings.php</code>-tiedosto havaittiin.
Kirjoita muuttujan <code>$wgUpgradeKey</code> arvo alla olevaan kenttään päivittääksesi asennuksen.
-Löydät sen LocalSettings.php-tiedostosta.',
- 'config-localsettings-cli-upgrade' => 'LocalSettings.php-tiedosto havaittiin.
-Päivitä asennus suorittamalla update.php.',
+Löydät sen <code>LocalSettings.php</code>-tiedostosta.',
+ 'config-localsettings-cli-upgrade' => '<code>LocalSettings.php</code>-tiedosto havaittiin.
+Päivitä asennus suorittamalla <code>update.php</code>.',
'config-localsettings-key' => 'Päivitysavain',
'config-localsettings-badkey' => 'Antamasi avain on virheellinen.',
- 'config-localsettings-incomplete' => 'Nykyinen LocalSettings.php-tiedosto näyttää olevan puutteellinen.
+ 'config-upgrade-key-missing' => 'Havaittiin aiempi MediaWiki-asennus.
+Päivittääksesi tämän asennuksen lisää <code>LocalSettings.php</code>-tiedostosi loppuun seuraava rivi:
+
+$1',
+ 'config-localsettings-incomplete' => 'Nykyinen <code>LocalSettings.php</code>-tiedosto näyttää olevan puutteellinen.
Muuttujaa $1 ei ole asetettu.
-Muuta LocalSettings.php-tiedostoa siten, että muuttuja on asetettu ja napsauta »Jatka».',
+Muuta <code>LocalSettings.php</code>-tiedostoa siten, että muuttuja on asetettu ja napsauta »{{int:Config-continue}}».',
+ 'config-localsettings-connection-error' => 'Virhe yhdistettäessä tietokantaan käyttäen tiedostossa <code>LocalSettings.php</code> tai <code>AdminSettings.php</code> määritettyjä asetuksia. Korjaa asetukset ja yritä uudelleen.
+
+$1',
'config-session-error' => 'Istunnon aloittaminen epäonnistui: $1',
'config-session-expired' => 'Istuntotietosi näyttävät olevan vanhentuneita.
Istuntojen elinajaksi on määritelty $1.
Voit muuttaa tätä asetusta vaihtamalla kohtaa <code>session.gc_maxlifetime</code> php.ini-tiedostossa.
Käynnistä asennusprosessi uudelleen.',
+ 'config-no-session' => 'Istuntosi tiedot menetettiin!
+Tarkista php.ini-tiedostosi ja varmista, että <code>session.save_path</code> on asetettu sopivaan kansioon.',
'config-your-language' => 'Asennuksen kieli',
'config-your-language-help' => 'Valitse kieli, jota haluat käyttää asennuksen ajan.',
'config-wiki-language' => 'Wikin kieli',
@@ -5136,6 +5654,7 @@ Asennus saattaa epäonnistua!",
'config-db-install-help' => 'Anna käyttäjätunnus ja salasana, joita käytetään asennuksen aikana.',
'config-db-account-lock' => 'Käytä samaa tunnusta ja salasanaa myös asennuksen jälkeen',
'config-db-prefix' => 'Tietokantataulujen etuliite',
+ 'config-db-charset' => 'Tietokannan merkistö',
'config-charset-mysql5-binary' => 'MySQL 4.1/5.0, binääri',
'config-charset-mysql5' => 'MySQL 4.1/5.0, UTF-8',
'config-charset-mysql4' => 'MySQL 4.0, taaksepäin yhteensopiva UTF-8',
@@ -5144,13 +5663,10 @@ Asennus saattaa epäonnistua!",
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
- 'config-support-ibm_db2' => '* $1 on kaupallinen tietokanta yrityskäyttöön.',
'config-header-mysql' => 'MySQL-asetukset',
'config-header-postgres' => 'PostgreSQL-asetukset',
'config-header-sqlite' => 'SQLite-asetukset',
'config-header-oracle' => 'Oracle-asetukset',
- 'config-header-ibm_db2' => 'IBM DB2 -asetukset',
'config-invalid-db-type' => 'Virheellinen tietokantatyyppi',
'config-missing-db-name' => 'Kenttä »Tietokannan nimi» on pakollinen',
'config-invalid-db-name' => '”$1” ei kelpaa tietokannan nimeksi.
@@ -5175,7 +5691,7 @@ Tämä '''ei ole suositeltavaa''', jos wikissäsi ei ole ongelmia.",
Voit [$1 aloittaa wikin käytön].',
'config-regenerate' => 'Luo LocalSettings.php uudelleen →',
- 'config-show-table-status' => 'Kysely SHOW TABLE STATUS epäonnistui!',
+ 'config-show-table-status' => 'Kysely <code>SHOW TABLE STATUS</code> epäonnistui!',
'config-mysql-engine' => 'Tallennusmoottori',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
@@ -5195,7 +5711,7 @@ Voit [$1 aloittaa wikin käytön].',
'config-admin-error-bademail' => 'Annoit virheellisen sähköpostiosoitteen.',
'config-almost-done' => 'Olet jo lähes valmis!
Voit ohittaa jäljellä olevat määritykset ja asentaa wikin juuri nyt.',
- 'config-profile-wiki' => 'Perinteinen wiki',
+ 'config-profile-wiki' => 'Avoin wiki',
'config-profile-no-anon' => 'Tunnuksen luonti vaaditaan',
'config-profile-private' => 'Yksityinen wiki',
'config-license' => 'Tekijänoikeus ja lisenssi:',
@@ -5208,7 +5724,7 @@ Voit ohittaa jäljellä olevat määritykset ja asentaa wikin juuri nyt.',
'config-install-step-failed' => 'epäonnistui',
'config-install-user-alreadyexists' => 'Käyttäjä $1 on jo olemassa',
'config-install-interwiki-list' => 'Tiedostoa <code>interwiki.list</code> ei voitu lukea.',
- 'config-download-localsettings' => 'Lataa LocalSettings.php',
+ 'config-download-localsettings' => 'Lataa <code>LocalSettings.php</code>',
'config-help' => 'ohje',
'mainpagetext' => "'''MediaWiki on onnistuneesti asennettu.'''",
'mainpagedocfooter' => "Lisätietoja käytöstä on sivulla [//meta.wikimedia.org/wiki/Help:Contents User's Guide].
@@ -5253,6 +5769,7 @@ $messages['fo'] = array(
* @author Verdy p
* @author Wyz
* @author Yumeki
+ * @author 아라
*/
$messages['fr'] = array(
'config-desc' => 'Le programme d’installation de MediaWiki',
@@ -5260,20 +5777,20 @@ $messages['fr'] = array(
'config-information' => 'Informations',
'config-localsettings-upgrade' => 'Un fichier <code>LocalSettings.php</code> a été détecté.
Pour mettre à jour cette installation, veuillez saisir la valeur de <code>$wgUpgradeKey</code> dans le champ ci-dessous.
-Vous la trouverez dans LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Un fichier LocalSettings.php a été détecté.
-Pour mettre à niveau cette installation, veuillez exécuter update.php',
+Vous la trouverez dans <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Un fichier <code>LocalSettings.php</code> a été détecté.
+Pour mettre à niveau cette installation, veuillez exécuter <code>update.php</code>',
'config-localsettings-key' => 'Clé de mise à jour :',
'config-localsettings-badkey' => 'La clé que vous avez fournie est incorrecte',
'config-upgrade-key-missing' => 'Une installation existante de MediaWiki a été détectée.
-Pour mettre à jour cette installation, veuillez ajouter la ligne suivante à la fin de votre fichier LocalSettings.php
+Pour mettre à jour cette installation, veuillez ajouter la ligne suivante à la fin de votre fichier <code>LocalSettings.php</code>
$1',
- 'config-localsettings-incomplete' => 'Le fichier LocalSettings.php existant semble être incomplet.
+ 'config-localsettings-incomplete' => 'Le fichier <code>LocalSettings.php</code> existant semble être incomplet.
La variable $1 n’est pas définie.
-Veuillez modifier LocalSettings.php de sorte que cette variable soit définie, puis cliquer sur « Continuer ».',
- 'config-localsettings-connection-error' => 'Une erreur est survenue lors de la connexion à la base de données en utilisant la configuration spécifiée dans LocalSettings.php ou AdminSettings.php. Veuillez corriger cette configuration puis réessayer.
+Veuillez modifier <code>LocalSettings.php</code> de sorte que cette variable soit définie, puis cliquer sur « {{int:Config-continue}} ».',
+ 'config-localsettings-connection-error' => 'Une erreur est survenue lors de la connexion à la base de données en utilisant la configuration spécifiée dans <code>LocalSettings.php</code> ou <code>AdminSettings.php</code>. Veuillez corriger cette configuration puis réessayer.
$1',
'config-session-error' => 'Erreur lors du démarrage de la session : $1',
@@ -5306,9 +5823,9 @@ Vérifiez votre fichier php.ini et assurez-vous que <code>session.save_path</cod
'config-page-existingwiki' => 'Wiki existant',
'config-help-restart' => "Voulez-vous effacer toutes les données enregistrées que vous avez entrées et relancer le processus d'installation ?",
'config-restart' => 'Oui, le relancer',
- 'config-welcome' => "=== Vérifications liées à l’environnement ===
-Des vérifications de base sont effectuées pour voir si cet environnement est adapté à l'installation de MediaWiki.
-Vous devriez indiquer les résultats de ces vérifications si vous avez besoin d’aide lors de l’installation.",
+ 'config-welcome' => '=== Vérifications liées à l’environnement ===
+Des vérifications de base vont maintenant être effectuées pour voir si cet environnement est adapté à l’installation de MediaWiki.
+Rappelez-vous d’inclure ces informations si vous recherchez de l’aide sur la manière de terminer l’installation.',
'config-copyright' => "=== Droit d'auteur et conditions ===
$1
@@ -5377,6 +5894,10 @@ MédiaWiki nécessite la gestion d’UTF-8 pour fonctionner correctement.",
Cette valeur est probablement trop faible.
Il est possible que l’installation échoue !",
'config-ctype' => "'''Fatal ''': PHP doit être compilé avec le support pour l'[http://www.php.net/manual/en/ctype.installation.php extension Ctype].",
+ 'config-json' => "'''Erreur fatale :''' PHP a été compilé sans le support de JSON.
+Vous devez soit installez l’extension JSON de PHP ou l’extension [http://pecl.php.net/package/jsonc PECL jsonc] avant d’installer MediaWiki.
+* L’extension PHP est comprise dans Red Hat Enterprise Linux (CentOS) 5 et 6, mais doit être activée dans <code>/etc/php.ini</code> ou <code>/etc/php.d/json.ini</code>.
+* Certaines distributions Linux après mai 2013 ne comprennent pas l’extension PHP, mais oint mis à la place l’extension PECL sous al forme <code>php5-json</code> ou <code>php-pecl-jsonc</code>.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] est installé',
'config-apc' => '[http://www.php.net/apc APC] est installé',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] est installé',
@@ -5385,6 +5906,8 @@ La mise en cache d'objets n'est pas activée.",
'config-mod-security' => "'''Attention''': Votre serveur web a [http://modsecurity.org/ mod_security] activé. S&il est mal configuré, cela peut poser des problèmes à MediaWiki ou à d'autres applications qui permettent aux utilisateurs de publier un contenu quelconque.
Reportez-vous à [http://modsecurity.org/documentation/ la documentation de mod_security] ou contactez le support de votre hébergeur si vous rencontrez des erreurs aléatoires.",
'config-diff3-bad' => 'GNU diff3 introuvable.',
+ 'config-git' => 'Logiciel de contrôle de version Git trouvé : <code>$1</code>.',
+ 'config-git-bad' => 'Logiciel de contrôle de version Git non trouvé.',
'config-imagemagick' => "ImageMagick trouvé : <code>$1</code>.
La miniaturisation d'images sera activée si vous activez le téléversement de fichiers.",
'config-gd' => "La bibliothèque graphique GD intégrée a été trouvée.
@@ -5405,7 +5928,8 @@ Installation interrompue.',
'config-using531' => 'MediaWiki ne peut pas être utilisé avec PHP $1 à cause d’un bogue affectant les paramètres passés par référence à <code>__call()</code>.
Veuillez mettre à jour votre système vers PHP 5.3.2 ou plus récent ou revenir à PHP 5.3.0 pour résoudre ce problème.
Installation interrompue.',
- 'config-suhosin-max-value-length' => 'Suhosin est installé et limite la longueur du paramètre GET à $1 octets. Le <code>ResourceLoader</code> de MediaWiki va répondre en respectant cette limite, mais ses performances seront dégradées. Si possible, vous devriez définir <code>suhosin.get.max_value_length</code> à 1024 ou plus dans le fichier <code>php.ini</code>, et fixer <code>$wgResourceLoaderMaxQueryLength</code> à la même valeur dans <code>LocalSettings.php</code>.',
+ 'config-suhosin-max-value-length' => 'Suhosin est installé et limite la <code>longueur</code> du paramètre GET à $1 octets.
+Le composant ResourceLoader de MediaWiki va répondre en respectant cette limite, mais ses performances seront dégradées. Si possible, vous devriez définir <code>suhosin.get.max_value_length</code> à 1024 ou plus dans le fichier <code>php.ini</code>, et fixer <code>$wgResourceLoaderMaxQueryLength</code> à la même valeur dans <code>LocalSettings.php</code>.',
'config-db-type' => 'Type de base de données :',
'config-db-host' => 'Nom d’hôte de la base de données :',
'config-db-host-help' => 'Si votre serveur de base de données est sur un serveur différent, saisissez ici son nom d’hôte ou son adresse IP.
@@ -5477,7 +6001,6 @@ Envisagez de placer la base de données ailleurs, par exemple dans <code>/var/li
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => "MediaWiki supporte ces systèmes de bases de données :
$1
@@ -5487,18 +6010,16 @@ Si vous ne voyez pas le système de base de données que vous essayez d'utiliser
'config-support-postgres' => "* $1 est un système de base de données populaire et ''open source'' qui peut être une alternative à MySQL ([http://www.php.net/manual/en/pgsql.installation.php how to compile PHP with PostgreSQL support]). Il peut contenir quelques bogues mineurs et n'est pas recommandé dans un environnement de production.",
'config-support-sqlite' => '* $1 est un système de base de données léger qui est bien supporté. ([http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], utilise PDO)',
'config-support-oracle' => '* $1 est un système commercial de gestion de base de données d’entreprise. ([http://www.php.net/manual/en/oci8.installation.php Comment compiler PHP avec le support OCI8])',
- 'config-support-ibm_db2' => "* $1 est une base de données d'entreprise commerciale.",
'config-header-mysql' => 'Paramètres de MySQL',
'config-header-postgres' => 'Paramètres de PostgreSQL',
'config-header-sqlite' => 'Paramètres de SQLite',
'config-header-oracle' => 'Paramètres d’Oracle',
- 'config-header-ibm_db2' => 'paramètres de IBM DB2',
'config-invalid-db-type' => 'Type de base de données non valide',
'config-missing-db-name' => 'Vous devez saisir une valeur pour « Nom de la base de données »',
'config-missing-db-host' => "Vous devez entrer une valeur pour « l'hôte de la base de données »",
'config-missing-db-server-oracle' => 'Vous devez saisir une valeur pour le « Nom TNS de la base de données »',
'config-invalid-db-server-oracle' => 'Le nom TNS de la base de données (« $1 ») est invalide.
-Il ne peut contenir que des lettres latines de base (a-z, A-Z), des chiffres (0-9), des caractères de soulignement (_) et des points (.).',
+Utilisez uniquement la chaîne "TNS Name" ou "Easy Connect" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Méthodes de nommage Oracle])',
'config-invalid-db-name' => 'Nom de la base de données invalide (« $1 »).
Il ne peut contenir que des lettres latines (a-z, A-Z), des chiffres (0-9), des caractères de soulignement (_) et des tirets (-).',
'config-invalid-db-prefix' => 'Préfixe de la base de données non valide « $1 ».
@@ -5555,7 +6076,7 @@ Ce '''n'est pas recommandé''' sauf si vous rencontrez des problèmes avec votre
Vous pouvez maintenant [$1 commencer à utiliser votre wiki].',
'config-regenerate' => 'Regénérer LocalSettings.php →',
- 'config-show-table-status' => 'Échec de la requête SHOW TABLE STATUS !',
+ 'config-show-table-status' => 'Échec de la requête <code>SHOW TABLE STATUS</code> !',
'config-unknown-collation' => "'''Attention:''' La base de données effectue un classement alphabétique (''collation'') inconnu.",
'config-db-web-account' => "Compte de la base de données pour l'accès Web",
'config-db-web-help' => "Sélectionnez le nom d'utilisateur et le mot de passe que le serveur web utilisera pour se connecter au serveur de base de données pendant le fonctionnement habituel du wiki.",
@@ -5571,6 +6092,12 @@ Le compte que vous spécifiez ici doit déjà exister.",
* il est plus sujet à la corruption que les autres moteurs
* le codebase MediaWiki ne gère pas toujours MyISAM comme il se doit
Si votre installation MySQL supporte InnoDB, il est fortement recommandé que vous le choisissez plutôt. Si votre installation MySQL ne supporte pas les tables InnoDB, il est peut-être temps de faire une mise à niveau.",
+ 'config-mysql-only-myisam-dep' => "'''Attention :''' MyISAM est le seul moteur de stockage disponible pour MySQL qui ne soit pas recommandé pour une utilsiation avec MédiaWiki, car :
+* il supporte très peu les accès concurrents à cause du verrouillage des tables
+* il est plus sujet à corruption que les autres moteurs
+* le code de base de MédiaWiki ne gère pas toujours MyISAM comme il faudrait
+
+Votre installation MySQL ne supporte pas InnoDB ; il est peut-être temps de la mettre à jour.",
'config-mysql-engine-help' => "'''InnoDB''' est presque toujours la meilleure option, car il supporte bien l'[http://fr.wikipedia.org/wiki/Ordonnancement_dans_les_syst%C3%A8mes_d%27exploitation ordonnancement].
'''MyISAM''' peut être plus rapide dans les installations monoposte ou en lecture seule. Les bases de données MyISAM ont tendance à se corrompre plus souvent que celles d'InnoDB.",
@@ -5581,7 +6108,6 @@ Si votre installation MySQL supporte InnoDB, il est fortement recommandé que vo
En ''mode binaire'', MediaWiki stocke le texte UTF-8 dans des champs binaires de la base de données. C'est plus efficace que le ''mode UTF-8'' de MySQL, et vous permet d'utiliser toute la gamme des caractères Unicode.
En ''mode UTF-8'', MySQL connaîtra le jeu de caractères de vos données et pourra présenter et convertir les données de manière appropriée, mais il ne vous laissera pas stocker les caractères au-dessus du [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes plan multilingue de base] (en anglais).",
- 'config-ibm_db2-low-db-pagesize' => "Votre base de données DB2 a un espace de stockage par défaut avec un pagesize insuffisant. Le pagesize doit être au minimum '''32K'''.",
'config-site-name' => 'Nom du wiki :',
'config-site-name-help' => 'Il apparaîtra dans la barre de titre du navigateur et en divers autres endroits.',
'config-site-name-blank' => 'Entrez un nom de site.',
@@ -5623,7 +6149,7 @@ Vous pouvez passer la configuration restante et installer immédiatement le wiki
'config-optional-continue' => 'Me poser davantage de questions.',
'config-optional-skip' => 'J’en ai assez, installer simplement le wiki.',
'config-profile' => 'Profil des droits d’utilisateurs :',
- 'config-profile-wiki' => 'Wiki traditionnel',
+ 'config-profile-wiki' => 'Wiki ouvert',
'config-profile-no-anon' => 'Création de compte requise',
'config-profile-fishbowl' => 'Éditeurs autorisés seulement',
'config-profile-private' => 'Wiki privé',
@@ -5633,7 +6159,7 @@ Avec MediaWiki, il est facile de vérifier les modifications récentes et de ré
Cependant, de nombreuses autres utilisations ont été trouvées au logiciel et il n’est pas toujours facile de convaincre tout le monde des bénéfices de l’esprit wiki.
Vous avez donc le choix.
-'''{{int:config-profile-wiki}}''' autorise toute personne à modifier, y compris sans s’identifier.
+Le modèle '''{{int:config-profile-wiki}}''' autorise toute personne à modifier, y compris sans s’identifier.
'''{{int:config-profile-no-anon}}''' fournit plus de contrôle, par l’identification, mais peut rebuter les contributeurs occasionnels.
'''{{int:config-profile-fishbowl}}''' autorise la modification par les utilisateurs approuvés, mais le public peut toujours lire les pages et leur historique.
@@ -5686,10 +6212,12 @@ Ensuite, activez cette option.",
'config-upload-deleted-help' => 'Choisissez un répertoire qui servira à archiver les fichiers supprimés.
Idéalement, il ne devrait pas être accessible depuis le web.',
'config-logo' => 'URL du logo :',
- 'config-logo-help' => "L'habillage (''skin'') par défaut de MediaWiki comprend l'espace pour un logo de 135x160 pixels dans le coin supérieur gauche.
-Téléchargez une image de la taille appropriée, et entrez l'URL ici.
+ 'config-logo-help' => 'L’habillage par défaut de MediaWiki comprend l’espace pour un logo de 135x160 pixels au-dessus de la barre de menu latérale.
+Téléchargez une image de la taille appropriée, et entrez son URL ici.
+
+Vous pouvez utiliser <code>$wgStylePath</code> ou <code>$wgScriptPath</code> si votre logo est relatif à ces chemins.
-Si vous ne voulez pas d'un logo, laissez cette case vide.",
+Si vous ne voulez pas de logo, laissez cette case vide.',
'config-instantcommons' => "Activer ''InstantCommons''",
'config-instantcommons-help' => "[//www.mediawiki.org/wiki/InstantCommons InstantCommons] est un service qui permet d'utiliser les images, les sons et les autres médias disponibles sur le site [//commons.wikimedia.org/ Wikimedia Commons].
Pour se faire, il faut que MediaWiki accède à Internet.
@@ -5722,8 +6250,8 @@ Si vous ne le connaissez pas, la valeur par défaut est 11211.",
Elles peuvent nécessiter une configuration supplémentaire, mais vous pouvez les activer maintenant',
'config-install-alreadydone' => "'''Attention''': Vous semblez avoir déjà installé MediaWiki et tentez de l'installer à nouveau.
S'il vous plaît, allez à la page suivante.",
- 'config-install-begin' => "En appuyant sur {{int:config-continue}}, vous commencerez l'installation de MediaWiki.
-Si vous voulez apporter des modifications, appuyez sur Retour.",
+ 'config-install-begin' => 'En appuyant sur {{int:config-continue}}, vous commencerez l\'installation de MediaWiki.
+Si vous voulez apporter des modifications, appuyez sur "{{int:config-back}}".',
'config-install-step-done' => 'fait',
'config-install-step-failed' => 'échec',
'config-install-extensions' => 'Inclusion des extensions',
@@ -5776,16 +6304,20 @@ $3
'''Note''': Si vous ne le faites pas maintenant, ce fichier de configuration généré ne sera pas disponible plus tard si vous quittez l'installation sans le télécharger.
Lorsque c'est fait, vous pouvez '''[$2 accéder à votre wiki]'''.",
- 'config-download-localsettings' => 'Télécharger LocalSettings.php',
+ 'config-download-localsettings' => 'Télécharger <code>LocalSettings.php</code>',
'config-help' => 'aide',
'config-nofile' => 'Le fichier « $1 » est introuvable. A-t-il été supprimé ?',
+ 'config-extension-link' => 'Saviez-vous que votre wiki supporte [//www.mediawiki.org/wiki/Manual:Extensions des extensions] ?
+
+Vous pouvez consulter les [//www.mediawiki.org/wiki/Category:Extensions_by_category extensions par catégorie] ou la [//www.mediawiki.org/wiki/Extension_Matrix Matrice des extensions] pourvoir la liste complète des extensions.',
'mainpagetext' => "'''MediaWiki a été installé avec succès.'''",
- 'mainpagedocfooter' => 'Consultez le [//meta.wikimedia.org/wiki/Aide:Contenu Guide de l’utilisateur] pour plus d’informations sur l’utilisation de ce logiciel.
+ 'mainpagedocfooter' => 'Consultez le [//meta.wikimedia.org/wiki/Aide:Contenu Guide de l’utilisateur] pour plus d’informations sur l’utilisation de ce logiciel de wiki.
-== Démarrer avec MediaWiki ==
+== Pour démarrer ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste des paramètres de configuration]
* [//www.mediawiki.org/wiki/Manual:FAQ/fr FAQ sur MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]',
);
/** Cajun French (français cadien)
@@ -5798,7 +6330,7 @@ $messages['frc'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Réglage]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki: Questions Souvent Posées]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki Liste à Malle]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki Liste à Malle]', # Fuzzy
);
/** Franco-Provençal (arpetan)
@@ -5859,19 +6391,17 @@ Portant, MediaWiki at fôta de PHP $2 ou ben ples hôt.',
'config-sqlite-dir' => 'Dossiér de les balyês SQLite :',
'config-oracle-def-ts' => "Èspâço de stocâjo (''tablespace'') per dèfôt :",
'config-oracle-temp-ts' => "Èspâço de stocâjo (''tablespace'') temporèro :",
- 'config-type-ibm_db2' => 'IBM DB2',
'config-header-mysql' => 'Paramètres de MySQL',
'config-header-postgres' => 'Paramètres de PostgreSQL',
'config-header-sqlite' => 'Paramètres de SQLite',
'config-header-oracle' => 'Paramètres d’Oracle',
- 'config-header-ibm_db2' => 'Paramètres d’IBM DB2',
'config-invalid-db-type' => 'Tipo de bâsa de balyês envalido',
'config-missing-db-name' => 'Vos dête buchiér una valor por « Nom de la bâsa de balyês »',
'config-missing-db-host' => 'Vos dête buchiér una valor por « Hôto de la bâsa de balyês »',
'config-missing-db-server-oracle' => 'Vos dête buchiér una valor por « TNS de la bâsa de balyês »',
'config-sqlite-readonly' => 'Lo fichiér <code>$1</code> est pas accèssiblo en ècritura.',
'config-regenerate' => 'Refâre LocalSettings.php →',
- 'config-show-table-status' => 'Falyita de la requéta SHOW TABLE STATUS !',
+ 'config-show-table-status' => 'Falyita de la requéta <code>SHOW TABLE STATUS</code> !',
'config-db-web-account' => 'Compto de la bâsa de balyês por l’accès vouèbe',
'config-db-web-account-same' => 'Utilisâd lo mémo compto que por l’enstalacion',
'config-db-web-create' => 'Féte lo compto s’ègziste p’oncor',
@@ -5897,7 +6427,7 @@ Portant, MediaWiki at fôta de PHP $2 ou ben ples hôt.',
'config-admin-email' => 'Adrèce èlèctronica :',
'config-optional-continue' => 'Mè posar més de quèstions.',
'config-profile' => 'Profil des drêts d’usanciér :',
- 'config-profile-wiki' => 'Vouiqui tradicionâl',
+ 'config-profile-wiki' => 'Vouiqui tradicionâl', # Fuzzy
'config-profile-no-anon' => 'Crèacion de compto nècèssèra',
'config-profile-fishbowl' => 'Solament los èditors ôtorisâs',
'config-profile-private' => 'Vouiqui privâ',
@@ -5950,7 +6480,7 @@ Portant, MediaWiki at fôta de PHP $2 ou ben ples hôt.',
'config-install-mainpage' => 'Crèacion de la pâge principâla avouéc un contegnu per dèfôt',
'config-install-extension-tables' => 'Crèacion de trâbles por les èxtensions activâs',
'config-install-mainpage-failed' => 'Empossiblo d’entrebetar la pâge principâla : $1',
- 'config-download-localsettings' => 'Tèlèchargiér LocalSettings.php',
+ 'config-download-localsettings' => 'Tèlèchargiér <code>LocalSettings.php</code>',
'config-help' => 'éde',
'mainpagetext' => "'''MediaWiki at étâ enstalâ avouéc reusséta.'''",
'mainpagedocfooter' => 'Vêde lo [//meta.wikimedia.org/wiki/Aide:Contenu guido d’usanciér] por més d’enformacions sur l’usâjo de la programeria vouiqui.
@@ -5958,22 +6488,22 @@ Portant, MediaWiki at fôta de PHP $2 ou ben ples hôt.',
== Emmodar avouéc MediaWiki ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista des paramètres de configuracion]
* [//www.mediawiki.org/wiki/Manual:FAQ/fr FDQ sur MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussion sur les distribucions de MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussion sur les distribucions de MediaWiki]', # Fuzzy
);
/** Northern Frisian (Nordfriisk)
+ * @author Murma174
* @author Pyt
*/
$messages['frr'] = array(
'mainpagetext' => "'''MediaWiki wörd ma erfolch instaliird.'''",
- 'mainpagedocfooter' => 'Heelp tu jü benjüting än konfigurasjoon foon e Wiki-software fanst dü önj dåt [//meta.wikimedia.org/wiki/Help:Contents Benutzerhandbuch].
-
-
-== Startheelpe ==
+ 'mainpagedocfooter' => "Consult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software.
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste der Konfigurationsvariablen]
-* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]',
+== Getting started ==
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
+* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localise MediaWiki for your language]",
);
/** Friulian (furlan)
@@ -5992,7 +6522,7 @@ $messages['fy'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings List mei ynstellings]
* [//www.mediawiki.org/wiki/Manual:FAQ Faak stelde fragen (FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglist foar oankundigings fan nije ferzjes]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglist foar oankundigings fan nije ferzjes]", # Fuzzy
);
/** Irish (Gaeilge)
@@ -6005,7 +6535,7 @@ $messages['ga'] = array(
'config-help' => 'Cuidiú',
'mainpagetext' => "'''D'éirigh le suiteáil MediaWiki.'''",
'mainpagedocfooter' => 'Féach ar [//meta.wikimedia.org/wiki/MediaWiki_localisation doiciméid um conas an chomhéadán a athrú]
-agus an [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Lámhleabhar úsáideora] chun cabhair úsáide agus fíoraíochta a fháil.',
+agus an [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Lámhleabhar úsáideora] chun cabhair úsáide agus fíoraíochta a fháil.', # Fuzzy
);
/** Gagauz (Gagauz)
@@ -6018,7 +6548,7 @@ $messages['gag'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Simplified Gan script (赣语(简体)‎)
@@ -6031,20 +6561,21 @@ $messages['gan-hans'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings MediaWiki 配置设定列表]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki 平常问题解答]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 发布email清单]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 发布email清单]', # Fuzzy
);
/** Traditional Gan script (贛語(繁體)‎)
+ * @author Symane
*/
$messages['gan-hant'] = array(
- 'mainpagetext' => "'''安裝正MediaWiki嘍。'''",
+ 'mainpagetext' => "'''安裝正MediaWiki哩。'''",
'mainpagedocfooter' => '參看[//meta.wikimedia.org/wiki/Help:Contents 用戶指南]裡頭會話到啷用wiki軟件
== 開始使用 ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings MediaWiki 配置設定列表]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki 平常問題解答]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 發佈email清單]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 發佈email清單]', # Fuzzy
);
/** Scottish Gaelic (Gàidhlig)
@@ -6057,12 +6588,14 @@ $messages['gd'] = array(
== Toiseach tòiseachaidh ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liosta suidheachadh nan roghainnean]
* [//www.mediawiki.org/wiki/Manual:FAQ CÀBHA MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liosta puist nan sgaoilidhean MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liosta puist nan sgaoilidhean MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Cuir do chànan air MediaWiki]",
);
/** Galician (galego)
* @author Elisardojm
* @author Toliño
+ * @author 아라
*/
$messages['gl'] = array(
'config-desc' => 'O programa de instalación de MediaWiki',
@@ -6070,19 +6603,19 @@ $messages['gl'] = array(
'config-information' => 'Información',
'config-localsettings-upgrade' => 'Detectouse un ficheiro <code>LocalSettings.php</code>.
Para actualizar esta instalación, introduza o valor de <code>$wgUpgradeKey</code> na caixa.
-Pode atopalo en LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Detectouse un ficheiro LocalSettings.php.
-Para actualizar esta instalación, execute update.php',
+Pode atopalo en <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Detectouse un ficheiro <code>LocalSettings.php</code>.
+Para actualizar esta instalación, execute <code>update.php</code>',
'config-localsettings-key' => 'Clave de actualización:',
'config-localsettings-badkey' => 'A clave dada é incorrecta',
'config-upgrade-key-missing' => 'Detectouse unha instalación existente de MediaWiki.
-Para actualizar esta instalación, inclúa esta liña ao final do ficheiro LocalSettings.php:
+Para actualizar esta instalación, inclúa esta liña ao final do ficheiro <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Semella que o ficheiro LocalSettings.php existente está incompleto.
+ 'config-localsettings-incomplete' => 'Semella que o ficheiro <code>LocalSettings.php</code> existente está incompleto.
A variable $1 non está establecida.
-Modifique o ficheiro LocalSettings.php de xeito que a variable quede establecida e prema en "Continuar".',
- 'config-localsettings-connection-error' => 'Atopouse un erro ao conectar coa base de datos empregando a configuración especificada no ficheiro LocalSettings.php ou no ficheiro AdminSettings.php. Corrixa esta configuración e inténteo de novo.
+Modifique o ficheiro <code>LocalSettings.php</code> de xeito que a variable quede establecida e prema en "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Atopouse un erro ao conectar coa base de datos empregando a configuración especificada no ficheiro <code>LocalSettings.php</code> ou no ficheiro <code>AdminSettings.php</code>. Corrixa esta configuración e inténteo de novo.
$1',
'config-session-error' => 'Erro ao iniciar a sesión: $1',
@@ -6116,8 +6649,8 @@ Comprobe o seu php.ini e asegúrese de que en <code>session.save_path</code> est
'config-help-restart' => 'Quere eliminar todos os datos gardados e reiniciar o proceso de instalación?',
'config-restart' => 'Si, reiniciala',
'config-welcome' => '=== Comprobación da contorna ===
-Cómpre realizar unhas comprobacións básicas para ver se a contorna é axeitada para a instalación de MediaWiki.
-Deberá proporcionar os resultados destas comprobacións se necesita axuda durante a instalación.',
+Cómpre realizar agora unhas comprobacións básicas para ver se a contorna é axeitada para a instalación de MediaWiki.
+Lembre incluír esta información se necesita axuda para completar a instalación.',
'config-copyright' => "=== Dereitos de autor e termos de uso ===
$1
@@ -6186,7 +6719,11 @@ MediaWiki necesita soporte UTF-8 para funcionar correctamente.",
'config-memory-bad' => "'''Atención:''' O parámetro <code>memory_limit</code> do PHP é $1.
Probablemente é un valor baixo de máis.
A instalación pode fallar!",
- 'config-ctype' => "'''Fatal:''' O PHP debe compilarse co soporte para a [http://www.php.net/manual/en/ctype.installation.php extensión Ctype].",
+ 'config-ctype' => "'''Erro fatal:''' O PHP debe compilarse co soporte para a [http://www.php.net/manual/en/ctype.installation.php extensión Ctype].",
+ 'config-json' => "'''Erro fatal:''' O PHP compilouse sen o soporte de JSON.
+Debe instalar ben a extensión JSON do PHP ou a extensión [http://pecl.php.net/package/jsonc PECL jsonc] antes de instalar MediaWiki.
+* A extensión do PHP está incluída en Red Hat Enterprise Linux (CentOS) 5 e 6, mais debe activarse <code>/etc/php.ini</code> ou <code>/etc/php.d/json.ini</code>.
+* Algunhas distribucións do Linux lanzadas despois de maio de 2013 omiten a extensión do PHP, pero inclúen a extensión PECL como <code>php5-json</code> ou <code>php-pecl-jsonc</code>.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] está instalado',
'config-apc' => '[http://www.php.net/apc APC] está instalado',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] está instalado',
@@ -6195,6 +6732,8 @@ A caché de obxectos está desactivada.",
'config-mod-security' => "'''Atención:''' O seu servidor web ten o [http://modsecurity.org/ mod_security] activado. Se estivese mal configurado, pode causar problemas a MediaWiki ou calquera outro software que permita aos usuarios publicar contidos arbitrarios.
Olle a [http://modsecurity.org/documentation/ documentación do mod_security] ou póñase en contacto co soporte do seu servidor se atopa erros aleatorios.",
'config-diff3-bad' => 'GNU diff3 non se atopou.',
+ 'config-git' => 'Atopouse o software de control da versión de Git: <code>$1</code>.',
+ 'config-git-bad' => 'Non se atopou o software de control da versión de Git.',
'config-imagemagick' => 'ImageMagick atopado: <code>$1</code>.
As miniaturas de imaxes estarán dispoñibles se activa as cargas.',
'config-gd' => 'Atopouse a biblioteca gráfica GD integrada.
@@ -6215,7 +6754,9 @@ Instalación abortada.',
'config-using531' => 'O PHP $1 non é compatible con MediaWiki debido a un erro que afecta aos parámetros de referencia de <code>__call()</code>.
Actualice o sistema á versión 5.3.2 ou posterior do PHP ou volva á versión 5.3.0 do PHP para arranxar o problema.
Instalación abortada.',
- 'config-suhosin-max-value-length' => 'Suhosin está instalado e limita a lonxitude do parámetro GET a $1 bytes. O compoñente ResourceLoader (xestor de recursos) de MediaWiki traballa neste límite, pero este prexudica o rendemento. Se é posible, debería establecer suhosin.get.max_value_length no valor 1024 ou superior en php.ini e establecer $wgResourceLoaderMaxQueryLength no mesmo valor en LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Suhosin está instalado e limita o parámetro GET <code>length</code> a $1 bytes.
+O compoñente ResourceLoader (xestor de recursos) de MediaWiki traballa neste límite, pero este prexudica o rendemento.
+Se é posible, debería establecer <code>suhosin.get.max_value_length</code> no valor 1024 ou superior en <code>php.ini</code> e establecer <code>$wgResourceLoaderMaxQueryLength</code> no mesmo valor en <code>LocalSettings.php</code>.',
'config-db-type' => 'Tipo de base de datos:',
'config-db-host' => 'Servidor da base de datos:',
'config-db-host-help' => 'Se o servidor da súa base de datos está nun servidor diferente, escriba o nome do servidor ou o enderezo IP aquí.
@@ -6289,7 +6830,6 @@ Considere poñer a base de datos nun só lugar, por exemplo en <code>/var/lib/me
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki soporta os seguintes sistemas de bases de datos:
$1
@@ -6298,19 +6838,17 @@ Se non ve listado a continuación o sistema de base de datos que intenta usar, s
'config-support-mysql' => '* $1 é o obxectivo principal para MediaWiki e está mellor soportado ([http://www.php.net/manual/en/mysql.installation.php como compilar o PHP con soporte MySQL])',
'config-support-postgres' => '* $1 é un sistema de base de datos popular e de código aberto como alternativa a MySQL ([http://www.php.net/manual/en/pgsql.installation.php como compilar o PHP con soporte PostgreSQL]). É posible que haxa algúns pequenos erros e non se recomenda o seu uso nunha contorna de produción.',
'config-support-sqlite' => '* $1 é un sistema de base de datos lixeiro moi ben soportado. ([http://www.php.net/manual/en/pdo.installation.php Como compilar o PHP con soporte SQLite], emprega PDO)',
- 'config-support-oracle' => '* $1 é un sistema comercial de xestión de base de datos de empresa. ([http://www.php.net/manual/en/oci8.installation.php Como compilar PHP con soporte OCI8])',
- 'config-support-ibm_db2' => '* $1 é unha base de datos de empresa comercial.',
+ 'config-support-oracle' => '* $1 é un sistema comercial de xestión de base de datos de empresa. ([http://www.php.net/manual/en/oci8.installation.php Como compilar o PHP con soporte OCI8])',
'config-header-mysql' => 'Configuración do MySQL',
'config-header-postgres' => 'Configuración do PostgreSQL',
'config-header-sqlite' => 'Configuración do SQLite',
'config-header-oracle' => 'Configuración do Oracle',
- 'config-header-ibm_db2' => 'Configuración de IBM DB2',
'config-invalid-db-type' => 'Tipo de base de datos incorrecto',
'config-missing-db-name' => 'Debe escribir un valor "Nome da base de datos"',
'config-missing-db-host' => 'Debe escribir un valor "Servidor da base de datos"',
'config-missing-db-server-oracle' => 'Debe escribir un valor "TNS da base de datos"',
'config-invalid-db-server-oracle' => 'O TNS da base de datos, "$1", é incorrecto.
-Só pode conter letras ASCII (a-z, A-Z), números (0-9), guións baixos (_) e puntos (.).',
+Utilice só "TNS Name" ou unha cadea de texto "Easy Connect" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm métodos de nomeamento de Oracle])',
'config-invalid-db-name' => 'O nome da base de datos, "$1", é incorrecto.
Só pode conter letras ASCII (a-z, A-Z), números (0-9), guións baixos (_) e guións (-).',
'config-invalid-db-prefix' => 'O prefixo da base de datos, "$1", é incorrecto.
@@ -6367,7 +6905,7 @@ Isto '''non é recomendable''' a menos que estea a ter problemas co seu wiki.",
Xa pode [$1 comezar a usar o seu wiki].',
'config-regenerate' => 'Rexenerar LocalSettings.php →',
- 'config-show-table-status' => 'A pescuda SHOW TABLE STATUS fallou!',
+ 'config-show-table-status' => 'A pescuda <code>SHOW TABLE STATUS</code> fallou!',
'config-unknown-collation' => "'''Atención:''' A base de datos está a empregar unha clasificación alfabética irrecoñecible.",
'config-db-web-account' => 'Conta na base de datos para o acceso á internet',
'config-db-web-help' => 'Seleccione o nome de usuario e contrasinal que o servidor web empregará para se conectar ao servidor da base de datos durante o funcionamento normal do wiki.',
@@ -6385,6 +6923,12 @@ A conta que se especifique aquí xa debe existir.',
Se a súa instalación MySQL soporta InnoDB, recoméndase elixilo no canto de MyISAM.
Se a súa instalación MySQL non soporta InnoDB, quizais sexa boa idea realizar unha actualización.",
+ 'config-mysql-only-myisam-dep' => "'''Atención:''' MyISAM é o único motor de almacenamento para MySQL, unha combinación non recomendada para MediaWiki, porque:
+* practicamente non soporta os accesos simultáneos debido ao bloqueo de táboas
+* é máis propenso a corromperse ca outros motores
+* o código base de MediaWiki non sempre manexa o MyISAM como debera
+
+A súa instalación MySQL non soporta InnoDB, quizais sexa boa idea realizar unha actualización.",
'config-mysql-engine-help' => "'''InnoDB''' é case sempre a mellor opción, dado que soporta ben os accesos simultáneos.
'''MyISAM''' é máis rápido en instalacións de usuario único e de só lectura.
@@ -6397,7 +6941,6 @@ Isto é máis eficaz ca o modo UTF-8 de MySQL e permítelle usar o rango complet
No '''modo UTF-8''', MySQL saberá o xogo de caracteres dos seus datos e pode presentar e converter os datos de maneira axeitada,
pero non lle deixará gardar caracteres por riba do [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes plan multilingüe básico].",
- 'config-ibm_db2-low-db-pagesize' => "A súa base de datos DB2 ten un espazo de táboa cun tamaño de páxina insuficiente. O tamaño de páxina debe ser '''32k''' ou maior.",
'config-site-name' => 'Nome do wiki:',
'config-site-name-help' => 'Isto aparecerá na barra de títulos do navegador e noutros lugares.',
'config-site-name-blank' => 'Escriba o nome do sitio.',
@@ -6440,7 +6983,7 @@ Neste paso pode saltar o resto da configuración e instalar o wiki agora mesmo.'
'config-optional-continue' => 'Facédeme máis preguntas.',
'config-optional-skip' => 'Xa estou canso. Instalade o wiki.',
'config-profile' => 'Perfil dos dereitos de usuario:',
- 'config-profile-wiki' => 'Wiki tradicional',
+ 'config-profile-wiki' => 'Wiki aberto',
'config-profile-no-anon' => 'Necesítase a creación dunha conta',
'config-profile-fishbowl' => 'Só os editores autorizados',
'config-profile-private' => 'Wiki privado',
@@ -6449,7 +6992,7 @@ En MediaWiki, é doado revisar os cambios recentes e reverter calquera dano feit
Porén, moita xente atopa MediaWiki útil nunha ampla variedade de papeis, e ás veces non é fácil convencer a todos dos beneficios que leva consigo o estilo wiki.
Vostede decide.
-O tipo '''{{int:config-profile-wiki}}''' permite a edición por parte de calquera, mesmo sen rexistro.
+O modelo '''{{int:config-profile-wiki}}''' permite a edición por parte de calquera, mesmo sen rexistro.
A opción '''{{int:config-profile-no-anon}}''' proporciona un control maior, pero pode desalentar os colaboradores casuais.
O escenario '''{{int:config-profile-fishbowl}}''' restrinxe a edición aos usuarios aprobados, pero o público pode ollar as páxinas, incluíndo os historiais.
@@ -6506,6 +7049,8 @@ O ideal é que non sexa accesible desde a web.',
'config-logo-help' => 'A aparencia de MediaWiki por defecto inclúe espazo para un logo de 135x160 píxeles por riba do menú lateral.
Cargue unha imaxe do tamaño axeitado e introduza o enderezo URL aquí.
+Pode utilizar <code>$wgStylePath</code> ou <code>$wgScriptPath</code> se o seu logo está relacionado con esas rutas.
+
Se non quere un logo, deixe esta caixa en branco.',
'config-instantcommons' => 'Activar Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons InstantCommons] é unha característica que permite aos wikis usar imaxes, sons e outros ficheiros multimedia atopados no sitio da [//commons.wikimedia.org/wiki/Portada_galega Wikimedia Commons].
@@ -6540,7 +7085,7 @@ Quizais necesite algunha configuración adicional, pero pode activalas agora',
'config-install-alreadydone' => "'''Atención:''' Semella que xa instalou MediaWiki e que o está a instalar de novo.
Vaia ata a seguinte páxina.",
'config-install-begin' => 'Ao premer en "{{int:config-continue}}", comezará a instalación de MediaWiki.
-Se aínda quere facer algún cambio, volva atrás.',
+Se aínda quere facer algún cambio, prema en "{{int:config-back}}".',
'config-install-step-done' => 'feito',
'config-install-step-failed' => 'erro',
'config-install-extensions' => 'Incluíndo as extensións',
@@ -6596,16 +7141,20 @@ $3
'''Nota:''' Se non fai iso agora, este ficheiro de configuración xerado non estará dispoñible máis adiante se sae da instalación sen descargalo.
Cando faga todo isto, xa poderá '''[$2 entrar no seu wiki]'''.",
- 'config-download-localsettings' => 'Descargar o LocalSettings.php',
+ 'config-download-localsettings' => 'Descargar o <code>LocalSettings.php</code>',
'config-help' => 'axuda',
- 'config-nofile' => 'Non se puido atopar o ficheiro "$1". Se cadra, foi borrado?',
+ 'config-nofile' => 'Non se puido atopar o ficheiro "$1". Se cadra, foi borrado.',
+ 'config-extension-link' => 'Sabía que o seu wiki soporta [//www.mediawiki.org/wiki/Manual:Extensions extensións]?
+
+Pode explorar as [//www.mediawiki.org/wiki/Category:Extensions_by_category extensións por categoría] ou a [//www.mediawiki.org/wiki/Extension_Matrix matriz de extensións] para ollar a lista completa de extensións.',
'mainpagetext' => "'''MediaWiki instalouse correctamente.'''",
'mainpagedocfooter' => 'Consulte a [//meta.wikimedia.org/wiki/Help:Contents guía de usuario] para obter máis información sobre como usar o software wiki.
== Primeiros pasos ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista das opcións de configuración]
* [//www.mediawiki.org/wiki/Manual:FAQ Preguntas máis frecuentes sobre MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo dos lanzamentos de MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo dos lanzamentos de MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localice MediaWiki á súa lingua]',
);
/** Goan Konkani (Latin script) (Konknni)
@@ -6627,7 +7176,7 @@ $messages['grc'] = array(
== Ἄρξασθε ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Διαλογή παραμέτρων διαμορφώσεως]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki: τὰ πολλάκις αἰτηθέντα]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Διαλογή διαλέξεων ἐπὶ τῶν διανομῶν τῆς MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Διαλογή διαλέξεων ἐπὶ τῶν διανομῶν τῆς MediaWiki]', # Fuzzy
);
/** Swiss German (Alemannisch)
@@ -6639,7 +7188,7 @@ $messages['gsw'] = array(
'config-information' => 'Information',
'config-localsettings-upgrade' => "'''Warnig:''' E Datei <code>LocalSettings.php</code> isch gfunde wore.
Fir d Aktualisierig vu dr däre Inschtallation, gib bitte dr Wärt vum Parameter <code>\$wgUpgradeKey</code> im Fäld unten yy.
-Du findsch dr Wärt in dr Datei LocalSettings.php.",
+Du findsch dr Wärt in dr Datei <code>LocalSettings.php</code>.",
'config-localsettings-key' => 'Aktualisierigsschlissel:',
'config-localsettings-badkey' => 'Dr Aktualisierigsschlissel, wu du aagee hesch, isch falsch.',
'config-session-error' => 'Fähler bim Starte vu dr Sitzig: $1',
@@ -6738,10 +7287,11 @@ S Objäktcaching isch wäge däm nit aktiviert.",
Miniaturaasichte vu Bilder sin megli, sobald s Uffelade vu Dateie aktiviert isch.',
'config-help' => 'Hilf',
'mainpagetext' => "'''MediaWiki isch erfolgrich inschtalliert worre.'''",
- 'mainpagedocfooter' => 'Lueg uf d [//meta.wikimedia.org/wiki/MediaWiki_localisation Dokumentation fir d Aapassig vu dr Benutzeroberflächi] un s [//meta.wikimedia.org/wiki/Help:Contents Benutzerhandbuech] fir d Hilf iber d Benutzig un s Yystelle.',
+ 'mainpagedocfooter' => 'Lueg uf d [//meta.wikimedia.org/wiki/MediaWiki_localisation Dokumentation fir d Aapassig vu dr Benutzeroberflächi] un s [//meta.wikimedia.org/wiki/Help:Contents Benutzerhandbuech] fir d Hilf iber d Benutzig un s Yystelle.', # Fuzzy
);
/** Gujarati (ગુજરાતી)
+ * @author Ashok modhvadia
* @author Dineshjk
*/
$messages['gu'] = array(
@@ -6751,7 +7301,8 @@ $messages['gu'] = array(
== શરૂઆતના તબક્કે ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings કોનફીગ્યુરેશન સેટીંગ્સની યાદી]
* [//www.mediawiki.org/wiki/Manual:FAQ વારંવાર પુછાતા પ્રશ્નો]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce મિડીયાવિકિ રીલીઝ મેઇલીંગ લીસ્ટ]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce મિડીયાવિકિ રીલીઝ મેઇલીંગ લીસ્ટ]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localise MediaWiki for your language]',
);
/** Manx (Gaelg)
@@ -6770,7 +7321,7 @@ $messages['hak'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings MediaWiki Phi-chṳ sat-thin chhîn-tân]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki Phìn-sòng mun-thì kié-tap]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki fat-phu email chhîn-tân]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki fat-phu email chhîn-tân]', # Fuzzy
);
/** Hawaiian (Hawai`i)
@@ -6783,6 +7334,7 @@ $messages['haw'] = array(
* @author Amire80
* @author YaronSh
* @author ערן
+ * @author 아라
*/
$messages['he'] = array(
'config-desc' => 'תכנית ההתקנה של מדיה־ויקי',
@@ -6790,19 +7342,19 @@ $messages['he'] = array(
'config-information' => 'פרטים',
'config-localsettings-upgrade' => 'זוהה קובץ <code>LocalSettings.php</code>.
כדי לשדרג את ההתקנה הזאת, נא להקליד את הערך של <code>$wgUpgradeKey</code> בתיבה להלן.
-אפשר למצוא אותו בקובץ LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'זוהה קובץ LocalSettings.php.
-כדי לשדרג את ההתקנה הזאת, הריצו את update.php ולא את הקובץ הזה.',
+אפשר למצוא אותו בקובץ <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'זוהה קובץ <code>LocalSettings.php</code>.
+כדי לשדרג את ההתקנה הזאת, הריצו את <code>update.php</code> ולא את הקובץ הזה.',
'config-localsettings-key' => 'מפתח השדרוג:',
'config-localsettings-badkey' => 'המפתח שהקלדתם שגוי',
'config-upgrade-key-missing' => 'זוהתה התקנה קיימת של מדיה־ויקי.
-כדי לשדרג את ההתקנה הזאת, אנא כתבו את השורה הבא בתחתית קובץ LocalSettings.php שלכם:
+כדי לשדרג את ההתקנה הזאת, אנא כתבו את השורה הבא בתחתית קובץ <code>LocalSettings.php</code> שלכם:
$1',
- 'config-localsettings-incomplete' => 'נראה שקובץ LocalSettings.php הקיים אינו שלם.
+ 'config-localsettings-incomplete' => 'נראה שקובץ <code>LocalSettings.php</code> הקיים אינו שלם.
המשתנה $1 אינו מוגדר.
-נו לשנות את קובץ LocalSettings.php שלכם כך שהמשתנה הזה יהיה מוגדר וללחוץ "המשך".',
- 'config-localsettings-connection-error' => 'אירעה שגיאה בעת חיבור למסד נתונים עם הגדרות ב־LocalSettings.php או ב־AdminSettings.php. נא לתקן את ההגדרות האלו ולנסות שוב.
+נו לשנות את קובץ <code>LocalSettings.php</code> שלכם כך שהמשתנה הזה יהיה מוגדר וללחוץ "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'אירעה שגיאה בעת חיבור למסד נתונים עם הגדרות ב־<code>LocalSettings.php</code> או ב־<code>AdminSettings.php</code>. נא לתקן את ההגדרות האלו ולנסות שוב.
$1',
'config-session-error' => 'שגיאה באתחול שיחה: $1',
@@ -6934,7 +7486,7 @@ $1
'config-using531' => 'אי־אפשר להשתמש במדיה־ויקי עם <span dir="ltr">PHP $1</span> בגלל באג בפרמטרים של הפניות (reference parameters) ל־<code dir="ltr">__call()</code>.
שדרגו ל־PHP 5.3.2 או לגרסה גבוהה יותר כדי לתקן את זה ([//bugs.php.net/bug.php?id=50394 bug filed with PHP]) או שַנמכו ל־PHP 5.3.0 כדי לפתור את הבעיה הזאת.
ההתקנה בוטלה.',
- 'config-suhosin-max-value-length' => 'מותקן פה Suhosin והוא מגביל את אורך פרמטר GET ל־$1 בתים. רכיב ResourceLoader של מדיה־ויקי יעקוף את המגלבה הזאת, אבל זה יפגע בביצועים. אם זה בכלל אפשרי, כדי לתקן את הערך של suhosin.get.max_value_length ל־1024 בקובץ php.ini ולהגדיר את ‎$wgResourceLoaderMaxQueryLength לאותו הערך בקובץ LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'מותקן פה Suhosin והוא מגביל את אורך פרמטר GET ל־$1 בתים. רכיב ResourceLoader של מדיה־ויקי יעקוף את המגלבה הזאת, אבל זה יפגע בביצועים. אם זה בכלל אפשרי, כדאי לתקן את הערך של <code>suhosin.get.max_value_length</code> ל־1024 או יותר בקובץ <code>php.ini</code> ולהגדיר את ‎<code>$wgResourceLoaderMaxQueryLength</code> לאותו הערך בקובץ LocalSettings.php.',
'config-db-type' => 'סוג מסד הנתונים:',
'config-db-host' => 'שרת מסד הנתונים:',
'config-db-host-help' => 'אם שרת מסד הנתונים שלכם נמצא על שרת אחר, הקלידו את שם המחשב או את כתובת ה־IP כאן.
@@ -7005,7 +7557,6 @@ $1
כדאי לשקול לשים את מסד הנתונים במקום אחר לגמרי, למשל ב־<code dir="ltr">/var/lib/mediawiki/yourwik</code>.',
'config-oracle-def-ts' => 'מרחב טבלאות לפי בררת מחדל (default tablespace):',
'config-oracle-temp-ts' => 'מרחב טבלאות זמני (temporary tablespace):',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'מדיה־ויקי תומכת במערכות מסדי הנתונים הבאות:
$1
@@ -7015,12 +7566,10 @@ $1
'config-support-postgres' => '$1 הוא מסד נתונים נפוץ בקוד פתוח והוא נפוץ בתור חלופה ל־MySQL (ר׳ [http://www.php.net/manual/en/pgsql.installation.php how to compile PHP with PostgreSQL support]). ייתכן שיש בתצורה הזאת באגים מסוימים והיא לא מומלצת לסביבות מבצעיות.',
'config-support-sqlite' => '* $1 הוא מסד נתונים קליל עם תמיכה טובה מאוד. (ר׳ [http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], משתמש ב־PDO)',
'config-support-oracle' => '* $1 הוא מסד נתונים עסקי מסחרי. (ר׳ [http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])',
- 'config-support-ibm_db2' => '* $1 הוא מסד נתונים מסחרי ארגוני.',
'config-header-mysql' => 'הגדרות MySQL',
'config-header-postgres' => 'הגדרות PostgreSQL',
'config-header-sqlite' => 'הגדרות SQLite',
'config-header-oracle' => 'הגדרות Oracle',
- 'config-header-ibm_db2' => 'תצורת IBM DB2',
'config-invalid-db-type' => 'סוג מסד הנתונים שגוי',
'config-missing-db-name' => 'עליך להזין ערך עבור "שם מסד הנתונים"',
'config-missing-db-host' => 'יש להכניס ערך לשדה "שרת מסד הנתונים"',
@@ -7083,7 +7632,7 @@ chmod a+w $3</pre></div>',
עכשיו אפשר [$1 להתחיל להשתמש בוויקי שלכם].',
'config-regenerate' => 'לחולל מחדש את LocalSettings.php ←',
- 'config-show-table-status' => 'שאילתת SHOW TABLE STATUS נכשלה!',
+ 'config-show-table-status' => 'שאילתת <code>SHOW TABLE STATUS</code> נכשלה!',
'config-unknown-collation' => "'''אזהרה:''' מסד הנתונים משתמש בשיטת מיון שאינה מוּכּרת.",
'config-db-web-account' => 'חשבון במסד הנתונים לגישה מהרשת',
'config-db-web-help' => 'לבחור את שם המשתמש ואת הססמה ששרת הווב ישתמש בו להתחברות לשרת מסד הנתונים בזמן פעילות רגילה של הוויקי.',
@@ -7112,7 +7661,6 @@ chmod a+w $3</pre></div>',
זה יעיל יותר ממצב UTF-8 של MySQL ומאפשר לכם להשתמש בכל הטווח של תווי יוניקוד.
ב'''מצב UTF-8'''&rlm; (UTF-8 mode)&rlm; MySQL יֵדַע מה קבוצת התווים (character set) של הטקסט שלכם ויציג וימיר אותו בהתאם, אבל לא יאפשר לכם לשמור תווים שאינם נמצאים בטווח הרב־לשוני הבסיסי ([//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane]).",
- 'config-ibm_db2-low-db-pagesize' => "במסד הנתונים DB2 שלכם יש מרחב טבלאות לפי מחדלי עם גודל דף בלתי־מספיק. גודל הדף צריך להיות '''32K''' או יותר.",
'config-site-name' => 'שם הוויקי:',
'config-site-name-help' => 'זה יופיע בשורת הכותרת של הדפדפן ובמקומות רבים אחרים.',
'config-site-name-blank' => 'נא להזין שם לאתר.',
@@ -7155,7 +7703,7 @@ chmod a+w $3</pre></div>',
'config-optional-continue' => 'הצגת שאלות נוספות.',
'config-optional-skip' => 'משעמם לי, תתקינו לי כבר את הוויקי הזה.',
'config-profile' => 'תסריט הרשאות משתמשים:',
- 'config-profile-wiki' => 'ויקי מסורתי',
+ 'config-profile-wiki' => 'ויקי פיתוח',
'config-profile-no-anon' => 'נדרשת יצירת חשבון',
'config-profile-fishbowl' => 'עורכים מורשים בלבד',
'config-profile-private' => 'ויקי פרטי',
@@ -7164,7 +7712,7 @@ chmod a+w $3</pre></div>',
עם זאת, אנשים שונים מצאו למדיה־ויקי שימושים מגוּונים ולעתים לא קל לשכנע את כולם ביתרונות של \"דרך הוויקי\" המסורתית. ולכן יש לכם בררה.
-באתר '''{{int:config-profile-wiki}}''' – לכולם יש הרשאה לערוך, אפילו בלי להיכנס לחשבון.
+באתר מסוג '''{{int:config-profile-wiki}}''' – לכולם יש הרשאה לערוך, אפילו בלי להיכנס לחשבון.
באתר וויקי מסוג '''{{int:config-profile-no-anon}}''' יש ביטחון גדול יותר, אבל הגדרה כזאת יכולה להרתיע תורמים מזדמנים.
בתסריט '''{{int:config-profile-fishbowl}}''' רק משתמשים שקיבלו אישור יכולים לערוך, אבל כל הגולשים יכולים לקרוא את הדפים ואת גרסאותיהם הקודמות.
@@ -7221,7 +7769,7 @@ chmod a+w $3</pre></div>',
'config-logo-help' => 'המראה ההתחלתי של מדיה־ויקי מכיל מקום לסמל של 135 על 160 פיקסלים בפינה העליונה מעל תפריט הצד.
יש להעלות תמונה בגודל מתאים ולהכניס את הכתובת כאן.
-אם אינכם רוצים סמל, השאירו את התיבה הזאת ריקה.',
+אם אינכם רוצים סמל, השאירו את התיבה הזאת ריקה.', # Fuzzy
'config-instantcommons' => 'להפעיל את Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] היא תכונה שמאפשרת לאתרי ויקי להשתמש בתמונות, בצלילים ובמדיה אחרת שנמצאת באתר [//commons.wikimedia.org/ ויקישיתוף] (Wikimedia Commons).
כדי לעשות את זה, מדיה־ויקי צריך לגשת לאינטרנט.
@@ -7255,7 +7803,7 @@ chmod a+w $3</pre></div>',
'config-install-alreadydone' => "'''אזהרה:''' נראה שכבר התקנתם את מדיה־ויקי ואתם מנסים להתקין אותה שוב.
אנה התקדמו לדף הבא.",
'config-install-begin' => 'כשתלחצו על "{{int:config-continue}}", תתחילו את ההתקנה של מדיה־ויקי.
-אם אתם עדיין רוצים לשנות משהו, לחצו על "הקודם".',
+אם אתם עדיין רוצים לשנות משהו, לחצו על "{{int:config-back}}"',
'config-install-step-done' => 'בוצע',
'config-install-step-failed' => 'נכשל',
'config-install-extensions' => 'כולל הרחבות',
@@ -7311,7 +7859,7 @@ $3
'''שימו לב''': אם לא תעשו זאת עכשיו, קובץ ההגדרות המחוּלל לא יהיה זמין לכם שוב.
אחרי שתעשו את זה, תוכלו '''[$2 להיכנס לוויקי שלכם]'''.",
- 'config-download-localsettings' => 'הורדת LocalSettings.php',
+ 'config-download-localsettings' => 'הורדת <code>LocalSettings.php</code>',
'config-help' => 'עזרה',
'config-nofile' => 'הקובץ "$1" לא נמצא. האם הוא נמחק?',
'mainpagetext' => "'''תוכנת מדיה־ויקי הותקנה בהצלחה.'''",
@@ -7320,7 +7868,8 @@ $3
== קישורים שימושיים ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings רשימת ההגדרות]
* [//www.mediawiki.org/wiki/Manual:FAQ שאלות ותשובות על מדיה־ויקי]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce רשימת התפוצה על השקת גרסאות]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce רשימת התפוצה על השקת גרסאות]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources תרגום מדיה־ויקי לשפה שלך]',
);
/** Hindi (हिन्दी)
@@ -7332,7 +7881,7 @@ $messages['hi'] = array(
== शुरुवात करें ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings कॉन्फिगरेशन सेटींगकी सूची]
* [//www.mediawiki.org/wiki/Manual:FAQ मीडियाविकिके बारे में प्राय: पूछे जाने वाले सवाल]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मीडियाविकि मेलिंग लिस्ट]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मीडियाविकि मेलिंग लिस्ट]', # Fuzzy
);
/** Fiji Hindi (Latin script) (Fiji Hindi)
@@ -7345,7 +7894,7 @@ $messages['hif-latn'] = array(
== Getting started ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Hiligaynon (Ilonggo)
@@ -7358,7 +7907,7 @@ $messages['hil'] = array(
== Pag-umpisa ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista sang mga konpigorasyon sang pagkay-o]
* [//www.mediawiki.org/wiki/Manual:FAQ Mga Masami Pamangkoton sa MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista sang mga ginapadal-an sang sulat kon may paguha-on nga MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista sang mga ginapadal-an sang sulat kon may paguha-on nga MediaWiki]", # Fuzzy
);
/** Croatian (hrvatski)
@@ -7366,11 +7915,12 @@ $messages['hil'] = array(
$messages['hr'] = array(
'mainpagetext' => "'''Softver MediaWiki je uspješno instaliran.'''",
'mainpagedocfooter' => 'Pogledajte [//meta.wikimedia.org/wiki/MediaWiki_localisation dokumentaciju o prilagodbi sučelja]
-i [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Vodič za suradnike] za pomoć pri uporabi i podešavanju.',
+i [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Vodič za suradnike] za pomoć pri uporabi i podešavanju.', # Fuzzy
);
/** Upper Sorbian (hornjoserbsce)
* @author Michawiki
+ * @author 아라
*/
$messages['hsb'] = array(
'config-desc' => 'Instalaciski program za MediaWiki',
@@ -7378,19 +7928,19 @@ $messages['hsb'] = array(
'config-information' => 'Informacije',
'config-localsettings-upgrade' => 'Dataja <code>LocalSettings.php</code> je so wotkryła.
Zo by tutu instalaciju aktualizował, zapodaj prošu hódnotu za parameter <code>$wgUpgradeKey</code> do slědowaceho pola.
-Namakaš tón parameter w dataji LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Dataja LocalSettings.php bu wotkryta.
-Zo by tutu instalaciju aktualizował, wuwjedźće update.php',
+Namakaš tón parameter w dataji <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Dataja <code>LocalSettings.php</code> bu wotkryta.
+Zo by tutu instalaciju aktualizował, wuwjedźće <code>update.php</code>',
'config-localsettings-key' => 'Aktualizaciski kluč:',
'config-localsettings-badkey' => 'Kluč, kotryž sy podał, je wopak',
'config-upgrade-key-missing' => 'Eksistowaca instalacija MediaWiki je so wotkryła.
-Zo by tutu instalaciju aktualizował, staj prošu slědowacu linku deleka w dataji LocalSettings.php:
+Zo by tutu instalaciju aktualizował, staj prošu slědowacu linku deleka w dataji <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Zda so, zo eksistwoaca dataja LocalSettings.php je njedospołna.
+ 'config-localsettings-incomplete' => 'Zda so, zo eksistwoaca dataja <code>LocalSettings.php</code> je njedospołna.
Wariabla $1 njeje nastajena.
-Prošu změń dataju LocalSettings.php, zo by so tuta wariabla nastajiła a klikń na "Dale".',
- 'config-localsettings-connection-error' => 'Při zwjazowanju z datowej banku z pomocu nastajenjow podatych w LocalSettings.php abo AdminSettings.php je zmylk wustupił. Prošu skoriguj tute nastajenja a spytaj hišće raz.
+Prošu změń dataju <code>LocalSettings.php</code>, zo by so tuta wariabla nastajiła a klikń na "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Při zwjazowanju z datowej banku z pomocu nastajenjow podatych w <code>LocalSettings.php</code> abo <code>AdminSettings.php</code> je zmylk wustupił. Prošu skoriguj tute nastajenja a spytaj hišće raz.
$1',
'config-session-error' => 'Zmylk při startowanju posedźenja: $1',
@@ -7514,16 +8064,13 @@ Změń ju jenož, jeli su přeswědčiwe přičiny za to.',
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-mysql' => '* $1 je primarny cil za MediaWiki a podpěruje so najlěpje ([http://www.php.net/manual/en/mysql.installation.php Nawod ke kompilowanju PHP z MySQL-podpěru])',
'config-support-postgres' => '* $1 je popularny system datoweje banki zjawneho žórła jako alternatiwa k MySQL ([http://www.php.net/manual/en/pgsql.installation.php nawod za kompilowanje PHP z podpěru PostgreSQL]). Móhło hišće někotre zmylki eksistować, a njeporuča so jón w produktiwnej wokolinje wužiwać.',
'config-support-oracle' => '* $1 je komercielna předewzaćelska datowa banka. ([http://www.php.net/manual/en/oci8.installation.php Nawod za kompilowanje PHP z OCI8-podpěru])',
- 'config-support-ibm_db2' => '* $1 je komercielna předewzaćelska datowa banka.',
'config-header-mysql' => 'Nastajenja MySQL',
'config-header-postgres' => 'Nastajenja PostgreSQL',
'config-header-sqlite' => 'Nastajenja SQLite',
'config-header-oracle' => 'Nastajenja Oracle',
- 'config-header-ibm_db2' => 'Nastajenja IBM DB2',
'config-invalid-db-type' => 'Njepłaćiwy typ datoweje banki',
'config-missing-db-name' => 'Dyrbiš hódnotu za "Mjeno datoweje banki" zapodać',
'config-missing-db-host' => 'Dyrbiš hódnotu za "Database host" zapodać',
@@ -7561,7 +8108,7 @@ Zo by je na MediaWiki $1 aktualizował, klikń na '''Dale'''.",
Móžeš nětko [$1 swój wiki wužiwać].',
'config-regenerate' => 'LocalSettings.php znowa wutworić →',
- 'config-show-table-status' => 'Naprašowanje SHOW TABLE STATUS je so njeporadźiło!',
+ 'config-show-table-status' => 'Naprašowanje <code>SHOW TABLE STATUS</code> je so njeporadźiło!',
'config-unknown-collation' => "'''Warnowanje:''' Datowa banka njeznatu kolaciju wužiwa.",
'config-db-web-account' => 'Konto datoweje banki za webpřistup',
'config-db-web-help' => 'wubjer wužiwarske mjeno a hesło, kotrejž webserwer budźe wužiwać, zo by z serwerom datoweje banki za wšědnu operaciju zwjazać',
@@ -7610,7 +8157,7 @@ Móžeš nětko zbytnu konfiguraciju přeskočić a wiki hnydom instalować.',
'config-optional-continue' => 'Dalše prašenja?',
'config-optional-skip' => 'Instaluj nětko wiki.',
'config-profile' => 'Profil wužiwarskich prawow:',
- 'config-profile-wiki' => 'Tradicionelny wiki',
+ 'config-profile-wiki' => 'Zjawny wiki',
'config-profile-no-anon' => 'Załoženje konto je trěbne',
'config-profile-fishbowl' => 'Jenož awtorizowani wobdźěłarjo',
'config-profile-private' => 'Priwatny wiki',
@@ -7668,7 +8215,7 @@ To móže sej přidatnu konfiguraciju wužadać, ale móžeš je nětko zmóžni
'config-install-alreadydone' => "'''Warnowanje:''' Zda so, zo sy hižo MediaWiki instalował a pospytuješ jón znowa instalować.
Prošu pokročuj z přichodnej stronu.",
'config-install-begin' => 'Přez kliknjenje na "{{int:config-continue}}" budźe so instalacija MediaWiki startować.
-Jeli hišće chceš něšto změnić, klikń na "Wróćo".',
+Jeli hišće chceš něšto změnić, klikń na "{{int:config-back}}".',
'config-install-step-done' => 'dokónčene',
'config-install-step-failed' => 'njeporadźiło',
'config-install-extensions' => 'Inkluziwnje rozšěrjenja',
@@ -7704,7 +8251,7 @@ Standardna lisćina sp přeskakuje.",
'config-install-mainpage' => 'Hłowna strona so ze standardnym wobsahom wutworja',
'config-install-extension-tables' => 'Tabele za zmóžnjene rozšěrjenja so tworja',
'config-install-mainpage-failed' => 'Powěsć njeda so zasunyć: $1',
- 'config-download-localsettings' => 'LocalSettings.php sćahnyć',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> sćahnyć',
'config-help' => 'pomoc',
'config-nofile' => 'Dataja "$1" njeje so namakała. Je so zhašała?',
'mainpagetext' => "'''MediaWiki bu wuspěšnje instalowany.'''",
@@ -7714,7 +8261,8 @@ Standardna lisćina sp přeskakuje.",
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Wo nastajenjach]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources MediaWiki za twoju rěč lokalizować]',
);
/** Haitian (Kreyòl ayisyen)
@@ -7728,31 +8276,32 @@ $messages['ht'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lis paramèt yo pou konfigirasyon]
* [//www.mediawiki.org/wiki/Manyèl:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lis diskisyon ki parèt sou MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lis diskisyon ki parèt sou MediaWiki]', # Fuzzy
);
/** Hungarian (magyar)
* @author Dani
* @author Glanthor Reviol
+ * @author 아라
*/
$messages['hu'] = array(
'config-desc' => 'A MediaWiki telepítője',
'config-title' => 'A MediaWiki $1 telepítése',
'config-information' => 'Információ',
'config-localsettings-upgrade' => 'Már létezik a <code>LocalSettings.php</code> fájl.
-A telepített szoftver frissítéséhez írd be az alábbi mezőbe a <code>$wgUpgradeKey</code> beállítás értékét, melyet a LocalSettings.php nevű fájlban találhatsz meg.',
- 'config-localsettings-cli-upgrade' => 'A LocalSettings.php fájl megtalálható.
-A telepített rendszer frissítéséhez futtasd az update.php-t.',
+A telepített szoftver frissítéséhez írd be az alábbi mezőbe a <code>$wgUpgradeKey</code> beállítás értékét, melyet a <code>LocalSettings.php</code> nevű fájlban találhatsz meg.',
+ 'config-localsettings-cli-upgrade' => 'A <code>LocalSettings.php</code> fájl megtalálható.
+A telepített rendszer frissítéséhez futtasd az <code>update.php</code>-t.',
'config-localsettings-key' => 'Frissítési kulcs:',
'config-localsettings-badkey' => 'A megadott kulcs érvénytelen.',
'config-upgrade-key-missing' => 'A telepítő a MediaWiki meglévő példányát észlelte.
-A telepített rendszer frissítéséhez helyezd el az alábbi sort a LocalSettings.php végére:
+A telepített rendszer frissítéséhez helyezd el az alábbi sort a <code>LocalSettings.php</code> végére:
$1',
- 'config-localsettings-incomplete' => 'A meglévő LocalSettings.php hiányosnak tűnik.
+ 'config-localsettings-incomplete' => 'A meglévő <code>LocalSettings.php</code> hiányosnak tűnik.
A(z) $1 változó értéke nincs beállítva.
-Módosítsd a LocalSettings.php fájlt úgy, hogy ez a változó be legyen állítva, majd kattints a „Folytatás” gombra.',
- 'config-localsettings-connection-error' => 'Nem sikerült csatlakozni az adatbázishoz a LocalSettings.php-ben vagy az AdminSettings.php-ben megadott adatokkal. Ellenőrizd a beállításokat, majd próbáld újra.
+Módosítsd a <code>LocalSettings.php</code> fájlt úgy, hogy ez a változó be legyen állítva, majd kattints a „{{int:Config-continue}}” gombra.',
+ 'config-localsettings-connection-error' => 'Nem sikerült csatlakozni az adatbázishoz a <code>LocalSettings.php</code>-ben vagy az <code>AdminSettings.php</code>-ben megadott adatokkal. Ellenőrizd a beállításokat, majd próbáld újra.
$1',
'config-session-error' => 'Nem sikerült elindítani a munkamenetet: $1',
@@ -7877,7 +8426,7 @@ Telepítés megszakítva.',
'config-using531' => 'A MediaWiki nem használható a PHP $1-es verziójával, mert hiba van a <code>__call()</code> függvénynek átadott referenciaparaméterekkel.
A probléma kiküszöböléséhez frissíts a PHP 5.3.2-es verziójára, vagy használd a korábbi, 5.3.0-ásat.
Telepítés megszakítva.',
- 'config-suhosin-max-value-length' => 'A Suhosin telepítve van, és a GET paraméter hosszát $1 bájtra korlátozza. A MediaWiki erőforrásbetöltő összetevője megkerüli a problémát, de így csökkenni fog a teljesítmény. Ha lehetséges, állítsd be a suhosin.get.max_value_length értékét legalább 1024-re a php.iniben, és állítsd be a $wgResourceLoaderMaxQueryLength változót ugyanerre az értékre a LocalSettings.php-ben.',
+ 'config-suhosin-max-value-length' => 'A Suhosin telepítve van, és a GET paraméter hosszát $1 bájtra korlátozza. A MediaWiki erőforrásbetöltő összetevője megkerüli a problémát, de így csökkenni fog a teljesítmény. Ha lehetséges, állítsd be a <code>suhosin.get.max_value_length</code> értékét legalább 1024-re a <code>php.ini</code>ben, és állítsd be a <code>$wgResourceLoaderMaxQueryLength</code> változót ugyanerre az értékre a LocalSettings.php-ben.', # Fuzzy
'config-db-type' => 'Adatbázis típusa:',
'config-db-host' => 'Adatbázis hosztneve:',
'config-db-host-help' => 'Ha az adatbázisszerver másik szerveren található, add meg a hosztnevét vagy az IP-címét.
@@ -7945,7 +8494,6 @@ A telepítő készít egy <code>.htaccess</code> fájlt az adatbázis mellé, az
Fontold meg az adatbázis más helyre történő elhelyezését, például a <code>/var/lib/mediawiki/tewikid</code> könyvtárba.",
'config-oracle-def-ts' => 'Alapértelmezett táblatér:',
'config-oracle-temp-ts' => 'Ideiglenes táblatér:',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'A MediaWiki a következő adatbázisrendszereket támogatja:
$1
@@ -7955,12 +8503,10 @@ Ha az alábbi listán nem találod azt a rendszert, melyet használni szeretnél
'config-support-postgres' => '* A $1 népszerű, nyílt forráskódú adatbázisrendszer, a MySQL alternatívája ([http://www.php.net/manual/en/pgsql.installation.php Hogyan fordítható a PHP PostgreSQL-támogatással]). Több apró, javítatlan hiba is előfordulhat, így nem ajánlott éles környezetben használni.',
'config-support-sqlite' => '* Az $1 egy könnyű, nagyon jól támogatott adatbázisrendszer. ([http://www.php.net/manual/en/pdo.installation.php Hogyan fordítható a PHP SQLite-támogatással], PDO-t használ)',
'config-support-oracle' => '* Az $1 kereskedelmi, vállalati adatbázisrendszer. ([http://www.php.net/manual/en/oci8.installation.php Hogyan fordítható a PHP OCI8-támogatással])',
- 'config-support-ibm_db2' => '* Az $1 kereskedelmi vállalati adatbázisrendszer.',
'config-header-mysql' => 'MySQL-beállítások',
'config-header-postgres' => 'PostgreSQL-beállítások',
'config-header-sqlite' => 'SQLite-beállítások',
'config-header-oracle' => 'Oracle-beállítások',
- 'config-header-ibm_db2' => 'IBM DB2-beállítások',
'config-invalid-db-type' => 'Érvénytelen adatbázistípus',
'config-missing-db-name' => 'Meg kell adnod az „Adatbázisnév” értékét',
'config-missing-db-host' => 'Meg kell adnod az „Adatbázis hosztneve” értékét',
@@ -8023,7 +8569,7 @@ Ez '''nem ajánlott''', csak akkor, ha problémák vannak a wikivel.",
Most már '''[$1 beléphetsz a wikibe]'''.",
'config-regenerate' => 'LocalSettings.php elkészítése újra →',
- 'config-show-table-status' => 'A SHOW TABLE STATUS lekérdezés nem sikerült!',
+ 'config-show-table-status' => 'A <code>SHOW TABLE STATUS</code> lekérdezés nem sikerült!',
'config-unknown-collation' => "'''Figyelmeztetés:''' az adatbázis ismeretlen egybevetést használ.",
'config-db-web-account' => 'A webes hozzáférésnél használt adatbázisfiók',
'config-db-web-help' => 'Add meg azt a felhasználónevet és jelszót, amit a webszerver a wiki általános működése során használ a csatlakozáshoz.',
@@ -8051,7 +8597,6 @@ A '''MyISAM''' gyorsabb megoldás lehet egyfelhasználós vagy csak olvasható k
Ez sokkal hatékonyabb a MySQL UTF-8-as módjánál, és lehetővé teszi a teljes Unicode-karakterkészlet használatát.
'''UTF-8-as módban''' a MySQL tudni fogja,hogy az adatok milyen karakterkészlettel rendelkeznek, és megfelelően átalakítja őket, azonban nem tárolhatóak olyan karakterek, melyek a [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane] felett vannak.",
- 'config-ibm_db2-low-db-pagesize' => "A DB2 adatbázisodnak alapértelmezett táblatere van elégtelen lapmérettel. A lapméretnek legalább '''32K'''-nak kell lennie.",
'config-site-name' => 'A wiki neve:',
'config-site-name-help' => 'A böngésző címsorában és még számos más helyen jelenik meg.',
'config-site-name-blank' => 'Add meg az oldal nevét.',
@@ -8094,7 +8639,7 @@ A további konfigurációt kihagyhatod, és most azonnal elindíthatod a wiki te
'config-optional-continue' => 'További információk megadása.',
'config-optional-skip' => 'Épp elég volt, települjön a wiki!',
'config-profile' => 'Felhasználói jogosultságok profilja:',
- 'config-profile-wiki' => 'Hagyományos wiki',
+ 'config-profile-wiki' => 'Hagyományos wiki', # Fuzzy
'config-profile-no-anon' => 'Felhasználói fiók létrehozása szükséges',
'config-profile-fishbowl' => 'Csak engedélyezett szerkesztők',
'config-profile-private' => 'Privát wiki',
@@ -8108,7 +8653,7 @@ Választhatsz!
Lehetőség van arra is, hogy '''{{lc:{{int:config-profile-fishbowl}}}}''' módosíthassák a lapokat, de a nyilvánosság ekkor megtekintheti a lapokat és azok laptörténetét is. '''{{int:config-profile-private}}''' esetén csak az engedélyezett szerkesztők tekinthetik meg a lapokat, és ugyanez a csoport szerkeszthet.
-Telepítés után jóval összetettebb jogosultságrendszer állítható össze, további információ a [//www.mediawiki.org/wiki/Manual:User_rights kézikönyv kapcsolódó bejegyzésében].",
+Telepítés után jóval összetettebb jogosultságrendszer állítható össze, további információ a [//www.mediawiki.org/wiki/Manual:User_rights kézikönyv kapcsolódó bejegyzésében].", # Fuzzy
'config-license' => 'Szerzői jog és licenc:',
'config-license-none' => 'Nincs licencjelzés',
'config-license-cc-by-sa' => 'Creative Commons Nevezd meg! - Így add tovább!',
@@ -8158,7 +8703,7 @@ Normális esetben ennek nem szabad elérhetőnek lennie az internetről.',
'config-logo-help' => 'A MediaWiki alapértelmezett felülete helyet ad egy 135×160 pixeles logónak a bal felső sarokban.
Tölts fel egy megfelelő méretű képet, majd írd be ide az URL-címét!
-Ha nem szeretnél logót használni, egyszerűen hagyd üresen a mezőt.',
+Ha nem szeretnél logót használni, egyszerűen hagyd üresen a mezőt.', # Fuzzy
'config-instantcommons' => 'Instant Commons engedélyezése',
'config-instantcommons-help' => 'Az [//www.mediawiki.org/wiki/InstantCommons Instant Commons] lehetővé teszi, hogy a wikin használhassák a [//commons.wikimedia.org/ Wikimedia Commons] oldalon található képeket, hangokat és más médiafájlokat.
A használatához a MediaWikinek internethozzáférésre van szüksége.
@@ -8192,7 +8737,7 @@ Lehetséges, hogy további beállításra lesz szükség hozzájuk, de már most
'config-install-alreadydone' => "'''Figyelmeztetés:''' Úgy tűnik, hogy a MediaWiki telepítve van, és te ismét megpróbálod telepíteni.
Folytasd a következő oldalon.",
'config-install-begin' => 'A „{{int:config-continue}}” gomb megnyomása elindítja a MediaWiki telepítését.
-Ha szeretnél módosítani a beállításokon, kattints a vissza gombra.',
+Ha szeretnél módosítani a beállításokon, kattints a vissza gombra.', # Fuzzy
'config-install-step-done' => 'kész',
'config-install-step-failed' => 'sikertelen',
'config-install-extensions' => 'Kiterjesztések beillesztése',
@@ -8243,7 +8788,7 @@ $3
'''Megjegyzés''': Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.
Ha végeztél a fájl elhelyezésével, '''[$2 beléphetsz a wikibe]'''.",
- 'config-download-localsettings' => 'LocalSettings.php letöltése',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> letöltése',
'config-help' => 'segítség',
'mainpagetext' => "'''A MediaWiki telepítése sikeresen befejeződött.'''",
'mainpagedocfooter' => "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [//meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.
@@ -8251,7 +8796,7 @@ Ha végeztél a fájl elhelyezésével, '''[$2 beléphetsz a wikibe]'''.",
== Alapok (angol nyelven) ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Beállítások listája]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki GyIK]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]", # Fuzzy
);
/** Magyar (magázó) (Magyar (magázó))
@@ -8330,7 +8875,7 @@ Ha ezzel készen van, '''[$2 beléphet a wikibe]'''.", # Fuzzy
== Alapok (angol nyelven) ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Beállítások listája]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki GyIK]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]", # Fuzzy
);
/** Armenian (Հայերեն)
@@ -8343,11 +8888,12 @@ $messages['hy'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Interlingua (interlingua)
* @author McDutchie
+ * @author 아라
*/
$messages['ia'] = array(
'config-desc' => 'Le installator de MediaWiki',
@@ -8355,19 +8901,19 @@ $messages['ia'] = array(
'config-information' => 'Information',
'config-localsettings-upgrade' => 'Un file <code>LocalSettings.php</code> ha essite detegite.
Pro actualisar iste installation, per favor entra le valor de <code>$wgUpgradeKey</code> in le quadro hic infra.
-Iste se trova in LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Un file LocalSettings.php file ha essite detegite.
-Pro actualisar iste installation, per favor executa upgrade.php.',
+Iste se trova in <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Un file <code>LocalSettings.php</code> file ha essite detegite.
+Pro actualisar iste installation, per favor executa <code>update.php</code>.',
'config-localsettings-key' => 'Clave de actualisation:',
'config-localsettings-badkey' => 'Le clave que tu forniva es incorrecte',
'config-upgrade-key-missing' => 'Un installation existente de MediaWiki ha essite detegite.
-Pro actualisar iste installation, es necessari adjunger le sequente linea al fin del file LocalSettings.php:
+Pro actualisar iste installation, es necessari adjunger le sequente linea al fin del file <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Le file LocalSettings.php existente pare esser incomplete.
+ 'config-localsettings-incomplete' => 'Le file <code>LocalSettings.php</code> existente pare esser incomplete.
Le variabile $1 non es definite.
-Per favor cambia LocalSettings.php de sorta que iste variabile es definite, e clicca "Continuar".',
- 'config-localsettings-connection-error' => 'Un error esseva incontrate durante le connexion al base de datos usante le configurationes specificate in LocalSettings.php o AdminSettings.php. Per favor repara iste configurationes e tenta lo de novo.
+Per favor cambia <code>LocalSettings.php</code> de sorta que iste variabile es definite, e clicca "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Un error esseva incontrate durante le connexion al base de datos usante le configurationes specificate in <code>LocalSettings.php</code> o <code>AdminSettings.php</code>. Per favor repara iste configurationes e tenta lo de novo.
$1',
'config-session-error' => 'Error al comenciamento del session: $1',
@@ -8501,7 +9047,9 @@ Installation abortate.',
'config-using531' => 'MediaWiki non pote esser usate con PHP $1 a causa de un defecto concernente parametros de referentia a <code>__call()</code>.
Actualisa a PHP 5.3.2 o plus recente, o retrograda a PHP 5.3.0 pro remediar isto.
Installation abortate.',
- 'config-suhosin-max-value-length' => 'Suhosin es installate e limita le longitude del parametro GET a $1 bytes. Le componente ResourceLoader de MediaWiki pote contornar iste limite, ma isto degradara le rendimento. Si possibile, tu deberea mitter suhosin.get.max_value_length a 1024 o plus in php.ini , e mitter $wgResourceLoaderMaxQueryLength al mesme valor in LocalSettings.php .',
+ 'config-suhosin-max-value-length' => 'Suhosin es installate e limita parametro <code>length</code> de GET a $1 bytes.
+Le componente ResourceLoader de MediaWiki va contornar iste limite, ma isto prejudicara le rendimento.
+Si possibile, tu deberea mitter <code>suhosin.get.max_value_length</code> a 1024 o superior in <code>php.ini</code>, e mitter <code>$wgResourceLoaderMaxQueryLength</code> al mesme valor in <code>LocalSettings.php</code>.',
'config-db-type' => 'Typo de base de datos:',
'config-db-host' => 'Servitor de base de datos:',
'config-db-host-help' => 'Si tu servitor de base de datos es in un altere servitor, entra hic le nomine o adresse IP del servitor.
@@ -8575,7 +9123,6 @@ Considera poner le base de datos in un loco completemente differente, per exempl
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki supporta le sequente systemas de base de datos:
$1
@@ -8585,18 +9132,16 @@ Si tu non vide hic infra le systema de base de datos que tu tenta usar, alora se
'config-support-postgres' => '* $1 es un systema de base de datos popular e open source, alternativa a MySQL ([http://www.php.net/manual/en/pgsql.installation.php como compilar PHP con supporto de PostgreSQL]). Es possibile que resta alcun minor defectos non resolvite, dunque illo non es recommendate pro uso in un ambiente de production.',
'config-support-sqlite' => '* $1 es un systema de base de datos legier que es multo ben supportate. ([http://www.php.net/manual/en/pdo.installation.php Como compilar PHP con supporto de SQLite], usa PDO)',
'config-support-oracle' => '* $1 es un banca de datos commercial pro interprisas. ([http://www.php.net/manual/en/oci8.installation.php Como compilar PHP con supporto de OCI8])',
- 'config-support-ibm_db2' => '* $1 es un systema commercial de base de datos pro interprisas.',
'config-header-mysql' => 'Configuration de MySQL',
'config-header-postgres' => 'Configuration de PostgreSQL',
'config-header-sqlite' => 'Configuration de SQLite',
'config-header-oracle' => 'Configuration de Oracle',
- 'config-header-ibm_db2' => 'Configurationes pro IBM DB2',
'config-invalid-db-type' => 'Typo de base de datos invalide',
'config-missing-db-name' => 'Tu debe entrar un valor pro "Nomine de base de datos"',
'config-missing-db-host' => 'Tu debe entrar un valor pro "Host del base de datos"',
'config-missing-db-server-oracle' => 'You must enter a value for "TNS del base de datos"',
'config-invalid-db-server-oracle' => 'TNS de base de datos "$1" invalide.
-Usa solmente litteras ASCII (a-z, A-Z), numeros (0-9), characteres de sublineamento (_) e punctos (.).',
+Usa o "TNS Name" o un catena "Easy Connect". ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Methodos de nomenclatura de Oracle])',
'config-invalid-db-name' => 'Nomine de base de datos "$1" invalide.
Usa solmente litteras ASCII (a-z, A-Z), numeros (0-9), characteres de sublineamento (_) e tractos de union (-).',
'config-invalid-db-prefix' => 'Prefixo de base de datos "$1" invalide.
@@ -8653,7 +9198,7 @@ Isto '''non es recommendate''' si tu non ha problemas con tu wiki.",
Tu pote ora [$1 comenciar a usar tu wiki].',
'config-regenerate' => 'Regenerar LocalSettings.php →',
- 'config-show-table-status' => 'Le consulta SHOW TABLE STATUS falleva!',
+ 'config-show-table-status' => 'Le consulta <code>SHOW TABLE STATUS</code> falleva!',
'config-unknown-collation' => "'''Aviso:''' Le base de datos usa un collation non recognoscite.",
'config-db-web-account' => 'Conto de base de datos pro accesso via web',
'config-db-web-help' => 'Selige le nomine de usator e contrasigno que le servitor web usara pro connecter al servitor de base de datos, durante le operation ordinari del wiki.',
@@ -8682,7 +9227,6 @@ Le bases de datos MyISAM tende a esser corrumpite plus frequentemente que le bas
Isto es plus efficiente que le modo UTF-8 de MySQL, e permitte usar le rango complete de characteres Unicode.
In '''modo UTF-8''', MySQL cognoscera le codification de characteres usate pro tu dats, e pote presentar e converter lo appropriatemente, ma illo non permittera immagazinar characteres supra le [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Plano Multilingue Basic].",
- 'config-ibm_db2-low-db-pagesize' => 'Tu base de datos DB2 ha un "tablespace" (spatio de tabella) predefinite con un "pagesize" (dimension de pagina) insufficiente. Le "pagesize" debe esser \'\'\'32K\'\'\' o plus.',
'config-site-name' => 'Nomine del wiki:',
'config-site-name-help' => 'Isto apparera in le barra de titulo del navigator e in varie altere locos.',
'config-site-name-blank' => 'Entra un nomine de sito.',
@@ -8725,7 +9269,7 @@ Tu pote ora saltar le configuration remanente e installar le wiki immediatemente
'config-optional-continue' => 'Pone me plus questiones.',
'config-optional-skip' => 'Isto me es jam tediose. Simplemente installa le wiki.',
'config-profile' => 'Profilo de derectos de usator:',
- 'config-profile-wiki' => 'Wiki traditional',
+ 'config-profile-wiki' => 'Wiki aperte',
'config-profile-no-anon' => 'Creation de conto obligatori',
'config-profile-fishbowl' => 'Modificatores autorisate solmente',
'config-profile-private' => 'Wiki private',
@@ -8735,7 +9279,7 @@ In MediaWiki, il es facile revider le modificationes recente, e reverter omne da
Nonobstante, multes ha trovate MediaWiki utile in un grande varietate de rolos, e alcun vices il non es facile convincer omnes del beneficios del principio wiki.
Dunque, a te le option.
-Un '''{{int:config-profile-wiki}}''' permitte a omnes de modificar, sin mesmo aperir un session.
+Le modello '''{{int:config-profile-wiki}}''' permitte a omnes de modificar, sin mesmo aperir un session.
Un wiki con '''{{int:config-profile-no-anon}}''' attribue additional responsabilitate, ma pote dissuader contributores occasional.
Le scenario '''{{int:config-profile-fishbowl}}''' permitte al usatores approbate de modificar, ma le publico pote vider le paginas, includente lor historia.
@@ -8792,6 +9336,8 @@ Idealmente, isto non debe esser accessibile ab le web.',
'config-logo-help' => 'Le apparentia predefinite de MediaWiki include spatio pro un logotypo de 135×160 pixels supra le menu del barra lateral.
Incarga un imagine con le dimensiones appropriate, e entra le URL hic.
+Tu pote usar <code>$wgStylePath</code> o <code>$wgScriptPath</code> si le loco de tu logotypo es relative a iste camminos.
+
Si tu non vole un logotypo, lassa iste quadro vacue.',
'config-instantcommons' => 'Activar "Instant Commons"',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] es un function que permitte a wikis de usar imagines, sonos e altere multimedia trovate in le sito [//commons.wikimedia.org/ Wikimedia Commons].
@@ -8826,7 +9372,7 @@ Istes pote requirer additional configuration, ma tu pote activar los ora.',
'config-install-alreadydone' => "'''Aviso:''' Il pare que tu ha jam installate MediaWiki e tenta installar lo de novo.
Per favor continua al proxime pagina.",
'config-install-begin' => 'Un clic sur "{{int:config-continue}}" comencia le installation de MediaWiki.
-Pro facer alterationes, clicca sur "Retro".',
+Pro facer alterationes, clicca sur "{{int:config-back}}".',
'config-install-step-done' => 'finite',
'config-install-step-failed' => 'fallite',
'config-install-extensions' => 'Include le extensiones',
@@ -8883,7 +9429,7 @@ $3
'''Nota''': Si tu non discarga iste file de configuration ora, illo non essera disponibile plus tarde.
Post facer isto, tu pote '''[$2 entrar in tu wiki]'''.",
- 'config-download-localsettings' => 'Discargar LocalSettings.php',
+ 'config-download-localsettings' => 'Discargar <code>LocalSettings.php</code>',
'config-help' => 'adjuta',
'config-nofile' => 'Le file "$1" non poteva esser trovate. Ha illo essite delite?',
'mainpagetext' => "'''MediaWiki ha essite installate con successo.'''",
@@ -8892,7 +9438,8 @@ Post facer isto, tu pote '''[$2 entrar in tu wiki]'''.",
== Pro initiar ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de configurationes]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ a proposito de MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de diffusion pro annuncios de nove versiones de MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de diffusion pro annuncios de nove versiones de MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Traducer MediaWiki in tu lingua]',
);
/** Indonesian (Bahasa Indonesia)
@@ -8900,6 +9447,7 @@ Post facer isto, tu pote '''[$2 entrar in tu wiki]'''.",
* @author IvanLanin
* @author Kenrick95
* @author Reedy
+ * @author 아라
*/
$messages['id'] = array(
'config-desc' => 'Penginstal untuk MediaWiki',
@@ -8907,19 +9455,19 @@ $messages['id'] = array(
'config-information' => 'Informasi',
'config-localsettings-upgrade' => 'Berkas <code>LocalSettings.php</code> sudah ada.
Untuk memutakhirkan instalasi ini, masukkan nilai <code>$wgUpgradeKey</code> dalam kotak yang tersedia di bawah ini.
-Anda dapat menemukan nilai tersebut dalam LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Berkas LocalSettings.php terdeteksi.
-Untuk meningkatkan versi, harap jalankan update.php.',
+Anda dapat menemukan nilai tersebut dalam <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Berkas <code>LocalSettings.php</code> terdeteksi.
+Untuk meningkatkan versi, harap jalankan <code>update.php</code>.',
'config-localsettings-key' => 'Kunci pemutakhiran:',
'config-localsettings-badkey' => 'Kunci yang Anda berikan tidak benar',
'config-upgrade-key-missing' => 'Suatu instalasi MediaWiki telah terdeteksi.
-Untuk memutakhirkan instalasi ini, silakan masukkan baris berikut di bagian bawah LocalSettings.php Anda:
+Untuk memutakhirkan instalasi ini, silakan masukkan baris berikut di bagian bawah <code>LocalSettings.php</code> Anda:
$1',
- 'config-localsettings-incomplete' => 'LocalSettings.php yang ada tampaknya tidak lengkap.
+ 'config-localsettings-incomplete' => '<code>LocalSettings.php</code> yang ada tampaknya tidak lengkap.
Variabel $1 tidak diatur.
-Silakan ubah LocalSettings.php untuk mengatur variabel ini dan klik "Lanjutkan".',
- 'config-localsettings-connection-error' => 'Timbul galat saat menghubungkan ke basis data dengan menggunakan setelan yang ditentukan di LocalSettings.php atau AdminSettings.php. Harap perbaiki setelan ini dan coba lagi.
+Silakan ubah <code>LocalSettings.php</code> untuk mengatur variabel ini dan klik "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Timbul galat saat menghubungkan ke basis data dengan menggunakan setelan yang ditentukan di <code>LocalSettings.php</code> atau <code>AdminSettings.php</code>. Harap perbaiki setelan ini dan coba lagi.
$1',
'config-session-error' => 'Kesalahan sesi mulai: $1',
@@ -9043,7 +9591,7 @@ Instalasi dibatalkan.',
'config-using531' => 'MediaWiki tidak dapat dijalankan dengan PHP $1 karena bug yang melibatkan parameter referensi untuk <code>__call()</code> .
Tingkatkan ke PHP 5.3.2 atau lebih baru, atau turunkan ke PHP versi 5.3.0 untuk menyelesaikan hal ini.
Instalasi dibatalkan.',
- 'config-suhosin-max-value-length' => 'Suhosin terpasang dan membatasi panjang parameter GET sebesar $1 bita. Komponen ResourceLoader MediaWiki akan mengatasi batasan ini, tapi penanganannya akan menurunkan kinerja. Jika memungkinkan, Anda sebaiknya menetapkan nilai suhosin.get.max_value_length menjadi 1024 atau lebih tinggi dalam php.ini dan menyetel $wgResourceLoaderMaxQueryLength dengan nilai yang sama dalam LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Suhosin terpasang dan membatasi panjang parameter GET sebesar $1 bita. Komponen ResourceLoader MediaWiki akan mengatasi batasan ini, tapi penanganannya akan menurunkan kinerja. Jika memungkinkan, Anda sebaiknya menetapkan nilai <code>suhosin.get.max_value_length</code> menjadi 1024 atau lebih tinggi dalam <code>php.ini</code> dan menyetel <code>$wgResourceLoaderMaxQueryLength</code> dengan nilai yang sama dalam LocalSettings.php.', # Fuzzy
'config-db-type' => 'Jenis basis data:',
'config-db-host' => 'Inang basis data:',
'config-db-host-help' => 'Jika server basis data Anda berada di server yang berbeda, masukkan nama inang atau alamat IP di sini.
@@ -9115,7 +9663,6 @@ Pertimbangkan untuk menempatkan basis data di tempat lain, misalnya di <code>/va
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki mendukung sistem basis data berikut:
$1
@@ -9125,12 +9672,10 @@ Jika Anda tidak melihat sistem basis data yang Anda gunakan tercantum di bawah i
'config-support-postgres' => '* $1 adalah sistem basis data sumber terbuka populer sebagai alternatif untuk MySQL ([http://www.php.net/manual/en/pgsql.installation.php cara mengompilasi PHP dengan dukungan PostgreSQL]). Mungkin ada beberapa bug terbuka dan alternatif ini tidak direkomendasikan untuk dipakai dalam lingkungan produksi.',
'config-support-sqlite' => '* $1 adalah sistem basis data yang ringan yang sangat baik dukungannya. ([http://www.php.net/manual/en/pdo.installation.php cara mengompilasi PHP dengan dukungan SQLite], menggunakan PDO)',
'config-support-oracle' => '* $1 adalah basis data komersial untuka perusahaan. ([http://www.php.net/manual/en/oci8.installation.php cara mengompilasi PHP dengan dukungan OCI8])',
- 'config-support-ibm_db2' => '* $1 adalah basis data-perusahaan komersial.',
'config-header-mysql' => 'Pengaturan MySQL',
'config-header-postgres' => 'Pengaturan PostgreSQL',
'config-header-sqlite' => 'Pengaturan SQLite',
'config-header-oracle' => 'Pengaturan Oracle',
- 'config-header-ibm_db2' => 'Pengaturan IBM DB2',
'config-invalid-db-type' => 'Jenis basis data tidak sah',
'config-missing-db-name' => 'Anda harus memasukkan nilai untuk "Nama basis data"',
'config-missing-db-host' => 'Anda harus memasukkan nilai untuk "Inang basis data"',
@@ -9193,7 +9738,7 @@ Tindakan ini '''tidak dianjurkan''' kecuali jika Anda mengalami masalah dengan w
Anda sekarang dapat [$1 mulai menggunakan wiki Anda].',
'config-regenerate' => 'Regenerasi LocalSettings.php →',
- 'config-show-table-status' => 'Kueri SHOW TABLE STATUS gagal!',
+ 'config-show-table-status' => 'Kueri <code>SHOW TABLE STATUS</code> gagal!',
'config-unknown-collation' => "'''Peringatan:''' basis data menggunakan kolasi yang tidak dikenal.",
'config-db-web-account' => 'Akun basis data untuk akses web',
'config-db-web-help' => 'Masukkan nama pengguna dan sandi yang akan digunakan server web untuk terhubung ke server basis data saat operasi normal wiki.',
@@ -9215,7 +9760,6 @@ Basis data MyISAM cenderung lebih sering rusak daripada basis data InnoDB.",
Ini lebih efisien daripada modus UTF-8 MySQL dan memungkinkan Anda untuk menggunakan ragam penuh karakter Unicode.
Dalam '''modus UTF-8''', MySQL akan tahu apa set karakter data dan dapat menampilkan dan mengubahnya sesuai keperluan, tetapi tidak akan mengizinkan Anda menyimpan karakter di atas [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane].",
- 'config-ibm_db2-low-db-pagesize' => "Basis data DB2 Anda tidak memiliki pagesize yang cukup untuk tablespace bawaan. Pagesize harus sama atau lebih dari '''32K'''.",
'config-site-name' => 'Nama wiki:',
'config-site-name-help' => 'Ini akan muncul di bilah judul peramban dan di berbagai tempat lainnya.',
'config-site-name-blank' => 'Masukkan nama situs.',
@@ -9256,7 +9800,7 @@ Anda sekarang dapat melewati sisa konfigurasi dan menginstal wiki sekarang.',
'config-optional-continue' => 'Berikan saya pertanyaan lagi.',
'config-optional-skip' => 'Saya sudah bosan, instal saja wikinya.',
'config-profile' => 'Profil hak pengguna:',
- 'config-profile-wiki' => 'Wiki tradisional',
+ 'config-profile-wiki' => 'Wiki tradisional', # Fuzzy
'config-profile-no-anon' => 'Pembuatan akun diperlukan',
'config-profile-fishbowl' => 'Khusus penyunting terdaftar',
'config-profile-private' => 'Wiki pribadi',
@@ -9270,7 +9814,7 @@ Namun, berbagai kegunaan lain dari MediaWiki telah ditemukan, dan kadang tidak m
'''{{int:config-profile-fishbowl}}''' memungkinkan pengguna yang disetujui untuk menyunting, tetapi publik dapat melihat halaman, termasuk riwayatnya.
'''{{int:config-profile-private}}''' hanya memungkinkan pengguna yang disetujui untuk melihat dan menyunting halaman.
-Konfigurasi hak pengguna yang lebih kompleks tersedia setelah instalasi. Lihat [//www.mediawiki.org/wiki/Manual:User_rights/id entri manual terkait].",
+Konfigurasi hak pengguna yang lebih kompleks tersedia setelah instalasi. Lihat [//www.mediawiki.org/wiki/Manual:User_rights/id entri manual terkait].", # Fuzzy
'config-license' => 'Hak cipta dan lisensi:',
'config-license-none' => 'Tidak ada lisensi',
'config-license-cc-by-sa' => 'Creative Commons Atribusi Berbagi Serupa',
@@ -9321,7 +9865,7 @@ Idealnya, direktori ini tidak boleh dapat diakses dari web.',
'config-logo-help' => 'Kulit bawaan MediaWiki memberikan ruang untuk logo berukuran 135x160 piksel di atas menu bilah samping.
Unggah gambar dengan ukuran yang sesuai, lalu masukkan URL di sini.
-Jika Anda tidak ingin menyertakan logo, biarkan kotak ini kosong.',
+Jika Anda tidak ingin menyertakan logo, biarkan kotak ini kosong.', # Fuzzy
'config-instantcommons' => 'Aktifkan Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] adalah fitur yang memungkinkan wiki untuk menggunakan gambar, suara, dan media lain dari [//commons.wikimedia.org/ Wikimedia Commons].
Untuk melakukannya, MediaWiki memerlukan akses ke Internet.
@@ -9355,7 +9899,7 @@ Ekstensi tersebut mungkin memerlukan konfigurasi tambahan, tetapi Anda dapat men
'config-install-alreadydone' => "'''Peringatan:''' Anda tampaknya telah menginstal MediaWiki dan mencoba untuk menginstalnya lagi.
Lanjutkan ke halaman berikutnya.",
'config-install-begin' => 'Dengan menekan "{{int:config-continue}}", Anda akan memulai instalasi MediaWiki.
-Jika Anda masih ingin membuat perubahan, tekan "{{int:config-back}}".',
+Jika Anda masih ingin membuat perubahan, tekan "{{int:config-back}}".', # Fuzzy
'config-install-step-done' => 'selesai',
'config-install-step-failed' => 'gagal',
'config-install-extensions' => 'Termasuk ekstensi',
@@ -9383,7 +9927,8 @@ Mengabaikan daftar bawaan.",
'config-install-keys' => 'Membuat kunci rahasia',
'config-insecure-keys' => "'''Peringatan:''' {{PLURAL:$2|Suatu|Beberapa}} kunci aman ($1) yang dibuat selama instalasi {{PLURAL:$2|tidak|tidak}} benar-benar aman. Pertimbangkan untuk mengubah {{PLURAL:$2|kunci|kunci-kunci}} tersebut secara manual.",
'config-install-sysop' => 'Membuat akun pengguna pengurus',
- 'config-install-subscribe-fail' => 'Tidak dapat berlangganan mediawiki-announce', # Fuzzy
+ 'config-install-subscribe-fail' => 'Tidak dapat berlangganan mediawiki-announce: $1',
+ 'config-install-subscribe-notpossible' => 'cURL tidak diinstal dan allow_url_fopen tidak tersedia.',
'config-install-mainpage' => 'Membuat halaman utama dengan konten bawaan',
'config-install-extension-tables' => 'Pembuatan tabel untuk ekstensi yang diaktifkan',
'config-install-mainpage-failed' => 'Tidak dapat membuat halaman utama: $1',
@@ -9402,16 +9947,17 @@ $3
'''Catatan''': Jika Anda tidak melakukannya sekarang, berkas konfigurasi yang dihasilkan ini tidak akan tersedia lagi setelah Anda keluar dari proses instalasi tanpa mengunduhnya.
Setelah melakukannya, Anda dapat '''[$2 memasuki wiki Anda]'''.",
- 'config-download-localsettings' => 'Unduh LocalSettings.php',
+ 'config-download-localsettings' => 'Unduh <code>LocalSettings.php</code>',
'config-help' => 'bantuan',
+ 'config-nofile' => 'Berkas "$1" tidak dapat ditemukan. Mungkin sudah dihapus?',
'mainpagetext' => "'''MediaWiki telah terpasang dengan sukses'''.",
'mainpagedocfooter' => 'Silakan baca [//www.mediawiki.org/wiki/Help:Contents/id Panduan Pengguna] untuk cara penggunaan perangkat lunak wiki ini.
== Memulai penggunaan ==
-
* [//www.mediawiki.org/wiki/Manual:Configuration_settings/id Daftar pengaturan konfigurasi]
* [//www.mediawiki.org/wiki/Manual:FAQ/id Daftar pertanyaan yang sering diajukan mengenai MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Terjemahkan MediaWiki ke bahasa Anda]',
);
/** Interlingue (Interlingue)
@@ -9424,6 +9970,12 @@ $messages['ie'] = array(
* @author Ukabia
*/
$messages['ig'] = array(
+ 'config-back' => '← Laàzú',
+ 'config-continue' => 'Gawazie →',
+ 'config-page-language' => 'Ásụ̀sụ̀',
+ 'config-page-name' => 'Áhà',
+ 'config-page-install' => 'Sụ̀ímé',
+ 'config-restart' => 'Eeh, bìdówárí ya.',
'config-admin-password' => 'Okwúngáfè:',
'config-admin-password-confirm' => 'Okwúngáfè mgbe ozor:',
'mainpagetext' => "'''MediaWiki a banyélé nke oma.'''",
@@ -9432,7 +9984,7 @@ $messages['ig'] = array(
== I bídó ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Ndétu ndósé ihe]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce wéfù ndétu nke ozi MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce wéfù ndétu nke ozi MediaWiki]", # Fuzzy
);
/** Iloko (Ilokano)
@@ -9451,7 +10003,7 @@ $messages['io'] = array(
== Komencar ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Listo di ''Configuration setting'']
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki OQQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki nova versioni posto-listo]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki nova versioni posto-listo]", # Fuzzy
);
/** Icelandic (íslenska)
@@ -9464,14 +10016,16 @@ $messages['is'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Listi yfir uppsetningarstillingar]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki Algengar spurningar MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Póstlisti MediaWiki-útgáfa]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Póstlisti MediaWiki-útgáfa]', # Fuzzy
);
/** Italian (italiano)
* @author Beta16
* @author Darth Kule
* @author F. Cosoleto
+ * @author Gianfranco
* @author Karika
+ * @author 아라
*/
$messages['it'] = array(
'config-desc' => 'Il programma di installazione per MediaWiki',
@@ -9479,19 +10033,19 @@ $messages['it'] = array(
'config-information' => 'Informazioni',
'config-localsettings-upgrade' => 'È stato rilevato un file <code>LocalSettings.php</code>.
Per aggiornare questa installazione, si prega di inserire il valore di <code>$wgUpgradeKey</code> nella casella qui sotto.
-Lo potete trovare in LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'È stato rilevato un file LocalSettings.php.
-Per aggiornare questa installazione, eseguire update.php',
+Lo potete trovare in <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'È stato rilevato un file <code>LocalSettings.php</code>.
+Per aggiornare questa installazione, eseguire <code>update.php</code>',
'config-localsettings-key' => 'Chiave di aggiornamento:',
'config-localsettings-badkey' => 'La chiave che hai fornito non è corretta.',
'config-upgrade-key-missing' => "È stata rilevata un'installazione esistente di MediaWiki.
-Per aggiornare questa installazione, si prega di inserire la seguente riga nella parte inferiore del tuo LocalSettings.php:
+Per aggiornare questa installazione, si prega di inserire la seguente riga nella parte inferiore del tuo <code>LocalSettings.php</code>:
$1",
- 'config-localsettings-incomplete' => 'Il file LocalSettings.php esistente sembra essere incompleto.
+ 'config-localsettings-incomplete' => 'Il file <code>LocalSettings.php</code> esistente sembra essere incompleto.
La variabile $1 non è impostata.
-Cambia LocalSettings.php in modo che questa variabile sia impostata e fai clic su "Continua".',
- 'config-localsettings-connection-error' => 'Si è verificato un errore durante la connessione al database utilizzando le impostazioni specificate in LocalSettings.php o AdminSettings.php. Si prega di correggere queste impostazioni e riprovare.
+Cambia <code>LocalSettings.php</code> in modo che questa variabile sia impostata e fai clic su "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Si è verificato un errore durante la connessione al database utilizzando le impostazioni specificate in <code>LocalSettings.php</code> o <code>AdminSettings.php</code>. Si prega di correggere queste impostazioni e riprovare.
$1',
'config-session-error' => "Errore nell'avvio della sessione: $1",
@@ -9524,8 +10078,17 @@ Controlla il tuo file php.ini ed assicurati che <code>session.save_path</code>
'config-help-restart' => 'Vuoi cancellare tutti i dati salvati che hai inserito e riavviare il processo di installazione?',
'config-restart' => 'Sì, riavvia',
'config-welcome' => "=== Controllo dell'ambiente ===
-Vengono eseguiti controlli di base per vedere se questo ambiente è adatto per l'installazione di MediaWiki.
-Se hai bisogno di aiuto durante l'installazione, è necessario fornire i risultati di questi controlli.",
+Saranno eseguiti controlli di base per vedere se questo ambiente è adatto per l'installazione di MediaWiki.
+Ricordati di includere queste informazioni se chiedi assistenza su come completare l'installazione.",
+ 'config-sidebar' => '* [//www.mediawiki.org Pagina principale MediaWiki]
+* [//www.mediawiki.org/wiki/Aiuto:Guida ai contenuti per utenti]
+* [//www.mediawiki.org/wiki/Manuale:Guida ai contenuti per admin]
+* [//www.mediawiki.org/wiki/Manuale:FAQ FAQ]
+----
+* <doclink href=Readme>Leggimi</doclink>
+* <doclink href=ReleaseNotes>Note di versione</doclink>
+* <doclink href=Copying>Copie</doclink>
+* <doclink href=UpgradeDoc>Aggiornamenti</doclink>',
'config-env-good' => "L'ambiente è stato controllato.
È possibile installare MediaWiki.",
'config-env-bad' => "L'ambiente è stato controllato.
@@ -9552,6 +10115,8 @@ L'installazione potrebbe non riuscire!",
'config-no-cache' => "'''Attenzione:''' [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache] non sono stati trovati.
La caching degli oggetti non è attivata.",
'config-diff3-bad' => 'GNU diff3 non trovato.',
+ 'config-git' => 'Trovato software di controllo della versione Git: <code>$1</code>.',
+ 'config-git-bad' => 'Software di controllo della versione Git non trovato.',
'config-imagemagick' => 'Trovato ImageMagick: <code>$1</code>.
Le miniature delle immagini saranno presenti se gli upload vengono abilitati.',
'config-gd' => 'Trovata la GD Graphics Library built-in.
@@ -9563,12 +10128,50 @@ Installazione interrotta.",
'config-no-cli-uri' => "'''Attenzione''': --scriptpath non specificato, si utilizza il valore predefinito: <code>$1</code>.",
'config-using-server' => 'Nome server in uso "<nowiki>$1</nowiki>".',
'config-using-uri' => 'URL del server in uso "<nowiki>$1$2</nowiki>".',
+ 'config-brokenlibxml' => 'Il tuo sistema ha una combinazione di versioni di PHP e libxml2 che è difettosa e che può provocare un danneggiamento non visibile di dati in MediaWiki ed in altre applicazioni per il web.
+Aggiorna a PHP 5.2.9 o successivo, ed a libxml2 2.7.3 o successivo ([//bugs.php.net/bug.php?id=45996 il bug è studiato dal lato PHP]).
+Installazione interrotta.',
+ 'config-using531' => 'MediaWiki non può essere usato con il PHP $1 a causa di un bug che coinvolge i parametri di riferimento a <code>__call()</code>.
+Aggiorna a PHP 5.3.2 o superiore, o fai un downgrade tornando a PHP 5.3.0 per risolvere il problema.
+Installazione interrotta.',
'config-db-type' => 'Tipo di database:',
+ 'config-db-host' => 'Host del database:',
+ 'config-db-host-help' => 'Se il server del tuo database è su un server diverso, immetti qui il nome dell\'host o il suo indirizzo IP.
+
+Se stai utilizzando un web hosting condiviso, il tuo hosting provider dovrebbe fornirti il nome host corretto nella sua documentazione.
+
+Se stai installando su un server Windows con uso di MySQL, l\'uso di "localhost" potrebbe non funzionare correttamente come nome del server. In caso di problemi, prova a impostare "127.0.0.1" come indirizzo IP locale.
+
+Se usi PostgreSQL, lascia questo campo vuoto per consentire di connettersi tramite un socket Unix.',
+ 'config-db-host-oracle' => 'TNS del database:',
+ 'config-db-wiki-settings' => 'Identifica questo wiki',
'config-db-name' => 'Nome del database:',
+ 'config-db-name-help' => 'Scegli un nome che identifica il tuo wiki.
+Non deve contenere spazi.
+
+Se utilizzi un web hosting condiviso, il tuo hosting provider o ti fornisce uno specifico nome di database da utilizzare, oppure ti consentirà di creare il database tramite un pannello di controllo.',
+ 'config-db-name-oracle' => 'Schema del database:',
+ 'config-db-install-account' => "Account utente per l'installazione",
+ 'config-db-username' => 'Nome utente del database:',
+ 'config-db-password' => 'Password del database:',
'config-db-password-empty' => 'Inserire una password per il nuovo utente del database: $1.
Anche se può essere possibile creare utenti senza password, questo non è sicuro.',
+ 'config-db-install-username' => "Inserisci il nome utente che verrà utilizzato per connettersi al database durante il processo di installazione.
+Questo non è il nome utente dell'account MediaWiki; ma quello per il tuo database.",
+ 'config-db-install-password' => "Inserisci la password che verrà utilizzato per connettersi al database durante il processo di installazione.
+Questa non è la password dell'account MediaWiki; ma quella per il tuo database.",
'config-db-install-help' => "Inserire il nome utente e la password che verranno usate per la connessione al database durante il processo d'installazione.",
+ 'config-db-account-lock' => 'Utilizza lo stesso nome utente e password durante il normale funzionamento',
+ 'config-db-wiki-account' => 'Account utente per il normale funzionamento',
+ 'config-db-wiki-help' => "Inserisci il nome utente e la password che verrà utilizzato per connettersi al database durante il normale funzionamento del wiki.
+Se l'account non esiste, e l'account di installazione dispone di privilegi sufficienti, verrà creato con privilegi minimi necessari per operare sul wiki.",
+ 'config-db-prefix' => 'Prefisso tabella del database:',
+ 'config-db-prefix-help' => "Se hai bisogno di condividere un database tra più wiki, o tra MediaWiki e un'altra applicazione web, puoi scegliere di aggiungere un prefisso a tutti i nomi di tabella, per evitare conflitti.
+Non utilizzare spazi.
+
+Solitamente, questo campo viene lasciato vuoto.",
'config-db-charset' => 'Set di caratteri del database',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binario',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
'config-charset-mysql4' => 'MySQL 4.0 con compatibilità UTF-8',
'config-mysql-old' => 'MySQL $1 o una versione successiva è necessaria, rilevata la $2.',
@@ -9578,22 +10181,78 @@ Anche se può essere possibile creare utenti senza password, questo non è sicur
Da cambiare solamente se si è sicuri di averne bisogno.',
'config-pg-test-error' => "Impossibile connettersi al database '''$1''': $2",
'config-sqlite-dir' => 'Directory data di SQLite:',
- 'config-type-ibm_db2' => 'IBM DB2',
+ 'config-oracle-def-ts' => 'Tablespace di default:',
+ 'config-oracle-temp-ts' => 'Tablespace temporaneo:',
+ 'config-support-info' => 'MediaWiki supporta i seguenti sistemi di database:
+
+$1
+
+Se fra quelli elencati qui sotto non vedi il sistema di database che vorresti utilizzare, seguire le istruzioni linkate sopra per abilitare il supporto.',
+ 'config-support-mysql' => '* $1 è la configurazione preferibile per MediaWiki ed è quella meglio supportata ([http://www.php.net/manual/en/mysql.installation.php come compilare PHP con supporto MySQL])',
'config-header-mysql' => 'Impostazioni MySQL',
'config-header-postgres' => 'Impostazioni PostgreSQL',
'config-header-sqlite' => 'Impostazioni SQLite',
'config-header-oracle' => 'Impostazioni Oracle',
- 'config-header-ibm_db2' => 'Impostazioni IBM DB2',
'config-invalid-db-type' => 'Tipo di database non valido',
+ 'config-missing-db-name' => 'È necessario immettere un valore per "Nome del database"',
+ 'config-missing-db-host' => 'È necessario immettere un valore per "Host del database"',
+ 'config-missing-db-server-oracle' => 'È necessario immettere un valore per "TNS del database"',
+ 'config-invalid-db-name' => 'Nome di database "$1" non valido.
+Utilizza soltanto caratteri ASCII come lettere (a-z, A-Z), numeri (0-9), sottolineatura (_) e trattini (-).',
+ 'config-invalid-db-prefix' => 'Prefisso database "$1" non valido.
+Utilizza soltanto caratteri ASCII come lettere (a-z, A-Z), numeri (0-9), sottolineatura (_) e trattini (-).',
+ 'config-connection-error' => '$1.
+
+Controlla host, nome utente e password e prova ancora.',
+ 'config-db-sys-create-oracle' => "Il programma di installazione supporta solo l'utilizzo di un account SYSDBA per la creazione di un nuovo account.",
+ 'config-db-sys-user-exists-oracle' => 'L\'account utente "$1" esiste già. SYSDBA può essere usato solo per la creazione di un nuovo account!',
+ 'config-postgres-old' => 'PostgreSQL $1 o una versione successiva è necessaria, rilevata la $2.',
+ 'config-sqlite-name-help' => 'Scegli un nome che identifichi il tuo wiki.
+Non utilizzare spazi o trattini.
+Questo servirà per il nome del file di dati SQLite.',
+ 'config-sqlite-mkdir-error' => 'Errore durante la creazione della directory dati "$1".
+Controlla la posizione e riprova.',
+ 'config-sqlite-dir-unwritable' => 'Impossibile scrivere nella directory "$1".
+Modifica le autorizzazioni in modo che il webserver possa scrivere in essa e riprova.',
+ 'config-sqlite-connection-error' => '$1.
+
+Controlla la directory dati e il nome del database qui sotto, poi riprova.',
+ 'config-sqlite-readonly' => 'Il file <code>$1</code> non è scrivibile.',
+ 'config-sqlite-cant-create-db' => 'Impossibile creare il file di database <code>$1</code> .',
+ 'config-sqlite-fts3-downgrade' => 'Il PHP è mancante del supporto FTS3, declassamento tabelle in corso',
+ 'config-can-upgrade' => "Ci sono tabelle di MediaWiki in questo database.
+Per aggiornarle a MediaWiki $1, fai clic su '''continua'''.",
+ 'config-upgrade-done' => "Aggiornamento completo.
+
+Puoi [$1 iniziare ad usare il tuo wiki].
+
+Se vuoi rigenerare il tuo file <code>LocalSettings.php</code>, clicca sul pulsante sotto. Questa operazione '''non è raccomandata''', a meno che non hai problemi con il tuo wiki.",
+ 'config-upgrade-done-no-regenerate' => 'Aggiornamento completo.
+
+Puoi [$1 iniziare ad usare il tuo wiki].',
+ 'config-regenerate' => 'Rigenera LocalSettings.php →',
+ 'config-show-table-status' => 'La query <code>SHOW TABLE STATUS</code> è fallita!',
+ 'config-unknown-collation' => "'''Attenzione:''' il database utilizza regole di confronto non riconosciute.",
+ 'config-db-web-account' => "Account del database per l'accesso web",
+ 'config-db-web-help' => 'Seleziona il nome utente e la password che il server web utilizzerà per connettersi al server di database, durante il normale funzionamento del wiki.',
+ 'config-db-web-account-same' => "Utilizza lo stesso account dell'installazione",
'config-db-web-create' => "Crea l'account se non esiste già",
+ 'config-db-web-no-create-privs' => "L'account usato per l'installazione non dispone dei privilegi necessari per creare un altro account.
+L'account indicato qui deve già esistere.",
'config-mysql-engine' => 'Storage engine:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
'config-mysql-charset' => 'Set di caratteri del database:',
+ 'config-mysql-binary' => 'Binario',
'config-mysql-utf8' => 'UTF-8',
- 'config-ibm_db2-low-db-pagesize' => "Il database DB2 in uso ha una tablespace predefinita con un insufficiente pagesize, che dovrebbe essere '''32K''' o maggiore.",
+ 'config-site-name' => 'Nome del wiki:',
+ 'config-site-name-help' => 'Questo verrà visualizzato nella barra del titolo del browser e in vari altri posti.',
+ 'config-site-name-blank' => 'Inserisci il nome del sito.',
+ 'config-project-namespace' => 'Namespace del progetto:',
'config-ns-generic' => 'Progetto',
- 'config-ns-site-name' => 'Stesso nome wiki: $1',
+ 'config-ns-site-name' => 'Stesso nome del wiki: $1',
+ 'config-ns-other' => 'Altro (specificare)',
+ 'config-ns-other-default' => 'MyWiki',
'config-admin-box' => 'Account amministratore',
'config-admin-name' => 'Tuo nome:',
'config-admin-password' => 'Password:',
@@ -9614,26 +10273,58 @@ Specificare un nome utente diverso.',
Inserire un indirizzo email se si desidera effettuare l'iscrizione alla mailing list.",
'config-almost-done' => 'Hai quasi finito!
Adesso puoi saltare la rimanente parte della configurazione e semplicemente installare la wiki.',
+ 'config-optional-continue' => 'Fammi altre domande.',
+ 'config-optional-skip' => 'Sono già stanco, installa solo il wiki.',
+ 'config-profile' => 'Profilo dei diritti utente:',
+ 'config-profile-wiki' => 'Wiki aperto',
+ 'config-profile-no-anon' => 'Creazione utenza obbligatoria',
+ 'config-profile-fishbowl' => 'Solo editori autorizzati',
+ 'config-profile-private' => 'Wiki privata',
'config-license' => 'Copyright e licenza:',
+ 'config-license-none' => 'Nessun piè di pagina per la licenza',
'config-license-cc-by-sa' => 'Creative Commons Attribuzione-Condividi allo stesso modo',
'config-license-cc-by' => 'Creative Commons Attribuzione',
'config-license-cc-by-nc-sa' => 'Creative Commons Attribuzione-Non commerciale-Condividi allo stesso modo',
'config-license-cc-0' => 'Creative Commons Zero (pubblico dominio)',
'config-license-gfdl' => 'GNU Free Documentation License 1.3 o versioni successive',
'config-license-pd' => 'Pubblico dominio',
+ 'config-license-cc-choose' => 'Seleziona una delle licenze Creative Commons',
+ 'config-license-help' => "Molti wiki pubblici rilasciano i loro contributi con una [http://freedomdefined.org/Definition licenza libera]. Questo aiuta a creare un senso di proprietà condivisa nella comunità e incoraggia a contribuire a lungo termine. Non è generalmente necessario per un wiki privato o aziendale.
+
+Se vuoi usare testi da Wikipedia, o desideri che Wikipedia possa essere in grado di accettare testi copiati dal tuo wiki, dovresti scegliere '''Creative Commons Attribution Share Alike'''.
+
+In precedenza Wikipedia ha utilizzato la GNU Free Documentation License. La GFDL è una licenza valida, ma è di difficile comprensione e complica il riutilizzo dei contenuti.",
'config-email-settings' => 'Impostazioni email',
+ 'config-enable-email' => 'Abilita la posta elettronica in uscita',
+ 'config-email-user' => 'Abilita invio email fra utenti',
+ 'config-email-usertalk' => 'Abilita le notifiche per le pagine di discussione utente',
+ 'config-email-watchlist' => 'Abilita le notifiche per gli osservati speciali',
'config-email-auth' => 'Abilita autenticazione via email',
+ 'config-email-sender' => 'Indirizzo email di ritorno:',
+ 'config-upload-settings' => 'Caricamenti di immagini e file',
+ 'config-upload-enable' => 'Consentire il caricamento di file',
'config-upload-deleted' => 'Directory per i file cancellati:',
'config-logo' => 'URL del logo:',
+ 'config-instantcommons' => 'Abilita Instant Commons',
'config-cc-again' => 'Seleziona di nuovo...',
'config-cc-not-chosen' => 'Scegliere quale licenza Creative Commons si desidera e cliccare su "procedi".',
'config-advanced-settings' => 'Configurazione avanzata',
+ 'config-memcached-servers' => 'Server di memcached:',
+ 'config-memcache-needservers' => 'È stato selezionato il tipo di caching Memcached, ma non è stato impostato alcun server.',
'config-memcache-badip' => 'È stato inserito un indirizzo IP non valido per Memcached: $1.',
+ 'config-memcache-badport' => 'I numeri di porta per memcached dovrebbero essere tra $1 e $2.',
'config-extensions' => 'Estensioni',
+ 'config-install-step-done' => 'fatto',
+ 'config-install-step-failed' => 'non riuscito',
+ 'config-install-database' => 'Configurazione database',
+ 'config-install-schema' => 'Creazione dello schema',
+ 'config-install-user' => 'Creazione di utente del database',
'config-install-user-alreadyexists' => 'L\'utente "$1" è già presente',
'config-install-user-create-failed' => 'Creazione dell\'utente "$1" non riuscita: $2',
'config-install-user-missing' => 'L\'utente indicato "$1" non esiste.',
+ 'config-install-tables' => 'Creazione tabelle',
'config-install-tables-failed' => "'''Errore''': La creazione della tabella non è riuscita: $1",
+ 'config-install-interwiki' => 'Riempimento della tabella interwiki predefinita',
'config-install-interwiki-list' => 'Impossibile leggere il file <code>interwiki.list</code>.',
'config-install-stats' => 'Inizializzazione delle statistiche',
'config-install-keys' => 'Generazione delle chiavi segrete',
@@ -9641,8 +10332,9 @@ Adesso puoi saltare la rimanente parte della configurazione e semplicemente inst
'config-install-subscribe-fail' => 'Impossibile sottoscrivere mediawiki-announce: $1',
'config-install-subscribe-notpossible' => 'cURL non è installato e allow_url_fopen non è disponibile.',
'config-install-mainpage' => 'Creazione della pagina principale con contenuto predefinito',
+ 'config-install-extension-tables' => 'Creazione delle tabelle per le estensioni attivate',
'config-install-mainpage-failed' => 'Impossibile inserire la pagina principale: $1',
- 'config-download-localsettings' => 'Scarica LocalSettings.php',
+ 'config-download-localsettings' => 'Scarica <code>LocalSettings.php</code>',
'config-help' => 'aiuto',
'config-nofile' => 'Il file "$1" non può essere trovato. È stato eliminato?',
'mainpagetext' => "'''Installazione di MediaWiki completata correttamente.'''",
@@ -9653,11 +10345,13 @@ I seguenti collegamenti sono in lingua inglese:
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Impostazioni di configurazione]
* [//www.mediawiki.org/wiki/Manual:FAQ Domande frequenti su MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list annunci MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list annunci MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localizza MediaWiki nella tua lingua]",
);
/** Japanese (日本語)
* @author Aphaia
+ * @author Fryed-peach
* @author Iwai.masaharu
* @author Mizusumashi
* @author Ninomy
@@ -9666,6 +10360,7 @@ I seguenti collegamenti sono in lingua inglese:
* @author Whym
* @author Yanajin66
* @author 青子守歌
+ * @author 아라
*/
$messages['ja'] = array(
'config-desc' => 'MediaWiki のインストーラー',
@@ -9673,25 +10368,29 @@ $messages['ja'] = array(
'config-information' => '情報',
'config-localsettings-upgrade' => 'ファイル <code>LocalSettings.php</code> を検出しました。
インストールされているものをアップグレードするには、<code>$wgUpgradeKey</code> の値を以下の欄に入力してください。
-この値は LocalSettings.php 内にあります。',
- 'config-localsettings-cli-upgrade' => 'ファイル LocalSettings.php を検出しました。
-インストールされているものをアップグレードするには、update.php を実行してください',
+この値は <code>LocalSettings.php</code> 内にあります。',
+ 'config-localsettings-cli-upgrade' => 'ファイル <code>LocalSettings.php</code> を検出しました。
+インストールされているものをアップグレードするには、<code>update.php</code> を実行してください',
'config-localsettings-key' => 'アップグレード キー:',
'config-localsettings-badkey' => '与えられたキーが間違っています',
'config-upgrade-key-missing' => 'MediaWiki が既にインストールされていることを検出しました。
-インストールされているものをアップグレードするために、以下の行を LocalSettings.php の末尾に挿入してください:
+インストールされているものをアップグレードするために、以下の行を <code>LocalSettings.php</code> の末尾に挿入してください:
$1',
- 'config-localsettings-incomplete' => '既存の LocalSettings.php の内容は不完全のようです。
+ 'config-localsettings-incomplete' => '既存の <code>LocalSettings.php</code> の内容は不完全のようです。
変数 $1 が設定されていません。
-LocalSettings.php 内でこの変数を設定して、「{{int:Config-continue}}」をクリックしてください。',
+<code>LocalSettings.php</code> 内でこの変数を設定して、「{{int:Config-continue}}」をクリックしてください。',
+ 'config-localsettings-connection-error' => '<code>LocalSettings.php</code> または <code>AdminSettings.php</code> で指定した設定を使用してデータベースに接続する際にエラーが発生しました。
+設定を修正してから再度試してください。
+
+$1',
'config-session-error' => 'セッションの開始エラー: $1',
'config-session-expired' => 'セッションの有効期限が切れたようです。
セッションの有効期間は$1に設定されています。
php.iniの<code>session.gc_maxlifetime</code>を設定することで、この問題を改善できます。
インストール作業を再起動させてください。',
- 'config-no-session' => 'セッションのデータが消失しました!
-php.iniを確認し、<code>session.save_path</code>が適切なディレクトリに設定されていることを確認してください。',
+ 'config-no-session' => 'セッションのデータが消失しました!
+php.ini 内で <code>session.save_path</code> が適切なディレクトリに設定されていることを確認してください。',
'config-your-language' => 'あなたの言語:',
'config-your-language-help' => 'インストール作業に使用する言語を選択してください。',
'config-wiki-language' => 'ウィキの言語:',
@@ -9713,20 +10412,20 @@ php.iniを確認し、<code>session.save_path</code>が適切なディレクト
'config-page-copying' => 'コピー',
'config-page-upgradedoc' => 'アップグレード',
'config-page-existingwiki' => '既存のウィキ',
- 'config-help-restart' => '入力した保存データをすべて消去して、インストール作業を再起動しますか?',
+ 'config-help-restart' => '入力した保存データをすべて消去して、インストール作業を再起動しますか?',
'config-restart' => 'はい、再起動します',
'config-welcome' => '=== 環境の確認 ===
-基本的な確認では、現在の環境がMediaWikiのインストールに適しているかを確認します。
-インストール中に助けが必要になった場合は、この確認結果を提供してください。',
+基本的な確認では、現在の環境が MediaWiki のインストールに適しているかを確認します。
+インストール方法について助けが必要になった場合は、必ずこの確認結果を添えてください。',
'config-copyright' => '=== 著作権および規約 ===
$1
-この作品はフリーソフトウェアです。あなたは、フリーソフトウェア財団の発行するGNU一般公衆利用許諾書 (GNU General Public License)(バージョン2、またはそれ以降のライセンス)の規約に基づき、このライブラリを再配布および改変できます。
+この作品はフリーソフトウェアです。あなたは、フリーソフトウェア財団の発行するGNU一般公衆利用許諾書 (GNU General Public License) (バージョン2、またはそれ以降のライセンス) の規約に基づき、このライブラリを再配布および改変できます。
この作品は、有用であることを期待して配布されていますが、商用あるいは特定の目的に適するかどうかも含めて、暗黙的にも、一切保証されません。
詳しくは、GNU一般公衆利用許諾書をご覧ください。
-あなたはこのプログラムと共に、<doclink href=Copying>GNU一般公衆利用許諾契約書の複製</doclink>を一部受け取ったはずです。もし受け取っていなければ、フリーソフトウェア財団(宛先は the Free Software Foundation, Inc., 59Temple Place, Suite 330, Boston, MA 02111-1307 USA)まで請求してください。',
+あなたはこのプログラムと共に、<doclink href=Copying>GNU一般公衆利用許諾契約書の複製</doclink>を一部受け取ったはずです。受け取っていない場合は、フリーソフトウェア財団 (宛先は the Free Software Foundation, Inc., 59Temple Place, Suite 330, Boston, MA 02111-1307 USA) まで請求してください。',
'config-sidebar' => '* [//www.mediawiki.org MediaWikiのホーム]
* [//www.mediawiki.org/wiki/Help:Contents 利用者向け案内]
* [//www.mediawiki.org/wiki/Manual:Contents 管理者向け案内]
@@ -9745,62 +10444,66 @@ MediaWikiのインストールはできません。',
しかし、MediaWikiには PHP $2 以上が必要です。',
'config-unicode-using-utf8' => 'Unicode正規化に、Brion Vibberのutf8_normalize.soを使用。',
'config-unicode-using-intl' => 'Unicode正規化に[http://pecl.php.net/intl intl PECL 拡張機能]を使用。',
- 'config-unicode-pure-php-warning' => "'''警告''':Unicode正規化の処理に [http://pecl.php.net/intl intl PECL 拡張機能]が使用可能ではなく、処理の遅いピュア PHP の実装を代わりに用いています。
-高トラフィックのサイトを運営する場合は、[//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode正規化に関するページ]をお読みください。",
- 'config-unicode-update-warning' => "'''警告''':インストールされているバージョンのUnicode正規化ラッパーは、[http://site.icu-project.org/ ICUプロジェクト]のライブラリの古いバージョンを使用しています。
-Unicodeを少しでも利用する可能性があるなら、[//www.mediawiki.org/wiki/Unicode_normalization_considerations アップグレード]する必要があります。",
+ 'config-unicode-pure-php-warning' => "'''警告:''' Unicode 正規化の処理に [http://pecl.php.net/intl intl PECL 拡張機能]を利用できないため、処理が遅いピュア PHP の実装を代わりに使用しています。
+高トラフィックのサイトを運営する場合は、[//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode 正規化]をお読みください。",
+ 'config-unicode-update-warning' => "'''警告:''' インストールされているバージョンの Unicode 正規化ラッパーは、[http://site.icu-project.org/ ICU プロジェクト]のライブラリの古いバージョンを使用しています。
+Unicode を少しでも利用する可能性がある場合は、[//www.mediawiki.org/wiki/Unicode_normalization_considerations アップグレード]してください。",
'config-no-db' => '適切なデータベース ドライバーが見つかりませんでした! PHP にデータベース ドライバーをインストールする必要があります。
以下の種類のデータベースに対応しています: $1
共有サーバーを使用している場合は、適切なデータベース ドライバーのインストールを、サーバーの管理者に依頼してください。
PHP を自分でコンパイルした場合は、例えば <code>./configure --with-mysql</code> を実行して、データベース クライアントを使用できるように再設定してください。
Debian または Ubuntu のパッケージから PHP をインストールした場合は、php5-mysql モジュールもインストールする必要があります。',
- 'config-no-fts3' => "'''警告''':SQLiteは[//sqlite.org/fts3.html FTS3]モジュールなしでコンパイルされており、検索機能はこのバックエンドで利用不可能になります。",
- 'config-register-globals' => "'''警告:PHPの<code>[http://php.net/register_globals register_globals]</code>オプションが有効になっています。'''
+ 'config-outdated-sqlite' => "'''警告:''' あなたは SQLite $1 を使用していますが、最低限必要なバージョン $2 より古いバージョンです。SQLite は利用できません。",
+ 'config-no-fts3' => "'''警告:''' SQLite は [//sqlite.org/fts3.html FTS3] モジュールなしでコンパイルされており、このバックエンドでは検索機能は利用できなくなります。",
+ 'config-register-globals' => "'''警告: PHP の <code>[http://php.net/register_globals register_globals]</code> オプションが有効になっています。'''
'''可能なら無効化してください。'''
-MediaWikiは動作しますが、サーバーは、潜在的なセキュリティ脆弱性を露呈します。",
- 'config-magic-quotes-runtime' => "'''致命的エラー:[http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]が動作しています!'''
+MediaWiki は動作しますが、サーバーの潜在的なセキュリティ脆弱性が露呈されます。",
+ 'config-magic-quotes-runtime' => "'''致命的エラー: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] が動作しています!'''
このオプションは、予期せずデータ入力を破壊します。
-このオプションが無効化されないかぎり、MediaWikiをインストールし利用することはできません。",
- 'config-magic-quotes-sybase' => "'''致命的エラー:[http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]が動作しています!'''
+このオプションを無効化しない限り、MediaWiki のインストールや使用はできません。",
+ 'config-magic-quotes-sybase' => "'''致命的エラー: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] が動作しています!'''
このオプションは、予期せずデータ入力を破壊します。
-このオプションが無効化されないかぎり、MediaWikiをインストールし利用することはできません。",
- 'config-mbstring' => "'''致命的エラー:[http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]が動作しています!'''
-このオプションは、エラーを引き起こし、予期せずデータ入力を破壊する可能性があります。
-このオプションが無効化されないかぎり、MediaWikiをインストールし利用することはできません。",
- 'config-ze1' => "'''致命的エラー:[http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode]が動作しています!'''
-このオプションは、MediaWikiにおいて深刻なバグを引き起こします。
-このオプションが無効化されないかぎり、MediaWikiをインストールし利用することはできません。",
- 'config-safe-mode' => "'''警告:'''PHPの[http://www.php.net/features.safe-mode セーフモード]が有効です。
-特にファイルのアップロード<code>math</code>のサポートにおいて、問題が発生する可能性があります。",
+このオプションを無効化しない限り、MediaWiki のインストールや使用はできません。",
+ 'config-mbstring' => "'''致命的エラー: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] が動作しています!'''
+このオプションは、エラーを引き起こし、予期せずデータを破壊するおそれがあります。
+このオプションを無効化しない限り、MediaWiki のインストールや使用はできません。",
+ 'config-ze1' => "'''致命的エラー: [http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode] が動作しています!'''
+このオプションは、MediaWiki において深刻なバグを引き起こします。
+このオプションを無効化しない限り、MediaWiki のインストールや使用はできません。",
+ 'config-safe-mode' => "'''警告:''' PHPの[http://www.php.net/features.safe-mode セーフモード]が有効になっています。
+特に、ファイルのアップロードや<code>math</code>機能で、問題が発生するおそれがあります。",
'config-xml-bad' => 'PHPのXMLモジュールが不足しています。
MediaWikiは、このモジュールの関数を必要としているため、この構成では動作しません。
Mandrakeを実行している場合、php-xmlパッケージをインストールしてください。',
'config-pcre' => 'PCREをサポートしているモジュールが不足しているようです。
MediaWikiは、Perl互換の正規表現関数の動作が必要です。',
- 'config-pcre-no-utf8' => "'''致命的エラー''': PHP の PCRE が PCRE_UTF8 対応なしでコンパイルされているようです。
+ 'config-pcre-no-utf8' => "'''致命的エラー:''' PHP の PCRE が PCRE_UTF8 対応なしでコンパイルされているようです。
MediaWiki を正しく動作させるには、UTF-8 対応が必要です。",
'config-memory-raised' => 'PHPの<code>memory_limit</code>は$1で、$2に引き上げられました。',
- 'config-memory-bad' => "'''警告:'''PHPの<code>memory_limit</code>は$1です。
-これは、非常に遅い可能性があります。
-インストールが失敗するかもしれません!",
+ 'config-memory-bad' => "'''警告:''' PHPの<code>memory_limit</code>に$1に設定されています。
+この値はおそらく小さすぎます。
+インストールが失敗するおそれがあります!",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] がインストール済み',
'config-apc' => '[http://www.php.net/apc APC] がインストール済み',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] がインストール済み',
- 'config-no-cache' => "'''警告:'''[http://www.php.net/apc APC]、[http://xcache.lighttpd.net/ XCache]、[http://www.iis.net/download/WinCacheForPhp WinCache] のいずれも見つかりませんでした。
+ 'config-no-cache' => "'''警告:''' [http://www.php.net/apc APC]、[http://xcache.lighttpd.net/ XCache]、[http://www.iis.net/download/WinCacheForPhp WinCache] のいずれも見つかりませんでした。
オブジェクトのキャッシュは有効化されません。",
'config-diff3-bad' => 'GNU diff3 が見つかりません。',
- 'config-imagemagick' => 'ImageMagickが見つかりました:<code>$1</code>。
-アップロードが有効なら、画像のサムネイルが利用できます。',
+ 'config-git' => 'バージョン管理ソフトウェア Git が見つかりました: <code>$1</code>',
+ 'config-git-bad' => 'バージョン管理ソフトウェア Git が見つかりません。',
+ 'config-imagemagick' => 'ImageMagickが見つかりました: <code>$1</code>。
+アップロードが有効であれば、画像のサムネイルを利用できます。',
'config-gd' => 'GD画像ライブラリが内蔵されていることが確認されました。
アップロードが有効なら、画像のサムネイルが利用できます。',
'config-no-scaling' => 'GDライブラリもImageMagickも見つかりませんでした。
画像のサムネイル生成は無効になります。',
- 'config-no-uri' => "'''エラー:'''現在のURIを決定できませんでした。
+ 'config-no-uri' => "'''エラー:''' 現在のURIを決定できませんでした。
インストールは中止されました。",
+ 'config-no-cli-uri' => "'''警告:''' --scriptpath が指定されていないため、既定値 <code>$1</code> を使用します。",
'config-using-server' => 'サーバー名「<nowiki>$1</nowiki>」を使用しています。',
'config-using-uri' => 'サーバー URL「<nowiki>$1$2</nowiki>」を使用しています。',
- 'config-uploads-not-safe' => "'''警告:'''アップロードの既定ディレクトリ <code>$1</code> に、任意のスクリプト実行に関する脆弱性があります。
+ 'config-uploads-not-safe' => "'''警告:''' アップロードの既定ディレクトリ <code>$1</code> に、任意のスクリプト実行に関する脆弱性があります。
MediaWiki はアップロードされたファイルのセキュリティ上の脅威を確認しますが、アップロードを有効化する前に、[//www.mediawiki.org/wiki/Manual:Security#Upload_security このセキュリティ上の脆弱性を解決する]ことを強く推奨します。",
'config-brokenlibxml' => 'このシステムで使われているPHPとlibxml2のバージョンのこの組み合わせにはバグがあります。具体的には、MediaWikiやその他のウェブアプリケーションでhiddenデータが破損する可能性があります。
PHPを5.2.9かそれ以降のバージョンに、libxml2を2.7.3かそれ以降のバージョンにアップグレードしてください([//bugs.php.net/bug.php?id=45996 PHPでのバグ情報])。
@@ -9808,7 +10511,9 @@ PHPを5.2.9かそれ以降のバージョンに、libxml2を2.7.3かそれ以降
'config-using531' => 'PHP$1は<code>__call()</code>の引数参照に関するバグのため、MediaWikiと互換性がありません。
PHP5.3.2以降に更新するか、この([//bugs.php.net/bug.php?id=50394 PHPに提出されたバグ])を修正するためにPHP5.3.0へ戻してください。
インストールは中止されました。',
- 'config-suhosin-max-value-length' => 'Suhosin がインストールされており、GETパラメータの長さを $1 バイトに制限しています。MediaWiki の ResourceLoader コンポーネントはこの制限を回避しますが、パフォーマンスは低下します。可能な限り、php.ini で suhosin.get.max_value_length を 1024 以上に設定し、同じ値を LocalSettings.php 中で $wgResourceLoaderMaxQueryLength に設定してください。',
+ 'config-suhosin-max-value-length' => 'Suhosin がインストールされており、GET パラメーターの <code>length</code> を $1 バイトに制限しています。
+MediaWiki の ResourceLoader コンポーネントはこの制限を回避しますが、パフォーマンスは低下します。
+可能な限り、<code>php.ini</code> で <code>suhosin.get.max_value_length</code> を 1024 以上に設定し、同じ値を <code>LocalSettings.php</code> 内で <code>$wgResourceLoaderMaxQueryLength</code> に設定してください。',
'config-db-type' => 'データベースの種類:',
'config-db-host' => 'データベースのホスト:',
'config-db-host-help' => '異なるサーバー上にデータベースサーバーがある場合、ホスト名またはIPアドレスをここに入力してください。
@@ -9819,7 +10524,7 @@ WindowsでMySQLを使用している場合に、「localhost」は、サーバ
PostgreSQLを使用している場合、UNIXソケットで接続するにはこの欄を空欄のままにしてください。',
'config-db-host-oracle' => 'データベース TNS:',
- 'config-db-host-oracle-help' => '有効な[http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm ローカル接続名]を入力してください。tnsnames.oraファイルは、このインストールに対して表示されてなければなりません、<br />もしクライアントライブラリ10gもしくはそれ以上を使用している場合、メソッドの名前を[http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 簡易接続]で利用できます。',
+ 'config-db-host-oracle-help' => '有効な[http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm ローカル接続名]を入力してください。tnsnames.ora ファイルは、このインストール先から参照できる場所に置いてください。<br />ご使用中のクライアント ライブラリが 10g 以降の場合、[http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] ネーミング メソッドを使用できます。',
'config-db-wiki-settings' => 'このウィキの識別情報',
'config-db-name' => 'データベース名:',
'config-db-name-help' => 'このウィキを識別する名前を入力してください。
@@ -9831,9 +10536,11 @@ PostgreSQLを使用している場合、UNIXソケットで接続するにはこ
'config-db-username' => 'データベースのユーザー名:',
'config-db-password' => 'データベースのパスワード:',
'config-db-password-empty' => '新しいデータベースの利用者名 $1 のパスワードを入力してください。
-パスワードを設定しないでユーザを作ることもできるかもしれませんが、安全ではありません。',
- 'config-db-install-username' => 'インストール中にデータベースに接続するために使うユーザ名を入力してください。これは MediaWiki アカウントのユーザ名 (利用者名) のことではありません。あなたのデータベースでのユーザ名です。',
- 'config-db-install-password' => 'インストール中にデータベースに接続するために使うパスワードを入力してください。これは MediaWiki アカウントパスワードのことではありません。あなたのデータベースでのパスワードです。',
+パスワードを設定せずにユーザーを作成できる場合もありますが、安全ではありません。',
+ 'config-db-install-username' => 'インストール中にデータベースへの接続で使用するユーザー名を入力してください。
+これは MediaWiki アカウントの利用者名のことではありません。あなたのデータベースでのユーザー名です。',
+ 'config-db-install-password' => 'インストール中にデータベースへの接続で使用するパスワードを入力してください。
+これは MediaWiki アカウントのパスワードのことではありません。あなたのデータベースでのパスワードです。',
'config-db-install-help' => 'インストール作業中にデータベースに接続するための利用者名とパスワードを入力してください。',
'config-db-account-lock' => 'インストール作業終了後も同じ利用者名とパスワードを使用する',
'config-db-wiki-account' => 'インストール作業終了後の利用者アカウント',
@@ -9859,6 +10566,7 @@ PostgreSQLを使用している場合、UNIXソケットで接続するにはこ
'config-db-schema' => 'MediaWiki のスキーマ:',
'config-db-schema-help' => '通常はこのスキーマで問題ありません。
必要な場合のみ変更してください。',
+ 'config-pg-test-error' => "データベース '''$1''' に接続できません: $2",
'config-sqlite-dir' => 'SQLite データ ディレクトリ:',
'config-sqlite-dir-help' => "SQLite は単一のファイル内にすべてのデータを格納しています。
@@ -9876,37 +10584,34 @@ PostgreSQLを使用している場合、UNIXソケットで接続するにはこ
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki は以下のデータベース システムに対応しています:
$1
使用しようとしているデータベース システムが下記の一覧にない場合は、上記リンク先の手順に従ってインストールしてください。',
- 'config-support-mysql' => '* $1はMediaWikiの主要な対象で、もっともサポートされています([http://www.php.net/manual/en/mysql.installation.php MySQLのサポート下でPHPをコンパイルする方法])',
- 'config-support-postgres' => '* $1は、MySQLの代替として、人気のあるオープンソースデータベースシステムです([http://www.php.net/manual/en/pgsql.installation.php PostgreSQLのサポート下でPHPをコンパイルする方法])',
- 'config-support-sqlite' => '* $1は、良くサポートされている、軽量データベースシステムです。([http://www.php.net/manual/en/pdo.installation.php SQLiteのサポート下でPHPをコンパイルする方法]、PDOを使用)',
- 'config-support-oracle' => '* $1は商業企業のデータベースです。([http://www.php.net/manual/en/oci8.installation.php OCI8サポートなPHPをコンパイルする方法])',
- 'config-support-ibm_db2' => '* $1 は商業企業のデータベースです。',
+ 'config-support-mysql' => '* $1はMediaWikiの主要な対象であり、最もサポートされています ([http://www.php.net/manual/en/mysql.installation.php MySQLに対応したPHPをコンパイルする方法])',
+ 'config-support-postgres' => '* $1は、MySQLの代替として人気があるオープンソースのデータベースシステムです ([http://www.php.net/manual/en/pgsql.installation.php PostgreSQLに対応したPHPをコンパイルする方法])',
+ 'config-support-sqlite' => '* $1は、良くサポートされている、軽量データベースシステムです。([http://www.php.net/manual/ja/pdo.installation.php SQLiteに対応したPHPをコンパイルする方法]、PDOを使用)',
+ 'config-support-oracle' => '* $1は商業企業のデータベースです。([http://www.php.net/manual/en/oci8.installation.php OCI8サポートなPHPをコンパイルする方法])',
'config-header-mysql' => 'MySQL の設定',
'config-header-postgres' => 'PostgreSQL の設定',
'config-header-sqlite' => 'SQLite の設定',
'config-header-oracle' => 'Oracle の設定',
- 'config-header-ibm_db2' => 'IBM DB2 の設定',
'config-invalid-db-type' => '無効なデータベースの種類',
'config-missing-db-name' => '「データベース名」を入力してください',
'config-missing-db-host' => '「データベースのホスト」を入力してください',
'config-missing-db-server-oracle' => '「データベース TNS」の値を入力してください',
'config-invalid-db-server-oracle' => '「$1」は無効なデータベース TNS です。
-アスキー英字(a-z、A-Z)、数字(0-9)、アンダーバー(_)、ドット(.)のみを使用してください。',
+「TNS 名」「Easy Connect」文字列のいずれかを使用してください ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle ネーミング メソッド])',
'config-invalid-db-name' => '「$1」は無効なデータベース名です。
-アスキー英字(a-z、A-Z)、数字(0-9)、アンダーバー(_)、ハイフン(-)のみを使用してください。',
- 'config-invalid-db-prefix' => '「$1」は無効なデータベース接頭語です。
-アスキー英字(a-z, A-Z)、数字(0-9)、下線(_)、ハイフン(-)のみを使用してください。',
+半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_)、ハイフン (-) のみを使用してください。',
+ 'config-invalid-db-prefix' => '「$1」は無効なデータベース接頭辞です。
+半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_)、ハイフン (-) のみを使用してください。',
'config-connection-error' => '$1。
-以下のホスト名、ユーザ名、パスワードをチェックして、再度試してみてください。',
+以下のホスト名、ユーザー名、パスワードを確認してから再度試してください。',
'config-invalid-schema' => '「$1」は MediaWiki のスキーマとして無効です。
-ASCII の英数字 (a-z、A-Z、0-9)、下線 (_) のみを使用してください。',
+半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_) のみを使用してください。',
'config-postgres-old' => 'PostgreSQL $1 以降が必要です。ご使用中の PostgreSQL は $2 です。',
'config-sqlite-name-help' => 'あなたのウェキと同一性のある名前を選んでください。
空白およびハイフンは使用しないでください。
@@ -9946,13 +10651,13 @@ chmod a+w $3</pre>',
[$1 ウィキを使い始める]ことができます。
-もし、<code>LocalSettings.php</code>ファイルを再生成したいのならば、下のボタンを押してください。
-ウィキに問題がないのであれば、これは'''推奨されません'''。",
+<code>LocalSettings.php</code> ファイルを再生成したい場合は、下のボタンを押してください。
+ウィキに問題がある場合を除き、再生成は'''推奨されません'''。",
'config-upgrade-done-no-regenerate' => 'アップグレードが完了しました。
[$1 ウィキの使用を開始]することができます。',
- 'config-regenerate' => 'LocalSettings.phpを再生成→',
- 'config-show-table-status' => 'SHOW TABLE STATUSクエリーが失敗しました!',
+ 'config-regenerate' => 'LocalSettings.php を再生成→',
+ 'config-show-table-status' => '<code>SHOW TABLE STATUS</code> クエリが失敗しました!',
'config-unknown-collation' => "'''警告:''' データベースは認識されない照合を使用しています。",
'config-db-web-account' => 'ウェブアクセスのためのデータベースアカウント',
'config-db-web-help' => 'ウィキの通常の操作の際に、ウェブ サーバーがデータベース サーバーに接続できるように、ユーザー名とパスワードを指定してください。',
@@ -9967,7 +10672,7 @@ chmod a+w $3</pre>',
'''MyISAM'''は、利用者が1人の場合、あるいは読み込み専用でインストールする場合に、より処理が早くなるでしょう。
ただし、MyISAMのデータベースは、InnoDBより高頻度で破損する傾向があります。",
- 'config-mysql-charset' => 'データベースの文字セット:',
+ 'config-mysql-charset' => 'データベースの文字セット:',
'config-mysql-binary' => 'バイナリ',
'config-mysql-utf8' => 'UTF-8',
'config-mysql-charset-help' => "'''バイナリ モード'''では、MediaWiki は、UTF-8 テキストをデータベースのバイナリ フィールドに格納します。
@@ -9984,49 +10689,50 @@ chmod a+w $3</pre>',
'config-ns-other' => 'その他 (指定してください)',
'config-ns-other-default' => 'マイウィキ',
'config-project-namespace-help' => "ウィキペディアの例に従い、多くのウィキは、コンテンツのページとは分離したポリシーページを「'''プロジェクトの名前空間'''」に持っています。
-この名前空間のページのタイトルはすべて、ある接頭辞で始まります。それをここで指定することができます。
-この接頭辞はウィキの名前に由来するのが伝統的ですが、「#」や「:」のような区切り文字を含めることはできません。",
- 'config-ns-invalid' => '"<nowiki>$1</nowiki>"のように指定された名前空間は無効です。
-違うプロジェクト名前空間を指定してください。',
+この名前空間内のページのページ名はすべて特定の接頭辞で始まります。それをここで指定できます。
+通常、この接頭辞はウィキ名に基づきますが、「#」や「:」のような区切り文字を含めることはできません。",
+ 'config-ns-invalid' => '指定した名前空間「<nowiki>$1</nowiki>」は無効です。
+別のプロジェクト名前空間を指定してください。',
'config-admin-box' => '管理アカウント',
'config-admin-name' => '名前:',
'config-admin-password' => 'パスワード:',
'config-admin-password-confirm' => 'パスワードの再入力:',
- 'config-admin-help' => '希望するユーザー名をここに入力してください (例: "Joe Bloggs")。
+ 'config-admin-help' => '希望するユーザー名をここに入力してください (例:「Joe Bloggs」)。
この名前でこのウィキにログインすることになります。',
- 'config-admin-name-blank' => '管理者のユーザ名を入力してください。',
- 'config-admin-name-invalid' => '指定されたユーザ名 "<nowiki>$1</nowiki>" は無効です。
-別のユーザ名を指定してください。',
+ 'config-admin-name-blank' => '管理者のユーザー名を入力してください。',
+ 'config-admin-name-invalid' => '指定したユーザー名「<nowiki>$1</nowiki>」は無効です。
+別のユーザー名を指定してください。',
'config-admin-password-blank' => '管理者アカウントのパスワードを入力してください。',
- 'config-admin-password-same' => 'ユーザ名と同じパスワードは使えません。',
+ 'config-admin-password-same' => 'ユーザー名と同じパスワードは使用できません。',
'config-admin-password-mismatch' => '入力された2つのパスワードが一致しません。',
'config-admin-email' => 'メールアドレス:',
'config-admin-email-help' => 'メールアドレスを入力してください。他の利用者からのメールの受け取り、パスワードのリセット、ウォッチリストに登録したページの更新通知に使用します。空欄のままにすることもできます。',
- 'config-admin-error-user' => '"<nowiki>$1</nowiki>"という名前の管理者を作成する際に内部エラーが発生しました。',
- 'config-admin-error-password' => '管理者"<nowiki>$1</nowiki>"のパスワードを設定する際に内部エラーが発生しました: <pre>$2</pre>',
+ 'config-admin-error-user' => '「<nowiki>$1</nowiki>」という名前の管理者を作成する際に内部エラーが発生しました。',
+ 'config-admin-error-password' => '管理者「<nowiki>$1</nowiki>」のパスワードを設定する際に内部エラーが発生しました: <pre>$2</pre>',
+ 'config-admin-error-bademail' => '無効なメールアドレスを入力しました。',
'config-subscribe' => '[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce リリース告知のメーリングリスト]を購読する。',
- 'config-subscribe-help' => 'これは、リリースの告知(重要なセキュリティに関する案内を含む)に使われる、低容量のメーリングリストです。
+ 'config-subscribe-help' => 'これは、リリースの告知 (重要なセキュリティに関する案内を含む) に使用される、流量が少ないメーリングリストです。
このメーリングリストを購読して、新しいバージョンが出た場合にMediaWikiを更新してください。',
- 'config-almost-done' => 'これでほぼ終わりました!
-残りの設定を飛ばして、今すぐにウィキをインストールできます。',
+ 'config-almost-done' => 'これでほぼ終わりました!
+残りの設定を飛ばして、ウィキを今すぐインストールできます。',
'config-optional-continue' => '私にもっと質問してください。',
'config-optional-skip' => 'もう飽きてしまったので、とにかくウィキをインストールしてください。',
'config-profile' => '利用者権限のプロファイル:',
- 'config-profile-wiki' => '伝統的なウィキ',
+ 'config-profile-wiki' => '公開ウィキ',
'config-profile-no-anon' => 'アカウントの作成が必要',
'config-profile-fishbowl' => '承認された編集者のみ',
'config-profile-private' => '非公開ウィキ',
- 'config-profile-help' => "ウィキは、たくさんの人が可能な限りそのウィキを編集できるとき、最も優れた働きをします。
-MediaWikiでは、最近の更新を確認し、神経質な、もしくは悪意を持った利用者からの損害を差し戻すことが、簡単にできます。
+ 'config-profile-help' => "ウィキは、できるだけ多くの人が編集できるようにすると最も優れた働きをします。
+MediaWikiでは、最近の更新を確認しやすく、神経質な、または悪意を持った利用者からの損害を簡単に差し戻せます。
しかし一方で、MediaWikiは、さらにさまざまな形態での利用も優れていると言われています。また、時には、すべての人にウィキ手法の利点を説得させるのは容易ではないかもしれません。
そこで、選択肢があります。
-'''{{int:config-profile-wiki}}'''は、ログインしなくても、誰でも編集できるものです。
-'''{{int:config-profile-no-anon}}'''なウィキは、各編集に対してより強い説明責任を付与しますが、気軽な投稿を阻害するかもしれません。
+「'''{{int:config-profile-wiki}}'''」モデルでは、ログインしなくても、誰でも編集できます。
+「'''{{int:config-profile-no-anon}}'''」なウィキでは、各編集に対してより強い説明責任を付与しますが、気軽な投稿を阻害するかもしれません。
-'''{{int:config-profile-fishbowl}}'''のウィキは、承認された利用者が編集でき、一方、一般の人はページ(とその履歴)を閲覧できます。
-'''{{int:config-profile-private}}'''は、承認された利用者のみがページを閲覧でき、そのグループが編集できます。
+「'''{{int:config-profile-fishbowl}}'''」シナリオでは、承認された利用者のみが編集でき、一般の人はページ (とその履歴) を閲覧できます。
+「'''{{int:config-profile-private}}'''」では、承認された利用者のみがページを閲覧でき、そのグループが編集できます。
より複雑な利用者権限の設定は、インストール後に設定できます。詳細は[//www.mediawiki.org/wiki/Manual:User_rights 関連するマニュアル]をご覧ください。",
'config-license' => '著作権とライセンス:',
@@ -10037,23 +10743,23 @@ MediaWikiでは、最近の更新を確認し、神経質な、もしくは悪
'config-license-gfdl' => 'GNU フリー文書利用許諾契約書 1.3 以降',
'config-license-pd' => 'パブリック・ドメイン',
'config-license-cc-choose' => 'その他のクリエイティブ・コモンズ・ライセンスを選択する',
- 'config-license-help' => "多くの公開ウィキでは、すべての寄稿物が[http://freedomdefined.org/Definition フリーライセンス]の元に置かれています。
+ 'config-license-help' => "多くの公開ウィキでは、すべての寄稿物が[http://freedomdefined.org/Definition フリーライセンス]のもとに置かれています。
こうすることにより、コミュニティによる共有の感覚が生まれ、長期的な寄稿が促されます。
私的ウィキや企業のウィキでは、通常、フリーライセンスにする必要はありません。
ウィキペディアにあるテキストをあなたのウィキで利用し、逆にあなたのウィキにあるテキストをウィキペディアに複製することを許可したい場合には、'''クリエイティブ・コモンズ 表示-継承'''を選択するべきです。
ウィキペディアは以前、GNUフリー文書利用許諾契約書(GFDL)を使用していました。
-GFDL は有効なライセンスですが、内容を理解するのは困難です。
-また、GFDL の元に置かれているコンテンツの再利用も困難です。",
+GFDLは有効なライセンスですが、内容を理解するのは困難です。
+また、GFDLのもとに置かれているコンテンツの再利用も困難です。",
'config-email-settings' => 'メールの設定',
'config-enable-email' => 'メール送信を有効にする',
'config-enable-email-help' => 'メールを使用したい場合は、[http://www.php.net/manual/en/mail.configuration.php PHP のメール設定]が正しく設定されている必要があります。
メールの機能を使用しない場合は、ここで無効にすることができます。',
'config-email-user' => '利用者間のメールを有効にする',
- 'config-email-user-help' => '設定において有効になっている場合、すべてのユーザがお互いにメールのやりとりを行うことを許可する。',
- 'config-email-usertalk' => 'ユーザのトークページにおける通知を有効にする',
- 'config-email-usertalk-help' => '設定で有効にしているならば、ユーザのトークページの変更の通知を受けることをユーザに許可する。',
+ 'config-email-user-help' => '設定で有効になっている場合、すべてのユーザーがお互いにメールのやりとりを行うことを許可する。',
+ 'config-email-usertalk' => 'ユーザーのトークページでの通知を有効にする',
+ 'config-email-usertalk-help' => '設定で有効にしている場合は、ユーザーのトークページの変更の通知を受けることをユーザーに許可する。',
'config-email-watchlist' => 'ウォッチリストの通知を有効にする',
'config-email-watchlist-help' => '利用者が設定で有効にしている場合、閲覧されたページに関する通知を受け取ることを許可する。',
'config-email-auth' => 'メールの認証を有効にする',
@@ -10075,15 +10781,17 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で
'config-upload-deleted-help' => '削除されるファイルを保存するためのディレクトリを選択してください。
これがウェブからアクセスできないことが理想です。',
'config-logo' => 'ロゴ のURL:',
- 'config-logo-help' => 'MediaWikiの既定の外装では、サイドバー上部に135x160ピクセルのロゴ用の余白があります。
-適切なサイズの画像をアップロードして、そのURLをここに入力してください。
+ 'config-logo-help' => 'MediaWiki の既定の外装では、サイドバー上部に135x160ピクセルのロゴ用の余白があります。
+適切なサイズの画像をアップロードして、その URL をここに入力してください。
-ロゴが不要の場合は、このボックスを空白のままにしてください。',
+ロゴが相対パスの場合は、<code>$wgStylePath</code> や <code>$wgScriptPath</code> を使用できます。
+
+ロゴが不要の場合は、この欄を空白のままにしてください。',
'config-instantcommons' => 'Instant Commons 機能を有効にする',
- 'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons InstantCommons]は、[//commons.wikimedia.org/ ウィキメディア・コモンズ]のサイトで見つかった画像や音声、その他のメディアをウィキ上で利用することができるようになる機能です。
-これを有効化するには、MediaWikiはインターネットに接続できなければなりません。
+ 'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] は、[//commons.wikimedia.org/ ウィキメディア・コモンズ]のサイトにある画像、音声、その他のメディアをウィキ上で利用できるようにする機能です。
+これを使用するには、MediaWiki がインターネットに接続できる必要があります。
-ウィキメディア・コモンズ以外のウィキを同じように設定する方法など、この機能に関する詳細な情報は、[//mediawiki.org/wiki/Manual:$wgForeignFileRepos マニュアル]をご覧ください。',
+ウィキメディア・コモンズ以外のウィキを同様に設定する手順など、この機能に関する詳細な情報は、[//mediawiki.org/wiki/Manual:$wgForeignFileRepos マニュアル]をご覧ください。',
'config-cc-error' => 'クリエイティブ・コモンズ・ライセンスの選択器から結果が得られませんでした。
ライセンスの名前を手動で入力してください。',
'config-cc-again' => 'もう一度選択してください...',
@@ -10094,12 +10802,15 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で
中〜大規模サイトではこれを有効にすることを強くお勧めします。小規模サイトでも同様に効果があります。',
'config-cache-none' => 'キャッシングしない(機能は取り払われます、しかもより大きなウィキサイト上でスピードの問題が発生します)',
'config-cache-accel' => 'PHP オブジェクト キャッシュ (APC、XCache、WinCache のいずれか)',
- 'config-cache-memcached' => 'Memcachedを使用(追加の設定が必要です)',
- 'config-memcached-servers' => 'メモリをキャッシュされたサーバ:',
+ 'config-cache-memcached' => 'memcached を使用 (追加の設定が必要)',
+ 'config-memcached-servers' => 'memcached サーバー:',
'config-memcached-help' => 'Memcachedを使用するIPアドレスの一覧。
カンマ区切りで、利用する特定のポートの指定が必要です。例:
127.0.0.1:11211
192.168.1.25:1234',
+ 'config-memcache-noport' => 'Memcached サーバー $1 で使用するポート番号を指定していません。
+ポート番号が分からない場合、既定値は 11211 です。',
+ 'config-memcache-badport' => 'Memcached のポート番号は $1 から $2 の範囲にしてください。',
'config-extensions' => '拡張機能',
'config-extensions-help' => '<code>./extensions</code>ディレクトリ内で、上記リストの拡張機能が発見されました。
@@ -10107,7 +10818,7 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で
'config-install-alreadydone' => "'''警告:''' 既にMediaWikiがインストール済みで、再びインストールし直そうとしています。
次のページへ進んでください。",
'config-install-begin' => '「{{int:config-continue}}」を押すと、MediaWiki のインストールを開始できます。
-変更したい設定がある場合は、「{{int:Config-back}}」を押してください。',
+変更したい設定がある場合は、「{{int:config-back}}」を押してください。',
'config-install-step-done' => '実行',
'config-install-step-failed' => '失敗した',
'config-install-extensions' => '拡張機能を含む',
@@ -10117,6 +10828,7 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で
'config-install-pg-schema-failed' => 'テーブルの作成に失敗しました。
利用者「$1」がスキーマ「$2」に書き込めるようにしてください。',
'config-install-pg-commit' => '変更を送信',
+ 'config-pg-no-plpgsql' => 'データベース $1 内に PL/pgSQL 言語をインストールする必要があります。',
'config-install-user' => 'データベースユーザーの作成',
'config-install-user-alreadyexists' => 'ユーザー「$1」は既に存在します',
'config-install-user-create-failed' => 'ユーザー「$1」の作成に失敗しました: $2',
@@ -10125,42 +10837,44 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で
'config-install-user-missing-create' => '指定したユーザー「$1」は存在しません。
アカウントを作成する場合は、下の「アカウント作成」をクリックしてください。',
'config-install-tables' => 'テーブルの作成',
- 'config-install-tables-exist' => "'''警告''':MediaWikiテーブルは既に存在するようです。
-作成を飛ばします。",
- 'config-install-tables-failed' => "'''エラー''': テーブルの作成が、以下のエラーにより失敗しました: $1",
+ 'config-install-tables-exist' => "'''警告:''' MediaWiki テーブルは既に存在するようです。
+作成を省略します。",
+ 'config-install-tables-failed' => "'''エラー:''' テーブルの作成が、以下のエラーにより失敗しました: $1",
'config-install-interwiki' => '既定のウィキ間テーブルの導入',
'config-install-interwiki-list' => 'ファイル <code>interwiki.list</code> から読み取れませんでした。',
- 'config-install-interwiki-exists' => "'''警告''':ウィキ間テーブルは既に登録されているようです。
+ 'config-install-interwiki-exists' => "'''警告:''' ウィキ間テーブルは既に登録されているようです。
既定のテーブルを無視します。",
'config-install-stats' => '統計情報の初期化',
'config-install-keys' => '秘密鍵の生成',
'config-install-sysop' => '管理者のアカウントの作成',
'config-install-mainpage' => 'メインページを既定の内容で作成',
'config-install-mainpage-failed' => 'メインページを挿入できませんでした: $1',
- 'config-install-done' => "'''おめでとうございます!'''
+ 'config-install-done' => "'''おめでとうございます!'''
MediaWikiのインストールに成功しました。
<code>LocalSettings.php</code>ファイルが生成されました。
-すべての設定がそのファイルに含まれています。
+このファイルはすべての設定を含んでいます。
-それをダウンロードし、ウィキをインストールした基準ディレクトリ(index.phpと同じディレクトリ)に設置する必要があります。ダウンロードは自動的に開始しているはずです。
+これをダウンロードして、ウィキをインストールした基準ディレクトリ (index.phpと同じディレクトリ) に設置する必要があります。ダウンロードは自動的に開始されるはずです。
-ダウンロードが開始していない場合、またダウンロードをキャンセルした場合は、下記のリンクからダウンロードを再開することができます:
+ダウンロードが開始されていない場合、またはダウンロードをキャンセルした場合は、下記のリンクをクリックしてダウンロードを再開できます:
$3
-'''注意''': もし、これを今しなければ、つまり、このファイルをダウンロードせずインストールを終了した場合、この生成された設定ファイルは利用されません。
+'''注意:''' この生成された設定ファイルをダウンロードせずにインストールを終了すると、このファイルは利用できなくなります。
-それを完了すれば、'''[$2 ウィキに入る]'''ことができます。",
- 'config-download-localsettings' => 'LocalSettings.php をダウンロード',
+上記の作業が完了すると、'''[$2 ウィキに入る]'''ことができます。",
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> をダウンロード',
'config-help' => 'ヘルプ',
+ 'config-nofile' => 'ファイル「$1」が見つかりませんでした。削除された可能性があります。',
'mainpagetext' => "'''MediaWiki のインストールに成功しました。'''",
'mainpagedocfooter' => 'ウィキソフトウェアの使い方に関する情報は[//meta.wikimedia.org/wiki/Help:Contents 利用者案内]を参照してください。
== はじめましょう ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings/ja 設定の一覧]
* [//www.mediawiki.org/wiki/Manual:FAQ/ja MediaWiki よくある質問と回答]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiリリース情報メーリングリスト]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki リリース情報メーリングリスト]
+* [//www.mediawiki.org/wiki/Localisation/ja MediaWiki のあなたの言語へのローカライズ]',
);
/** Jamaican Creole English (Patois)
@@ -10173,7 +10887,7 @@ $messages['jam'] = array(
== Taatop ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Jutish (jysk)
@@ -10181,7 +10895,7 @@ $messages['jam'] = array(
*/
$messages['jut'] = array(
'mainpagetext' => "'''MediaWiki er nu installeret.'''",
- 'mainpagedocfooter' => "Se vores engelskspråĝede [//meta.wikimedia.org/wiki/MediaWiki_localisation dokumentåsje tilpasnenge'm åf æ brugergrænseflade] og [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide æ brugervejlednenge] før åplysnenger åpsætnenge'm og anvendelse.",
+ 'mainpagedocfooter' => "Se vores engelskspråĝede [//meta.wikimedia.org/wiki/MediaWiki_localisation dokumentåsje tilpasnenge'm åf æ brugergrænseflade] og [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide æ brugervejlednenge] før åplysnenger åpsætnenge'm og anvendelse.", # Fuzzy
);
/** Javanese (Basa Jawa)
@@ -10192,7 +10906,7 @@ $messages['jv'] = array(
== Miwiti panggunan ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Daftar pangaturan préférènsi]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]", # Fuzzy
);
/** Georgian (ქართული)
@@ -10240,7 +10954,6 @@ $messages['ka'] = array(
'config-header-postgres' => 'PostgreSQL-ის პარამეტრები',
'config-header-sqlite' => 'SQLite-ის პარამეტრები',
'config-header-oracle' => 'Oracle-ის პარამეტრები',
- 'config-header-ibm_db2' => 'IBM DB2-ის პარამეტრები',
'config-invalid-db-type' => 'არასწორი მონაცემთა ბაზის ტიპი',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
@@ -10258,7 +10971,8 @@ $messages['ka'] = array(
'config-admin-password-confirm' => 'პაროლი ხელმეორედ:',
'config-admin-name-blank' => 'შეიყვანეთ ადმინისტრატორის მომხმარებლის სახელი.',
'config-admin-email' => 'ელ. ფოსტის მისამართი:',
- 'config-profile-wiki' => 'ტრადიციული ვიკი',
+ 'config-profile' => 'მომხმარებელთა უფლებების პროფილი:',
+ 'config-profile-wiki' => 'ღია ვიკი',
'config-profile-private' => 'დახურული ვიკი',
'config-license' => 'საავტორო უფლები და ლიცენზია:',
'config-license-cc-by-sa' => 'Creative Commons Attribution Share Alike',
@@ -10277,7 +10991,8 @@ $messages['ka'] = array(
'config-install-step-done' => 'შესრულდა',
'config-install-step-failed' => 'ვერ მოხერხდა',
'config-install-tables' => 'ცხრილების შექმნა',
- 'config-download-localsettings' => 'LocalSettings.php-ის გადმოწერა',
+ 'config-install-interwiki-list' => 'ვერ მოიძებნა ფაილი <code>interwiki.list</code>.',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code>-ის გადმოწერა',
'config-help' => 'დახმარება',
'mainpagetext' => "'''მედიავიკი წარმატებით ჩაიტვირთა.'''",
'mainpagedocfooter' => 'ვიკი პროგრამის გამოყენების ინფორმაციისთვის იხილეთ [//meta.wikimedia.org/wiki/Help:Contents მომხმარებლის მეგზური].
@@ -10286,7 +11001,8 @@ $messages['ka'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings კონფიგურაციის მაჩვენებლების სია]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce მედიავიკის გამოცემის დაგზავნის სია]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce მედიავიკის გამოცემის დაგზავნის სია]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources მედიავიკის ლოკალიზება თქვენ ენაზე]',
);
/** Kara-Kalpak (Qaraqalpaqsha)
@@ -10298,7 +11014,7 @@ $messages['kaa'] = array(
== Baslaw ushın ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Konfiguratsiya sazlaw dizimi]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWikidin' Ko'p Soralatug'ın Sorawları]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki haqqında xat tarqatıw dizimi]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki haqqında xat tarqatıw dizimi]", # Fuzzy
);
/** Адыгэбзэ (Адыгэбзэ)
@@ -10312,14 +11028,14 @@ $messages['kbd-cyrl'] = array(
== Къыщхьэпэгъуэ хъуфынухэр ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Зэгъэзэхуэгъуэ гуэрэхэм я тхылъ];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-м упщӀэ нахъыбу ятхэмрэ я жэуапхэмрэ];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-м и версиэ щӀэуэ къэжахэм я къэӀохугъуэ].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-м и версиэ щӀэуэ къэжахэм я къэӀохугъуэ].', # Fuzzy
);
/** Khowar (کھوار)
* @author Rachitrali
*/
$messages['khw'] = array(
- 'mainpagetext' => "\"<big>'''میڈیاوکیو کامیابیو سورا چالو کورونو بیتی شیر۔.'''</big>\"",
+ 'mainpagetext' => "'''میڈیاوکیو کامیابیو سورا چالو کورونو بیتی شیر۔.'''",
);
/** Kirmanjki (Kırmancki)
@@ -10332,7 +11048,7 @@ $messages['kiu'] = array(
== Gamê verêni ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista ayarunê vırastene]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki de ÇZP]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki ra lista serbest-daena postey]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki ra lista serbest-daena postey]", # Fuzzy
);
/** Kazakh (Arabic script) (قازاقشا (تٴوتە)‏)
@@ -10344,7 +11060,7 @@ $messages['kk-arab'] = array(
== باستاۋ ٴۇشىن ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings باپتالىم قالاۋلارىنىڭ ٴتىزىمى]
* [//www.mediawiki.org/wiki/Manual:FAQ مەدىياۋىيكىيدىڭ جىيى قويىلعان ساۋالدارى]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce مەدىياۋىيكىي شىعۋ تۋرالى حات تاراتۋ ٴتىزىمى]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce مەدىياۋىيكىي شىعۋ تۋرالى حات تاراتۋ ٴتىزىمى]', # Fuzzy
);
/** Kazakh (Cyrillic script) (қазақша (кирил)‎)
@@ -10356,7 +11072,7 @@ $messages['kk-cyrl'] = array(
== Бастау үшін ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Бапталым қалауларының тізімі]
* [//www.mediawiki.org/wiki/Manual:FAQ МедиаУикидің Жиы Қойылған Сауалдары]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce МедиаУики шығу туралы хат тарату тізімі]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce МедиаУики шығу туралы хат тарату тізімі]', # Fuzzy
);
/** Kazakh (Latin script) (qazaqşa (latın)‎)
@@ -10368,7 +11084,7 @@ $messages['kk-latn'] = array(
== Bastaw üşin ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Baptalım qalawlarınıñ tizimi]
* [//www.mediawiki.org/wiki/Manual:FAQ MedïaWïkïdiñ Jïı Qoýılğan Sawaldarı]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MedïaWïkï şığw twralı xat taratw tizimi]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MedïaWïkï şığw twralı xat taratw tizimi]', # Fuzzy
);
/** Khmer (ភាសាខ្មែរ)
@@ -10398,7 +11114,7 @@ $messages['km'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings បញ្ជីកំណត់ទម្រង់]
* [//www.mediawiki.org/wiki/Manual:FAQ/km សំណួរញឹកញាប់​មេឌាវិគី]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce បញ្ជី​ពិភាក្សា​ការផ្សព្វផ្សាយ​របស់​មេឌាវិគី]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce បញ្ជី​ពិភាក្សា​ការផ្សព្វផ្សាយ​របស់​មេឌាវិគី]', # Fuzzy
);
/** Kannada (ಕನ್ನಡ)
@@ -10411,7 +11127,7 @@ $messages['kn'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ ಮೀಡಿಯವಿಕಿ FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]', # Fuzzy
);
/** Korean (한국어)
@@ -10419,24 +11135,24 @@ $messages['kn'] = array(
* @author 아라
*/
$messages['ko'] = array(
- 'config-desc' => '미디어위키 설치 마법사',
+ 'config-desc' => '미디어위키 설치 프로그램',
'config-title' => 'MediaWiki $1 설치',
'config-information' => '정보',
- 'config-localsettings-upgrade' => '<code>LocalSettings.php</code> 파일이 감지되었습니다.
+ 'config-localsettings-upgrade' => '<code>LocalSettings.php</code> 파일을 감지했습니다.
이 설치를 업그레이드하려면 아래 상자에 <code>$wgUpgradeKey</code>의 값을 입력하세요.
-LocalSettings.php에 찾으세요.',
- 'config-localsettings-cli-upgrade' => 'LocalSettings.php 파일이 감지되었습니다.
-이 설치를 업그레이드하려면 update.php를 대신 실행하세요',
+<code>LocalSettings.php</code>에 찾을 수 있습니다.',
+ 'config-localsettings-cli-upgrade' => '<code>LocalSettings.php</code> 파일을 감지했습니다.
+이 설치를 업그레이드하려면 <code>update.php</code>를 대신 실행하세요',
'config-localsettings-key' => '업그레이드 키:',
'config-localsettings-badkey' => '제공한 키가 잘못되었습니다.',
'config-upgrade-key-missing' => '미디어위키의 기존 설치가 감지되었습니다.
-이 설치를 업그레이드하려면 LocalSettings.php의 아래에 다음 줄을 넣으세요:
+이 설치를 업그레이드하려면 <code>LocalSettings.php</code>의 아래에 다음 줄을 넣으세요:
$1',
- 'config-localsettings-incomplete' => '기존 LocalSettings.php가 완전하지 않은 것 같습니다.
+ 'config-localsettings-incomplete' => '기존 <code>LocalSettings.php</code>가 완전하지 않은 것 같습니다.
$1 변수가 설정되어 있지 않습니다.
-이 변수가 설정되도록 LocalSettings.php를 바꾸고 "계속"을 클릭하세요.',
- 'config-localsettings-connection-error' => 'LocalSettings.php 또는 AdminSettings.php에 지정한 설정을 사용하여 데이터베이스에 연결할 때 오류가 발생했습니다. 이러한 설정을 수정하고 다시 시도하세요.
+이 변수가 설정되도록 <code>LocalSettings.php</code>를 바꾸고 "{{int:Config-continue}}"을 클릭하세요.',
+ 'config-localsettings-connection-error' => '<code>LocalSettings.php</code> 또는 <code>AdminSettings.php</code>에 지정한 설정을 사용하여 데이터베이스에 연결할 때 오류가 발생했습니다. 이러한 설정을 수정하고 다시 시도하세요.
$1',
'config-session-error' => '세션 시작 오류: $1',
@@ -10444,12 +11160,12 @@ $1',
세션은 $1의 작동 시간 동안 구성됩니다.
php.ini에 있는 <code>session.gc_maxlifetime</code>에서 설정해 이를 증가시킬 수 있습니다.
설치 과정을 다시 시작합니다.',
- 'config-no-session' => '세션 데이터가 손실되었습니다!
+ 'config-no-session' => '세션 데이터가 없어졌습니다!
php.ini를 확인하고 <code>session.save_path</code>가 적절한 디렉토리로 설정되어 있는지 확인하세요.',
'config-your-language' => '설치 언어:',
'config-your-language-help' => '설치 과정에서 사용할 언어를 선택하세요.',
'config-wiki-language' => '위키 언어:',
- 'config-wiki-language-help' => '주로 작성될 위키에 대한 언어를 선택하세요.',
+ 'config-wiki-language-help' => '위키에 주로 작성될 언어를 선택하세요.',
'config-back' => '← 뒤로',
'config-continue' => '계속 →',
'config-page-language' => '언어',
@@ -10467,11 +11183,11 @@ php.ini를 확인하고 <code>session.save_path</code>가 적절한 디렉토리
'config-page-copying' => '전문',
'config-page-upgradedoc' => '업그레이드하기',
'config-page-existingwiki' => '기존 위키',
- 'config-help-restart' => '당신이 입력한 모든 저장된 데이터를 지우고 설치 과정을 다시 시작하겠습니까?',
+ 'config-help-restart' => '입력한 모든 저장된 데이터를 지우고 설치 과정을 다시 시작하겠습니까?',
'config-restart' => '예, 다시 시작합니다',
'config-welcome' => '=== 사용 환경 검사 ===
-이 환경이 미디어위키 설치에 적합한지 기본 검사를 실행합니다.
-설치 중 도움이 필요하다면 이 검사 결과를 함께 제공해주어야 합니다.',
+기본 검사는 지금 이 환경이 미디어위키 설치에 적합한지 수행합니다.
+설치를 완료하는 방법에 대한 지원을 찾는다면 이 정보를 포함해야 하는 것을 기억하세요.',
'config-copyright' => "=== 저작권 및 이용 약관 ===
$1
@@ -10498,37 +11214,37 @@ $1
'config-env-php' => 'PHP $1(이)가 설치되었습니다.',
'config-env-php-toolow' => 'PHP $1(이)가 설치되었습니다.
하지만 미디어위키는 PHP $2 이상이 필요합니다.',
- 'config-unicode-using-utf8' => '유니코드 정규화에 대해 Brion Vibber의 utf8_normalize.so를 사용합니다.',
- 'config-unicode-using-intl' => '유니코드 정규화에 대해 [http://pecl.php.net/intl intl PECL 확장]을 사용합니다.',
- 'config-unicode-pure-php-warning' => "'''경고''': [http://pecl.php.net/intl intl PECL 확장]은 PHP만으로 구현하는 데에는 느려질 정도로 성능이 떨어지는 유니코드 정규화를 처리할 수 없습니다.
-높은 트래픽의 사이트에서 실행하려면 [//www.mediawiki.org/wiki/Unicode_normalization_considerations 유니코드 정규화]에 대해 약간 참고해야 합니다.",
+ 'config-unicode-using-utf8' => '유니코드 정규화에 Brion Vibber의 utf8_normalize.so를 사용합니다.',
+ 'config-unicode-using-intl' => '유니코드 정규화에 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용합니다.',
+ 'config-unicode-pure-php-warning' => "'''경고''': 유니코드 정규화를 처리할 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용할 수 없기 때문에 느린 순수한 PHP 구현을 대신 사용합니다.
+높은 트래픽 사이트에서 실행하려면 [//www.mediawiki.org/wiki/Unicode_normalization_considerations 유니코드 정규화]를 읽어보시기 바랍니다.",
'config-unicode-update-warning' => "'''경고''': 유니코드 정규화 래퍼의 설치된 버전은 [http://site.icu-project.org/ ICU 프로젝트]의 라이브러리의 이전 버전을 사용합니다.
만약 유니코드를 사용하는 것에 대해 우려가 된다면 [//www.mediawiki.org/wiki/Unicode_normalization_considerations 업그레이드]해야합니다.",
'config-no-db' => '적절한 데이터베이스 드라이버를 찾을 수 없습니다! PHP에 데이터베이스 드라이버를 설치해야 합니다.
다음 데이터베이스 유형을 지원합니다 : $1.
-호스팅을 공유하고 있다면 적절한 데이터베이스 드라이버를 설치하도록 호스팅 제공 업체에 문의하세요.
-PHP를 직접 컴파일할 경우 데이터베이스 클라이언트를 사용하여 활성화하도록 다시 설정하세요. 예들 들어 <code>./configure --with-mysql</code>을 사용하세요.
+공유하는 호스팅을 사용하고 있다면 적절한 데이터베이스 드라이버를 설치하도록 호스팅 제공 업체에 문의하세요.
+PHP를 직접 컴파일했다면 예를 들어 <code>./configure --with-mysql</code>을 사용하여 데이터베이스 클라이언트를 활성화하도록 다시 설정하세요.
데비안이나 우분트 패키지에서 PHP를 설치했다면 php-mysql 모듈도 설치해야 합니다.',
'config-outdated-sqlite' => "'''경고''': SQLite 필요한 최소 $2 버전보다 낮은 $1(이)가 있습니다. SQLite는 사용할 수 없습니다.",
'config-no-fts3' => "'''경고''': SQLite는 [//sqlite.org/fts3.html FTS3 모듈] 없이 컴파일되어, 검색 기능은 백엔드에 사용할 수 없습니다.",
'config-register-globals' => "'''경고: PHP의 <code>[http://php.net/register_globals register_globals]</code> 옵션이 활성화되어 있습니다.'''
'''가능하면 이를 비활성화하십시오.'''
-미디어위키는 작동하지만 서버에 잠재적인 보안 취약점에 노출됩니다.",
+미디어위키는 작동하지만 서버에 잠재적인 보안 취약점이 노출됩니다.",
'config-magic-quotes-runtime' => "'''치명: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]이 활성합니다!'''
이 옵션은 데이터를 입력하는 데 예기치 않는 손상이 일어납니다.
-설치할 수 없습니다. 또는 미디어위키가 사용하지 않는 이 옵션을 비활성화하십시오.",
+이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
'config-magic-quotes-sybase' => "'''치명: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]이 활성합니다!'''
이 옵션은 데이터를 입력하는 데 예기치 않는 손상이 일어납니다.
-설치할 수 없습니다. 또는 미디어위키가 사용하지 않는 이 옵션을 비활성화하십시오.",
+이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
'config-mbstring' => "'''치명: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]이 활성합니다!'''
이 옵션은 오류가 발생하고 데이터를 입력하는 데 예기치 않는 손상이 일어날 수 있습니다.
-설치할 수 없습니다. 또는 미디어위키가 사용하지 않는 이 옵션을 비활성화하십시오.",
+이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
'config-ze1' => "'''치명: [http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode]이 활성합니다!'''
-이 옵션은 미디어위키에 끔찍한 버그를 일으킵니다.
-설치할 수 없습니다. 또는 미디어위키가 사용하지 않는 이 옵션을 비활성화하십시오.",
+이 옵션은 미디어위키에 심간한 버그를 일으킵니다.
+이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
'config-safe-mode' => "'''경고:''' [http://www.php.net/features.safe-mode 안전 모드]이 활성합니다!
-이는 특히 파일을 올리거나 <code>math</code>를 지원하는 데 문제가 발생할 수 있습니다.",
+특히 파일을 올리거나 <code>math</code>를 지원하는 데 문제가 발생할 수 있습니다.",
'config-xml-bad' => 'PHP의 XML 모듈이 없습니다.
미디어위키는 이 모듈의 기능이 필요하며 이 설정에서는 작동하지 않습니다.
Mandrake를 실행하고 있다면 php-xml 패키지를 설치하세요.',
@@ -10538,69 +11254,74 @@ Mandrake를 실행하고 있다면 php-xml 패키지를 설치하세요.',
미디어위키가 제대로 작동하려면 UTF-8 지원이 필요합니다.",
'config-memory-raised' => 'PHP의 <code>memory_limit</code>는 $1이며 $2(으)로 늘리세요.',
'config-memory-bad' => "'''경고:''' PHP의 <code>memory_limit</code>는 $1입니다.
-이는 아마도 너무 낮은 것 같습니다.
+아마도 너무 낮은 것 같습니다.
설치가 실패할 수 있습니다!",
'config-ctype' => "'''치명''': PHP는 [http://www.php.net/manual/en/ctype.installation.php Ctype 확장 기능]에 대해 지원하여 컴파일해야 합니다.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache]가 설치되었습니다',
'config-apc' => '[http://www.php.net/apc APC]가 설치되었습니다',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache]가 설치되었습니다',
'config-no-cache' => "'''경고:''' [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] 또는 [http://www.iis.net/download/WinCacheForPhp WinCache]를 찾을 수 없습니다.
-개체 캐싱이 활성화되지 않습니다.",
+개체 캐싱을 활성화하지 않습니다.",
'config-mod-security' => "'''경고''': 웹 서버에 [http://modsecurity.org/ mod_security]가 허용되었습니다. 잘못 설정된 경우 미디어위키나 사용자가 임의의 콘텐츠를 게시할 수 있는 다른 소프트웨어에 대한 문제를 일으킬 수 있습니다.
[http://modsecurity.org/documentation/ mod_security] 문서를 참고하거나 임의의 오류가 발생할 경우 호스트의 지원 요청에 문의하십시오.",
'config-diff3-bad' => 'GNU diff3를 찾을 수 없습니다.',
+ 'config-git' => 'Git 버전 관리 소프트웨어를 찾았습니다: <code>$1</code>.',
+ 'config-git-bad' => 'Git 버전 관리 소프트웨어를 찾을 수 없습니다.',
'config-imagemagick' => 'ImageMagick를 찾았습니다: <code>$1</code>.
-올리기를 활성화할 경우 그림 섬네일이 활성화될 것입니다.',
+올리기를 활성화할 경우 그림 섬네일이 활성화됩니다.',
'config-gd' => '내장된 GD 그래픽 라이브러리를 찾았습니다.
-올리기를 활성화할 경우 그림 섬네일이 활성화될 것입니다.',
+올리기를 활성화할 경우 그림 섬네일이 활성화됩니다.',
'config-no-scaling' => 'GD 라이브러리나 ImageMagick를 찾을 수 없습니다.
-그림 섬네일이 비활성화될 것입니다.',
+그림 섬네일이 비활성화됩니다.',
'config-no-uri' => "'''오류:''' 현재 URI를 확인할 수 없습니다.
설치가 중단되었습니다.",
'config-no-cli-uri' => "'''경고''': 기본값을 사용하여 --scriptpath를 지정하지 않았습니다: <code>$1</code>.",
'config-using-server' => '"<nowiki>$1</nowiki>"(을)를 서버 이름으로 사용합니다.',
'config-using-uri' => '"<nowiki>$1$2</nowiki>"(을)를 서버 URL로 사용합니다.',
- 'config-uploads-not-safe' => "'''경고:''' 올리기에 대한 기본 디렉토리(<code>$1</code>)는 임의의 스크립트 실행에 취약합니다.
-미디어위키는 보안 위협에 대한 모든 올린 파일을 검사하지만, 이는 올리기를 활성화하기 전에 [//www.mediawiki.org/wiki/Manual:Security#Upload_security 이 보안 취약점을 해결할 것]을 매우 권장합니다.",
- 'config-no-cli-uploads-check' => "'''경고:''' 올리기에 대한 기본 디렉토리(<code>$1</code>)는 CLI를 설치하는 동안 임의의 스크립트 실행에 대한 취약점에 대해 검사되지 않습니다.",
+ 'config-uploads-not-safe' => "'''경고:''' 올리기에 대한 기본 디렉터리(<code>$1</code>)는 임의의 스크립트 실행에 취약합니다.
+미디어위키는 보안 위협에 대한 모든 올린 파일을 검사하지만, 올리기를 활성화하기 전에 [//www.mediawiki.org/wiki/Manual:Security#Upload_security 이 보안 취약점을 해결할 것]을 매우 권장합니다.",
+ 'config-no-cli-uploads-check' => "'''경고:''' 올리기에 대한 기본 디렉터리(<code>$1</code>)는 CLI를 설치하는 동안 임의의 스크립트 실행에 대한 취약점에 대해 검사되지 않습니다.",
'config-brokenlibxml' => '시스템에 버그가 있는 PHP와 libxml2의 조합이 있으며 미디어위키나 다른 웹 어플리케이션에 숨겨진 데이터 손상을 일으킬 수 있습니다.
-PHP 5.2.9 이후와 libxml2 2.7.3 이후로 업그레이드하세요 ([//bugs.php.net/bug.php?id=45996 PHP에 제기한 버그]).
+PHP 5.2.9 이후와 libxml2 2.7.3 이후로 업그레이드하세요. ([//bugs.php.net/bug.php?id=45996 PHP에 제기한 버그])
설치가 중단되었습니다.',
'config-using531' => '미디어위키는 <code>__call()</code>을 참고로 매개 변수를 포함하는 버그로 인해 PHP $1(와)과 함께 사용할 수 없습니다.
문제를 해결하려면 PHP 5.3.2 이상로 업그레이드하거나 PHP 5.3.0으로 다운그레이드를 하세요.
설치가 중단되었습니다.',
- 'config-suhosin-max-value-length' => 'Suhosin(수호신)이 설치되었고 $1 바이트로 GET 매개 변수 길이를 제한하고 있습니다. 미디어위키의 ResourceLoader 구성 요소는 이 제한을 해결하지만 성능이 저하됩니다. 가능하면 php.ini의 suhosin.get.max_value_length에 1024 이상으로 설정하고 LocalSettings.php의 $wgResourceLoaderMaxQueryLength에 같은 값을 설정해야 합니다.',
+ 'config-suhosin-max-value-length' => '수호신(Suhosin)이 설치되었고 $1 바이트로 GET 매개 변수인 <code>length</code>를 제한하고 있습니다.
+미디어위키의 ResourceLoader 구성 요소는 이 제한을 해결하지만 성능이 저하됩니다.
+가능하면 <code>php.ini</code>의 <code>suhosin.get.max_value_length</code>에 1024 이상으로 설정하고 <code>LocalSettings.php</code>의 <code>$wgResourceLoaderMaxQueryLength</code>에 같은 값을 설정해야 합니다.',
'config-db-type' => '데이터베이스 종류:',
'config-db-host' => '데이터베이스 호스트:',
- 'config-db-host-help' => '데이터베이스 서버가 다른 서버에 있을 경우 여기에 호스트 이름이나 IP 주소를 입력하세요.
+ 'config-db-host-help' => '데이터베이스 서버가 다른 서버에 있으면 여기에 호스트 이름이나 IP 주소를 입력하세요.
-웹 호스팅을 공유하여 사용하는 경우 호스팅 공급자는 당신에게 이들 설명서의 올바른 호스트 이름을 표기해야 합니다.
+공유하는 웹 호스팅을 사용하고 있으면 호스팅 제공 업체는 호스트 이름을 설명하고 있을 것입니다.
-윈도 서버에 설치하고 MySQL을 사용할 경우 "localhost"는 서버 이름으로 작동하지 않을 수 있습니다. 그렇지 않으면 로컬 IP 주소로 "127.0.0.1"를 시도하세요.
+윈도 서버에 설치하고 MySQL을 사용하면 "localhost"는 서버 이름으로 작동하지 않을 수 있습니다. 그렇게 된다면 로컬 IP 주소로 "127.0.0.1"를 시도하세요.
-PostgreSQL을 사용할 경우 유닉스 소켓을 통해 연결되도록 입력란을 비워두세요.',
+PostgreSQL을 사용하면 유닉스 소켓을 통해 연결되도록 입력란을 비워두세요.',
'config-db-host-oracle' => '데이터베이스 TNS:',
- 'config-db-host-oracle-help' => '유효한 [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm 로컬 연결 이름]을 입력하세요. tnsnames.ora 파일이 이 설치에 보여야 합니다.<br />10g 이후의 클라이언트 라이브러리를 사용하는 경우 [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 쉬운 연결] 네이밍 메소드도 사용할 수 있습니다.',
+ 'config-db-host-oracle-help' => '올바른 [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm 로컬 연결 이름]을 입력하세요. tnsnames.ora 파일이 이 설치에 보여야 합니다.<br />10g 이후의 클라이언트 라이브러리를 사용하는 경우 [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 쉬운 연결] 네이밍 메소드도 사용할 수 있습니다.',
'config-db-wiki-settings' => '이 위키 식별',
'config-db-name' => '데이터베이스 이름:',
'config-db-name-help' => '위키를 식별하기 위한 이름을 선택하세요.
-이는 공백이 없어야 합니다.
+공백이 없어야 합니다.
-웹 호스팅을 공유해 사용하는 경우 호스팅 제공 업체도 당신에게 제어판을 통해 데이터베이스를 사용하거나 만들 수 있도록 특정 데이터베이스 이름을 제공합니다.',
+공유하는 웹 호스팅 사용하면 호스팅 제공 업체가 특정 데이터베이스 이름을 제공하거나 관리 패널에서 데이터베이스를 만들 수 있습니다.',
'config-db-name-oracle' => '데이터베이스 스키마:',
'config-db-account-oracle-warn' => '데이터베이스 백엔드로 오라클을 설치하기 위해 지원하는 세 가지 시나리오가 있습니다:
설치 과정의 일부로 데이터베이스 계정을 만들려면 설치를 위해 데이터베이스 계정으로 SYSDBA 역할을 가진 계정을 제공하고 웹 접근 계정에 대해 원하는 자격 증명을 지정하세요, 그렇지 않으면 수동으로 웹 접근 계정을 만들 수 있으며 (필요한 경우 권한 스키마 개체를 만들어야 합니다) 또는 다른 계정 두 개를 만들고 권한을 가진 하나의 웹 접근을 위한 제한된 하나를 제공할 수 있습니다.
-필요한 권한을 가진 계정을 만드는 스크립트는 이 설치의 "maintenance/oracle/" 디렉토리에서 찾을 수 있습니다. 제한된 계정을 사용하면 기본 계정으로 모든 관리 기능을 비활성화할 것을 염두해 두십시오.',
+필요한 권한을 가진 계정을 만드는 스크립트는 이 설치의 "maintenance/oracle/" 디렉토리에서 찾을 수 있습니다. 제한된 계정을 사용하면 기본 계정으로 모든 관리 기능을 비활성화할 것을 유의하십시오.',
'config-db-install-account' => '설치를 위한 사용자 계정',
'config-db-username' => '데이터베이스 사용자 이름:',
'config-db-password' => '데이터베이스 비밀번호:',
'config-db-password-empty' => '새 데이터베이스 사용자의 비밀번호를 입력하세요: $1.
-비밀번호 없이 사용자를 만들 수도 있지만 이는 안전하지 않습니다.',
+비밀번호 없이 사용자를 만들 수도 있지만 안전하지 않습니다.',
'config-db-install-username' => '설치 과정 중에 데이터베이스에 연결할 때 사용할 사용자 이름을 입력하세요.
-이는 미디어위키 계정의 사용자 이름이 아닌 데이터베이스에 대한 사용자 이름입니다.',
- 'config-db-install-password' => '설치 과정 중에 데이터베이스에 연결할 때 사용할 비밀번호을 입력하세요. 이는 미디어위키 계정의 비밀번호가 아닌 데이터베이스에 대한 비밀번호입니다.',
+미디어위키 계정의 사용자 이름이 아닌 데이터베이스에 대한 사용자 이름입니다.',
+ 'config-db-install-password' => '설치 과정 중에 데이터베이스에 연결할 때 사용할 비밀번호을 입력하세요.
+미디어위키 계정의 비밀번호가 아닌 데이터베이스에 대한 비밀번호입니다.',
'config-db-install-help' => '설치 과정 중에 데이터베이스에 연결할 때 사용할 사용자 이름과 비밀번호를 입력하세요.',
'config-db-account-lock' => '정상적으로 작동하는 동안 같은 사용자 이름과 비밀번호를 사용함',
'config-db-wiki-account' => '정상적인 작동을 위한 사용자 계정',
@@ -10615,54 +11336,51 @@ PostgreSQL을 사용할 경우 유닉스 소켓을 통해 연결되도록 입력
'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 바이너리',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
'config-charset-mysql4' => 'MySQL 4.0 UTF-8 하위 호환성',
- 'config-charset-help' => "'''경고:''' MySQL 4.1에서 '''UTF-8 하위 호환성'''을 사용하고 나서 <code>mysqldump</code>로 데이터베이스에 백업한다면 이는 모든 ASCII가 아닌 문자를 파괴하고 손상한 백업을 되돌릴 수 없습니다!
+ 'config-charset-help' => "'''경고:''' MySQL 4.1에서 '''UTF-8 하위 호환성'''을 사용하고 나서 <code>mysqldump</code>로 데이터베이스에 백업한다면 모든 ASCII가 아닌 문자를 파괴하고 손상한 백업을 되돌릴 수 없습니다!
'''바이너리 모드'''에서는 미디어위키는 바이너리 필드의 데이터베이스에 UTF-8 텍스트를 저장합니다.
-이는 MySQL의 UTF-8 모드를 보다 더 효율적이고 유니코드 문자의 전체 범위를 사용할 수 있습니다.
-'''UTF-8 모드'''에서는 MySQL은 데이터를 설정하는 어떤 문자열인지를 알 것이며, 표현하고 적절하게 그것을 변환할 수 있지만
-[//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes 기본 다국어 범위] 상의 문자를 저장하지 못하게 될 수 있습니다.",
+MySQL의 UTF-8 모드를 보다 더 효율적이고 유니코드 문자의 전체 범위를 사용할 수 있습니다.
+'''UTF-8 모드'''에서는 MySQL은 데이터를 설정하는 문자 집합을 알고 있기 때문에 적절하게 표현하고 변환할 수 있지만
+[//ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C_%ED%8F%89%EB%A9%B4#.EA.B8.B0.EB.B3.B8_.EB.8B.A4.EA.B5.AD.EC.96.B4_.ED.8F.89.EB.A9.B4 기본 다국어 평면] 밖의 문자를 저장할 수 없습니다.",
'config-mysql-old' => 'MySQL $1 이상이 필요하나 $2(이)가 있습니다.',
'config-db-port' => '데이터베이스 포트:',
'config-db-schema' => '미디어위키에 대한 스키마:',
- 'config-db-schema-help' => '이 스키마는 보통 괜찮습니다.
-필요로 알고 있을 경우에만 이를 바꾸세요.',
+ 'config-db-schema-help' => '보통 이 스키마는 문제가 없습니다.
+필요한 경우에만 바꾸세요.',
'config-pg-test-error' => "'''$1''' 데이터베이스에 연결할 수 없습니다: $2",
- 'config-sqlite-dir' => 'SQLite 데이터 디렉토리:',
- 'config-sqlite-dir-help' => 'SQLite는 하나의 파일에 모든 데이터를 저장합니다.
+ 'config-sqlite-dir' => 'SQLite 데이터 디렉터리:',
+ 'config-sqlite-dir-help' => "SQLite는 하나의 파일에 모든 데이터를 저장합니다.
-제공하는 디렉토리는 설치하는 동안 웹 서버에 의해 쓸 수 있어야 합니다.
+제공하는 디렉토리는 설치하는 동안 웹 서버가 쓸 수 있어야 합니다.
-PHP 파일이 있는 곳을 우리가 이를 맡길 수 없는 이유는 웹을 통해 접근할 수 없다는 것입니다.
+이 디렉토리는 웹을 통해 접근할 수 '''없어야''' 하는데 PHP 파일이 있는 곳에 넣을 수 없는 것은 이 때문입니다.
-설치 마법사가 이과 함께 .htaccess 파일을 만들지만 거기서 실패하면 누군가는 원본 데이터베이스에 접근하는 데 실패합니다.
-이는 원시 사용자 데이터(이메일 주소, 암호 해시) 뿐만 아니라 삭제된 개정판과 위키의 다른 제한된 데이터를 포함합니다.
+설치 프로그램은 <code>.htaccess</code> 파일을 작성하지만 이것이 실패하면 누군가가 원본 데이터베이스에 접근할 수 있습니다.
+데이터베이스는 원본 사용자 데이터(이메일 주소, 해시한 비밀번호) 뿐만 아니라 삭제된 판과 위키의 다른 제한된 데이터를 포함합니다.
-<code>/var/lib/mediawiki/yourwiki</code>와 같이 모두 다른 곳에서 데이터베이스를 넣어보도록 하세요.',
+예를 들어 <code>/var/lib/mediawiki/yourwiki</code>와 같이 다른 곳에 데이터베이스를 넣는 것이 좋습니다.",
'config-oracle-def-ts' => '기본 테이블공간:',
'config-oracle-temp-ts' => '임시 테이블공간:',
- 'config-type-oracle' => '오라클',
- 'config-type-ibm_db2' => 'IBM DB2',
+ 'config-type-oracle' => 'Oracle',
'config-support-info' => '미디어위키는 다음의 데이터베이스 시스템을 지원합니다:
$1
-데이터베이스 시스템이 표시되지 않을 때 아래에 나열된 다음 지원을 활성화하려면 당신은 위의 링크된 지시에 따라 사용해볼 수도 있습니다.',
+데이터베이스 시스템이 표시되지 않을 때 아래에 나열된 다음 지원을 활성화하려면 위의 링크된 지시에 따라 설치해볼 수 있습니다.',
'config-support-mysql' => '* $1은 미디어위키의 기본 대상으로 가장 잘 지원합니다. ([http://www.php.net/manual/en/mysql.installation.php MySQL을 지원하여 PHP를 컴파일하는 방법])',
- 'config-support-postgres' => '* $1은 MySQL의 대안으로 인기있는 오픈 소스 데이터베이스 시스템입니다. ([http://www.php.net/manual/en/pgsql.installation.php PostgreSQL을 지원하여 PHP를 컴파일하는 방법]) 몇가지 사소한 해결하지 못한 버그가 있을 수 있으며, 이를 제작 환경에서 사용하지 않는 것이 좋습니다.',
+ 'config-support-postgres' => '* $1은 MySQL의 대안으로 인기 있는 오픈 소스 데이터베이스 시스템입니다. ([http://www.php.net/manual/en/pgsql.installation.php PostgreSQL을 지원하여 PHP를 컴파일하는 방법]) 몇가지 사소한 해결하지 못한 버그가 있을 수 있으며, 이를 제작 환경에서 사용하지 않는 것이 좋습니다.',
'config-support-sqlite' => '* $1는 매우 잘 지원하는 가벼운 데이터베이스 시스템입니다. ([http://www.php.net/manual/en/pdo.installation.php SQLite를 지원하여 PHP를 컴파일하는 방법], PDO 사용)',
'config-support-oracle' => '* $1은 상용 엔터프라이스 데이터베이스입니다. ([http://www.php.net/manual/en/oci8.installation.php OCI8을 지원하여 PHP를 컴파일하는 방법])',
- 'config-support-ibm_db2' => '* $1는 상용 엔터프라이즈 데이터베이스입니다.',
'config-header-mysql' => 'MySQL 설정',
'config-header-postgres' => 'PostgreSQL 설정',
'config-header-sqlite' => 'SQLite 설정',
- 'config-header-oracle' => '오라클 설정',
- 'config-header-ibm_db2' => 'IBM DB2 설정',
+ 'config-header-oracle' => 'Oracle 설정',
'config-invalid-db-type' => '잘못된 데이터베이스 종류',
'config-missing-db-name' => '"데이터베이스 이름"에 대한 값을 입력해야 합니다',
'config-missing-db-host' => '"데이터베이스 호스트"에 대한 값을 입력해야 합니다',
'config-missing-db-server-oracle' => '"데이터베이스 TNS"에 대한 값을 입력해야 합니다',
'config-invalid-db-server-oracle' => '"$1" 데이터베이스 TNS가 잘못됐습니다.
-ASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하세요.',
+"TNS Name"이나 "Easy Connect" 문자열 중 하나를 사용하세요 ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle 네이밍 메서드])',
'config-invalid-db-name' => '"$1" 데이터베이스 이름이 잘못되었습니다.
ASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하세요.',
'config-invalid-db-prefix' => '"$1" 데이터베이스 접두어가 잘못됐습니다.
@@ -10672,39 +11390,39 @@ ASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하
호스트, 계정 이름과 비밀번호를 확인하고 다시 시도하세요.',
'config-invalid-schema' => '미디어위키 "$1"에 대한 스키마가 잘못됐습니다.
ASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하세요.',
- 'config-db-sys-create-oracle' => '설치 마법사는 새 계정을 만들기 위한 SYSDBA 계정만을 지원합니다.',
+ 'config-db-sys-create-oracle' => '설치 프로그램은 새 계정을 만들기 위한 SYSDBA 계정만을 지원합니다.',
'config-db-sys-user-exists-oracle' => '"$1" 사용자 계정이 이미 존재합니다. SYSDBA는 새 계정을 만드는 데에만 사용할 수 있습니다!',
'config-postgres-old' => 'PostgreSQL $1 이상이 필요하나 $2(이)가 있습니다.',
'config-sqlite-name-help' => '위키를 식별하기 위한 이름을 선택하세요.
공백이나 하이픈을 사용하지 마십시오.
-이는 SQLite 데이터 파일 이름에 사용됩니다.',
- 'config-sqlite-parent-unwritable-group' => '<code><nowiki>$1</nowiki></code> 데이터 디렉토리를 만들 수 없으며 <code><nowiki>$2</nowiki></code> 상위 디렉토리에 웹 서버에 의해 쓸 수 없기 때문입니다.
+SQLite 데이터 파일 이름에 사용됩니다.',
+ 'config-sqlite-parent-unwritable-group' => '<code><nowiki>$1</nowiki></code> 데이터 디렉토리를 만들 수 없으며 웹 서버는 <code><nowiki>$2</nowiki></code> 상위 디렉토리에 쓸 수 없기 때문입니다.
-설치 마법사는 웹 서버로 실행중인 사용자를 결정할 수 없습니다.
-계속하려면 이를 쓸 수 있는 <code><nowiki>$3</nowiki></code> 디렉토리를 만드세요.
+설치 프로그램은 웹 서버로 실행중인 사용자를 지정할 수 없습니다.
+계속하려면 웹 서버가 쓸 수 있는 <code><nowiki>$3</nowiki></code> 디렉토리를 만드세요.
유닉스/리눅스 시스템에서의 수행:
<pre>cd $2
mkdir $3
chgrp $4 $3
chmod g+w $3</pre>',
- 'config-sqlite-parent-unwritable-nogroup' => '<code><nowiki>$1</nowiki></code> 데이터 디렉토리를 만들 수 없으며 <code><nowiki>$2</nowiki></code> 상위 디렉토리에 웹 서버에 의해 쓸 수 없기 때문입니다.
+ 'config-sqlite-parent-unwritable-nogroup' => '<code><nowiki>$1</nowiki></code> 데이터 디렉토리를 만들 수 없으며 웹 서버는 <code><nowiki>$2</nowiki></code> 상위 디렉토리에 쓸 수 없기 때문입니다.
-설치 마법사는 웹 서버로 실행중인 사용자를 결정할 수 없습니다.
-계속하려면 이(와 기타!)를 전역으로 쓸 수 있는 <code><nowiki>$3</nowiki></code> 디렉토리를 만드세요.
+설치 프로그램은 웹 서버로 실행중인 사용자를 지정할 수 없습니다.
+계속하려면 웹 서버(와 기타!)가 전역으로 쓸 수 있는 <code><nowiki>$3</nowiki></code> 디렉토리를 만드세요.
유닉스/리눅스 시스템에서의 수행:
<pre>cd $2
mkdir $3
chmod a+w $3</pre>',
- 'config-sqlite-mkdir-error' => '"$1" 데이터 디렉토리를 만드는 중 오류났습니다.
+ 'config-sqlite-mkdir-error' => '"$1" 데이터 디렉토리를 만드는 중 오류가 났습니다.
경로를 확인하고 다시 시도하세요.',
'config-sqlite-dir-unwritable' => '"$1" 디렉토리에 쓸 수 없습니다.
웹 서버를 쓸 수 있도록 권한을 바꾸고 다시 시도하세요.',
'config-sqlite-connection-error' => '$1.
호스트, 계정 이름과 비밀번호를 확인하고 다시 시도하세요.',
- 'config-sqlite-readonly' => '<code>$1</code> 파일은 쓰기가 불가능합니다.',
+ 'config-sqlite-readonly' => '<code>$1</code> 파일은 쓸 수 없습니다.',
'config-sqlite-cant-create-db' => '<code>$1</code> 데이터베이스 파일을 만들 수 없습니다.',
'config-sqlite-fts3-downgrade' => 'PHP가 FTS3 지원이 없어졌습니다. 테이블을 다운그레이드하세요.',
'config-can-upgrade' => "이 데이터베이스에 미디어위키 테이블이 있습니다.
@@ -10714,12 +11432,12 @@ chmod a+w $3</pre>',
이제 [$1 위키를 시작]할 수 있습니다.
만약 <code>LocalSettings.php</code> 파일을 다시 만들기를 원하면 아래의 버튼을 클릭하세요.
-이것은 위키에 문제가 있지 않는 한 '''권장하지 않습니다'''.",
+위키에 문제가 있지 않는 한 '''권장하지 않습니다'''.",
'config-upgrade-done-no-regenerate' => '업그레이드가 완료되었습니다.
이제 [$1 위키를 시작]할 수 있습니다.',
'config-regenerate' => 'LocalSettings.php 다시 만들기 →',
- 'config-show-table-status' => 'SHOW TABLE STATUS 쿼리 실패!',
+ 'config-show-table-status' => '<code>SHOW TABLE STATUS</code> 쿼리를 실패했습니다!',
'config-unknown-collation' => "'''경고:''' 데이터베이스가 인식하지 않는 정렬을 사용하고 있습니다.",
'config-db-web-account' => '웹 접근을 위한 데이터베이스 계정',
'config-db-web-help' => '위키의 일반적인 작업 중에 데이터베이스 서버에 연결하는 데 사용할 웹 서버에 대한 계정 이름과 비밀번호를 선택하세요.',
@@ -10730,14 +11448,20 @@ chmod a+w $3</pre>',
'config-mysql-engine' => '스토리지 엔진:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
- 'config-mysql-myisam-dep' => "'''경고''': 미디어위키와 함께 사용하도록 권장하지 않는 MySQL에 대한 스토리지 엔진으로 MyISAM을 선택하였습니다. 이유는:
-* 이는 테이블이 잠겨있어 동시성을 거의 지원하지 않습니다
-* 이는 다른 엔진보다 손상이 더 자주 발생합니다
-* 미디어위키 바탕 코드가 항상 정상적으로 MyISAM을 처리하지 않습니다
+ 'config-mysql-myisam-dep' => "'''경고''': 미디어위키에 사용하지 않는 것이 좋은 MySQL에 대한 스토리지 엔진으로 MyISAM을 선택하였습니다. 이유는:
+* 테이블 잠금에 의해 간신히 동시성을 지원합니다
+* 다른 엔진보다 손상하는 경향이 있습니다
+* 미디어위키 코드베이스가 항상 정상적으로 MyISAM을 처리하지 않습니다
+
+MySQL 설치가 InnoDB를 지원한다면, 그 선택 대신에 InnoDB를 선택할 것을 매우 권장합니다.
+MySQL 설치가 InnoDB를 지원하지 않는다면, 아마도 업그레이드를 할 시간입니다.",
+ 'config-mysql-only-myisam-dep' => "'''경고''': 미디어위키에 사용하지 않는 것이 좋은 MySQL에 대한 유일하게 사용할 수 있는 스토리지 엔진입니다. 이유는:
+* 테이블 잠금에 의해 간신히 동시성을 지원합니다
+* 다른 엔진보다 손상하는 경향이 있습니다
+* 미디어위키 코드베이스가 항상 정상적으로 MyISAM을 처리하지 않습니다
-MySQL 설치가 InnoDB를 지원한다면 그 선택 대신에 InnoDB를 선택할 것을 매우 권장합니다.
-MySQL 설치가 InnoDB를 지원하지 않는다면 아마도 업그레이드를 해야 할 수도 있습니다.",
- 'config-mysql-engine-help' => "'''InnoDB'''는 동시적인 지원에 좋기 때문에 거의 항상 최고의 옵션입니다.
+MySQL 설치가 InnoDB를 지원하지 않으며, 아마도 업그레이드를 할 시간입니다.",
+ 'config-mysql-engine-help' => "'''InnoDB'''는 동시적인 지원에 좋기 때문에 대부분 최고의 옵션입니다.
'''MyISAM'''은 단일 사용자 또는 읽기 전용 설치에 빠를 수 있습니다.
MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실될 수 있습니다.",
@@ -10745,12 +11469,11 @@ MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실
'config-mysql-binary' => '바이너리',
'config-mysql-utf8' => 'UTF-8',
'config-mysql-charset-help' => "'''바이너리 모드'''에서는 미디어위키는 바이너리 필드의 데이터베이스에 UTF-8 텍스트를 저장합니다.
-이는 MySQL의 UTF-8 모드를 보다 더 효율적이고 유니코드 문자의 전체 범위를 사용할 수 있습니다.
-'''UTF-8 모드'''에서는 MySQL은 데이터를 설정하는 어떤 문자열인지를 알 것이며, 표현하고 적절하게 그것을 변환할 수 있지만
-[//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes 기본 다국어 범위] 상의 문자를 저장하지 못하게 될 수 있습니다.",
- 'config-ibm_db2-low-db-pagesize' => "당신의 DB2 데이터베이스에 부족한 페이지 크기가 기본 테이블 공간에 있습니다. 페이지 크기는 '''32K''' 이상이어야 합니다.",
+MySQL의 UTF-8 모드를 보다 더 효율적이고 유니코드 문자의 전체 범위를 사용할 수 있습니다.
+'''UTF-8 모드'''에서는 MySQL은 데이터를 설정하는 문자 집합을 알고 있기 때문에 적절하게 표현하고 변환할 수 있지만
+[//ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C_%ED%8F%89%EB%A9%B4#.EA.B8.B0.EB.B3.B8_.EB.8B.A4.EA.B5.AD.EC.96.B4_.ED.8F.89.EB.A9.B4 기본 다국어 평면] 밖의 문자를 저장할 수 없습니다.",
'config-site-name' => '위키 이름:',
- 'config-site-name-help' => '이는 브라우저 제목 표시줄과 다른 여러 곳에 나타날 것입니다.',
+ 'config-site-name-help' => '브라우저 제목 표시줄과 다른 여러 곳에 나타납니다.',
'config-site-name-blank' => '사이트 이름을 입력하세요.',
'config-project-namespace' => '프로젝트 이름공간:',
'config-ns-generic' => '프로젝트',
@@ -10759,7 +11482,7 @@ MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실
'config-ns-other-default' => '내위키',
'config-project-namespace-help' => '위키백과의 예를 따라서, 많은 위키는 "\'\'\'프로젝트 이름공간\'\'\'"에 그들의 콘텐츠 페이지에서 그들의 정책 페이지는 별도로 보관합니다.
이 이름공간에 있는 모든 페이지의 제목은 여기서 지정할 수 있는 특정 접두어로 시작합니다.
-보통 이 접두어는 위키의 이름에서 파생되지만, 이는 "#" 또는 ":"와 같은 특수 문자를 포함할 수 없습니다.',
+보통 이 접두어는 위키의 이름에서 파생되지만, "#" 또는 ":"와 같은 특수 문자를 포함할 수 없습니다.',
'config-ns-invalid' => '특정 "<nowiki>$1</nowiki>" 이름공간이 잘못되었습니다.
다른 프로젝트 이름공간을 지정하세요.',
'config-ns-conflict' => '특정 "<nowiki>$1</nowiki>" 이름공간이 기본 미디어위키 이름공간과 충돌합니다.
@@ -10769,7 +11492,7 @@ MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실
'config-admin-password' => '비밀번호:',
'config-admin-password-confirm' => '비밀번호 확인:',
'config-admin-help' => '"홍길동"과 같이 여기에 원하는 사용자 이름을 입력하세요.
-이는 위키에 로그인하는 데 사용되는 이름입니다.',
+위키에 로그인하는 데 사용되는 이름입니다.',
'config-admin-name-blank' => '관리자의 사용자 이름을 입력하세요.',
'config-admin-name-invalid' => '특정 "<nowiki>$1</nowiki>" 사용자 이름이 잘못되었습니다.
다른 사용자 이름을 지정하세요.',
@@ -10782,8 +11505,8 @@ MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실
'config-admin-error-password' => '"<nowiki>$1</nowiki>" 관리자의 비밀번호를 설정하는 중 내부 오류가 발생했습니다: <pre>$2</pre>',
'config-admin-error-bademail' => '이메일 주소를 잘못 입력하였습니다.',
'config-subscribe' => '[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce 배포 발표 메일링 리스트]에 가입합니다.',
- 'config-subscribe-help' => '이는 중요한 보안 알림을 포함한 배포 알림에 대해 사용되는 로우 볼륨 메일링 리스트입니다.
-당신이 이를 구독하고 나서 새 버전이 나올 때 미디어위키 설치를 업데이트해야합니다.',
+ 'config-subscribe-help' => '중요한 보안 알림을 포함한 배포 알림에 대해 사용되는 로우 볼륨 메일링 리스트입니다.
+이 리스트를 구독하고 나서 새 버전이 나올 때 미디어위키 설치를 업데이트하십시오.',
'config-subscribe-noemail' => '이메일 주소를 제공하지 않고 배포 발표 메일링 리스트에 가입하려 합니다.
메일링 리스트에 가입하고자 할 경우 이메일 주소를 제공하세요.',
'config-almost-done' => '거의 다 완료했습니다!
@@ -10791,22 +11514,23 @@ MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실
'config-optional-continue' => '더 많은 질문을 물어보세요.',
'config-optional-skip' => '지겨워요, 그냥 위키를 설치할래요.',
'config-profile' => '사용자 권한 프로필:',
- 'config-profile-wiki' => '평범한 위키',
+ 'config-profile-wiki' => '열린 위키',
'config-profile-no-anon' => '계정 만들기 필요',
- 'config-profile-fishbowl' => '승인된 편집자만 이용 가능',
+ 'config-profile-fishbowl' => '승인된 편집자만',
'config-profile-private' => '비공개 위키',
- 'config-profile-help' => "위키는 당신이 가능한 한 많은 사람들이 편집하도록 할 때 최고로 적합합니다.
-미디어위키에서는 최근 바뀜을 검토하고, 선하거나 악의적인 사용자에 의해 수행되는 모든 손실을 되돌리는 것이 쉽습니다.
+ 'config-profile-help' => "위키는 많은 사람들이 가능한 한 편집할 수 있도록 하면 가장 뛰어난 역할을 합니다.
+미디어위키에서는 최근 바뀜을 검토하기 쉽고, 선하거나 악의적인 사용자의 어떠한 손실을 되돌리는 것이 쉽습니다.
-그러나 많은 사람들이 미디어위키가 다양한 역할의 유용하지만, 때로는 그것이 위키 방식의 장점을 모두 설득하기 쉽지 않음을 발견했습니다.
+그러나 많은 사람이 미디어위키는 다양한 역할로 유용하지만, 때로는 모든 사람에게 위키 방식의 장점을 설득하기 쉽지 않을 지도 모릅니다.
그래서 선택할 수 있습니다.
-'''{{int:config-profile-wiki}}'''는 로그인하지 않고도 누구나 편집할 수 있습니다.
-'''{{int:config-profile-no-anon}}'''는 추가적으로 필요한 책임을 제공하지만, 기존의 기여자를 망칠 수도 있습니다.
+'''{{int:config-profile-wiki}}''' 모델은 로그인하지 않고도 누구나 편집할 수 있습니다.
+'''{{int:config-profile-no-anon}}'''인 위키는 각 편집에 추가적으로 강한 책임을 제공하지만, 부담 없는 기여를 저해할 수도 있습니다.
-'''{{int:config-profile-fishbowl}}''' 같은 경우는 승인된 사용자만 편집할 수 있지만, 대중은 역사를 포함하여 페이지를 볼 수 있습니다. '''{{int:config-profile-private}}'''는 승인된 사용자만 같은 그룹에서 편집할 수 있고 볼 수 있습니다.
+'''{{int:config-profile-fishbowl}}''' 시나리오는 승인된 사용자만 편집할 수 있지만, 대중은 역사를 포함하여 문서를 볼 수 있습니다.
+'''{{int:config-profile-private}}'''는 승인된 사용자만 문서를 볼 수 있으며 해당 그룹을 편집할 수 있습니다.
-더 복잡한 사용자 권한을 설정하여 설치한 후 사용할 수 있도록 하려면 [//www.mediawiki.org/wiki/Manual:User_rights 관련 매뉴얼 항목]을 참고하세요.",
+더 복잡한 사용자 권한을 설정은 설치한 후 사용할 수 있으며 [//www.mediawiki.org/wiki/Manual:User_rights 관련 설명서 항목]을 참고하세요.",
'config-license' => '저작권 및 라이선스:',
'config-license-none' => '라이선스 바닥글 없음',
'config-license-cc-by-sa' => '크리에이티브 커먼즈 저작자표시-동일조건변경허락',
@@ -10816,15 +11540,15 @@ MyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실
'config-license-gfdl' => 'GNU 자유 문서 사용 허가서 1.3 이상',
'config-license-pd' => '퍼블릭 도메인',
'config-license-cc-choose' => '다른 크리에이티브 커먼즈 라이선스 선택',
- 'config-license-help' => '많은 공개 위키는 모든 기여를 [http://freedomdefined.org/Definition 자유 라이선스] 하에 넣습니다.
-이럴 경우 커뮤니티 소유권의 이해를 할 수 있도록 하고 장기적인 기여를 장려합니다.
-이는 일반적으로 개인 또는 회사 위키에 대해서는 필요하지 않습니다.
+ 'config-license-help' => "많은 공개 위키는 모든 기여를 [http://freedomdefined.org/Definition 자유 라이선스]에 따라 넣습니다.
+이렇게 하면 커뮤니티 소유권의 이해를 할 수 있도록 하고 장기적인 기여를 장려합니다.
+일반적으로 개인 또는 회사 위키에 대해서는 필요하지 않습니다.
-위키백과의 텍스트를 사용할 수 있도록 하고 위키백과가 위키에서 복사한 텍스트를 사용할 수 있도록 원한다면 크리에이티브 커먼즈 저작자표시-동일조건변경허락으로 선택해야 합니다.
+위키백과의 텍스트를 사용할 수 있도록 하고 위키백과가 위키에서 복사한 텍스트를 사용할 수 있도록 원한다면 '''크리에이티브 커먼즈 저작자표시-동일조건변경허락'''으로 선택해야 합니다.
위키백과는 이전에 GNU 자유 문서 사용 허가서를 사용했습니다.
-GFDL은 유효한 라이선스이지만 이는 이해하기 어렵습니다.
-이는 GFDL 하에 라이선스 내용을 재사용하는 것도 어렵습니다.',
+GFDL은 유효한 라이선스이지만 내용을 이해하기 어렵습니다.
+GFDL에 따라 사용이 허가된 내용을 재사용하는 것도 어렵습니다.",
'config-email-settings' => '이메일 설정',
'config-enable-email' => '발신 이메일 활성화',
'config-enable-email-help' => '이메일을 작동하려면 [http://www.php.net/manual/en/mail.configuration.php PHP의 메일 설정]을 올바르게 설정해야 합니다.
@@ -10836,28 +11560,30 @@ GFDL은 유효한 라이선스이지만 이는 이해하기 어렵습니다.
'config-email-watchlist' => '주시문서 목록 알림 활성화',
'config-email-watchlist-help' => '환경 설정에서 활성화한 경우 사용자가 주시한 문서에 대한 알림을 받도록 활성화합니다.',
'config-email-auth' => '이메일 인증 활성화',
- 'config-email-auth-help' => "이 설정이 활성화되어 있으면 사용자는 이메일 주소를 설정하거나 바꿀 때마다 그들에게 보낸 링크를 사용하여 이메일 주소를 확인해야 합니다.
+ 'config-email-auth-help' => "이 설정이 활성화되어 있으면 사용자는 이메일 주소를 설정하거나 바꿀 때마다 링크를 사용하여 이메일 주소를 확인해야 합니다.
인증된 이메일 주소만 다른 사용자로부터의 이메일이나 바뀜 알림 이메일을 받을 수 있습니다.
이메일 기능의 남용 가능성이 있기 때문에 이 옵션을 설정하는 것은 공개 위키에서 '''권장'''합니다.",
'config-email-sender' => '반송 이메일 주소',
'config-email-sender-help' => '발신한 이메일에 대한 반송 주소로 사용할 이메일 주소를 입력하세요.
-이는 반송할 때 보내는 주소입니다.
+반송할 때 보내는 주소입니다.
대부분의 메일 서버는 적어도 도메인 이름 부분은 유효합니다.',
'config-upload-settings' => '그림과 파일 올리기',
'config-upload-enable' => '파일 올리기 활성화',
'config-upload-help' => '파일 올리기는 서버에 잠재적인 보안 위험에 쉽게 노출될 수 있습니다.
-자세한 내용은 매뉴얼의 [//www.mediawiki.org/wiki/Manual:Security 보안 문단]을 읽어보세요.
+자세한 내용은 매뉴얼의 [//www.mediawiki.org/wiki/Manual:Security 보안 문단]을 참고하세요.
파일 올리기를 활성화하려면 미디어위키의 루트 디렉토리에 있는 <code>images</code> 하위 디렉토리에서 웹 서버가 기록할 수 있도록 모드를 바꿉니다.
그 다음 이 옵션을 활성화합니다.',
- 'config-upload-deleted' => '삭제된 파일에 대한 디렉토리:',
+ 'config-upload-deleted' => '삭제된 파일에 대한 디렉터리:',
'config-upload-deleted-help' => '삭제된 파일을 보관할 디렉토리를 선택하세요.
이상적으로 웹에서 접근할 수 없게 해야 합니다.',
'config-logo' => '로고 URL:',
- 'config-logo-help' => '미디어위키 기본 스킨은 사이드바 메뉴 위에 135×160픽셀의 로고를 포함하고 있습니다.
-적당한 크기로 이미지를 올리고 URL을 여기에 입력하세요.
+ 'config-logo-help' => '미디어위키의 기본 스킨은 사이드바 메뉴 위에 135×160 픽셀의 로고의 공간을 포함하고 있습니다.
+적당한 크기로 그림을 올리고 여기에 URL을 입력하세요.
-로고 사용을 원하지 않으면 이 상자를 비워 두십시오.',
+로고가 상대적인 경로에 있으면 <code>$wgStylePath</code>나 <code>$wgScriptPath</code>를 사용할 수 있습니다.
+
+로고 사용을 원하지 않으면 이 상자를 비우세요.',
'config-instantcommons' => '인스턴트 공용 활성화',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons 인스턴트 공용]은 [//commons.wikimedia.org/ 위키미디어 공용] 사이트에서 찾을 수 있는 그림, 소리 및 다른 미디어를 위키에서 사용할 수 있도록 하는 기능입니다.
이렇게 하려면 미디어위키가 인터넷에 접근해야합니다.
@@ -10887,11 +11613,11 @@ GFDL은 유효한 라이선스이지만 이는 이해하기 어렵습니다.
'config-extensions' => '확장 기능',
'config-extensions-help' => '위에 나열된 확장 기능이 <code>./extensions</code>에서 발견되었습니다.
-이는 추가적인 설정이 필요할 수 있습니다만 지금 활성화시킬 수 있습니다.',
- 'config-install-alreadydone' => "'''경고:''' 당신은 이미 미디어위키를 설치하였고 다시 설치하려고 합니다.
+추가적인 설정이 필요할 수 있습니다만 지금 활성화시킬 수 있습니다.',
+ 'config-install-alreadydone' => "'''경고:''' 이미 미디어위키를 설치했고 다시 설치하려고 합니다.
다음 페이지에서 진행하세요.",
'config-install-begin' => '"{{int:config-continue}}"을 누르면 미디어위키의 설치를 시작합니다.
-그래도 바꾸는 것을 원한다면 뒤로를 누릅니다.',
+그래도 바꾸는 것을 원한다면 "{{int:config-back}}"를 누르세요.',
'config-install-step-done' => '완료',
'config-install-step-failed' => '실패',
'config-install-extensions' => '확장 기능을 포함하는 중',
@@ -10909,7 +11635,7 @@ GFDL은 유효한 라이선스이지만 이는 이해하기 어렵습니다.
현재 미디어위키는 테이블을 웹 사용자가 소유해야 합니다. 다른 웹 계정 이름을 지정하거나 "뒤로"를 클릭하고 적절한 권한의 설치할 사용자를 지정하세요.',
'config-install-user' => '데이터베이스 사용자를 만드는 중',
- 'config-install-user-alreadyexists' => '"$1" 사용자가 이미 있음',
+ 'config-install-user-alreadyexists' => '"$1" 사용자가 이미 존재합니다',
'config-install-user-create-failed' => '"$1" 사용자 만드는 중 실패: $2',
'config-install-user-grant-failed' => '"$1" 사용자에 대한 권한 부여 실패: $2',
'config-install-user-missing' => '지정한 "$1" 사용자가 존재하지 않습니다.',
@@ -10935,29 +11661,33 @@ GFDL은 유효한 라이선스이지만 이는 이해하기 어렵습니다.
'config-install-done' => "'''축하합니다!'''
미디어위키가 성공적으로 설치되었습니다.
-설치 마법사가 <code>LocalSettings.php</code> 파일을 만들었습니다.
-이는 모든 설정이 포함되어 있습니다.
+설치 프로그램이 <code>LocalSettings.php</code> 파일을 만들었습니다.
+모든 설정이 포함되어 있습니다.
-이를 다운로드하여 위키 설치의 거점에 넣어야 합니다 (index.php와 같은 디렉토리). 다운로드가 자동으로 시작됩니다.
+파일을 다운로드하여 위키 설치의 거점에 넣어야 합니다. (index.php와 같은 디렉터리) 다운로드가 자동으로 시작됩니다.
다운로드가 제공되지 않을 경우나 그것을 취소한 경우에는 아래의 링크를 클릭하여 다운로드를 다시 시작할 수 있습니다:
$3
-'''참고''': 지금 이렇게 하지 않으면, 이 설정 파일을 다운로드하지 않고 설치를 종료할 경우 만들어진 설정 파일은 나중에 사용할 수 없습니다.
+'''참고:''' 이 생성한 설정 파일을 다운로드하지 않고 설치를 끝내면 이 파일은 나중에 사용할 수 없습니다.
완료되었으면 '''[$2 위키에 들어갈 수 있습니다]'''.",
- 'config-download-localsettings' => 'LocalSettings.php 다운로드',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> 다운로드',
'config-help' => '도움말',
'config-nofile' => '"$1" 파일을 찾을 수 없습니다. 이미 삭제되었나요?',
+ 'config-extension-link' => '당신의 위키가 [//www.mediawiki.org/wiki/Manual:Extensions 확장 기능]을 지원한다는 것을 알고 계십니까?
+
+전체 확장 기능의 목록을 확인하려면 [//www.mediawiki.org/wiki/Category:Extensions_by_category 분류별 확장 기능]이나 [//www.mediawiki.org/wiki/Extension_Matrix 확장 기능 표]를 찾아보실 수 있습니다.',
'mainpagetext' => "'''미디어위키가 성공적으로 설치되었습니다.'''",
- 'mainpagedocfooter' => '[//meta.wikimedia.org/wiki/Help:Contents 이곳]에서 위키 프로그램에 대한 정보를 얻을 수 있습니다.
+ 'mainpagedocfooter' => '[//meta.wikimedia.org/wiki/Help:Contents 이곳]에서 위키 소프트웨어에 대한 정보를 얻을 수 있습니다.
== 시작하기 ==
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings 설정하기]
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings 설정하기 목록]
* [//www.mediawiki.org/wiki/Manual:FAQ 미디어위키 FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce 미디어위키 발표 메일링 리스트]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce 미디어위키 발표 메일링 리스트]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources 내 언어로 미디어위키 지역화]',
);
/** Karachay-Balkar (къарачай-малкъар)
@@ -10970,7 +11700,7 @@ $messages['krc'] = array(
== Файдалы ресурсла ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings тюрлендириулени списогу (ингил.)];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-ни юсюнден кёб берилген соруула];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-ни джангы версиясыны чыкъгъанын билдириу письмола].",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-ни джангы версиясыны чыкъгъанын билдириу письмола].", # Fuzzy
);
/** Colognian (Ripoarisch)
@@ -10982,21 +11712,21 @@ $messages['ksh'] = array(
'config-desc' => 'Et Projramm för Mediwiki opzesäze.',
'config-title' => 'MediaWiki $1 opsäze',
'config-information' => 'Enfomazjuhn',
- 'config-localsettings-upgrade' => 'De Dattei <code lang="en">LocalSettings.php</code> es ald doh.
+ 'config-localsettings-upgrade' => 'De Dattei <code lang="en"><code>LocalSettings.php</code></code> es ald doh.
De Projramme vum Wiki künne op der neußte Shtand jebraat wääde:
Donn doför dä Wäät vum <code lang="en">$wgUpgradeKey</code> en dat heh Feld enjävve.
-Do fenggs_et en dä Dattei <code lang="en">LocalSettings.php</code> om ẞööver.',
- 'config-localsettings-cli-upgrade' => 'En Dattei <code lang="en">LocalSettings.php</code> es jefonge woode.
+Do fenggs_et en dä Dattei <code lang="en"><code>LocalSettings.php</code></code> om ẞööver.',
+ 'config-localsettings-cli-upgrade' => 'En Dattei <code lang="en"><code>LocalSettings.php</code></code> es jefonge woode.
Öm et Wiki_Projramm op ene neue Shtand ze bränge, donn <code lang="en">update.php</code> oproofe.',
'config-localsettings-key' => 'Der Schlößel för et Projramm op ene neue Schtand ze bränge:',
'config-localsettings-badkey' => 'Dinge Schlößel paß nit.',
'config-upgrade-key-missing' => 'Mer han jefonge, dat MediaWiki ald enschtalleed es.
-Üm de Projramme un Daate o der neue Schtand bränge ze künne, dunn aan et Engk vun dä Dattei <code lang="en">LocalSettings.php</code> op dämm ẞööver:
+Üm de Projramme un Daate o der neue Schtand bränge ze künne, dunn aan et Engk vun dä Dattei <code lang="en"><code>LocalSettings.php</code></code> op dämm ẞööver:
$1
aanhange.',
- 'config-localsettings-incomplete' => 'Mer han en Dattei <code lang="en">LocalSettings.php:</code> jefonge, ävver di schingk nit kumplätt ze sin.
+ 'config-localsettings-incomplete' => 'Mer han en Dattei <code lang="en"><code>LocalSettings.php</code>:</code> jefonge, ävver di schingk nit kumplätt ze sin.
De Varijable <code lang="en">$1</code> es nit jesatz.
Bes esu joot, un donn di Dattei esu aanpaße, dat se jesaz ea, un dann donn op „{{int:config-continue}}“ klecke.',
'config-localsettings-connection-error' => 'Ene Fähler es opjetrodde wi mer en Verbendung noh de Datebangk opmaache wullte met dä Enshtellunge uß dä Dattei <code lang="en">LocalSettings</code> udder uß dä Dattei <code lang="en">LocalSettings</code> un et hät nit jeflupp. Bes esu joot un dat repareere un versöhg et dann norr_ens.
@@ -11100,7 +11830,7 @@ Dat heiß, mer moß en affschallde, söns jeiht nix.",
MediaWiki bruch Funxjohne en däm Modul un deiht et esu nit.
Wann De <i lang="en">Mandrake</i> aam loufehäs, donn dat Pakätt <code lang="en">php-xml</code> enstalleere.',
'config-pcre' => 'Dem PHP sing Modul för <i lang="en">PCRE</i> schingk ze fähle.
-MediaWiki deiht et nit ohne de Funxjohne för de <i lang="en">Perl-compatible regular expressions</i>.',
+MediaWiki deiht et nit der ohne de Funxjohne för de rejolähre Ußdrök vun dä Zoot, wi <i lang="en">Perl</i> se kännt.',
'config-pcre-no-utf8' => "'''Dä:''' Et PHP-Modul <i lang=\"en\">PCRE</i> schingk ohne de <i lang=\"en\">PCRE_UTF8</i>-Aandeile övversaz ze sin.
MediaWiki bruch dä UTF-8-Krohm ävver, öm ohne Fähler loufe ze künne.",
'config-memory-raised' => 'Der jrühzte zohjelasse Shpeisherbedarf vum PHP, et <code lang="en">memory_limit</code>, shtund op $1 un es op $2 erop jesaz woode.',
@@ -11115,6 +11845,8 @@ Et Enreeschte kunnt doh draan kappott jon!",
Et <i lang="en">object caching</i> es nit müjjelesh un ußjeschalldt.',
'config-mod-security' => "'''Opjepaß''': Dinge Webßööver hät <code lang=\"en\">[http://modsecurity.org/ mod_security]</code> enjeschalldt. Wann doh derbei en Enschtällong nit janz akeraat paßß, dann kann et goot sin, dat mer Probleme met MeedijaWiki un oc met ander Projramme kritt, die zohlööt, dat vun ußerhallef öhndsene Krohm op dä Webßööver jebraat wääde künnt.Beloor Der di Sigg <code lang=\"en\">[http://modsecurity.org/documentation/ mod_security documentation]</code> udder donn met dä Fachlück för Dinge Webßööver kalle, wann zohfälleje un koomijje Fähler bemerke deihß.",
'config-diff3-bad' => 'Mer han <i lang="en">GNU</i> <code lang="en">diff3</code> nit jefonge.',
+ 'config-git' => 'Mer han de Väsjohn <code>$1</code> vun däm Väsjohnsverwalldongsprojamm <i lang="en">Git</i> jefonge.',
+ 'config-git-bad' => 'Dat Väsjohnsverwalldongsprojamm <i lang="en">Git</i> ham_mer nit jefonge.',
'config-imagemagick' => 'Mer han <i lang="en">ImageMagick</i> jefonge: <code>$1</code>.
Et Ömrääschne en Minni-Beldsche weed müjjelesch sin, wann De et Belder Huhlaade zohlöhß.',
'config-gd' => 'Mer han de ennjeboute GD-Jrafik-Projramm-Biblijotheek jefonge.
@@ -11134,7 +11866,7 @@ Heh jeihd et nit wigger.',
'config-using531' => 'MediaWiki läuf nit met PHP $1 zosamme wääje enem [//bugs.php.net/bug.php?id=50394 Fähler em Zosammehang met Parrameetere för <code lang="en">__call()</code>].
Jangk op de Version 5.3.2 vum <i lang="en">PHP</i> ov dohnoh, udder op de Version 5.3.0 udder dovöör, öm dat Problem ze ömjonn.
Heh jeiht et nit wigger.',
- 'config-suhosin-max-value-length' => '<i lang="en">Suhosin</i> es enschtalleet. Dröm kann ene <code lang="en">GET</code>-Parrameeter nit övver {{PLURAL:$1|ei Byte|$q Bytes|noll Byte}} lang wääde. En MediaWiki singe <i lang="en">ResourceLoader</i> kütt doh zwa drömeröm, ävver dat brems. Wann müjelesch, doht <code lang="en">suhosin.get.max_value_length</code> en dä Dattei <code lang="en">php.ini</code> op 1024 Bytes udder drövver enschtälle. un dann moß <code lang="en">$wgResourceLoaderMaxQueryLength</code> en dä Dattei <code lang="en">LocalSettings.php</code> op däsälve Wäät jesaz wääde.',
+ 'config-suhosin-max-value-length' => '<i lang="en">Suhosin</i> es enschtalleet. Dröm kann ene <code lang="en">GET</code>-Parrameeter nit övver {{PLURAL:$1|ei Byte|$1 Bytes|noll Byte}} lang wääde. Dem MediaWiki singe <i lang="en">ResourceLoader</i> kütt doh zwa drömeröm, ävver dat bräms. Wann müjelesch, doht <code lang="en">suhosin.get.max_value_length</code> en dä Dattei <code lang="en">php.ini</code> op 1024 Bytes udder drövver enschtälle, un dann moß <code lang="en">$wgResourceLoaderMaxQueryLength</code> en dä Dattei <code lang="en">LocalSettings.php</code> op däsälve Wäät jesaz wääde.',
'config-db-type' => 'De Zoot Daatebangk:',
'config-db-host' => 'Dä Name vun däm Rääschner met dä Daatebangk:',
'config-db-host-help' => 'Wann Dinge ẞööver för de Daatebangk ob enem andere Rääschner es, donn heh dämm singe Name udder dämm sing <i lang="en">IP</i>-Addräß enjävve.
@@ -11216,7 +11948,6 @@ Donn Ding Daatebangk et beß janz woh anders hen, noh <code lang="en">/var/lib/m
'config-type-postgres' => '<i lang="en">PostgreSQL</i>',
'config-type-sqlite' => '<i lang="en">SQLite</i>',
'config-type-oracle' => '<i lang="en">Oracle</i>',
- 'config-type-ibm_db2' => 'Dä <i lang="en">IBM</i> ier <i lang="en">DB2</i>',
'config-support-info' => 'MediaWiki kann met heh dä Daatebangk_Süßteeme zosamme jonn:
$1
@@ -11226,12 +11957,10 @@ Wann dat Daatebangk_Süßteem, wat De nämme wells, onge nit dobei es, dann donn
'config-support-postgres' => '* <i lang="en">$1</i> es e bikannt Daatebangksüßteem met offe Quälltäxde, un en och en Wahl nävve <i lang="en">MySQL</i> ([http://www.php.net/manual/de/pgsql.installation.php Aanleidung för et Övversäze un Enreeschte von PHP met <i lang="en">PostgreSQL</i> dobei, op Deutsch]) Et sinn_er ävver paa klein Fählershe bekannt, um kunne dat em Momang för et reschtijje Werke nit emfähle.',
'config-support-sqlite' => '* <i lang="en">$1</i> es e eijfach Daatebangksüßteem, wat joot ongershtöz weed. ([http://www.php.net/manual/de/pdo.installation.php Aanleidong för et Övversäze un Enreeschte von PHP met <i lang="en">SQLite</i> dobei, op Deutsch])',
'config-support-oracle' => '* <i lang="en">$1</i> es e jeschäfflesch Daatebangksüßteem för Ferme. ([http://www.php.net/manual/de/oci8.installation.php Aanleidong för et Övversäze un Enreeschte von PHP met <i lang="en">OCI8</i> dobei, op Deutsch])',
- 'config-support-ibm_db2' => '* $1 es en Datebengk för et Jeschäff un fö Ongernehme.',
'config-header-mysql' => 'De Enshtällunge för de <i lang="en">MySQL</i> Daatebangk',
'config-header-postgres' => 'De Enshtällunge för de <i lang="en">PostgreSQL</i> Daatebangk',
'config-header-sqlite' => 'De Enshtällunge för de <i lang="en">SQLite</i> Daatebangk',
'config-header-oracle' => 'De Enshtällunge för de <i lang="en">Oracle</i> Daatebangk',
- 'config-header-ibm_db2' => 'De Enshtällunge för de <i lang="en">IBM</i> ier <i lang="en">DB2</i>',
'config-invalid-db-type' => 'Dat es en onjöltijje Zoot Daatebangk.',
'config-missing-db-name' => 'Do moß jät enjävve för dä Name vun dä Daatebangk.',
'config-missing-db-host' => 'Do moß jät enjävve för dä Name vun däm Rääschner met dä Daatebangk.',
@@ -11290,7 +12019,7 @@ Dat dom_mer ävver '''nit vörschlonn'''em Jääjedeil, ußer, wann et Probleme
Mer kann dat Wiki jäz [$1 bruche].',
'config-regenerate' => 'Donn de Dattei <code lang="en">LocalSettings.php</code> neu opsäze →',
- 'config-show-table-status' => 'Et Kommando <code lang="en">SHOW TABLE STATUS</code> aan de Daatebangk es donävve jejange!',
+ 'config-show-table-status' => 'Et Kommando <code lang="en"><code>SHOW TABLE STATUS</code></code> aan de Daatebangk es donävve jejange!',
'config-unknown-collation' => "'''Opjepaß:''' De Daatabangk deiht en onbikannte Reijefollsch bruche, för Booshtaabe un Zeishe ze verjliishe un ze zotteere.",
'config-db-web-account' => 'Dä Zohjang zor Daatebangk för et Wiki',
'config-db-web-help' => 'Donn ene Name un e Paßwoot för der Zohjang zor Daatebangk för et Wiki em nomaale Bedrief aanjävve.',
@@ -11308,6 +12037,13 @@ Dä aanjejovve Zohjang för der Nomaalbedrief moß dröm schunn enjersht sen!',
Wann Ding <i lang="en">MySQL</i> et Schpeischere en <i lang="en">InnoDB</i>-Datteije ongerschtöze deiht, dom_mer dat nohdröcklesch ämfähle.
Kann dä ẞööver dat nit, künnd et joode jelääjeheit sin, dä ens op der neuste Schtand ze bränge.',
+ 'config-mysql-only-myisam-dep' => '\'\'\'Opjepaß:\'\'\' <i lang="en">MyISAM</i> es de einzeje Zoot Speischerprojramm för <i lang="en">MySQL</i>, di nit för MediaWiki ze ämfähle es, weil:
+* wääje dem Schpärre vun jannze Tabälle sin koum paralleele Axjuhne en dä Daatebangk möjjelesch,
+* et es aanfällesch för Probleeme met de Daate es, un
+* et weed vun MediaWiki nit emmer jood ongerschtöz.
+
+Ding Enschtallazjuhn vum <i lang="en">MySQL</i> kann nit met <i lang="en">InnoDB</i> ömjonn.
+Wi wöhr et met ener neuere väsjohn vum <i lang="en">MySQL</i>?',
'config-mysql-engine-help' => "'''InnoDB''' es fö jewöhnlesch et beß, weil vill Zohjreffe op eijmohl joot ongershtöz wääde.
'''MyISAM''' es flöcker op Rääschnere met bloß einem Minsch draan, un bei Wikis, di mer bloß lässe un nit schrieeve kann.
@@ -11320,7 +12056,6 @@ Dat es flöcker un spaasaamer wi et UTF-8 Fommaat vum <i lang=\"en\">MySQL</i> u
Beim Shpeishere em '''UTF-8 Fomaat''' deiht et <i lang=\"en\">MySQL</i> der Zeishesaz un de Kodeerung vun dä Daate känne, un kann se akeraat aanzeije un ömwandelle,
allerdengs künne kein Zeishe ußerhalv vum [//de.wikipedia.org/wiki/Basic_Multilingual_Plane#Gliederung_in_Ebenen_und_Bl.C3.B6cke jrundlääje Knubbel för vill Shprooche (<i lang=\"en\">Basic Multilingual Plane — BMP</i>)] afjeshpeishert wääde.",
- 'config-ibm_db2-low-db-pagesize' => "De <i lang=\"en\">DB2</i> Daatebangk heh hät ene standattmääßeje Plaz för Tabälle met zoh klein Sigge. Dä Plaz en de Sigge moß '''32K''' udder mieh sin.",
'config-site-name' => 'Däm Wiki singe Name:',
'config-site-name-help' => 'Dä douch em Tittel vun de Brauserfinstere un aan ätlije andere Shtälle op.',
'config-site-name-blank' => 'Donn ene Name för di Sait aanjävve.',
@@ -11363,25 +12098,25 @@ Do künnts jez der Räß vun de einzel Enshtellunge övverjonn, un et Wiki tirä
'config-optional-continue' => 'De wells noch mieh Frore jeshtallt krijje un noch mieh Enshtällunge maache?',
'config-optional-skip' => 'Nä, lohß dä Ömshtand, donn eifarr_et Wiki opsäze.',
'config-profile' => 'Enshtällunge för de Metmaacher ier Rääschte:',
- 'config-profile-wiki' => 'E tradizjonäll offe Wiki',
+ 'config-profile-wiki' => 'En offe Wiki',
'config-profile-no-anon' => 'Schriever möße enlogge',
'config-profile-fishbowl' => 'Bloß ußdröcklesch zohjelohße Schriever',
'config-profile-private' => 'E jeschloße Privat_Wiki',
- 'config-profile-help' => "Wikis loufe et beß, wam_mer esu vill Lück wi müjjelesch draan metmaache un schrieve löht.
-Met MediaWiki es et ejfach, de neuste Änderunge ze beloore un wat ahnungslose udder fiese Lück kapott jemaat han wider retuur ze maache.
+ 'config-profile-help' => "Wikis loufe et bäß, wam_mer esu vill Lück wi möjjelesch draan metmaache un schrieve löht.
+Met MediaWiki es et ejfach, de neuste Änderonge ze beloore un wat ahnungslose udder fiese Lück kapott jemaat han wider retuur ze maache.
-Bloß, mänsh eine häd_eruß jefonge, dat mer MediaWiki jood en en jruuße Zahl ongerscheidlijje Rolle bruche kann, un nit emmer es et leish, ene vum onverfälschte Wiki_Wääsch ze övverzeuje.
+Bloß, mänsch eine häd_eruß jefonge, dat mer MediaWiki jood en en jruuße Zahl ongerscheidlijje Rolle bruche kann, un nit emmer es et leisch, ene vum onverfälschte Wiki_Wääsch ze övverzeuje.
Esu häß De de Wahl:
-'''{{int:config-profile-wiki}}''' löht jeder_ein metschrieve, och ohne enzelogge.
+'''{{int:config-profile-wiki}}''' löht jeder_ein metschrieve, och ohne sesch enzelogge.
-'''{{int:config-profile-no-anon}}''', dat sorsh för mieh seeshbaa Verantwootlishkeite, künnt ävver zohfällije Methellefer verschrecke.
+'''{{int:config-profile-no-anon}}''', dat sorsch för mieh seeschbaa Verantwootlischkeite, künnt ävver zohfällije Methellefer verschrecke.
'''{{int:config-profile-fishbowl}}''' löht nor de ußjesöhk Metmaacher schrieve, ävver de janze Öffentleshkeit kann et lässe un süht och de ällder Versione, un wat wää wann draan jedonn hät.
'''{{int:config-profile-private}}''' kann nur lässe, wäh en et Wiki zohjelohße es, un desellve Jropp kann uch schrieve.
-Noch ander un un opwändijere Enshtellunge för de Rääschte sin müjjelesch, wann et Wiki ens aam Loufe es. Loor Der doför de [//www.mediawiki.org/wiki/Manual:User_rights zopaß Hölp em Handbooch] aan.",
+Noch ander un un opwändijere Enschtellunge för de Rääschte sin möjjelesch, wann et Wiki ens aam Loufe es. Loor Der doför de [//www.mediawiki.org/wiki/Manual:User_rights zopaß Hölp em Handbooch] aan.",
'config-license' => 'Urhävverrääsch un Lizänz:',
'config-license-none' => 'Kein Fooßreih övver de Lizänz',
'config-license-cc-by-sa' => '<i lang="en">Creative Commons</i> Der Name moß jenannt sin, et Wiggerjävve es zohjelohße onger dersellve Bedengunge',
@@ -11430,6 +12165,8 @@ Et bäß es, wam_mer vum <i lang="en">world wide web</i> doh nit drahn kumme kan
'config-logo-help' => 'De Schtandart_Bedeen_Bovverfläsch vum MediaWiki hät e Logo bovve en der Eck met 135x160 Pixele.
Donn e zopaß Logo huh laade, un donn däm sing URL heh endraare.
+Do kanns <code lang="en">$wgStylePath</code> udder <code lang="en">$wgScriptPath</code> nämme, wann Ding Logo en einem vun dänne Pahde litt.
+
Wells De kei Logo han, draach heh nix en.',
'config-instantcommons' => 'Donn <i lang="en">InstantCommons</i> zohlohße.',
'config-instantcommons-help' => '<i lang="en">[//www.mediawiki.org/wiki/InstantCommons InstantCommons]</i> es en Eijeschaff, di et för Wikis müjjelesch määt, Belder, Tondatteie un ander Meedijedatteie enzebenge, di op dä Webßait vun de <i lang="en">[//commons.wikimedia.org/ Wikimedia Commons]</i> ongerjebraat sin. Öm dat noze ze künne, moß dä ẞööver vum MediaWiki en Verbendung nohm Internet opnämme künne.
@@ -11456,7 +12193,7 @@ Se sullte ein pro Reih opjeschrevve sin, un en Pooz (<i lang="en">port</i>) ier
'config-memcache-noport' => 'Do has kein Pooz (<code lang="en">port</code>) Nommer aanjejovve för mem <code lang="en">memcached</code> ẞööver ze bruche: $1.
Wann De di Nommer nit weiß, der Shtandatt es 11211.',
'config-memcache-badport' => 'Dem <code lang="en">memcached</code> ẞööver singe Pooz (<code lang="en">port</code>) Nommere sullte zwesche $1 un $2 sin.',
- 'config-extensions' => 'Projramm-Zosätz (<i lang="en">extensions</i>)',
+ 'config-extensions' => 'Projramm-Zohsäz (<i lang="en">Extensions</i>)',
'config-extensions-help' => 'Di bovve opjeleß Zohsazprojramme för et MediaWiki sin em Verzeischneß <code lang="en">./extensions</code> ald ze fenge.
Do kann se heh un jez aanschallde, ävver se künnte noch zohsäzlesch Enshtellunge bruche.',
@@ -11524,14 +12261,18 @@ Wann De mem Ronger- un widder Huhlaade fäädesh bes, kanns De '''[\$2 en Ding W
'config-download-localsettings' => 'Donn di Dattei <code lang="en">LocalSettings.php</code> eronger laade',
'config-help' => 'Hölp',
'config-nofile' => 'De Dattei „$1“ ham_mer nit jefonge. Es di fottjeschmeße?',
+ 'config-extension-link' => 'Häs De jewoß, dat et Wiki [//www.mediawiki.org/wiki/Manual:Extensions Zohsazprojramme] hann kann?
+
+Do kanns [//www.mediawiki.org/wiki/Category:Extensions_by_category Zohsazprojramme noh Saachjroppe] söhke udder en de [//www.mediawiki.org/wiki/Extension_Matrix Tabäll met de Zohsazprojramme] kike, öm de kumplätte Leß met de Zohsazprojramme ze krijje.',
'mainpagetext' => "'''MediaWiki es jetz enstalleet.'''",
- 'mainpagedocfooter' => 'Luur en et (änglesche) [//meta.wikimedia.org/wiki/Help:Contents Handboch] wann De wesse wells wie de Wiki-Soffwär jebruch un bedeent wääde muss.
+ 'mainpagedocfooter' => 'Luur en et (änglesche) [//meta.wikimedia.org/wiki/Help:Contents Handbooch] wann De wesse wells wie de Wiki-Soffwär jebruch un bedeent wääde moß.
== För dä Aanfang ==
Dat es och all op Änglesch:
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Donn MediaWiki op Ding Schprooch aanpaße]',
);
/** Kurdish (Latin script) (Kurdî (latînî)‎)
@@ -11553,7 +12294,7 @@ $messages['ku-latn'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lîsteya varîyablên konfîgûrasîyonê]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lîsteya e-nameyên versyonên nuh yê MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lîsteya e-nameyên versyonên nuh yê MediaWiki]', # Fuzzy
);
/** Ladino (Ladino)
@@ -11566,11 +12307,13 @@ $messages['lad'] = array(
== En Empeçando ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings La lista de los arreglamientos de la konfiggurasyón]
* [//www.mediawiki.org/wiki/Manual:FAQ/lad DDS de MedyaViki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce La lista de las letrales (e-mail) de MedyaViki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce La lista de las letrales (e-mail) de MedyaViki]', # Fuzzy
);
/** Luxembourgish (Lëtzebuergesch)
* @author Robby
+ * @author Soued031
+ * @author 아라
*/
$messages['lb'] = array(
'config-desc' => 'Den Installatiounsprogramm vu MediaWiki',
@@ -11578,12 +12321,14 @@ $messages['lb'] = array(
'config-information' => 'Informatioun',
'config-localsettings-upgrade' => "'''Opgepasst''': E Fichier <code>LocalSettings.php</code> gouf fonnt.
Är Software kann aktualiséiert ginn, setzt w.e.g. de Wäert vum <code>\$wgUpgradeKey</code> an d'Këscht.
-Dir fannt en am LocalSettings.php.",
+Dir fannt en am <code>LocalSettings.php</code>.",
+ 'config-localsettings-cli-upgrade' => "E Fichier <code>LocalSettings.php</code> gouf fonnt.
+Fir dës Installatioun z'aktuaéliséieren start w.e.g. <code>update.php</code>",
'config-localsettings-key' => 'Aktualisatiounsschlëssel:',
'config-localsettings-badkey' => 'De Schlëssel deen Dir aginn hutt ass net korrekt',
- 'config-localsettings-incomplete' => 'De Fichier LocalSettings.php schéngt net komplett ze sinn.
+ 'config-localsettings-incomplete' => 'De Fichier <code>LocalSettings.php</code> schéngt net komplett ze sinn.
D\'Variabel $1 ass net definéiert.
-Ännert w.e.g. de Fichier LocalSettings.php esou datt déi Variabel definéiert ass a klickt op "Virufueren".',
+Ännert w.e.g. de Fichier <code>LocalSettings.php</code> esou datt déi Variabel definéiert ass a klickt op "{{int:Config-continue}}".',
'config-session-error' => 'Feeler beim Starte vun der Sessioun: $1',
'config-no-session' => "D'Donnéeë vun ärer Sessioun si verluergaangen!
Kuckt Är php.ini no a vergewëssert Iech datt <code>session.save_path</code> op adequate REpertoire agestallt ass.",
@@ -11612,7 +12357,7 @@ Kuckt Är php.ini no a vergewëssert Iech datt <code>session.save_path</code> o
'config-restart' => 'Jo, neistarten',
'config-welcome' => "=== Iwwerpréifung vum Installatiounsenvironnement ===
Et gi grondsätzlech Iwwerpréifunge gemaach fir ze kucken ob den Environnment gëeegent ass fir MediaWiki z'installéieren.
-Dir sollt d'Resultater vun dëser Iwwerpréifung ugi wann Dir während der Installatioun Hëllef braucht.",
+Dir sollt d'Resultater vun dëser Iwwerpréifung ugi wann Dir während der Installatioun Hëllef frot wéi Dir D'Installatioun ofschléisse kënnt.",
'config-sidebar' => '* [//www.mediawiki.org MediaWiki Haaptsäit]
* [//www.mediawiki.org/wiki/Help:Contents Benotzerguide]
* [//www.mediawiki.org/wiki/Manual:Contents Guide fir Administrateuren]
@@ -11636,9 +12381,10 @@ Dës Datebank-Type ginn ënnerstëtzt: $1.
Wann Dir op engem gesharte Server sidd, da frot Ären Hosting-Provider fir de passenden Datebank-Driver z'installéieren.
Wann Dir PHP selwer compiléiert hutt, da reconfiguréiert en mat dem ageschalten Datebank-Client, zum Beispill an deem Dir <code>./configure --with-mysql</code> benotzt.
Wann Dir PHP vun engem Debian oder Ubuntu Package aus installéiert hutt, da musst Dir och den php5-mysql Modul installéieren.",
+ 'config-outdated-sqlite' => "'''Warnung:''' SQLite $1 ass installéiert. Allerdengs brauch MediaWiki SQLite $2 oder méi nei. SQLite ass dofir net disponibel.",
'config-memory-bad' => "'''Opgepasst:''' De Parameter <code>memory_limit</code> vu PHP ass $1.
Dat ass wahrscheinlech ze niddreg.
-D'Installatioun kéint net fonctionnéieren.",
+D'Installatioun kéint net funktionéieren.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] ass installéiert',
'config-apc' => '[http://www.php.net/apc APC] ass installéiert',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] ass installéiert',
@@ -11646,7 +12392,9 @@ D'Installatioun kéint net fonctionnéieren.",
'config-no-uri' => "'''Feeler:''' Déi aktuell URI konnt net festgestallt ginn.
Installatioun ofgebrach.",
'config-using-server' => 'De Servernumm "<nowiki>$1</nowiki>" gëtt benotzt.',
+ 'config-using-uri' => 'D\'Server URL "<nowiki>$1$2</nowiki>" gëtt benotzt.',
'config-db-type' => 'Datebanktyp:',
+ 'config-db-host' => 'Host vun der Datebank:',
'config-db-host-oracle' => 'Datebank-TNS:',
'config-db-wiki-settings' => 'Dës Wiki identifizéieren',
'config-db-name' => 'Numm vun der Datebank:',
@@ -11675,13 +12423,10 @@ Wann et de Kont net gëtt, a wann den Installatiouns-Kont genuch Rechter huet, g
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
- 'config-support-ibm_db2' => '* $1 ass eng kommerziell Firma fir Datebanken',
'config-header-mysql' => 'MySQL-Astellungen',
'config-header-postgres' => 'PostgreSQL-Astellungen',
'config-header-sqlite' => 'SQLite-Astellungen',
'config-header-oracle' => 'Oracle-Astellungen',
- 'config-header-ibm_db2' => 'IBM DB2-Astellungen',
'config-invalid-db-type' => 'Net valabelen Datebank-Typ',
'config-missing-db-name' => 'Dir musst en Numm fir de Wäert "Numm vun der Datebank" uginn',
'config-missing-db-host' => 'Dir musst e Wäert fir "Database host" uginn',
@@ -11723,18 +12468,18 @@ Dësen Numm gëtt da gebraucht fir sech an d\'Wiki anzeloggen.',
Spezifizéiert en anere Benotzernumm.',
'config-admin-password-blank' => 'Gitt e Passwuert fir den Adminstateur-Kont an.',
'config-admin-password-same' => "D'Passwuert däerf net dat selwecht si wéi de Benotzernumm.",
- 'config-admin-password-mismatch' => 'Déi zwee Passwierder Déi dir aginn stëmmen net iwwerteneen.',
- 'config-admin-email' => 'E-Mailadress:',
+ 'config-admin-password-mismatch' => 'Déi zwee Passwierder Déi Dir aginn hutt stëmmen net iwwereneen.',
+ 'config-admin-email' => 'E-Mail-Adress:',
'config-admin-error-user' => 'Interne Feeler beim uleeë vun engem Administrateur mam Numm "<nowiki>$1</nowiki>".',
'config-admin-error-password' => 'Interne Feeler beim Setze vum Passwuert fir den Admin "<nowiki>$1</nowiki>": <pre>$2</pre>',
- 'config-admin-error-bademail' => 'Dir hutt eng E-Mailadress aginn déi net valabel ass',
+ 'config-admin-error-bademail' => 'Dir hutt eng E-Mail-Adress aginn déi net valabel ass',
'config-subscribe' => "Sech op d'[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Ukënnegunge vun neie Versiounen] abonnéieren.",
'config-almost-done' => "Dir sidd bal fäerdeg!
Dir kënnt elo déi Astellungen déi nach iwwreg sinn iwwersprangen an d'Wiki elo direkt installéieren.",
'config-optional-continue' => 'Stellt mir méi Froen.',
'config-optional-skip' => "Ech hunn es genuch, installéier just d'Wiki.",
'config-profile' => 'Profil vun de Benotzerrechter:',
- 'config-profile-wiki' => 'Traditionell Wiki',
+ 'config-profile-wiki' => 'Oppe Wiki',
'config-profile-no-anon' => 'Uleeë vun engem Benotzerkont verlaangt',
'config-profile-fishbowl' => 'Nëmmen autoriséiert Editeuren',
'config-profile-private' => 'Privat Wiki',
@@ -11750,39 +12495,45 @@ Dir kënnt elo déi Astellungen déi nach iwwreg sinn iwwersprangen an d'Wiki el
'config-email-sender' => 'E-Mailadress fir Äntwerten:',
'config-upload-settings' => 'Eropgeluede Biller a Fichieren',
'config-upload-enable' => 'Eropluede vu Fichieren aschalten',
- 'config-upload-deleted' => 'Repertoire fir geläschte Fichieren:',
+ 'config-upload-deleted' => 'Repertoire fir geläscht Fichieren:',
'config-logo' => 'URL vum Logo:',
'config-instantcommons' => '"Instant Commons" aktivéieren',
'config-cc-again' => 'Nach eng kéier eraussichen...',
'config-advanced-settings' => 'Erweidert Astellungen',
'config-extensions' => 'Erweiderungen',
'config-install-step-done' => 'fäerdeg',
- 'config-install-step-failed' => 'huet net fonctionnéiert',
+ 'config-install-step-failed' => 'huet net funktionéiert',
'config-install-extensions' => 'Mat den Ereiderungen',
'config-install-database' => 'Datebank gëtt installéiert',
+ 'config-install-pg-plpgsql' => 'No der Sprooch PL/pgSQL sichen',
'config-pg-no-plpgsql' => "Fir d'Datebank $1 muss d'Datebanksprooch PL/pgSQL installéiert ginn",
'config-install-user' => 'Datebank Benotzer uleeën',
'config-install-user-alreadyexists' => 'De Benotzer "$1" gëtt et schonn!',
- 'config-install-user-create-failed' => 'D\'Opmaache vum Benotzer "$1" huet net fonctionnéiert: $2',
+ 'config-install-user-create-failed' => 'D\'Opmaache vum Benotzer "$1" huet net funktionéiert: $2',
+ 'config-install-user-grant-failed' => 'D\'Bäisetze vu Rechter fir de Benotzer "$1" huet net funktionéiert: $2',
'config-install-user-missing' => 'De Benotzer "$1" deen ugi gouf gëtt et net.',
+ 'config-install-user-missing-create' => 'De spezifizéierte Benotzer "$1" gëtt et net.
+Klickt d\'Checkbox "Benotzerkont uleeën" wann Dir dee Benotzer uleeë wëllt.',
'config-install-tables' => 'Tabelle ginn ugeluecht',
'config-install-interwiki' => 'Standard Interwiki-Tabell gëtt ausgefëllt',
'config-install-interwiki-list' => 'De Fichier <code>interwiki.list</code> gouf net fonnt.',
'config-install-stats' => 'Initialisatioun vun de Statistiken',
'config-install-keys' => 'Generéiere vum Geheimschlëssel',
'config-install-sysop' => 'Administrateur Benotzerkont gëtt ugeluecht',
+ 'config-install-mainpage' => 'Haaptsäit mat Standard-Inhalt gëtt ugeluecht',
'config-install-extension-tables' => "D'Tabelle fir déi aktivéiert Erweiderunge ginn ugeluecht",
'config-install-mainpage-failed' => "D'Haaptsäit konnt net dragesat ginn: $1",
- 'config-download-localsettings' => 'LocalSettings.php eroflueden',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> eroflueden',
'config-help' => 'Hëllef',
'config-nofile' => 'De Fichier "$1" gouf net fonnt. Gouf e geläscht?',
'mainpagetext' => "'''MediaWiki gouf installéiert.'''",
- 'mainpagedocfooter' => "Kuckt w.e.g. [//meta.wikimedia.org/wiki/Help:Contents d'Benotzerhandbuch] fir den Interface ze personnaliséieren.
+ 'mainpagedocfooter' => "Kuckt w.e.g. [//meta.wikimedia.org/wiki/Help:Contents d'Benotzerhandbuch] fir Informatiounen iwwer de Gebruach vun der Wiki Software.
-== Starthëllefen ==
+== Fir unzefänken ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Hëllef bei der Konfiguratioun]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglëscht vun neie MediaWiki-Versiounen]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglëscht vun neie MediaWiki-Versiounen]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Lokaliséiert MediaWiki fir Är Sprooch]",
);
/** Lingua Franca Nova (Lingua Franca Nova)
@@ -11795,7 +12546,7 @@ $messages['lfn'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de ajustas de la desinia]
* [//www.mediawiki.org/wiki/Manual:FAQ Demandas comun de MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista per receta anunsias de novas supra MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista per receta anunsias de novas supra MediaWiki]', # Fuzzy
);
/** Ganda (Luganda)
@@ -11808,7 +12559,7 @@ $messages['lg'] = array(
== Amagezi agakuyamba okutandika ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lukalala lw'eby'enteekateeka yo]
* [//www.mediawiki.org/wiki/Manual:FAQ Ebiter'okubuuzibwa ku MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Wewandise ofunenga amawulire aga email ag'ebifa ku MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Wewandise ofunenga amawulire aga email ag'ebifa ku MediaWiki]", # Fuzzy
);
/** Limburgish (Limburgs)
@@ -11821,7 +12572,7 @@ $messages['li'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lies mit instellinge]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki VGV (FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki mailinglies veur nuuj versies]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki mailinglies veur nuuj versies]", # Fuzzy
);
/** Lao (ລາວ)
@@ -11831,8 +12582,98 @@ $messages['lo'] = array(
);
/** Lithuanian (lietuvių)
+ * @author Eitvys200
+ * @author Mantak111
*/
$messages['lt'] = array(
+ 'config-desc' => 'MediaWiki diegimas',
+ 'config-title' => 'MediaWiki $1 diegimas',
+ 'config-information' => 'Informacija',
+ 'config-your-language' => 'Jūsų kalba:',
+ 'config-wiki-language' => 'Viki kalba:',
+ 'config-back' => '← Atgal',
+ 'config-continue' => 'Toliau →',
+ 'config-page-language' => 'Kalba',
+ 'config-page-welcome' => 'Sveiki atvykę į MediaWiki!',
+ 'config-page-dbconnect' => 'Prisijungti prie duomenų bazės',
+ 'config-page-dbsettings' => 'Duomenų bazės nustatymai',
+ 'config-page-name' => 'Vardas',
+ 'config-page-options' => 'Parinktys',
+ 'config-page-install' => 'Įdiegti',
+ 'config-page-complete' => 'Baigta!',
+ 'config-page-restart' => 'Iš naujo paleiskite diegimą',
+ 'config-page-readme' => 'Perskaityk manę',
+ 'config-page-copying' => 'Kopijuojama',
+ 'config-page-upgradedoc' => 'Atnaujinama',
+ 'config-restart' => 'Taip, paleiskite jį iš naujo',
+ 'config-env-php' => 'PHP $1 yra įdiegtas.',
+ 'config-env-php-toolow' => 'PHP $1 įdiegta.
+Tačiau, MediaWiki reikia PHP $2 ar naujesnės.',
+ 'config-db-type' => 'Duomenų bazės tipas:',
+ 'config-db-host' => 'Duomenų bazės serveris:',
+ 'config-db-name' => 'Duomenų bazės pavadinimas:',
+ 'config-db-name-oracle' => 'Duomenų bazės schema:',
+ 'config-db-username' => 'Duomenų bazės vartotojo vardas:',
+ 'config-db-password' => 'Duomenų bazės slaptažodis:',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-db-port' => 'Duomenų bazės prievadas:',
+ 'config-db-schema' => 'MediaWiki schema:',
+ 'config-header-mysql' => 'MySQL nustatymai',
+ 'config-header-postgres' => 'PostgreSQL nustatymai',
+ 'config-header-sqlite' => 'SQLite nustatymai',
+ 'config-header-oracle' => 'Oracle nustatymai',
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-site-name' => 'Viki pavadinimas:',
+ 'config-site-name-blank' => 'Įveskite svetainės pavadinimą.',
+ 'config-project-namespace' => 'Projekto pavadinimas:',
+ 'config-ns-generic' => 'Projektas',
+ 'config-ns-site-name' => 'Toks pat kaip viki pavadinimas: $1',
+ 'config-ns-other-default' => 'ManoWiki',
+ 'config-admin-box' => 'Administratoriaus paskyra',
+ 'config-admin-name' => 'Jūsų vardas:',
+ 'config-admin-password' => 'Slaptažodis:',
+ 'config-admin-password-confirm' => 'Slaptažodis dar kartą:',
+ 'config-admin-name-blank' => 'Įveskite administratoriaus vartotojo vardą.',
+ 'config-admin-password-blank' => 'Įvesti administratoriaus paskyros slaptažodį.',
+ 'config-admin-password-same' => 'Slaptažodis turi būti ne toks pat, kaip vartotojo vardas.',
+ 'config-admin-password-mismatch' => 'Įvesti slaptažodžiai nesutampa.',
+ 'config-admin-email' => 'El. pašto adresas:',
+ 'config-optional-continue' => 'Paklausti daugiau klausimų.',
+ 'config-optional-skip' => 'Man jau nuobodu, tiesiog įdiekite viki.',
+ 'config-profile' => 'Vartotojo teisių paskyra:',
+ 'config-profile-wiki' => 'Tradicinė viki', # Fuzzy
+ 'config-profile-private' => 'Privati viki',
+ 'config-license-pd' => 'Viešas Domenas',
+ 'config-email-settings' => 'El. pašto nustatymai',
+ 'config-upload-enable' => 'Įgalinti failų įkėlimus',
+ 'config-logo' => 'Logotipo URL:',
+ 'config-cc-again' => 'Pasirinkti dar kartą...',
+ 'config-extensions' => 'Plėtiniai',
+ 'config-install-step-done' => 'atlikta',
+ 'config-install-step-failed' => 'nepavyko',
+ 'config-install-schema' => 'Kuriama schema',
+ 'config-install-tables' => 'Kuriamos lentelės',
+ 'config-install-stats' => 'Inicijuojamos statistikos',
+ 'config-install-keys' => 'Generuojami slapti raktai',
+ 'config-install-done' => "'''Sveikiname!'''
+Jūs sėkmingai įdiegėte MediaWiki.
+
+Įdiegimo programa sukūrė <code>LocalSettings.php</code> failą.
+Jame yra visos jūsų konfigūracijos.
+
+Jums reikės atsisiųsti ir įdėti jį į savo wiki įdiegimo bazę (pačiame kataloge, kaip index.php). Atsisiuntimas turėtų prasidėti automatiškai.
+
+Jei atsisiuntimas nebuvo pasiūlytas, arba jį atšaukėte, galite iš naujo atsisiųsti paspaudę žemiau esančią nuorodą:
+
+$3
+
+'''Pastaba:''' Jei jūs to nepadarysite dabar, tada šis sukurtas konfigūracijos failas nebus galimas vėliau, jei išeisite iš įdiegimo be atsisiuntimo.
+
+Kai baigsite, jūs galėsite '''[$2 įeiti į savo wiki]'''.",
+ 'config-download-localsettings' => 'Atsisiųsti <code>LocalSettings.php</code>',
+ 'config-help' => 'pagalba',
'mainpagetext' => "'''MediaWiki sėkmingai įdiegta.'''",
'mainpagedocfooter' => 'Informacijos apie wiki programinės įrangos naudojimą, ieškokite [//meta.wikimedia.org/wiki/Help:Contents žinyne].
@@ -11840,7 +12681,7 @@ $messages['lt'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Konfigūracijos nustatymų sąrašas]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki DUK]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki pranešimai paštu apie naujas versijas]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki pranešimai paštu apie naujas versijas]', # Fuzzy
);
/** Latvian (latviešu)
@@ -11867,7 +12708,7 @@ $messages['lv'] = array(
== Pirmie soļi ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Konfigurācijas iespēju saraksts]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki J&A]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Parakstīties uz paziņojumiem par jaunām MediaWiki versijām]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Parakstīties uz paziņojumiem par jaunām MediaWiki versijām]', # Fuzzy
);
/** Literary Chinese (文言)
@@ -11880,7 +12721,7 @@ $messages['lzh'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Lazuri (Lazuri)
@@ -11893,7 +12734,7 @@ $messages['lzz'] = array(
== Ağani na gyoç’k’u maxmarepe ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Ok'iduşi ayarepeşi liste]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki P'anda Na-k'itxu K'itxalape]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-mailepeşiş liste]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-mailepeşiş liste]", # Fuzzy
);
/** Maithili (मैथिली)
@@ -11906,7 +12747,7 @@ $messages['mai'] = array(
==प्रारम्भ कोना करी==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Moksha (мокшень)
@@ -11919,7 +12760,7 @@ $messages['mdf'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Васьфневи арафнематнень кярькссь]
* [//www.mediawiki.org/wiki/Manual:FAQ МедиаВикить Сидеста Кеподеви Кизефксне]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce МедиаВикить од верзиятнень колга кулянь пачфтема]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce МедиаВикить од верзиятнень колга кулянь пачфтема]', # Fuzzy
);
/** Malagasy (Malagasy)
@@ -11953,7 +12794,7 @@ $messages['mg'] = array(
'config-admin-name' => 'Ny anaranao :',
'config-admin-password' => 'Tenimiafina :',
'config-admin-email' => 'Adiresy imailaka :',
- 'config-profile-wiki' => 'Wiki tsotra',
+ 'config-profile-wiki' => 'Wiki tsotra', # Fuzzy
'config-profile-no-anon' => 'Mila mamorona kaonty',
'config-profile-fishbowl' => 'Mpanova mahazo alalana ihany',
'config-profile-private' => 'Wiki tsy sarababem-bahoaka',
@@ -11977,7 +12818,7 @@ $messages['mg'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lisitra ny paramètre de configuration]
* [//www.mediawiki.org/wiki/Manual:FAQ/fr FAQ momba ny MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Resaka momba ny fizaràn'ny MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Resaka momba ny fizaràn'ny MediaWiki]", # Fuzzy
);
/** Eastern Mari (олык марий)
@@ -11987,21 +12828,23 @@ $messages['mhr'] = array(
);
/** Minangkabau (Baso Minangkabau)
+ * @author Iwan Novirion
* @author Luthfi94
*/
$messages['min'] = array(
'mainpagetext' => "'''MediaWiki alah tapasang jo sukses'''.",
- 'mainpagedocfooter' => 'Silakan baco [//www.mediawiki.org/wiki/Help:Contents/id Panduan Pangguno] untuak caro panggunoan parangkaik lunak wiki iko.
+ 'mainpagedocfooter' => 'Konsultasian [//meta.wikimedia.org/wiki/Help:Contents/min Panduan Panggunoan] untuak informasi caro panggunoan parangkaik lunak wiki.
== Mamulai panggunoan ==
-
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings/id Dafta pangaturan konfigurasi]
-* [//www.mediawiki.org/wiki/Manual:FAQ/id Dafta patanyoan nan acok diajukan manganai MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]',
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings/id Daftar pangaturan konfigurasi]
+* [//www.mediawiki.org/wiki/Manual:FAQ/id Daftar patanyoan nan acok diajukan manganai MediaWiki]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Palokalan MediaWiki untuak bahaso Sanak]',
);
/** Macedonian (македонски)
* @author Bjankuloski06
+ * @author 아라
*/
$messages['mk'] = array(
'config-desc' => 'Инсталатор на МедијаВики',
@@ -12009,19 +12852,19 @@ $messages['mk'] = array(
'config-information' => 'Информации',
'config-localsettings-upgrade' => 'Востановена е податотека <code>LocalSettings.php</code>.
За да ја надградите инсталцијава, внесете ја вредноста на <code>$wgUpgradeKey</code> во полето подолу.
-Тоа е го најдете во LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Утврдено е присуството на податотеката „LocalSettings.php“.
-За да ја надградите инсталацијата, пуштете ја „update.php“ наместо горенаведената.',
+Тоа е го најдете во <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Утврдено е присуството на податотеката „<code>LocalSettings.php</code>“.
+За да ја надградите инсталацијата, пуштете ја „<code>update.php</code>“ наместо горенаведената.',
'config-localsettings-key' => 'Надградбен клуч:',
'config-localsettings-badkey' => 'Клучот што го наведовте е погрешен',
'config-upgrade-key-missing' => 'Востановена е постоечка инсталација на МедијаВики.
-За да ја надградите, вметнете го следниов ред на дното од вашата страница LocalSettings.php:
+За да ја надградите, вметнете го следниов ред на дното од вашата страница <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Постоечката страница LocalSettings.php е нецелосна.
+ 'config-localsettings-incomplete' => 'Постоечката страница <code>LocalSettings.php</code> е нецелосна.
Не е поставена променливата $1.
-Изменете ја страницата LocalSettings.php така што ќе ѝ зададете вредност на променливата, па стиснете на „Продолжи“.',
- 'config-localsettings-connection-error' => 'Се појави грешка при поврзувањето со базата користејќи ги поставките назначени во LocalSettings.php или AdminSettings.php. Исправете ги овие поставки и обидете се повторно.
+Изменете ја страницата <code>LocalSettings.php</code> така што ќе ѝ зададете вредност на променливата, па стиснете на „{{int:Config-continue}}“.',
+ 'config-localsettings-connection-error' => 'Се појави грешка при поврзувањето со базата користејќи ги поставките назначени во <code>LocalSettings.php</code> или <code>AdminSettings.php</code>. Исправете ги овие поставки и обидете се повторно.
$1',
'config-session-error' => 'Грешка при започнување на сесијата: $1',
@@ -12054,9 +12897,8 @@ $1',
'config-page-existingwiki' => 'Постоечко вики',
'config-help-restart' => 'Дали сакате да ги исчистите сите зачувани податоци што ги внесовте и да ја започнете инсталацијата одново?',
'config-restart' => 'Да, почни одново',
- 'config-welcome' => '=== Environmental checks ===
-Се вршат основни проверки за да се востанови дали околината е погодна за инсталирање на МедијаВики.
-Ако ви затреба помош при инсталацијата, ќе треба да ги наведете резултатите од овие проверки.',
+ 'config-welcome' => '=== Проверки на околината ===
+Сега ќе се извршиме основни проверки за да се востанови дали околината е погодна за инсталирање на МедијаВики. Не заборавајте да ги приложите овие информации ако барате помош со довршување на инсталацијата.',
'config-copyright' => "=== Авторски права и услови ===
$1
@@ -12127,6 +12969,10 @@ $1
Ова е веројатно премалку.
Инсталацијата може да не успее!",
'config-ctype' => "'''Фатална грешка''': PHP мора да се состави со поддршка за [http://www.php.net/manual/en/ctype.installation.php додатокот Ctype].",
+ 'config-json' => "'''Кобно:''' PHP беше срочен без поддршка од JSON.
+Ќе мора да го инсталирате додатокот за JSON во PHP, или додатокот [http://pecl.php.net/package/jsonc PECL jsonc] пред да го инсталирате МедијаВики.
+* Додатокот за PHP е вклучен во верзиите 5 и 6 на Linux (од Red Hat Enterprise) (CentOS), но мора да се активира во <code>/etc/php.ini</code> или <code>/etc/php.d/json.ini</code>.
+* Некои варијанти на Linux излезени по мај 2013 г. не го содржат додатокот за PHP, туку го пакуваат додатокот PECL како <code>php5-json</code> или <code>php-pecl-jsonc</code>.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] е инсталиран',
'config-apc' => '[http://www.php.net/apc APC] е инсталиран',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] е инсталиран',
@@ -12135,6 +12981,8 @@ $1
'config-mod-security' => "'''Предупредување''': на вашиот опслужувач има овозможено [http://modsecurity.org/ mod_security]. Ако не е поставено како што треба, ова може да предизвика проблеми кај МедијаВики и други програми што им овозможуваат на корисниците да објавуваат произволни содржини.
Погледнете ја [http://modsecurity.org/documentation/ mod_security документацијата] или обратете се кај домаќинот ако наидете на случајни грешки.",
'config-diff3-bad' => 'GNU diff3 не е пронајден.',
+ 'config-git' => 'Го пронајдов Git програмот за контрола на верзии: <code>$1</code>.',
+ 'config-git-bad' => 'Не го пронајдов Git-програмот за контрола на верзии.',
'config-imagemagick' => 'Пронајден е ImageMagick: <code>$1</code>.
Ако овозможите подигање, тогаш ќе биде овозможена минијатуризација на сликите.',
'config-gd' => 'Утврдив дека има вградена GD графичка библиотека.
@@ -12154,7 +13002,7 @@ $1
Надградете го на PHP 5.2.9 и libxml2 2.7.3 или нивни понови верзии! ПРЕКИНУВАМ ([//bugs.php.net/bug.php?id=45996 грешката е заведена во PHP]).',
'config-using531' => 'МедијаВики не може да се користи со PHP $1 поради грешка кај упатните параметри за <code>__call()</code>.
За да го решите проблемот, надградете го на PHP 5.3.2 или понова верзија, или пак користете го постариот PHP 5.3.0.',
- 'config-suhosin-max-value-length' => 'Suhosin е инсталиран и ја ограничува должината на параметарот GET на $1 bytes. Делот ResourceLoader на МедијаВики ќе ја заобиколува ова граница, но со тоа ќе се влоши делотворноста. Ако е воопшто можно, на suhosin.get.max_value_length треба да го наместите на 1024 или поевеќе во php.ini , и да му ја зададете истата вредност на $wgResourceLoaderMaxQueryLength во LocalSettings.php .',
+ 'config-suhosin-max-value-length' => 'Suhosin е инсталиран и ја ограничува должината на параметарот GET на $1 бајти. Делот ResourceLoader на МедијаВики ќе ја заобиколува ова граница, но со тоа ќе се влоши делотворноста. Ако е воопшто можно, на <code>suhosin.get.max_value_length</code> треба да го наместите на 1024 или повеќе во <code>php.ini</code>, и да му ја зададете истата вредност на <code>$wgResourceLoaderMaxQueryLength</code> во <code>LocalSettings.php</code>.',
'config-db-type' => 'Тип на база:',
'config-db-host' => 'Домаќин на базата:',
'config-db-host-help' => 'Ако вашата база е на друг опслужувач, тогаш тука внесете го името на домаќинот или IP-адресата.
@@ -12228,7 +13076,6 @@ $1
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'МедијаВики ги поддржува следниве системи на бази на податоци:
$1
@@ -12238,18 +13085,16 @@ $1
'config-support-postgres' => '* $1 е популарен систем на бази на податоци со отворен код кој претставува алтернатива на MySQL ([http://www.php.net/manual/en/pgsql.installation.php како да составите PHP со поддршка за PostgreSQL]). Може сè уште да има некои грешки. па затоа не се препорачува за употреба во производна средина.',
'config-support-sqlite' => '* $1 е лесен систем за бази на податоци кој е многу добро поддржан. ([http://www.php.net/manual/en/pdo.installation.php Како да составите PHP со поддршка за SQLite], користи PDO)',
'config-support-oracle' => '* $1 е база на податоци на комерцијално претпријатие. ([http://www.php.net/manual/en/oci8.installation.php Како да составите PHP со поддршка за OCI8])',
- 'config-support-ibm_db2' => '* $1 is комерцијална база на податоциза фирми.',
'config-header-mysql' => 'Нагодувања на MySQL',
'config-header-postgres' => 'Нагодувања на PostgreSQL',
'config-header-sqlite' => 'Нагодувања на SQLite',
'config-header-oracle' => 'Нагодувања на Oracle',
- 'config-header-ibm_db2' => 'Нагодувања на IBM DB2',
'config-invalid-db-type' => 'Неважечки тип на база',
'config-missing-db-name' => 'Мора да внесете значење за параметарот „Име на базата“',
'config-missing-db-host' => 'Мора да внесете вредност за „Домаќин на базата на податоци“',
'config-missing-db-server-oracle' => 'Мора да внесете вредност за „TNS на базата“',
- 'config-invalid-db-server-oracle' => 'Неважечки TNS „$1“ за базата.
-Користете само знаци по ASCII - букви (a-z, A-Z), бројки (0-9), долни црти (_) и точки (.).',
+ 'config-invalid-db-server-oracle' => 'Неважечки TNS „$1“.
+Користете или „TNS Name“ или низата „Easy Connect“ ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методи на именување за Oracle])',
'config-invalid-db-name' => 'Неважечко име на базата „$1“.
Користете само ASCII-букви (a-z, A-Z), бројки (0-9), долни црти (_) и цртички (-).',
'config-invalid-db-prefix' => 'Неважечки префикс за базата „$1“.
@@ -12306,7 +13151,7 @@ chmod a+w $3</pre>',
Сега можете да [$1 почнете да го користите викито].',
'config-regenerate' => 'Пресоздај LocalSettings.php →',
- 'config-show-table-status' => 'Барањето SHOW TABLE STATUS не успеа!',
+ 'config-show-table-status' => 'Барањето <code>SHOW TABLE STATUS</code> не успеа!',
'config-unknown-collation' => "'''Предупредување:''' Базата корисни непрепознаена упатна споредба.",
'config-db-web-account' => 'Сметка на базата за мрежен пристап',
'config-db-web-help' => 'Одберете корисничко име и лозинка што ќе ги користи мрежниот опслужувач за поврзување со опслужувачот на базта на податоци во текот на редовната работа со викито.',
@@ -12324,6 +13169,11 @@ chmod a+w $3</pre>',
Ако вашата инсталација на MySQL поддржува InnoDB, тогаш сериозно препорачуваме да го користите него наместо MyISAM.
Ако вашата инсталација на MySQL не поддржува InnoDB, веројатно дошло време за надградба.",
+ 'config-mysql-only-myisam-dep' => "'''Предупредување:''' MyISAM е единствениот достапен складишен погон за MySQL, што не се препорачува за употреба со МедијаВики, бидејќи:
+* речиси не поддржува истовремено извршување на задачите поради заклучувањето на табелите
+* поподложен е на расипувања од другите погони
+* кодната база на МедијаВИки не секогаш работи исправно со MyISAM
+Вашата инсталација на MySQL не поддржува InnoDB. Можеби е време да ја надградите.",
'config-mysql-engine-help' => "'''InnoDB''' речиси секогаш е најдобар избор, бидејќи има добра поддршка за едновременост.
'''MyISAM''' може да е побрз кај инсталациите наменети за само еден корисник или незаписни инсталации (само читање).
@@ -12335,7 +13185,6 @@ chmod a+w $3</pre>',
Ова е поефикасно отколку TF-8 режимот на MySQL, и ви овозможува да ја користите целата палета на уникодни знаци.
Во '''UTF-8 режим''', MySQL ќе знае на кој збир знаци припаѓаат вашите податоци, и може соодветно да ги претстави и претвори, но нема да ви дозволи да складиратезнаци над [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Основната повеќејазична рамнина].",
- 'config-ibm_db2-low-db-pagesize' => "Вашата база на податоци DB2 има основно-зададен табеларен простор со недоволна големина на страниците. Таа треба да изнесува барем '''32 килобајти'''.",
'config-site-name' => 'Име на викито:',
'config-site-name-help' => 'Ова ќе се појавува во заглавната лента на прелистувачот и на разни други места.',
'config-site-name-blank' => 'Внесете име на мрежното место.',
@@ -12345,8 +13194,8 @@ chmod a+w $3</pre>',
'config-ns-other' => 'Друго (наведете)',
'config-ns-other-default' => 'МоеВики',
'config-project-namespace-help' => "По примерот на Википедија, многу викија ги чуваат страниците со правила на посебно место од самите содржини, т.е. во „'''проектен именски простор'''“.
-Сите наслови на страниците во овој именски простор почнуваат со извесен префикс, којшто можете да го укажете тука.
-По традиција префиксот произлегува од името на викито, но не смее да содржи интерпункциски знаци како „#“ или „:“.",
+Сите наслови на страниците во овој именски простор почнуваат со извесна претставка, којшто можете да го укажете тука.
+По традиција претставката произлегува од името на викито, но не смее да содржи интерпункциски знаци како „#“ или „:“.",
'config-ns-invalid' => 'Назначениот именски простор „<nowiki>$1</nowiki>“ е неважечки.
Назначете друг проектен именски простор.',
'config-ns-conflict' => 'Наведениот именски простор „<nowiki>$1</nowiki>“ се коси со основниот именски простор на МедијаВики.
@@ -12378,7 +13227,7 @@ chmod a+w $3</pre>',
'config-optional-continue' => 'Постави ми повеќе прашања.',
'config-optional-skip' => 'Веќе ми здосади, дај само инсталирај го викито.',
'config-profile' => 'Профил на кориснички права:',
- 'config-profile-wiki' => 'Традиционално вики',
+ 'config-profile-wiki' => 'Отворено вики',
'config-profile-no-anon' => 'Задолжително отворање сметка',
'config-profile-fishbowl' => 'Само овластени уредници',
'config-profile-private' => 'Приватно вики',
@@ -12388,7 +13237,7 @@ chmod a+w $3</pre>',
Многумина имаат најдено најразлични полезни примени за МедијаВики, но понекогаш не е лесно да убедите некого во предностите на вики-концептот.
Значи имате избор.
-'''{{int:config-profile-wiki}}''' — секој може да го уредува, дури и без најавување.
+'''{{int:config-profile-wiki}}''' — модел според кој секој може да уредува, дури и без најавување.
Ако имате вики со '''задолжително отворање на сметка''', тогаш добивате повеќе контрола, но ова може даги одврати спонтаните учесници.
'''{{int:config-profile-fishbowl}}''' — може да уредуваат само уредници што имаат добиено дозвола за тоа, но јавноста може да ги гледа страниците, вклучувајќи ја нивната историја.
@@ -12397,13 +13246,13 @@ chmod a+w $3</pre>',
По инсталацијата имате на избор и посложени кориснички права и поставки. Погледајте во [//www.mediawiki.org/wiki/Manual:User_rights прирачникот].",
'config-license' => 'Авторски права и лиценца:',
'config-license-none' => 'Без подножје за лиценца',
- 'config-license-cc-by-sa' => 'Creative Commons НаведиИзвор СподелиПодИстиУслови',
+ 'config-license-cc-by-sa' => 'Криејтив комонс НаведиИзвор СподелиПодИстиУслови',
'config-license-cc-by' => 'Криејтив комонс НаведиИзвор',
- 'config-license-cc-by-nc-sa' => 'Creative Commons НаведиИзвор-Некомерцијално-СподелиПодИстиУслови',
+ 'config-license-cc-by-nc-sa' => 'Криејтив комонс НаведиИзвор-Некомерцијално-СподелиПодИстиУслови',
'config-license-cc-0' => 'Криејтив комонс Нула (јавна сопственост)',
'config-license-gfdl' => 'ГНУ-ова лиценца за слободна документација 1.3 или понова',
'config-license-pd' => 'Јавна сопственост',
- 'config-license-cc-choose' => 'Одберете друга Creative Commons лиценца по ваш избор',
+ 'config-license-cc-choose' => 'Одберете друга лиценца на Криејтив комонс по ваш избор',
'config-license-help' => "Многу јавни викија ги ставаат сите придонеси под [http://freedomdefined.org/Definition слободна лиценца].
Со ова се создава атмосфера на општа сопственост и поттикнува долгорочно учество.
Ова не е неопходно за викија на поединечни физички или правни лица.
@@ -12442,8 +13291,9 @@ chmod a+w $3</pre>',
'config-upload-deleted-help' => 'Одберете во која папка да се архивираат избришаните податотеки.
Најдобро би било ако таа не е достапна преку интернет.',
'config-logo' => 'URL за логото:',
- 'config-logo-help' => 'Матичното руво на МедијаВики има простор за лого од 135 x 160 пиксели над страничната лента.
-Подигнете слика со соодветна големина, и тука внесете ја URL-адресата.
+ 'config-logo-help' => 'Матичното руво на МедијаВики има простор за лого од 135x160 пиксели над страничната лента.
+
+Можете да употребите <code>$wgStylePath</code> или <code>$wgScriptPath</code> ако вашето лого е релативно на тие патеки.
Ако не сакате да имате лого, тогаш оставете го ова поле празно.',
'config-instantcommons' => 'Овозможи Instant Commons',
@@ -12451,10 +13301,10 @@ chmod a+w $3</pre>',
За да може ова да работи, МедијаВики бара пристап до интернет.
За повеќе информации за оваа функција и напатствија за нејзино поставување на вики (сите други освен Ризницата), коносултирајте го [//mediawiki.org/wiki/Manual:$wgForeignFileRepos прирачникот].',
- 'config-cc-error' => 'Изборникот на Creative Commons лиценца не даде резултати.
+ 'config-cc-error' => 'Изборникот на лиценци од Криејтив комонс не даде резултати.
Внесете го името на лиценцата рачно.',
'config-cc-again' => 'Одберете повторно...',
- 'config-cc-not-chosen' => 'Одберете ја саканата Creative Commons лиценца и кликнете на „продолжи“.',
+ 'config-cc-not-chosen' => 'Одберете ја саканата лиценца од Криејтив комонс и стиснете на „продолжи“.',
'config-advanced-settings' => 'Напредни нагодувања',
'config-cache-options' => 'Нагодувања за кеширање на објекти:',
'config-cache-help' => 'Кеширањето на објекти се користи за зголемување на брзината на МедијаВики со кеширање на често употребуваните податоци.
@@ -12479,7 +13329,7 @@ chmod a+w $3</pre>',
'config-install-alreadydone' => "'''Предупредување:''' Изгледа дека веќе го имате инсталирано МедијаВики и сега сакате да го инсталирате повторно.
Продолжете на следната страница.",
'config-install-begin' => 'Стискајќи на „{{int:config-continue}}“ ќе ја започнете инсталацијата на МедијаВики.
-Ако сакате да направите измени во досегашното, стиснете на „Назад“.',
+Ако сакате да направите измени во досегашното, стиснете на „{{int:config-back}}“.',
'config-install-step-done' => 'готово',
'config-install-step-failed' => 'не успеа',
'config-install-extensions' => 'Вклучувам додатоци',
@@ -12535,16 +13385,20 @@ $3
'''Напомена''': Ако ова не го направите сега, податотеката со поставки повеќе нема да биде на достапна.
Откога ќе завршите со тоа, можете да '''[$2 влезете на вашето вики]'''.",
- 'config-download-localsettings' => 'Преземи го LocalSettings.php',
+ 'config-download-localsettings' => 'Преземи го <code>LocalSettings.php</code>',
'config-help' => 'помош',
'config-nofile' => 'Податотеката „$1“ не е пронајдена. Да не е избришана?',
+ 'config-extension-link' => 'Дали сте знаеле дека вашето вики поддржува [//www.mediawiki.org/wiki/Manual:Extensions додатоци]?
+
+Можете да ги прелистате [//www.mediawiki.org/wiki/Category:Extensions_by_category по категории] или да ја посетите [//www.mediawiki.org/wiki/Extension_Matrix матрицата], каде ќе најдете полн список на додатоци.',
'mainpagetext' => "'''МедијаВики е успешно инсталиран.'''",
'mainpagedocfooter' => 'Погледнете го [//meta.wikimedia.org/wiki/Help:Contents Упатството за корисници] за подетални иформации како се користи вики-програмот.
==Од каде да почнете==
* [//meta.wikimedia.org/wiki/Manual:Configuration_settings Список на нагодувања]
* [//meta.wikimedia.org/wiki/Manual:FAQ ЧПП (често поставувани прашања) за МедијаВики].
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Поштенски список на МедијаВики за нови верзии]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Поштенски список на МедијаВики за нови верзии]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Локализирајте го МедијаВики на вашиот јазик]',
);
/** Malayalam (മലയാളം)
@@ -12646,7 +13500,7 @@ $1
ബാക്കിയുള്ളവ അവഗണിച്ച് വിക്കി ഇൻസ്റ്റോൾ ചെയ്യാവുന്നതാണ്.',
'config-optional-continue' => 'കൂടുതൽ ചോദ്യങ്ങൾ ചോദിക്കൂ.',
'config-optional-skip' => 'എനിക്ക് മടുത്തു, ഒന്ന് ഇൻസ്റ്റോൾ ചെയ്ത് തീർക്ക്.',
- 'config-profile-wiki' => 'പരമ്പരാഗത വിക്കി',
+ 'config-profile-wiki' => 'പരമ്പരാഗത വിക്കി', # Fuzzy
'config-profile-no-anon' => 'അംഗത്വ സൃഷ്ടി ചെയ്യേണ്ടതുണ്ട്',
'config-profile-fishbowl' => 'അനുവാദമുള്ളവർ മാത്രം തിരുത്തുക',
'config-profile-private' => 'സ്വകാര്യ വിക്കി',
@@ -12700,7 +13554,7 @@ $3
== പ്രാരംഭസഹായികൾ ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings ക്രമീകരണങ്ങളുടെ പട്ടിക]
* [//www.mediawiki.org/wiki/Manual:FAQ മീഡിയവിക്കി പതിവുചോദ്യങ്ങൾ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce മീഡിയവിക്കി പ്രകാശന മെയിലിങ് ലിസ്റ്റ്]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce മീഡിയവിക്കി പ്രകാശന മെയിലിങ് ലിസ്റ്റ്]', # Fuzzy
);
/** Mongolian (монгол)
@@ -12714,7 +13568,7 @@ $messages['mn'] = array(
== Эхлэх ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Тохиргоо]
* [//www.mediawiki.org/wiki/Manual:FAQ МедиаВикигийн тогтмол тавигддаг асуултууд]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce МедиаВикигийн мэдээний мэйл явуулах жагсаалт]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce МедиаВикигийн мэдээний мэйл явуулах жагсаалт]', # Fuzzy
);
/** Marathi (मराठी)
@@ -12727,13 +13581,24 @@ $messages['mr'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings कॉन्फिगरेशन सेटींगची यादी]
* [//www.mediawiki.org/wiki/Manual:FAQ मीडियाविकी नेहमी विचारले जाणारे प्रश्न]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मीडियाविकि मेलिंग लिस्ट]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मीडियाविकि मेलिंग लिस्ट]', # Fuzzy
);
/** Malay (Bahasa Melayu)
* @author Anakmalaysia
+ * @author Pizza1016
*/
$messages['ms'] = array(
+ 'config-desc' => 'Pemasang MediaWiki',
+ 'config-title' => 'Pasangan MediaWiki $1',
+ 'config-information' => 'Maklumat',
+ 'config-localsettings-key' => 'Kunci naik taraf:',
+ 'config-localsettings-badkey' => 'Kunci yang anda berikan tidak betul.',
+ 'config-session-error' => 'Ralat ketika memulakan sesi: $1',
+ 'config-your-language' => 'Bahasa anda:',
+ 'config-your-language-help' => 'Pilihkan bahasa untuk digunakan dalam proses pemasangan ini.',
+ 'config-wiki-language' => 'Bahasa wiki:',
+ 'config-wiki-language-help' => 'Pilih bahasa utama wiki yang bakal dicipta ini.',
'config-back' => '← Undur',
'config-continue' => 'Teruskan →',
'config-page-language' => 'Bahasa',
@@ -12744,13 +13609,26 @@ $messages['ms'] = array(
'config-page-name' => 'Nama',
'config-page-options' => 'Pilihan',
'config-page-install' => 'Pasang',
+ 'config-page-complete' => 'Selesai!',
+ 'config-page-restart' => 'Mulakan semula pemasangan',
+ 'config-page-readme' => 'Baca saya',
+ 'config-page-releasenotes' => 'Catatan keluaran',
+ 'config-page-copying' => 'Sedang menyalin',
+ 'config-page-upgradedoc' => 'Sedang menaik taraf',
+ 'config-page-existingwiki' => 'Wiki sedia ada',
'config-env-php' => 'PHP $1 dipasang.',
'config-env-php-toolow' => 'PHP $1 dipasang.
Bagaimanapun, MediaWiki memerlukan PHP $2 ke atas.',
'config-unicode-using-utf8' => 'utf8_normalize.so oleh Brion Vibber digunakan untuk penormalan Unicode.',
'config-unicode-using-intl' => '[http://pecl.php.net/intl Sambungan intl PECL] digunakan untuk penormalan Unicode.',
'config-db-charset' => 'Peranggu aksara pangkalan data',
- 'config-type-ibm_db2' => 'IBM DB2',
+ 'config-header-mysql' => 'Keutamaan MySQL',
+ 'config-header-postgres' => 'Keutamaan PostgreSQL',
+ 'config-header-sqlite' => 'Keutamaan SQLite',
+ 'config-header-oracle' => 'Keutamaan Oracle',
+ 'config-invalid-db-type' => 'Jenis pangkalan data tidak sah',
+ 'config-db-web-account-same' => 'Gunakan akaun yang sama seperti dalam pemasangan',
+ 'config-db-web-create' => 'Ciptakan akaun jika belum wujud',
'config-mysql-engine' => 'Enjin storan:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
@@ -12765,34 +13643,52 @@ Bagaimanapun, MediaWiki memerlukan PHP $2 ke atas.',
'config-ns-site-name' => 'Sama dengan nama wiki: $1',
'config-ns-other' => 'Lain-lain (nyatakan)',
'config-ns-other-default' => 'MyWiki',
+ 'config-admin-box' => 'Akaun penyelia',
+ 'config-admin-name' => 'Nama kamu:',
'config-admin-password' => 'Kata laluan:',
+ 'config-admin-password-confirm' => 'Kata laluan lagi:',
+ 'config-admin-password-mismatch' => 'Kata-kata laluan yang kamu berikan tidak sepadan.',
'config-admin-email' => 'Alamat e-mel:',
+ 'config-admin-error-bademail' => 'Kamu telah memberikan alamat e-mel yang tidak betul.',
+ 'config-optional-skip' => 'Saya sudah bosan, pasangkanlah wiki sahaja.',
'config-license' => 'Hak cipta dan lesen:',
'config-license-none' => 'Tiada pengaki lesen',
'config-license-cc-by-sa' => 'Creative Commons Attribution Share Alike',
'config-license-cc-by' => 'Creative Commons Attribution',
'config-license-cc-by-nc-sa' => 'Creative Commons Attribution Non-Commercial Share Alike',
'config-license-cc-0' => 'Creative Commons Zero (Domain Awam)',
- 'config-license-gfdl' => 'Lesen Dokumentasi Bebas GNU 1.3 ke atas',
+ 'config-license-gfdl' => 'Lesen Dokumentasi Bebas GNU 1.3 atau ke atas',
'config-license-pd' => 'Domain Awam',
'config-email-settings' => 'Tetapan e-mel',
'config-install-step-done' => 'siap',
'config-install-step-failed' => 'gagal',
+ 'config-install-user-alreadyexists' => 'Pengguna "$1" sudah wujud',
'config-help' => 'bantuan',
'mainpagetext' => "'''MediaWiki telah berjaya dipasang.'''",
'mainpagedocfooter' => 'Sila rujuk [//meta.wikimedia.org/wiki/Help:Contents Panduan Penggunaan] untuk maklumat mengenai penggunaan perisian wiki ini.
-== Untuk bermula ==
+== Permulaan ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Senarai tetapan konfigurasi]
* [//www.mediawiki.org/wiki/Manual:FAQ Soalan Lazim MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Senarai mel bagi keluaran MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Senarai surat keluaran MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Terjemahkan MediaWiki ke dalam bahasa anda]',
);
/** Maltese (Malti)
* @author Chrisportelli
*/
$messages['mt'] = array(
+ 'config-title' => "Installazzjoni ta' MediaWiki $1",
+ 'config-information' => 'Informazzjoni',
+ 'config-localsettings-key' => 'Ċavetta tal-aġġornament:',
+ 'config-localsettings-badkey' => 'Iċ-ċavetta li tajt hija ħażina.',
+ 'config-your-language' => 'Il-lingwa tiegħek:',
+ 'config-your-language-help' => "Agħżel lingwa li tixtieq tuża' matul il-proċess ta' installazzjoni.",
+ 'config-wiki-language' => 'Lingwi tal-wiki:',
+ 'config-wiki-language-help' => 'Agħżel il-lingwa li l-wiki se tkun l-aktar użata fil-wiki.',
+ 'config-back' => '← Lura',
+ 'config-continue' => 'Kompli →',
'config-page-language' => 'Lingwa',
'config-page-welcome' => 'Merħba fuq MediaWiki!',
'config-page-dbconnect' => 'Aqbad mad-databażi',
@@ -12805,13 +13701,77 @@ $messages['mt'] = array(
'config-page-restart' => "Erġa' ibda l-installazzjoni",
'config-page-readme' => 'Aqrani',
'config-page-releasenotes' => 'Noti tal-verżjoni',
+ 'config-page-upgradedoc' => 'Aġġornament',
+ 'config-page-existingwiki' => 'Wiki eżistenti',
+ 'config-restart' => "Iva, erġa' ibda",
+ 'config-env-php' => 'PHP $1 huwa installat.',
+ 'config-db-wiki-settings' => 'Identifika din il-wiki',
+ 'config-db-name' => 'Isem tad-databażi:',
+ 'config-db-install-account' => 'Kont tal-utent għall-installazzjoni',
+ 'config-db-username' => 'Isem tal-utent tad-databażi:',
+ 'config-db-password' => 'Password tad-databażi:',
+ 'config-db-port' => 'Port tad-databażi:',
+ 'config-db-schema' => 'Skema għal MediaWiki:',
+ 'config-db-web-create' => 'Oħloq il-kont jekk għadu ma jeżistix',
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-charset' => "Sett ta' karattri tad-databażi:",
+ 'config-mysql-binary' => 'Binarju',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-site-name' => 'Isem tal-wiki:',
+ 'config-site-name-help' => "Dan se jidher fil-barra tat-titlu tal-browżer u f'diversi postijiet oħra.",
+ 'config-site-name-blank' => 'Daħħal isem tas-sit.',
+ 'config-project-namespace' => 'Spazju tal-isem tal-proġett:',
+ 'config-ns-generic' => 'Proġett',
+ 'config-ns-site-name' => 'L-istess bħall-isem tal-wiki: $1',
+ 'config-ns-other' => 'Oħrajn (speċifika)',
+ 'config-ns-other-default' => 'MyWiki',
+ 'config-ns-invalid' => 'L-ispazju speċifikat "<nowiki>$1</nowiki>" huwa ħażin.
+Speċifika spazju tal-isem tal-proġett differenti.',
+ 'config-ns-conflict' => 'L-ispazju speċifikat "<nowiki>$1</nowiki>" joħloq kunflitt ma\' spazju tal-isem ieħor tal-MediaWiki.
+Speċifika spazju tal-isem tal-proġett differenti.',
+ 'config-admin-box' => 'Kont tal-amministratur',
+ 'config-admin-name' => 'Ismek:',
+ 'config-admin-password' => 'Password:',
+ 'config-admin-password-confirm' => "Erġa' daħħal il-password:",
+ 'config-admin-help' => 'Daħħal l-isem tal-utent preferit hawnhekk, per eżempju "Joe Borg".
+Dan huwa l-isem li se tuża\' kull darba li tidħol fil-wiki.',
+ 'config-admin-name-blank' => 'Daħħal isem tal-utent għall-amministratur.',
+ 'config-admin-name-invalid' => 'L-isem tal-utent speċifikat "<nowiki>$1</nowiki>" huwa ħażin.
+Speċifika isem tal-utent differenti.',
+ 'config-admin-password-blank' => 'Daħħal password għall-kont tal-amministratur.',
+ 'config-admin-password-same' => 'Il-password ma tistax tkun l-istess bħall-isem tal-utent.',
+ 'config-admin-password-mismatch' => 'Il-passwords li daħħalt ma jaqblux.',
+ 'config-admin-email' => 'Indirizz elettroniku:',
+ 'config-admin-error-bademail' => 'Daħħalt indirizz elettroniku ħażin.',
+ 'config-almost-done' => "Kważi lest!
+Jekk trid tista' taqbeż il-parti li jmiss tal-konfigurazzjoni u sempliċiment tinstalla l-wiki.",
+ 'config-optional-continue' => 'Staqsini aktar mistoqsijiet.',
+ 'config-optional-skip' => 'Xbajt diġà, installa l-wiki.',
+ 'config-profile-wiki' => 'Wiki tradizzjonali', # Fuzzy
+ 'config-profile-no-anon' => 'Huwa obbligatorju l-ħolqien tal-kont',
+ 'config-profile-fishbowl' => 'Edituri awtorizzati biss',
+ 'config-profile-private' => 'Wiki privata',
+ 'config-license-cc-by-sa' => 'Creative Commons Attribution Share Alike',
+ 'config-license-cc-by' => 'Creative Commons Attribution',
+ 'config-license-cc-by-nc-sa' => 'Creative Commons Attribution Non-Commercial Share Alike',
+ 'config-license-cc-0' => 'Creative Commons Zero (dominju pubbliku)',
+ 'config-license-pd' => 'Dominju pubbliku',
+ 'config-license-cc-choose' => 'Agħżel waħda mil-liċenzji tal-Creative Commons',
+ 'config-upload-deleted' => 'Direttorju għall-fajls imħassra:',
+ 'config-upload-deleted-help' => "Agħżel direttorju fejn iżżomm fajls imħassra.
+Idealment, dan m'għandux ikun aċċessibbli mill-web.",
+ 'config-logo' => 'URL tal-logo:',
+ 'config-download-localsettings' => 'Niżżel <code>LocalSettings.php</code>',
+ 'config-help' => 'għajnuna',
+ 'config-nofile' => 'Il-fajl "$1" ma setax jinstab. Dan ġie mħassar?',
'mainpagetext' => "'''MediaWiki ġie installat b'suċċess.'''",
'mainpagedocfooter' => "Ikkonsulta l-[//meta.wikimedia.org/wiki/Help:Contents Gwida għall-utenti] sabiex tikseb iktar informazzjoni dwar kif tuża' s-softwer tal-wiki.
== Biex tibda ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista ta' preferenzi għall-konfigurazzjoni]
* [//www.mediawiki.org/wiki/Manual:FAQ Mistoqsijiet rikorrenti fuq il-MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Il-lista tal-posta tħabbar 'l MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Il-lista tal-posta tħabbar 'l MediaWiki]", # Fuzzy
);
/** Burmese (မြန်မာဘာသာ)
@@ -12859,12 +13819,13 @@ $messages['nan'] = array(
== 入門 ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings 配置的設定]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki時常問答]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki的公布列單]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki的公布列單]', # Fuzzy
);
-/** Norwegian Bokmål (norsk (bokmål)‎)
+/** Norwegian Bokmål (norsk bokmål)
* @author Event
* @author Nghtwlkr
+ * @author 아라
*/
$messages['nb'] = array(
'config-desc' => 'Installasjonsprogrammet for MediaWiki',
@@ -12872,19 +13833,19 @@ $messages['nb'] = array(
'config-information' => 'Informasjon',
'config-localsettings-upgrade' => 'En <code>LocalSettings.php</code>-fil har blitt oppdaget.
For å oppgradere denne installasjonen, skriv inn verdien av <code>$wgUpgradeKey</code> i boksen nedenfor.
-Du finner denne i LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => "Filen ''LocalSettings.php'' er funnet.
+Du finner denne i <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => "Filen ''<code>LocalSettings.php</code>'' er funnet.
For å oppgradere denne installasjonen, vennligst kjør ''update.php'' i stedet",
'config-localsettings-key' => 'Oppgraderingsnøkkel:',
'config-localsettings-badkey' => 'Nøkkelen du oppga er feil.',
'config-upgrade-key-missing' => "En eksisterende installasjon av MediaWiki er funnet.
-For å oppgradere denne installasjonen, vær vennlig å legge til følgende linje helt til slutt i din ''LocalSettings.php''-fil:
+For å oppgradere denne installasjonen, vær vennlig å legge til følgende linje helt til slutt i din ''<code>LocalSettings.php</code>''-fil:
$1",
- 'config-localsettings-incomplete' => "Den eksisterende ''LocalSettings.php'' ser ut til å være ufullstendig.
+ 'config-localsettings-incomplete' => "Den eksisterende ''<code>LocalSettings.php</code>'' ser ut til å være ufullstendig.
Variabelen $1 har ingen verdi.
-Vær vennlig å endre ''LocalSettings.php'' slik at variabelen får en verdi, og klikk ''Fortsett''.",
- 'config-localsettings-connection-error' => "Det ble funnet en feil ved tilknytning av databasen med innstillingene i ''LocalSettings.php'' eller ''AdminSettings.php''. Vær vennlig å rette opp disse innstillingene og prøv igjen.
+Vær vennlig å endre ''<code>LocalSettings.php</code>'' slik at variabelen får en verdi, og klikk ''{{int:Config-continue}}''.",
+ 'config-localsettings-connection-error' => "Det ble funnet en feil ved tilknytning av databasen med innstillingene i ''<code>LocalSettings.php</code>'' eller ''<code>AdminSettings.php</code>''. Vær vennlig å rette opp disse innstillingene og prøv igjen.
$1",
'config-session-error' => 'Feil under oppstart av økt: $1',
@@ -13017,7 +13978,7 @@ Installasjon abortert.',
'config-using531' => 'MediaWiki kan ikke brukes med PHP $1 på grunn av en feil med referanseparametere til <code>__call()</code>.
Oppgrader til PHP 5.3.2 eller høyere, eller nedgrader til PHP 5.3.0 for å løse dette.
Installasjonen avbrutt.',
- 'config-suhosin-max-value-length' => 'Suhosin er installert og begrenser GET-parameterlengder til $1 bytes. MediaWiki\'s ResourceLoader-komponent klarer å komme rundt denne begrensningen, med med redusert ytelse. På mulig bør du sette suhosin.get.max_value_length til minst 1024 i php.ini, og sette $wgResourceLoaderMaxQueryLength til samme verdi i LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Suhosin er installert og begrenser GET-parameterlengder til $1 bytes. MediaWiki\'s ResourceLoader-komponent klarer å komme rundt denne begrensningen, med med redusert ytelse. På mulig bør du sette <code>suhosin.get.max_value_length</code> til minst 1024 i <code>php.ini</code>, og sette <code>$wgResourceLoaderMaxQueryLength</code> til samme verdi i LocalSettings.php.', # Fuzzy
'config-db-type' => 'Databasetype:',
'config-db-host' => 'Databasevert:',
'config-db-host-help' => 'Hvis databasen kjører på en annen tjenermaskin, skriv inn vertsnavnet eller IP-adressen her.
@@ -13092,7 +14053,6 @@ Vurder å plassere databasen et helt annet sted, for eksempel i <code>/var/lib/m
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki støtter følgende databasesystem:
$1
@@ -13102,12 +14062,10 @@ Hvis du ikke ser databasesystemet du prøver å bruke i listen nedenfor, følg i
'config-support-postgres' => '* $1 er et populært åpen kildekode-databasesystem som er et alternativ til MySQL ([http://www.php.net/manual/en/pgsql.installation.php hvordan kompilere PHP med PostgreSQL-støtte]). Det kan være noen små utestående feil og det anbefales ikke for bruk i et produksjonsmiljø.',
'config-support-sqlite' => '* $1 er et lettvekts-databasesystem som er veldig godt støttet. ([http://www.php.net/manual/en/pdo.installation.php hvordan kompilere PHP med SQLite-støtte], bruker PDO)',
'config-support-oracle' => '* $1 er en kommersiell bedriftsdatabase. ([http://www.php.net/manual/en/oci8.installation.php Hvordan kompilere PHP med OCI8-støtte])',
- 'config-support-ibm_db2' => '* $1 er en kommersiell bedriftsdatabase.',
'config-header-mysql' => 'MySQL-innstillinger',
'config-header-postgres' => 'PostgreSQL-innstillinger',
'config-header-sqlite' => 'SQLite-innstillinger',
'config-header-oracle' => 'Oracle-innstillinger',
- 'config-header-ibm_db2' => 'IBM DB2-innstillinger',
'config-invalid-db-type' => 'Ugyldig databasetype',
'config-missing-db-name' => 'Du må skrive inn en verdi for «Databasenavn»',
'config-missing-db-host' => 'Du må skrive inn en verdi for «Databasevert»',
@@ -13170,7 +14128,7 @@ Dette er '''ikke anbefalt''' med mindre du har problemer med wikien din.",
Du kan nå [$1 begynne å bruke wikien din].',
'config-regenerate' => 'Regenerer LocalSettings.php →',
- 'config-show-table-status' => 'SHOW TABLE STATUS etterspørselen mislyktes!',
+ 'config-show-table-status' => '<code>SHOW TABLE STATUS</code> etterspørselen mislyktes!',
'config-unknown-collation' => "'''Advarsel:''' Databasen bruker en ukjent sortering.",
'config-db-web-account' => 'Databasekonto for nettilgang',
'config-db-web-help' => 'Velg brukernavnet og passordet som nettjeneren skal bruke for å koble til databasetjeneren under ordinær drift av wikien.',
@@ -13193,7 +14151,6 @@ Dette er mer effektivt enn MySQLs UTF-8 modus og tillater deg å bruke hele spek
I '''UTF-8 mode''' vil MySQL vite hvilket tegnsett dataene dine er i og kan presentere og konvertere det på en riktig måte,
men det vil ikke la deg lagre tegn over «[//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes the Basic Multilingual Plane]».",
- 'config-ibm_db2-low-db-pagesize' => "DB2-databasen din har et standard tabellområde med en utilstrekkelig pagesize. Pagesize må være '''32K''' eller større.",
'config-site-name' => 'Navn på wiki:',
'config-site-name-help' => 'Dette vil vises i tittellinjen i nettleseren og diverse andre steder.',
'config-site-name-blank' => 'Skriv inn et nettstedsnavn.',
@@ -13234,7 +14191,7 @@ Du kan hoppe over de resterende konfigurasjonene og installere wikien nå.',
'config-optional-continue' => 'Spør meg flere spørsmål.',
'config-optional-skip' => 'Jeg er lei, bare installer wikien.',
'config-profile' => 'Brukerrettighetsprofil:',
- 'config-profile-wiki' => 'Tradisjonell wiki',
+ 'config-profile-wiki' => 'Tradisjonell wiki', # Fuzzy
'config-profile-no-anon' => 'Kontoopprettelse påkrevd',
'config-profile-fishbowl' => 'Kun autoriserte bidragsytere',
'config-profile-private' => 'Privat wiki',
@@ -13250,7 +14207,7 @@ En wiki med '''{{int:config-profile-no-anon}}''' tilbyr ekstra ansvarlighet, men
'''{{int:config-profile-fishbowl}}'''-scenariet tillater godkjente brukere å redigere, mens publikum kan se sider, og også historikken.
En '''{{int:config-profile-private}}''' tillater kun godkjente brukere å se sider, den samme gruppen som får lov til å redigere dem.
-Mer komplekse konfigurasjoner av brukerrettigheter er tilgjengelig etter installasjon, se det [//www.mediawiki.org/wiki/Manual:User_rights relevante manualavsnittet].",
+Mer komplekse konfigurasjoner av brukerrettigheter er tilgjengelig etter installasjon, se det [//www.mediawiki.org/wiki/Manual:User_rights relevante manualavsnittet].", # Fuzzy
'config-license' => 'Opphavsrett og lisens:',
'config-license-none' => 'Ingen lisensbunntekst',
'config-license-cc-by-sa' => 'Creative Commons Navngivelse Del på samme vilkår',
@@ -13311,7 +14268,7 @@ For mer informasjon om denne funksjonen, inklusive instruksjoner om hvordan man
'config-install-user-grant-failed' => 'Å gi tillatelse til brukeren «$1» mislyktes: $2',
'config-install-tables' => 'Oppretter tabeller',
'config-install-mainpage-failed' => 'Kunne ikke sette inn hovedside: $1',
- 'config-download-localsettings' => 'Last ned LocalSettings.php',
+ 'config-download-localsettings' => 'Last ned <code>LocalSettings.php</code>',
'config-help' => 'hjelp',
'config-nofile' => 'Filen "$1" ble ikke funnet. Kan den være blitt slettet?',
'mainpagetext' => "'''MediaWiki-programvaren er nå installert.'''",
@@ -13320,18 +14277,24 @@ For mer informasjon om denne funksjonen, inklusive instruksjoner om hvordan man
==Å starte==
*[//www.mediawiki.org/wiki/Manual:Configuration_settings Oppsettsliste]
*[//www.mediawiki.org/wiki/Manual:FAQ Ofte stilte spørsmål]
-*[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-postliste]',
+*[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-postliste]', # Fuzzy
);
/** Low German (Plattdüütsch)
+ * @author Joachim Mos
*/
$messages['nds'] = array(
+ 'config-page-name' => 'Naam',
+ 'config-ns-generic' => 'Projekt',
+ 'config-admin-name' => 'Dien Naam:',
+ 'config-admin-password' => 'Passwoord:',
+ 'config-help' => 'Hülp',
'mainpagetext' => "'''De MediaWiki-Software is mit Spood installeert worrn.'''",
'mainpagedocfooter' => 'Kiek de [//meta.wikimedia.org/wiki/MediaWiki_localisation Dokumentatschoon för dat Anpassen vun de Brukerböversiet]
-un dat [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Brukerhandbook] för Hülp to de Bruuk un Konfiguratschoon.',
+un dat [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Brukerhandbook] för Hülp to de Bruuk un Konfiguratschoon.', # Fuzzy
);
-/** Nedersaksisch (Nedersaksisch)
+/** Low Saxon (Netherlands) (Nedersaksies)
* @author Servien
*/
$messages['nds-nl'] = array(
@@ -13341,7 +14304,8 @@ $messages['nds-nl'] = array(
== Meer hulpe ==
* [//www.mediawiki.org/wiki/Help:Configuration_settings Lieste mit instellingen]
* [//www.mediawiki.org/wiki/Help:FAQ MediaWiki-vragen die vake esteld wörden]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-postlieste veur nieje versies]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-postlieste veur nieje versies]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Maak MediaWiki beschikbaor in joew taal]',
);
/** Nepali (नेपाली)
@@ -13355,7 +14319,7 @@ $messages['ne'] = array(
== सुरू गर्नको लागि ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings विन्यास सेटिङ्ग सूची]
* [//www.mediawiki.org/wiki/Manual:FAQ मेडियाविकि सामान्य प्रश्नका उत्तरहरु]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मेडियाविकि सुचना मेलिङ्ग सूची]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मेडियाविकि सुचना मेलिङ्ग सूची]', # Fuzzy
);
/** Dutch (Nederlands)
@@ -13365,6 +14329,7 @@ $messages['ne'] = array(
* @author SPQRobin
* @author Siebrand
* @author Tjcool007
+ * @author 아라
*/
$messages['nl'] = array(
'config-desc' => 'Het installatieprogramma voor MediaWiki',
@@ -13372,19 +14337,19 @@ $messages['nl'] = array(
'config-information' => 'Gegevens',
'config-localsettings-upgrade' => 'Er is een bestaand instellingenbestand <code>LocalSettings.php</code> gevonden.
Voer de waarde van <code>$wgUpgradeKey</code> in in onderstaande invoerveld om deze installatie bij te werken.
-De instelling is terug te vinden in LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Het bestand LocalSettings.php is al aanwezig.
-Voer update.php uit om deze installatie bij te werken.',
+De instelling is terug te vinden in <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Het bestand <code>LocalSettings.php</code> is al aanwezig.
+Voer <code>update.php</code> uit om deze installatie bij te werken.',
'config-localsettings-key' => 'Upgradesleutel:',
'config-localsettings-badkey' => 'De sleutel die u hebt opgegeven is onjuist',
'config-upgrade-key-missing' => 'Er is een bestaande installatie van MediaWiki aangetroffen.
-Plaats de volgende regel onderaan uw LocalSettings.php om deze installatie bij te werken:
+Plaats de volgende regel onderaan uw <code>LocalSettings.php</code> om deze installatie bij te werken:
$1',
- 'config-localsettings-incomplete' => 'De bestaande inhoud van LocalSettings.php lijkt incompleet.
+ 'config-localsettings-incomplete' => 'De bestaande inhoud van <code>LocalSettings.php</code> lijkt incompleet.
De variabele $1 is niet ingesteld.
-Wijzig LocalSettings.php zodat deze variabele is ingesteld en klik op "Doorgaan".',
- 'config-localsettings-connection-error' => 'Er is een fout opgetreden tijdens het verbinden van de database met de instellingen uit LocalSettings.php of AdminSettings.php. Los het probleem met de instellingen op en probeer het daarna opnieuw.
+Wijzig <code>LocalSettings.php</code> zodat deze variabele is ingesteld en klik op "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Er is een fout opgetreden tijdens het verbinden van de database met de instellingen uit <code>LocalSettings.php</code> of <code>AdminSettings.php</code>. Los het probleem met de instellingen op en probeer het daarna opnieuw.
$1',
'config-session-error' => 'Fout bij het begin van de sessie: $1',
@@ -13448,9 +14413,9 @@ U kunt MediaWiki niet installeren.',
MediaWiki heeft PHP $2 of hoger nodig om correct te kunnen werken.',
'config-unicode-using-utf8' => 'Voor Unicode-normalisatie wordt utf8_normalize.so van Brion Vibber gebruikt.',
'config-unicode-using-intl' => 'Voor Unicode-normalisatie wordt de [http://pecl.php.net/intl PECL-extensie intl] gebruikt.',
- 'config-unicode-pure-php-warning' => "'''Waarschuwing''': De [http://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicode-normalisatie af te handelen en daarom wordt de langzame PHP-implementatie gebruikt.
-Als u MediaWiki voor een website met veel verkeer installeert, lees u dan in over [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode-normalisatie].",
- 'config-unicode-update-warning' => "'''Waarschuwing''': De geïnstalleerde versie van de Unicode-normalisatiewrapper maakt gebruik van een oudere versie van [http://site.icu-project.org/ de bibliotheek van het ICU-project].
+ 'config-unicode-pure-php-warning' => "'''Waarschuwing''': de [http://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzame PHP-implementatie gebruikt.
+Als u MediaWiki voor een website met veel verkeer installeert, lees u dan in over [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicodenormalisatie].",
+ 'config-unicode-update-warning' => "'''Waarschuwing''': de geïnstalleerde versie van de Unicodenormalisatiewrapper maakt gebruik van een oudere versie van [http://site.icu-project.org/ de bibliotheek van het ICU-project].
U moet [//www.mediawiki.org/wiki/Unicode_normalization_considerations bijwerken] als Unicode voor u van belang is.",
'config-no-db' => 'Het was niet mogelijk een geschikte databasedriver te vinden voor PHP.
U moet een databasedriver installeren voor PHP.
@@ -13461,7 +14426,7 @@ Als u PHP zelf hebt gecompileerd, wijzig dan uw instellingen zodat een databased
Als u PHP hebt geïnstalleerd via een Debian- of Ubuntu-package, installeer dan ook de module php5-mysql.',
'config-outdated-sqlite' => "''' Waarschuwing:''' u gebruikt SQLite $1. SQLite is niet beschikbaar omdat de minimaal vereiste versie $2 is.",
'config-no-fts3' => "'''Waarschuwing''': SQLite is gecompileerd zonder de module [//sqlite.org/fts3.html FTS3]; er zijn geen zoekfuncties niet beschikbaar.",
- 'config-register-globals' => "'''Waarschuwing: De PHP-optie <code>[http://php.net/register_globals register_globals]</code> is ingeschakeld.'''
+ 'config-register-globals' => "'''Waarschuwing: de PHP-optie <code>[http://php.net/register_globals register_globals]</code> is ingeschakeld.'''
'''Schakel deze uit als dat mogelijk is.'''
MediaWiki kan ermee werken, maar uw server is dan meer kwetsbaar voor beveiligingslekken.",
'config-magic-quotes-runtime' => "'''Onherstelbare fout: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] is actief!'''
@@ -13494,11 +14459,13 @@ De installatie kan mislukken!",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] is op dit moment geïnstalleerd',
'config-apc' => '[http://www.php.net/apc APC] is op dit moment geïnstalleerd',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] is op dit moment geïnstalleerd',
- 'config-no-cache' => "'''Waarschuwing:''' [http://www.php.net/apc APC] of [http://trac.lighttpd.net/ xcache / XCache] is niet aangetroffen.
+ 'config-no-cache' => "'''Waarschuwing:''' [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] of [http://www.iis.net/download/WinCacheForPhp WinCache] is niet aangetroffen.
Het cachen van objecten is niet ingeschakeld.",
'config-mod-security' => "'''Waarschuwing:''' uw webserver heeft de module [http://modsecurity.org/ mod_security] ingeschakeld. Als deze onjuist is ingesteld, kan dit problemen geven in combinatie met MediaWiki of andere software die gebruikers in staat stelt willekeurige inhoud te posten.
Lees de [http://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.",
'config-diff3-bad' => 'GNU diff3 niet aangetroffen.',
+ 'config-git' => 'Versiecontrolesoftware git is aangetroffen: <code>$1</code>',
+ 'config-git-bad' => 'Geen git versiecontrolesoftware aangetroffen.',
'config-imagemagick' => 'ImageMagick aangetroffen: <code>$1</code>.
Het aanmaken van miniaturen van afbeeldingen wordt ingeschakeld als u uploaden inschakelt.',
'config-gd' => 'Ingebouwde GD grafische bibliotheek aangetroffen.
@@ -13508,17 +14475,20 @@ Het maken van miniaturen van afbeeldingen wordt uitgeschakeld.',
'config-no-uri' => "'''Fout:''' de huidige URI kon niet vastgesteld worden.
De installatie is afgebroken.",
'config-no-cli-uri' => "'''Waarschuwing:''' de parameter ==scriptpath is niet opgegeven. De standaardwaarde wordt gebruikt: <code>$1</code>.",
- 'config-using-server' => 'Servernaam "<nowiki>$1</nowiki>" wordt gebruikt.',
+ 'config-using-server' => 'De servernaam "<nowiki>$1</nowiki>" wordt gebruikt.',
'config-using-uri' => 'De server-URL "<nowiki>$1$2</nowiki>" wordt gebruikt.',
'config-uploads-not-safe' => "'''Waarschuwing:''' uw uploadmap <code>$1</code> kan gebruikt worden voor het arbitrair uitvoeren van scripts.
-Hoewel MediaWiki alle toegevoegde bestanden controleert op bedreigingen, is het zeer aan te bevelen het [//www.mediawiki.org/wiki/Manual:Security#Upload_security beveiligingslek te verhelpen] alvorens uploads in te schakelen.",
+Hoewel MediaWiki alle toegevoegde bestanden controleert op bedreigingen, is het zeer aan te bevelen het [//www.mediawiki.org/wiki/Manual:Security#Upload_security beveiligingslek te verhelpen] alvorens uploads in te schakelen.",
'config-no-cli-uploads-check' => "''Waarschuwing:'' uw standaardmap voor uploads (<code>$1</code>) wordt niet gecontroleerd op kwetsbaarheden voor het uitvoeren van willekeurige scripts gedurende de CLI-installatie.",
'config-brokenlibxml' => 'Uw systeem heeft een combinatie van PHP- en libxml2-versies geïnstalleerd die is foutgevoelig is en kan leiden tot onzichtbare beschadiging van gegevens in MediaWiki en andere webapplicaties.
-Upgrade naar PHP 5.2.9 of hoger en libxml2 2.7.3 of hoger! De installatie wordt afgebroken ([//bugs.php.net/bug.php?id=45996 bij PHP gerapporteerde fout]).',
+Upgrade naar PHP 5.2.9 of hoger en libxml2 2.7.3 of hoger([//bugs.php.net/bug.php?id=45996 bij PHP gerapporteerde fout]).
+De installatie wordt afgebroken.',
'config-using531' => 'PHP $1 is niet compatibel met MediaWiki vanwege een fout met betrekking tot referentieparameters met <code>__call()</code>.
Werk uw PHP bij naar PHP 5.3.2 of hoger of werk bij naar de lagere versie PHP 5.3.0 om dit op te lossen.
De installatie wordt afgebroken.',
- 'config-suhosin-max-value-length' => 'Suhosin is geïnstalleerd en beperkt de lengte van de GET-parameter tot $1 bytes. De ResourceLoader van MediaWiki omzeilt deze beperking, maar dat is slecht voor de prestaties. Als het mogelijk is, moet u de waarde "suhosin.get.max_value_length" in php.ini instellen op 1024 of hoger en $wgResourceLoaderMaxQueryLength in LocalSettings.php op dezelfde waarde instellen.',
+ 'config-suhosin-max-value-length' => 'Suhosin is geïnstalleerd en beperkt de GET-parameter <code>length</code> tot $1 bytes.
+De ResourceLoader van MediaWiki omzeilt deze beperking, maar dat is slecht voor de prestaties.
+Als het mogelijk is, moet u de waarde "<code>suhosin.get.max_value_length</code>" in <code>php.ini</code> instellen op 1024 of hoger en <code>$wgResourceLoaderMaxQueryLength</code> in LocalSettings.php op dezelfde waarde instellen.',
'config-db-type' => 'Databasetype:',
'config-db-host' => 'Databasehost:',
'config-db-host-help' => 'Als uw databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.
@@ -13552,7 +14522,7 @@ Hoewel het wellicht mogelijk is gebruikers aan te maken zonder wachtwoord, is di
'config-db-install-help' => 'Voer de gebruikersnaam en het wachtwoord in die worden gebruikt voor de databaseverbinding tijdens het installatieproces.',
'config-db-account-lock' => 'Dezelfde gebruiker en wachwoord gebruiken na de installatie',
'config-db-wiki-account' => 'Gebruiker voor na de installatie',
- 'config-db-wiki-help' => 'Selecteer de gebruikersnaam en het wachtwoord die gebruikt worden om verbinding te maken met de database na de installatie.
+ 'config-db-wiki-help' => 'Voer de gebruikersnaam en het wachtwoord in die gebruikt worden om verbinding te maken met de database na de installatie.
Als de gebruiker niet bestaat en de gebruiker die tijdens de installatie gebruikt wordt voldoende rechten heeft, wordt deze gebruiker aangemaakt met de minimaal benodigde rechten voor het laten werken van de wiki.',
'config-db-prefix' => 'Databasetabelvoorvoegsel:',
'config-db-prefix-help' => "Als u een database moet gebruiken voor meerdere wiki's, of voor MediaWiki en een andere applicatie, dan kunt u ervoor kiezen om een voorvoegsel toe te voegen aan de tabelnamen om conflicten te voorkomen.
@@ -13579,12 +14549,12 @@ Wijzig het alleen als u weet dat dit nodig is.',
'config-sqlite-dir' => 'Gegevensmap voor SQLite:',
'config-sqlite-dir-help' => "SQLite slaat alle gegevens op in een enkel bestand.
-De map die u opgeeft moet schrijfbaar zijn voor de webserver tijdens de installatie.
+De map die u opgeeft moet beschrijfbaar zijn voor de webserver tijdens de installatie.
Deze mag '''niet toegankelijk''' zijn via het web en het bestand mag dus niet tussen de PHP-bestanden staan.
Het installatieprogramma schrijft het bestand <code>.htaccess</code> weg met het databasebestand, maar als dat niet werkt kan iemand zich toegang tot het ruwe databasebestand verschaffen.
-Ook de gebruikersgegevens (e-mailsadressen, wachtwoordhashes) en verwijderde versies en overige gegevens met beperkte toegang via MediaWiki zijn dan onbeschermd.
+Ook de gebruikersgegevens (e-mailadressen, wachtwoordhashes) en verwijderde versies en overige gegevens met beperkte toegang via MediaWiki zijn dan onbeschermd.
Overweeg om de database op een totaal andere plaats neer te zetten, bijvoorbeeld in <code>/var/lib/mediawiki/yourwiki</code>.",
'config-oracle-def-ts' => 'Standaard tablespace:',
@@ -13593,7 +14563,6 @@ Overweeg om de database op een totaal andere plaats neer te zetten, bijvoorbeeld
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki ondersteunt de volgende databasesystemen:
$1
@@ -13603,25 +14572,23 @@ Als u het databasesysteem dat u wilt gebruiken niet in de lijst terugvindt, volg
'config-support-postgres' => '* $1 is een populair open source databasesysteem als alternatief voor MySQL ([http://www.php.net/manual/en/pgsql.installation.php hoe PHP gecompileerd moet zijn met ondersteuning voor PostgreSQL]). Het is mogelijk dat er een aantal bekende problemen zijn met MediaWiki in combinatie met deze database en daarom wordt PostgreSQL niet aanbevolen voor een productieomgeving.',
'config-support-sqlite' => '* $1 is een zeer goed ondersteund lichtgewicht databasesysteem ([http://www.php.net/manual/en/pdo.installation.php hoe PHP gecompileerd zijn met ondersteuning voor SQLite]; gebruikt PDO)',
'config-support-oracle' => '* $1 is een commerciële data voor grote bedrijven ([http://www.php.net/manual/en/oci8.installation.php PHP compileren met ondersteuning voor OCI8]).',
- 'config-support-ibm_db2' => '* $1 is een commerciële enterprisedatabase.',
'config-header-mysql' => 'MySQL-instellingen',
'config-header-postgres' => 'PostgreSQL-instellingen',
'config-header-sqlite' => 'SQLite-instellingen',
'config-header-oracle' => 'Oracle-instellingen',
- 'config-header-ibm_db2' => 'Instellingen voor IBM DB2',
'config-invalid-db-type' => 'Ongeldig databasetype',
- 'config-missing-db-name' => 'U moet een waarde ingeven voor "Databasenaam"',
+ 'config-missing-db-name' => 'U moet een waarde opgeven voor "Databasenaam"',
'config-missing-db-host' => 'U moet een waarde invoeren voor "Databaseserver"',
- 'config-missing-db-server-oracle' => 'U moet een waarde voor "Database-TNS" ingeven',
- 'config-invalid-db-server-oracle' => 'Ongeldige database-TMS "$1".
-Gebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_).',
+ 'config-missing-db-server-oracle' => 'U moet een waarde opgeven voor "Database-TNS"',
+ 'config-invalid-db-server-oracle' => 'Ongeldige database-TNS "$1".
+Gebruik "TNS Names" of een "Easy Connect" tekst ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle naamgevingsmethoden])',
'config-invalid-db-name' => 'Ongeldige databasenaam "$1".
Gebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).',
'config-invalid-db-prefix' => 'Ongeldig databasevoorvoegsel "$1".
Gebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).',
'config-connection-error' => '$1.
-Controleer de host, gebruikersnaam en wachtwoord hieronder in en probeer het opnieuw.',
+Controleer de host, gebruikersnaam en wachtwoord en probeer het opnieuw.',
'config-invalid-schema' => 'Ongeldig schema voor MediaWiki "$1".
Gebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_).',
'config-db-sys-create-oracle' => 'Het installatieprogramma biedt alleen de mogelijkheid een nieuwe gebruiker aan te maken met de SYSDBA-gebruiker.',
@@ -13630,7 +14597,7 @@ Gebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_).',
U gebruikt $2.',
'config-sqlite-name-help' => 'Kies een naam die uw wiki identificeert.
Gebruik geen spaties of koppeltekens.
-Deze naam wordt gebruikt voor het gegevensbestands van SQLite.',
+Deze naam wordt gebruikt voor het gegevensbestand van SQLite.',
'config-sqlite-parent-unwritable-group' => 'Het was niet mogelijk de gegevensmap <code><nowiki>$1</nowiki></code> te maken omdat in de bovenliggende map <code><nowiki>$2</nowiki></code> niet geschreven mag worden door de webserver.
Het installatieprogramma heeft vast kunnen stellen onder welke gebruiker de webserver draait.
@@ -13650,14 +14617,14 @@ Voer op een Linux-systeem de volgende opdrachten uit:
<pre>cd $2
mkdir $3
chmod a+w $3</pre>',
- 'config-sqlite-mkdir-error' => 'Er is een fout opgetreden bij het aanmaken van de gegevensmap "$1".
+ 'config-sqlite-mkdir-error' => 'Er is een fout opgetreden tijdens het aanmaken van de gegevensmap "$1".
Controleer de locatie en probeer het opnieuw.',
'config-sqlite-dir-unwritable' => 'Het was niet mogelijk in de map "$1" te schrijven.
Wijzig de rechten zodat de webserver erin kan schrijven en probeer het opnieuw.',
'config-sqlite-connection-error' => '$1.
Controleer de map voor gegevens en de databasenaam hieronder en probeer het opnieuw.',
- 'config-sqlite-readonly' => 'Het bestand <code>$1</code> kan niet geschreven worden.',
+ 'config-sqlite-readonly' => 'Er kan niet naar bestand <code>$1</code> worden geschreven.',
'config-sqlite-cant-create-db' => 'Het was niet mogelijk het databasebestand <code>$1</code> aan te maken.',
'config-sqlite-fts3-downgrade' => 'PHP heeft geen ondersteuning voor FTS3.
De tabellen worden gedowngrade.',
@@ -13671,9 +14638,9 @@ Als u uw <code>LocalSettings.php</code> opnieuw wilt aanmaken, klik dan op de kn
Dit is '''niet aan te raden''' tenzij u problemen hebt met uw wiki.",
'config-upgrade-done-no-regenerate' => 'Het bijwerken is afgerond.
-U kunt u [$1 uw wiki gebruiken].',
+U kunt nu [$1 uw wiki gebruiken].',
'config-regenerate' => 'LocalSettings.php opnieuw aanmaken →',
- 'config-show-table-status' => 'Het uitvoeren van SHOW TABLE STATUS is mislukt!',
+ 'config-show-table-status' => 'Het uitvoeren van <code>SHOW TABLE STATUS</code> is mislukt!',
'config-unknown-collation' => "'''Waarschuwing:''' de database gebruikt een collatie die niet wordt herkend.",
'config-db-web-account' => 'Databasegebruiker voor webtoegang',
'config-db-web-help' => 'Selecteer de gebruikersnaam en het wachtwoord die de webserver gebruikt om verbinding te maken met de databaseserver na de installatie.',
@@ -13684,13 +14651,19 @@ De gebruiker die u hier opgeeft moet al bestaan.',
'config-mysql-engine' => 'Opslagmethode:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
- 'config-mysql-myisam-dep' => "'''Waarschuwing''': U hebt MyISAM geselecteerd als opslagengine voor MySQL. Dit is niet aan te raden voor MediaWiki omdat:
+ 'config-mysql-myisam-dep' => "'''Waarschuwing''': u hebt MyISAM geselecteerd als opslagengine voor MySQL. Dit is niet aan te raden voor MediaWiki omdat:
* het nauwelijks ondersteuning biedt voor gebruik door meerdere gebruikers tegelijkertijd door het locken van tabellen;
* het meer vatbaar is voor corruptie dan andere engines;
* de code van MediaWiki niet alstijd omgaat met MyISAM zoals dat zou moeten.
Als uw installatie van MySQL InnoDB ondersteunt, gebruik dat dan vooral.
Als uw installatie van MySQL geen ondersteuning heeft voor InnoDB, denk dan na over upgraden.",
+ 'config-mysql-only-myisam-dep' => "'''Waarschuwing:''' MyISAM is enige beschikbare opslagmethode voor MySQL, en deze wordt niet aangeraden voor gebruik met MediaWiki, omdat:
+* er nauwelijks ondersteuning is voor meerdere gelijktijdige transacties omdat tabellen op slot gezet worden;
+* tabellen makkelijker stuk kunnen gaan;
+* de code van MediaWiki niet altijd op de juiste wijze omgaat met MyISAM.
+
+Uw installatie van MySQL heeft geen ondersteuning voor InnoDB. We raden u aan om een meer recente versie te gebruiken.",
'config-mysql-engine-help' => "'''InnoDB''' is vrijwel altijd de beste instelling, omdat deze goed omgaat met meerdere verzoeken tegelijkertijd.
'''MyISAM''' is bij een zeer beperkt aantal gebruikers mogelijk sneller, of als de wiki alleen-lezen is.
@@ -13699,11 +14672,10 @@ MyISAM-databases raken vaker corrupt dan InnoDB-databases.",
'config-mysql-binary' => 'Binair',
'config-mysql-utf8' => 'UTF-8',
'config-mysql-charset-help' => "In '''binaire modus''' slaat MediaWiki tekst in UTF-8 op in binaire databasevelden.
-Dit is efficiënter dan de UTF-8-modus van MySQL en stelt u in staat de volledige reeks Unicode-tekens te gebruiken.
+Dit is efficiënter dan de UTF-8-modus van MySQL en stelt u in staat de volledige reeks Unicodetekens te gebruiken.
In '''UTF-8-modus''' kent MySQL de tekenset van uw gegevens en kan de databaseserver ze juist weergeven en converteren.
Het is dat niet mogelijk tekens op te slaan die de \"[//nl.wikipedia.org/wiki/Lijst_van_Unicode-subbereiken#Basic_Multilingual_Plane Basic Multilingual Plane]\" te boven gaan.",
- 'config-ibm_db2-low-db-pagesize' => "Uw DB2-database heeft een standaard tablespace met een onvoldoende grote pagesize. De pagesize moet tenminste '''32K''' zijn.",
'config-site-name' => 'Naam van de wiki:',
'config-site-name-help' => 'Deze naam verschijnt in de titelbalk van browsers en op andere plaatsen.',
'config-site-name-blank' => 'Geef een naam op voor de site.',
@@ -13713,11 +14685,11 @@ Het is dat niet mogelijk tekens op te slaan die de \"[//nl.wikipedia.org/wiki/Li
'config-ns-other' => 'Andere (geef aan welke)',
'config-ns-other-default' => 'MijnWiki',
'config-project-namespace-help' => "In het kielzog van Wikipedia beheren veel wiki's hun beleidspagina's apart van hun inhoudelijke pagina's in een \"'''projectnaamruimte'''\".
-Alle paginanamen in deze naamruimte beginnen met een bepaald voorvoegsel dat u hier kunt aangeven.
+Alle paginanamen in deze naamruimte beginnen met een bepaald voorvoegsel dat u hier kunt opgeven.
Dit voorvoegsel wordt meestal afgeleid van de naam van de wiki, maar het kan geen bijzondere tekens bevatten als \"#\" of \":\".",
- 'config-ns-invalid' => 'De aangegeven naamruimte "<nowiki>$1</nowiki>" is ongeldig.
+ 'config-ns-invalid' => 'De opgegeven naamruimte "<nowiki>$1</nowiki>" is ongeldig.
Geef een andere naamruimte op.',
- 'config-ns-conflict' => 'De aangegeven naamruimte "<nowiki>$1</nowiki>" conflicteert met een standaard naamruimte in MediaWiki.
+ 'config-ns-conflict' => 'De opgegeven naamruimte "<nowiki>$1</nowiki>" conflicteert met een standaard naamruimte in MediaWiki.
Geef een andere naam op voor de projectnaamruimte.',
'config-admin-box' => 'Beheerdersgebruiker',
'config-admin-name' => 'Uw naam:',
@@ -13740,13 +14712,13 @@ Kies een andere gebruikersnaam.',
'config-subscribe-help' => 'Dit is een mailinglijst met een laag volume voor aankondigingen van nieuwe versies, inclusief belangrijke aankondigingen met betrekking tot beveiliging.
Abonneer uzelf erop en werk uw MediaWiki-installatie bij als er nieuwe versies uitkomen.',
'config-subscribe-noemail' => 'U hebt geprobeerd zich te abonneren op de mailinglijst voor release-aankondigingen zonder een e-mailadres op te geven.
-Geef een e-mailadres op als u zich wil abonneren op de mailinglijst.',
+Geef een e-mailadres op als u zich wilt abonneren op de mailinglijst.',
'config-almost-done' => 'U bent bijna klaar!
Als u wilt kunt u de overige instellingen overslaan en de wiki nu installeren.',
'config-optional-continue' => 'Stel me meer vragen.',
'config-optional-skip' => 'Laat dat maar, installeer gewoon de wiki.',
'config-profile' => 'Gebruikersrechtenprofiel:',
- 'config-profile-wiki' => 'Traditionele wiki',
+ 'config-profile-wiki' => 'Open wiki',
'config-profile-no-anon' => 'Gebruiker aanmaken verplicht',
'config-profile-fishbowl' => 'Alleen voor geautoriseerde bewerkers',
'config-profile-private' => 'Privéwiki',
@@ -13756,7 +14728,7 @@ In MediaWiki is het eenvoudig om de recente wijzigingen te controleren en eventu
Daarnaast vinden velen MediaWiki goed inzetbaar in vele andere rollen, en soms is het niet handig om helemaal \"op de wikimanier\" te werken.
Daarom biedt dit installatieprogramma u de volgende keuzes voor de basisinstelling van gebruikersvrijheden:
-Een '''{{int:config-profile-wiki}}''' staat iedereen toe te bewerken, zonder zelfs aan te melden.
+Het profiel '''{{int:config-profile-wiki}}''' staat iedereen toe te bewerken, zonder zelfs aan te melden.
Een wiki met '''{{int:config-profile-no-anon}}\" biedt extra verantwoordelijkheid, maar kan afschrikken toevallige gebruikers afschrikken.
Het scenario '''{{int:config-profile-fishbowl}}''' laat gebruikers waarvoor dat is ingesteld bewerkt, maar andere gebruikers kunnen alleen pagina's bekijken, inclusief de bewerkingsgeschiedenis.
@@ -13783,25 +14755,25 @@ Dit is nog steeds een geldige licentie, maar deze licentie is lastig te begrijpe
Het is ook lastig inhoud te hergebruiken onder de GFDL.",
'config-email-settings' => 'E-mailinstellingen',
'config-enable-email' => 'Uitgaande e-mail inschakelen',
- 'config-enable-email-help' => "Als u wilt dat e-mailen mogelijk is, dan moeten [http://www.php.net/manual/en/mail.configuration.php PHP's e-mailinstellingen] correct zijn.
-Als u niet wilt dat e-mailen mogelijk is, dan kunt u de instellingen hier uitschakelen.",
+ 'config-enable-email-help' => 'Als u wilt dat e-mailen mogelijk is, dan moeten de [http://www.php.net/manual/en/mail.configuration.php e-mailinstellingen van PHP] correct zijn.
+Als u niet wilt dat e-mailen mogelijk is, dan kunt u de instellingen hier uitschakelen.',
'config-email-user' => 'E-mail tussen gebruikers inschakelen',
'config-email-user-help' => 'Gebruikers toestaan e-mail aan elkaar te verzenden als dit in de voorkeuren is ingesteld.',
- 'config-email-usertalk' => 'Gebruikersoverlegnotificatie inschakelen',
- 'config-email-usertalk-help' => 'Gebruikers toestaan notificaties te ontvangen bij wijzigingen op de eigen overlegpagina als dit in de voorkeuren is ingesteld',
- 'config-email-watchlist' => 'Volglijstnotificatie inschakelen',
- 'config-email-watchlist-help' => "Gebruikers toestaan notificaties te ontvangen bij wijzigingen van pagina's op hun volglijst als dit in de voorkeuren is ingesteld",
+ 'config-email-usertalk' => 'Gebruikersoverlegmeldingen inschakelen',
+ 'config-email-usertalk-help' => 'Gebruikers toestaan meldingen te ontvangen bij wijzigingen op de eigen overlegpagina, als dit in de voorkeuren is ingesteld.',
+ 'config-email-watchlist' => 'Volglijstmeldingen inschakelen',
+ 'config-email-watchlist-help' => "Gebruikers toestaan meldingen te ontvangen bij wijzigingen van pagina's op hun volglijst, als dit in de voorkeuren is ingesteld.",
'config-email-auth' => 'E-mailbevestiging inschakelen',
'config-email-auth-help' => "Als deze instelling actief is, moeten gebruikers hun e-mailadres bevestigen via een verwijziging die ze per e-mail wordt toegezonden.
Alleen bevestigde e-mailadressen kunnen e-mail ontvangen van andere gebruikers of wijzigingsnotificaties ontvangen.
-Het inschakelen van deze instelling is '''aan te raden''' voor openbare wiki's vanwege de mogelijkheden voor misbruik van e-mailmogelijkheden.",
+Het inschakelen van deze instelling wordt '''aangeraden''' voor openbare wiki's vanwege de mogelijkheden voor misbruik van e-mailmogelijkheden.",
'config-email-sender' => 'E-mailadres voor antwoorden:',
'config-email-sender-help' => 'Voer het e-mailadres in dat u wilt gebruiken als antwoordadres voor uitgaande e-mail.
Als een e-mail niet bezorgd kan worden, wordt dat op dit e-mailadres gemeld.
Veel mailservers vereisen dat tenminste het domein bestaat.',
'config-upload-settings' => 'Afbeeldingen en bestanden uploaden',
'config-upload-enable' => 'Uploaden van bestanden inschakelen',
- 'config-upload-help' => "Het uploaden van bestanden stelt uw server mogelijk bloot aan beveiligingsrisico's.
+ 'config-upload-help' => "Het toestaan van het uploaden van bestanden stelt uw server mogelijk bloot aan beveiligingsrisico's.
Er is meer [//www.mediawiki.org/wiki/Manual:Security informatie over beveiliging] beschikbaar in de handleiding.
Om het bestandsuploads mogelijk te maken kunt u de rechten op de submap <code>images</code> onder de hoofdmap van MediaWiki aanpassen, zodat de webserver erin kan schrijven.
@@ -13813,6 +14785,8 @@ Idealiter is deze map niet via het web te benaderen.',
'config-logo-help' => 'Het standaarduiterlijk van MediaWiki bevat ruimte voor een logo van 135x160 pixels boven het menu.
Upload een afbeelding met de juiste afmetingen en voer de URL hier in.
+U kunt <code>$wgStylePath</code> of <code>$wgScriptPath</code> gebruiken als uw logo relatief is aan een van deze paden.
+
Als u geen logo wilt gebruiken, kunt u dit veld leeg laten.',
'config-instantcommons' => 'Instant Commons inschakelen',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] is functie die het mogelijk maakt om afbeeldingen, geluidsbestanden en andere mediabestanden te gebruiken van de website [//commons.wikimedia.org/ Wikimedia Commons].
@@ -13822,13 +14796,13 @@ Meer informatie over deze functie en hoe deze in te stellen voor andere wiki\'s
'config-cc-error' => 'De licentiekiezer van Creative Commons heeft geen resultaat opgeleverd.
Voer de licentie handmatig in.',
'config-cc-again' => 'Opnieuw kiezen...',
- 'config-cc-not-chosen' => 'Kies alstublieft de Creative Commons-licentie die u wilt gebruiken en klik op "doorgaan".',
+ 'config-cc-not-chosen' => 'Kies de Creative Commonslicentie die u wilt gebruiken en klik op "doorgaan".',
'config-advanced-settings' => 'Gevorderde instellingen',
'config-cache-options' => 'Instellingen voor het cachen van objecten:',
'config-cache-help' => 'Het cachen van objecten wordt gebruikt om de snelheid van MediaWiki te verbeteren door vaak gebruikte gegevens te bewaren.
Middelgrote tot grote websites wordt geadviseerd dit in te schakelen en ook kleine sites merken de voordelen.',
'config-cache-none' => 'Niets cachen.
-Er gaat geen functionaliteit verloren, maar dit kan invloed hebben op de snelheid.',
+Er gaat geen functionaliteit verloren, maar dit kan invloed hebben op de prestaties.',
'config-cache-accel' => 'Cachen van objecten via PHP (APC, XCache of WinCache)',
'config-cache-memcached' => 'Memcached gebruiken (dit vereist aanvullende instellingen)',
'config-memcached-servers' => 'Memcachedservers:',
@@ -13847,11 +14821,11 @@ De standaardpoort is 11211.',
Mogelijk moet u aanvullende instellingen maken, maar u kunt deze uitbreidingen nu inschakelen.',
'config-install-alreadydone' => "'''Waarschuwing:''' het lijkt alsof u MediaWiki al hebt geïnstalleerd en probeert het programma opnieuw te installeren.
-Ga alstublieft door naar de volgende pagina.",
+Ga door naar de volgende pagina.",
'config-install-begin' => 'Als u nu op "{{int:config-continue}}" klikt, begint de installatie van MediaWiki.
-Als u nog wijzigingen wilt maken, klik dan op "Terug".',
- 'config-install-step-done' => 'Afgerond',
- 'config-install-step-failed' => 'Mislukt',
+Als u nog wijzigingen wilt maken, klik dan op "{{int:config-back}}".',
+ 'config-install-step-done' => 'afgerond',
+ 'config-install-step-failed' => 'mislukt',
'config-install-extensions' => 'Inclusief uitbreidingen',
'config-install-database' => 'Database inrichten',
'config-install-schema' => 'Het schema wordt aangemaakt',
@@ -13872,9 +14846,9 @@ MediaWiki vereist momenteel dat de tabellen van de webgebruiker zijn. Geef een a
'config-install-user-grant-failed' => 'Het geven van rechten aan gebruiker "$1" is mislukt: $2',
'config-install-user-missing' => 'De opgegeven gebruiker "$1" bestaat niet.',
'config-install-user-missing-create' => 'De opgegeven gebruiker "$1" bestaat niet.
-Klik op "registreren" onderaan als u het wilt aanmaken.',
+Klik op "registreren" onderaan als u de gebruiker wilt aanmaken.',
'config-install-tables' => 'Tabellen aanmaken',
- 'config-install-tables-exist' => "'''Waarschuwing''': de MediaWiki-tabellen lijken al te bestaan.
+ 'config-install-tables-exist' => "'''Waarschuwing''': de MediaWikitabellen lijken al te bestaan.
Het aanmaken wordt overgeslagen.",
'config-install-tables-failed' => "'''Fout''': het aanmaken van een tabel is mislukt met de volgende foutmelding: $1",
'config-install-interwiki' => 'Bezig met het vullen van de interwikitabel',
@@ -13899,16 +14873,18 @@ Dit bevat al uw instellingen.
U moet het bestand downloaden en in de hoofdmap van uw wiki-installatie plaatsten; in dezelfde map als index.php.
De download moet u automatisch zijn aangeboden.
-Als de download niet is aangeboden of als u de download hebt geannuleerd, dan kunt u de download opnieuw starten door op de onderstaande verwijzing te klikken:
+Als de download niet is aangeboden of als u de download hebt geannuleerd, dan kunt u de download opnieuw starten door op de onderstaande koppeling te klikken:
$3
'''Let op''': als u dit niet nu doet, dan het is bestand als u later de installatieprocedure afsluit zonder het bestand te downloaden niet meer beschikbaar.
Na het plaatsen van het bestand met instellingen kunt u '''[$2 uw wiki betreden]'''.",
- 'config-download-localsettings' => 'LocalSettings.php downloaden',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> downloaden',
'config-help' => 'hulp',
'config-nofile' => 'Het bestand "$1" is niet gevonden. Is het verwijderd?',
+ 'config-extension-link' => 'Weet u dat u [//www.mediawiki.org/wiki/Manual:Extensions uitbreidingen] kunt gebruiken voor uw wiki?
+U kunt [//www.mediawiki.org/wiki/Category:Extensions_by_category uitbreidingen op categorie] bekijken of ga naar de [//www.mediawiki.org/wiki/Extension_Matrix Uitbreidingenmatrix] om de volledige lijst met uitbreidingen te bekijken.',
'mainpagetext' => "'''De installatie van MediaWiki is geslaagd.'''",
'mainpagedocfooter' => 'Raadpleeg de [//meta.wikimedia.org/wiki/NL_Help:Inhoudsopgave handleiding] voor informatie over het gebruik van de wikisoftware.
@@ -13916,10 +14892,256 @@ Na het plaatsen van het bestand met instellingen kunt u '''[$2 uw wiki betreden]
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lijst met instellingen]
* [//www.mediawiki.org/wiki/Manual:FAQ Veelgestelde vragen (FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglijst voor aankondigingen van nieuwe versies]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglijst voor aankondigingen van nieuwe versies]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Maak MediaWiki beschikbaar in uw taal]',
);
-/** Norwegian Nynorsk (norsk (nynorsk)‎)
+/** Nederlands (informeel)‎ (Nederlands (informeel)‎)
+ * @author Siebrand
+ */
+$messages['nl-informal'] = array(
+ 'config-localsettings-badkey' => 'De sleutel die je hebt opgegeven is onjuist',
+ 'config-upgrade-key-missing' => 'Er is een bestaande installatie van MediaWiki aangetroffen.
+Plaats de volgende regel onderaan je <code>LocalSettings.php</code> om deze installatie bij te werken:
+
+$1',
+ 'config-session-expired' => 'Je sessiegegevens zijn verlopen.
+Sessies zijn ingesteld om een levensduur van $1 te hebben.
+Je kunt deze wijzigen via de instelling <code>session.gc_maxlifetime</code> in php.ini.
+Begin het installatieproces opnieuw.',
+ 'config-no-session' => 'Je sessiegegevens zijn verloren gegaan.
+Controleer je php.ini en zorg dat er een juiste map is ingesteld voor <code>session.save_path</code>.',
+ 'config-your-language' => 'Jouw taal:',
+ 'config-help-restart' => 'Wil je alle opgeslagen gegevens die je hebt ingevoerd wissen en het installatieproces opnieuw starten?',
+ 'config-welcome' => '=== Controle omgeving ===
+Er worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.
+Als je hulp nodig hebt bij de installatie, lever deze gegevens dan ook aan.',
+ 'config-copyright' => "=== Auteursrechten en voorwaarden ===
+
+$1
+
+Dit programma is vrije software. Je mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar eigen keuze - enige latere versie.
+
+Dit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.
+Zie de GNU General Public License voor meer informatie.
+
+Samen met dit programma hoor je een <doclink href=Copying>exemplaar van de GNU General Public License</doclink> ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [http://www.gnu.org/copyleft/gpl.html lees de licentie online].",
+ 'config-env-good' => 'De omgeving is gecontroleerd.
+Je kunt MediaWiki installeren.',
+ 'config-env-bad' => 'De omgeving is gecontroleerd.
+Je kunt MediaWiki niet installeren.',
+ 'config-unicode-pure-php-warning' => "'''Waarschuwing''': de [http://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzame PHP-implementatie gebruikt.
+Als je MediaWiki voor een website met veel verkeer installeert, lees je dan in over [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicodenormalisatie].",
+ 'config-unicode-update-warning' => "'''Waarschuwing''': de geïnstalleerde versie van de Unicodenormalisatiewrapper maakt gebruik van een oudere versie van [http://site.icu-project.org/ de bibliotheek van het ICU-project].
+Je moet [//www.mediawiki.org/wiki/Unicode_normalization_considerations bijwerken] als Unicode voor jou van belang is.",
+ 'config-no-db' => 'Het was niet mogelijk een geschikte databasedriver te vinden voor PHP.
+Je moet een databasedriver installeren voor PHP.
+De volgende databases worden ondersteund: $1.
+
+Als je op een gedeelde omgeving zit, vraag dan aan je hostingprovider een geschikte databasedriver te installeren.
+Als je PHP zelf hebt gecompileerd, wijzig dan je instellingen zodat een databasedriver wordt geactiveerd, bijvoorbeeld via <code>./configure --with-mysql</code>.
+Als je PHP hebt geïnstalleerd via een Debian- of Ubuntu-package, installeer dan ook de module php5-mysql.',
+ 'config-outdated-sqlite' => "''' Waarschuwing:''' je gebruikt SQLite $1. SQLite is niet beschikbaar omdat de minimaal vereiste versie $2 is.",
+ 'config-register-globals' => "'''Waarschuwing: de PHP-optie <code>[http://php.net/register_globals register_globals]</code> is ingeschakeld.'''
+'''Schakel deze uit als dat mogelijk is.'''
+MediaWiki kan ermee werken, maar je server is dan meer kwetsbaar voor beveiligingslekken.",
+ 'config-magic-quotes-runtime' => "'''Onherstelbare fout: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] is actief!'''
+Deze instelling zorgt voor gegevenscorruptie.
+Je kunt MediaWiki niet installeren tenzij deze instelling is uitgeschakeld.",
+ 'config-magic-quotes-sybase' => "'''Onherstelbare fout: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_sybase] is actief!'''
+Deze instelling zorgt voor gegevenscorruptie.
+Je kunt MediaWiki niet installeren tenzij deze instelling is uitgeschakeld.",
+ 'config-mbstring' => "'''Onherstelbare fout: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] is actief!'''
+Deze instelling zorgt voor gegevenscorruptie.
+Je kunt MediaWiki niet installeren tenzij deze instelling is uitgeschakeld.",
+ 'config-ze1' => "'''Onherstelbare fout: [http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode] is actief!'''
+Deze instelling zorgt voor grote problemen in MediaWiki.
+Je kunt MediaWiki niet installeren tenzij deze instelling is uitgeschakeld.",
+ 'config-xml-bad' => 'De XML-module van PHP ontbreekt.
+MediaWiki heeft de functies van deze module nodig en werkt niet zonder deze module.
+Als je gebruik maakt van Mandrake, installeer dan het package php-xml.',
+ 'config-mod-security' => "'''Waarschuwing:''' je webserver heeft de module [http://modsecurity.org/ mod_security] ingeschakeld. Als deze onjuist is ingesteld, kan dit problemen geven in combinatie met MediaWiki of andere software die gebruikers in staat stelt willekeurige inhoud te posten.
+Lees de [http://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van je provider als je tegen problemen aanloopt.",
+ 'config-imagemagick' => 'ImageMagick aangetroffen: <code>$1</code>.
+Het aanmaken van miniaturen van afbeeldingen wordt ingeschakeld als je uploaden inschakelt.',
+ 'config-gd' => 'Ingebouwde GD grafische bibliotheek aangetroffen.
+Het aanmaken van miniaturen van afbeeldingen wordt ingeschakeld als je uploaden inschakelt.',
+ 'config-uploads-not-safe' => "'''Waarschuwing:''' je uploadmap <code>$1</code> kan gebruikt worden voor het arbitrair uitvoeren van scripts.
+Hoewel MediaWiki alle toegevoegde bestanden controleert op bedreigingen, is het zeer aan te bevelen het [//www.mediawiki.org/wiki/Manual:Security#Upload_security beveiligingslek te verhelpen] alvorens uploads in te schakelen.",
+ 'config-no-cli-uploads-check' => "''Waarschuwing:'' je standaardmap voor uploads (<code>$1</code>) wordt niet gecontroleerd op kwetsbaarheden voor het uitvoeren van willekeurige scripts gedurende de CLI-installatie.",
+ 'config-brokenlibxml' => 'Je systeem heeft een combinatie van PHP- en libxml2-versies geïnstalleerd die is foutgevoelig is en kan leiden tot onzichtbare beschadiging van gegevens in MediaWiki en andere webapplicaties.
+Upgrade naar PHP 5.2.9 of hoger en libxml2 2.7.3 of hoger([//bugs.php.net/bug.php?id=45996 bij PHP gerapporteerde fout]).
+De installatie wordt afgebroken.',
+ 'config-db-host-help' => 'Als je databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.
+
+Als je gebruik maakt van gedeelde webhosting, hoort je provider je de juiste hostnaam te hebben verstrekt.
+
+Als je MediaWiki op een Windowsserver installeert en MySQL gebruikt, dan werkt "localhost" mogelijk niet als servernaam.
+Als het inderdaad niet werkt, probeer dan "127.0.0.1" te gebruiken als lokaal IP-adres.
+
+Als je PostgreSQL gebruikt, laat dit veld dan leeg om via een Unix-socket te verbinden.',
+ 'config-db-host-oracle-help' => 'Voer een geldige [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] in; een tnsnames.ora-bestand moet zichtbaar zijn voor deze installatie.<br />Als je gebruik maakt van clientlibraries 10g of een latere versie, kan je ook gebruik maken van de naamgevingsmethode [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].',
+ 'config-db-name-help' => 'Kies een naam die je wiki identificeert.
+Er mogen geen spaties gebruikt worden.
+Als je gebruik maakt van gedeelde webhosting, dan hoort je provider ofwel jou een te gebruiken databasenaam gegeven te hebben, of je aangegeven te hebben hoe je databases kunt aanmaken.',
+ 'config-db-account-oracle-warn' => 'Er zijn drie ondersteunde scenario\'s voor het installeren van Oracle als databasebackend:
+
+Als je een databasegebruiker wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een databasegebruiker in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor de gebruiker met webtoegang. Je kunt ook de gebruiker met webtoegang handmatig aanmaken en alleen van die gebruiker de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende gebruikers op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.
+
+Een script voor het aanmaken van een gebruiker met de vereiste rechten is te vinden in de map "maintenance/oracle/" van deze installatie. Onthoud dat het gebruiken van een gebruiker met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met de standaard gebruiker onmogelijk maakt.',
+ 'config-db-prefix-help' => "Als je een database moet gebruiken voor meerdere wiki's, of voor MediaWiki en een andere applicatie, dan kan je ervoor kiezen om een voorvoegsel toe te voegen aan de tabelnamen om conflicten te voorkomen.
+Gebruik geen spaties.
+
+Dit veld wordt meestal leeg gelaten.",
+ 'config-charset-help' => "'''Waarschuwing:''' als je '''achterwaarts compatibel met UTF-8''' gebruikt met MySQL 4.1+ en een back-up van de database maakt met <code>mysqldump</code>, dan kunnen alle niet-ASCII-tekens in je back-ups onherstelbaar beschadigd raken.
+
+In '''binaire modus''' slaat MediaWiki tekst in UTF-8 op in binaire databasevelden.
+Dit is efficiënter dan de UTF-8-modus van MySQL en stelt je in staat de volledige reeks Unicode-tekens te gebruiken.
+In '''UTF-8-modus''' kent MySQL de tekenset van je gegevens en kan de databaseserver ze juist weergeven en converteren.
+Het is dan niet mogelijk tekens op te slaan die de \"[//nl.wikipedia.org/wiki/Lijst_van_Unicode-subbereiken#Basic_Multilingual_Plane Basic Multilingual Plane]\" te boven gaan.",
+ 'config-mysql-old' => 'Je moet MySQL $1 of later gebruiken.
+Jij gebruikt $2.',
+ 'config-db-schema-help' => 'Dit schema klopt meestal.
+Wijzig het alleen als je weet dat dit nodig is.',
+ 'config-sqlite-dir-help' => "SQLite slaat alle gegevens op in een enkel bestand.
+
+De map die je opgeeft moet beschrijfbaar zijn voor de webserver tijdens de installatie.
+
+Deze mag '''niet toegankelijk''' zijn via het web en het bestand mag dus niet tussen de PHP-bestanden staan.
+
+Het installatieprogramma schrijft het bestand <code>.htaccess</code> weg met het databasebestand, maar als dat niet werkt kan iemand zich toegang tot het ruwe databasebestand verschaffen.
+Ook de gebruikersgegevens (e-mailadressen, wachtwoordhashes) en verwijderde versies en overige gegevens met beperkte toegang via MediaWiki zijn dan onbeschermd.
+
+Overweeg om de database op een totaal andere plaats neer te zetten, bijvoorbeeld in <code>/var/lib/mediawiki/yourwiki</code>.",
+ 'config-support-info' => 'MediaWiki ondersteunt de volgende databasesystemen:
+
+$1
+
+Als je het databasesysteem dat je wilt gebruiken niet in de lijst terugvindt, volg dan de handleiding waarnaar hierboven wordt verwezen om ondersteuning toe te voegen.',
+ 'config-missing-db-name' => 'Je moet een waarde opgeven voor "Databasenaam"',
+ 'config-missing-db-host' => 'Je moet een waarde invoeren voor "Databaseserver"',
+ 'config-missing-db-server-oracle' => 'Je moet een waarde opgeven voor "Database-TNS"',
+ 'config-postgres-old' => 'PostgreSQL $1 of hoger is vereist.
+Jij gebruikt $2.',
+ 'config-sqlite-name-help' => 'Kies een naam die je wiki identificeert.
+Gebruik geen spaties of koppeltekens.
+Deze naam wordt gebruikt voor het gegevensbestand van SQLite.',
+ 'config-upgrade-done' => "Het bijwerken is afgerond.
+
+Je kunt [$1 je wiki nu gebruiken].
+
+Als je je <code>LocalSettings.php</code> opnieuw wilt aanmaken, klik dan op de knop hieronder.
+Dit is '''niet aan te raden''' tenzij je problemen hebt met je wiki.",
+ 'config-upgrade-done-no-regenerate' => 'Het bijwerken is afgerond.
+
+Je kunt nu [$1 je wiki gebruiken].',
+ 'config-db-web-no-create-privs' => 'De gebruiker die je hebt opgegeven voor de installatie heeft niet voldoende rechten om een gebruiker aan te maken.
+De gebruiker die je hier opgeeft moet al bestaan.',
+ 'config-mysql-myisam-dep' => "'''Waarschuwing''': je hebt MyISAM geselecteerd als opslagengine voor MySQL. Dit is niet aan te raden voor MediaWiki omdat:
+* het nauwelijks ondersteuning biedt voor gebruik door meerdere gebruikers tegelijkertijd door het locken van tabellen;
+* het meer vatbaar is voor corruptie dan andere engines;
+* de code van MediaWiki niet alstijd omgaat met MyISAM zoals dat zou moeten.
+
+Als je installatie van MySQL InnoDB ondersteunt, gebruik dat dan vooral.
+Als je installatie van MySQL geen ondersteuning heeft voor InnoDB, denk dan na over upgraden.",
+ 'config-mysql-charset-help' => "In '''binaire modus''' slaat MediaWiki tekst in UTF-8 op in binaire databasevelden.
+Dit is efficiënter dan de UTF-8-modus van MySQL en stelt je in staat de volledige reeks Unicodetekens te gebruiken.
+
+In '''UTF-8-modus''' kent MySQL de tekenset van je gegevens en kan de databaseserver ze juist weergeven en converteren.
+Het is dat niet mogelijk tekens op te slaan die de \"[//nl.wikipedia.org/wiki/Lijst_van_Unicode-subbereiken#Basic_Multilingual_Plane Basic Multilingual Plane]\" te boven gaan.",
+ 'config-project-namespace-help' => "In het kielzog van Wikipedia beheren veel wiki's hun beleidspagina's apart van hun inhoudelijke pagina's in een \"'''projectnaamruimte'''\".
+Alle paginanamen in deze naamruimte beginnen met een bepaald voorvoegsel dat je hier kunt opgeven.
+Dit voorvoegsel wordt meestal afgeleid van de naam van de wiki, maar het kan geen bijzondere tekens bevatten als \"#\" of \":\".",
+ 'config-admin-name' => 'Je naam:',
+ 'config-admin-password-mismatch' => 'De twee door jou ingevoerde wachtwoorden komen niet overeen.',
+ 'config-admin-email-help' => "Voer hier een e-mailadres in om e-mail te kunnen ontvangen van andere gebruikers op de wiki, je wachtwoord opnieuw in te kunnen stellen en op de hoogte te worden gehouden van wijzigingen van pagina's op uw volglijst. Je kunt het veld leeg laten.",
+ 'config-admin-error-bademail' => 'Je hebt een ongeldig e-mailadres opgegeven',
+ 'config-subscribe-help' => 'Dit is een mailinglijst met een laag volume voor aankondigingen van nieuwe versies, inclusief belangrijke aankondigingen met betrekking tot beveiliging.
+Abonneer jezelf erop en werk je MediaWiki-installatie bij als er nieuwe versies uitkomen.',
+ 'config-subscribe-noemail' => 'Je hebt geprobeerd je te abonneren op de mailinglijst voor release-aankondigingen zonder een e-mailadres op te geven.
+Geef een e-mailadres op als je je wilt abonneren op de mailinglijst.',
+ 'config-almost-done' => 'Je bent bijna klaar!
+Als je wilt kan je de overige instellingen overslaan en de wiki nu installeren.',
+ 'config-profile-help' => "Wiki's werken het beste als ze door zoveel mogelijk gebruikers worden bewerkt.
+In MediaWiki is het eenvoudig om de recente wijzigingen te controleren en eventuele foutieve of kwaadwillende bewerkingen terug te draaien.
+
+Daarnaast vinden velen MediaWiki goed inzetbaar in vele andere rollen, en soms is het niet handig om helemaal \"op de wikimanier\" te werken.
+Daarom biedt dit installatieprogramma je de volgende keuzes voor de basisinstelling van gebruikersvrijheden:
+
+Een '''{{int:config-profile-wiki}}''' staat iedereen toe te bewerken, zonder zelfs aan te melden.
+Een wiki met '''{{int:config-profile-no-anon}}\" biedt extra verantwoordelijkheid, maar kan afschrikken toevallige gebruikers afschrikken.
+
+Het scenario '''{{int:config-profile-fishbowl}}''' laat gebruikers waarvoor dat is ingesteld bewerkt, maar andere gebruikers kunnen alleen pagina's bekijken, inclusief de bewerkingsgeschiedenis.
+In een '''{{int:config-profile-private}}''' kunnen alleen goedgekeurde gebruikers pagina's bekijken en bewerken.
+
+Meer complexe instellingen voor gebruikersrechten zijn te maken na de installatie; hierover is meer te lezen in de [//www.mediawiki.org/wiki/Manual:User_rights handleiding].", # Fuzzy
+ 'config-license-help' => "In veel openbare wiki's zijn alle bijdragen beschikbaar onder een [http://freedomdefined.org/Definition vrije licentie].
+Dit helpt bij het creëren van een gevoel van gemeenschappelijk eigendom en stimuleert bijdragen op lange termijn.
+Dit is over het algemeen niet nodig is voor een particuliere of zakelijke wiki.
+
+Als je teksten uit Wikipedia wilt kunnen gebruiken en je wilt het mogelijk maken teksten uit je wiki naar Wikipedia te kopiëren, kies dan de licentie '''Creative Commons Naamsvermelding-Gelijk delen'''.
+
+De GNU Free Documentation License is de oude licentie voor inhoud uit Wikipedia.
+Dit is nog steeds een geldige licentie, maar deze licentie is lastig te begrijpen.
+Het is ook lastig inhoud te hergebruiken onder de GFDL.",
+ 'config-enable-email-help' => 'Als je wilt dat e-mailen mogelijk is, dan moeten de [http://www.php.net/manual/en/mail.configuration.php e-mailinstellingen van PHP] correct zijn.
+Als je niet wilt dat e-mailen mogelijk is, dan kan je de instellingen hier uitschakelen.',
+ 'config-upload-help' => "Het toestaan van het uploaden van bestanden stelt je server mogelijk bloot aan beveiligingsrisico's.
+Er is meer [//www.mediawiki.org/wiki/Manual:Security informatie over beveiliging] beschikbaar in de handleiding.
+
+Om het bestandsuploads mogelijk te maken kan je de rechten op de submap <code>images</code> onder de hoofdmap van MediaWiki aanpassen, zodat de webserver erin kan schrijven.
+Daarmee wordt deze functie ingeschakeld.",
+ 'config-logo-help' => 'Het standaarduiterlijk van MediaWiki bevat ruimte voor een logo van 135x160 pixels boven het menu.
+Upload een afbeelding met de juiste afmetingen en voer de URL hier in.
+
+Als je geen logo wilt gebruiken, kan je dit veld leeg laten.', # Fuzzy
+ 'config-cc-not-chosen' => 'Kies de Creative Commonslicentie die je wilt gebruiken en klik op "doorgaan".',
+ 'config-memcache-needservers' => 'Je hebt Memcached geselecteerd als je cache, maar je hebt geen servers opgegeven.',
+ 'config-memcache-badip' => 'Je hebt een ongeldig IP-adres ingevoerd voor Memcached: $1.',
+ 'config-memcache-noport' => 'Je hebt geen poort opgegeven voor de Memcachedserver: $1.
+De standaardpoort is 11211.',
+ 'config-extensions-help' => 'De bovenstaande uitbreidingen zijn aangetroffen in de map <code>./extensions</code>.
+
+Mogelijk moet je aanvullende instellingen maken, maar je kunt deze uitbreidingen nu inschakelen.',
+ 'config-install-alreadydone' => "'''Waarschuwing:''' het lijkt alsof je MediaWiki al hebt geïnstalleerd en probeert het programma opnieuw te installeren.
+Ga door naar de volgende pagina.",
+ 'config-install-begin' => 'Als je nu op "{{int:config-continue}}" klikt, begint de installatie van MediaWiki.
+Als je nog wijzigingen wilt maken, klik dan op "Terug".', # Fuzzy
+ 'config-pg-no-plpgsql' => 'Je moet de taal PL/pgSQL installeren in de database $1',
+ 'config-pg-no-create-privs' => 'De gebruiker die je hebt opgegeven door de installatie heeft niet voldoende rechten om een gebruiker aan te maken.',
+ 'config-pg-not-in-role' => 'De gebruiker die je hebt opgegeven voor de webgebruiker bestaat al.
+De gebruiker die je hebt opgegeven voor installatie is geen superuser en geen lid van de rol van de webgebruiker, en kan het dus geen objecten aanmaken die van de webgebruiker zijn.
+
+MediaWiki vereist momenteel dat de tabellen van de webgebruiker zijn. Geef een andere webgebruikersnaam op, of klik op "terug" en geef een gebruiker op die voldoende installatierechten heeft.',
+ 'config-install-user-missing-create' => 'De opgegeven gebruiker "$1" bestaat niet.
+Klik op "registreren" onderaan als je de gebruiker wilt aanmaken.',
+ 'config-install-done' => "'''Gefeliciteerd!'''
+Je hebt MediaWiki met geïnstalleerd.
+
+Het installatieprogramma heeft het bestand <code>LocalSettings.php</code> aangemaakt.
+Dit bevat al je instellingen.
+
+Je moet het bestand downloaden en in de hoofdmap van uw wikiinstallatie plaatsten; in dezelfde map als index.php.
+De download moet je automatisch zijn aangeboden.
+
+Als de download niet is aangeboden of als je de download hebt geannuleerd, dan kan je de download opnieuw starten door op de onderstaande koppeling te klikken:
+
+$3
+
+'''Let op''': als je dit niet nu doet, dan het is bestand als u later de installatieprocedure afsluit zonder het bestand te downloaden niet meer beschikbaar.
+
+Na het plaatsen van het bestand met instellingen kan je '''[$2 je wiki betreden]'''.",
+ 'mainpagedocfooter' => 'Raadpleeg de [//meta.wikimedia.org/wiki/NL_Help:Inhoudsopgave handleiding] voor informatie over het gebruik van de wikisoftware.
+
+== Meer hulp over MediaWiki ==
+
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lijst met instellingen]
+* [//www.mediawiki.org/wiki/Manual:FAQ Veelgestelde vragen (FAQ)]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglijst voor aankondigingen van nieuwe versies]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Maak MediaWiki beschikbaar in jouw taal]',
+);
+
+/** Norwegian Nynorsk (norsk nynorsk)
* @author Harald Khan
* @author Nghtwlkr
*/
@@ -13966,7 +15188,7 @@ Berre bruk ASCII-bokstavar (a-z, A-Z), tal (0-9) og undestrekar (_).',
==Kome i gang==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste over oppsettsinnstillingar]
* [//www.mediawiki.org/wiki/Manual:FAQ Spørsmål og svar om MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-postliste med informasjon om nye MediaWiki-versjonar]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-postliste med informasjon om nye MediaWiki-versjonar]', # Fuzzy
);
/** Norwegian (bokmål)‬ (‪norsk (bokmål)‬) */
@@ -13975,8 +15197,61 @@ $messages['no'] = array(
);
/** Occitan (occitan)
+ * @author Cedric31
*/
$messages['oc'] = array(
+ 'config-desc' => 'Lo programa d’installacion de MediaWiki',
+ 'config-title' => 'Installacion de MediaWiki $1',
+ 'config-information' => 'Informacions',
+ 'config-localsettings-key' => 'Clau de mesa a jorn :',
+ 'config-localsettings-badkey' => "La clau qu'avètz provesida es incorrècta",
+ 'config-session-error' => "Error al moment de l'aviada de la sesilha : $1",
+ 'config-your-language' => 'Vòstra lenga :',
+ 'config-wiki-language' => 'Lenga del wiki :',
+ 'config-back' => '← Retorn',
+ 'config-continue' => 'Contunhar →',
+ 'config-page-language' => 'Lenga',
+ 'config-page-welcome' => 'Benvenguda sus MediaWiki !',
+ 'config-page-dbconnect' => 'Se connectar a la banca de donadas',
+ 'config-page-dbsettings' => 'Paramètres de la banca de donadas',
+ 'config-page-name' => 'Nom',
+ 'config-page-options' => 'Opcions',
+ 'config-page-install' => 'Installar',
+ 'config-page-complete' => 'Acabat !',
+ 'config-page-restart' => 'Reaviar l’installacion',
+ 'config-page-readme' => 'Legissètz-me',
+ 'config-page-releasenotes' => 'Nòtas de version',
+ 'config-page-copying' => 'Còpia',
+ 'config-page-upgradedoc' => 'Mesa a jorn',
+ 'config-page-existingwiki' => 'Wiki existent',
+ 'config-restart' => 'Òc, lo reaviar',
+ 'config-env-php' => 'PHP $1 es installat.',
+ 'config-db-host-oracle' => 'Nom TNS de la banca de donadas :',
+ 'config-db-wiki-settings' => 'Identificar aqueste wiki',
+ 'config-db-name' => 'Nom de la banca de donadas :',
+ 'config-db-name-oracle' => 'Esquèma de banca de donadas :',
+ 'config-db-install-account' => "Compte d'utilizaire per l'installacion",
+ 'config-db-username' => "Nom d'utilizaire de la banca de donadas :",
+ 'config-db-password' => 'Senhal de la banca de donadas :',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binari',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-charset-mysql4' => 'MySQL 4.0 retrocompatible UTF-8',
+ 'config-db-port' => 'Pòrt de la banca de donadas :',
+ 'config-db-schema' => 'Esquèma per MediaWiki',
+ 'config-header-mysql' => 'Paramètres de MySQL',
+ 'config-header-postgres' => 'Paramètres de PostgreSQL',
+ 'config-header-sqlite' => 'Paramètres de SQLite',
+ 'config-header-oracle' => 'Paramètres d’Oracle',
+ 'config-mysql-engine' => "Motor d'emmagazinatge :",
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-binary' => 'Binari',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-site-name' => 'Nom del wiki :',
+ 'config-ns-generic' => 'Projècte',
+ 'config-ns-other-default' => 'MonWiki',
+ 'config-admin-name' => 'Vòstre nom :',
+ 'config-admin-password' => 'Senhal :',
'mainpagetext' => "'''MediaWiki es estat installat amb succès.'''",
'mainpagedocfooter' => "Consultatz lo [//meta.wikimedia.org/wiki/Ajuda:Contengut Guida de l'utilizaire] per mai d'entresenhas sus l'utilizacion d'aqueste logicial.
@@ -13984,10 +15259,10 @@ $messages['oc'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista dels paramètres de configuracion]
* [//www.mediawiki.org/wiki/Manual:FAQ/fr FAQ MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussions de las parucions de MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussions de las parucions de MediaWiki]", # Fuzzy
);
-/** Oriya (ଓଡ଼ିଆ)
+/** Oriya (ଓଡ଼ିଆ)
* @author Jnanaranjan Sahu
*/
$messages['or'] = array(
@@ -14022,7 +15297,7 @@ $messages['pam'] = array(
== Pamagumpisa ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Picard (Picard)
@@ -14046,16 +15321,20 @@ $messages['pdc'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lischt vun Gnepp zum Konfiguriere]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Eposchde-Lischt fer neie MediaWiki-Versione]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Eposchde-Lischt fer neie MediaWiki-Versione]", # Fuzzy
);
/** Polish (polski)
* @author Beau
* @author BeginaFelicysym
+ * @author Chrumps
* @author Holek
+ * @author Matma Rex
+ * @author Michał Roszka
* @author Saper
* @author Sp5uhe
* @author Woytecr
+ * @author 아라
*/
$messages['pl'] = array(
'config-desc' => 'Instalator MediaWiki',
@@ -14063,19 +15342,19 @@ $messages['pl'] = array(
'config-information' => 'Informacja',
'config-localsettings-upgrade' => 'Plik <code>LocalSettings.php</code> istnieje.
Aby oprogramowanie zostało zaktualizowane musisz wstawić wartość <code>$wgUpgradeKey</code> w poniższe pole.
-Odnajdziesz ją w LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Wykryto obecność pliku LocalSettings.php.
-Aktualizację należy wykonać poprzez uruchomienie update.php',
+Odnajdziesz ją w <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Wykryto obecność pliku <code>LocalSettings.php</code>.
+Aktualizację należy wykonać poprzez uruchomienie <code>update.php</code>',
'config-localsettings-key' => 'Klucz aktualizacji',
'config-localsettings-badkey' => 'Podany klucz jest nieprawidłowy',
'config-upgrade-key-missing' => 'Wykryto zainstalowane wcześniej MediaWiki.
-Jeśli chcesz je zaktualizować dodaj na koniec pliku LocalSettings.php poniższą linię tekstu.
+Jeśli chcesz je zaktualizować dodaj na koniec pliku <code>LocalSettings.php</code> poniższą linię tekstu.
$1',
- 'config-localsettings-incomplete' => 'Istniejący plik LocalSettings.php wygląda na niekompletny.
+ 'config-localsettings-incomplete' => 'Istniejący plik <code>LocalSettings.php</code> wygląda na niekompletny.
Brak wartości zmiennej $1.
-Zmień plik LocalSettings.php, tak by zawierał deklarację wartości tej zmiennej, a następnie kliknij „Dalej”.',
- 'config-localsettings-connection-error' => 'Wystąpił błąd podczas łączenia z bazą danych z wykorzystaniem danych z LocalSettings.php lub AdminSettings.php.
+Zmień plik <code>LocalSettings.php</code>, tak by zawierał deklarację wartości tej zmiennej, a następnie kliknij „{{int:Config-continue}}”.',
+ 'config-localsettings-connection-error' => 'Wystąpił błąd podczas łączenia z bazą danych z wykorzystaniem danych z <code>LocalSettings.php</code> lub <code>AdminSettings.php</code>.
Popraw ustawienia i spróbuj ponownie.
$1',
@@ -14110,8 +15389,8 @@ Sprawdź plik php.ini i upewnij się, że <code>session.save_path</code> wskazuj
'config-help-restart' => 'Czy chcesz usunąć wszystkie zapisane dane i uruchomić ponownie proces instalacji?',
'config-restart' => 'Tak, zacznij od nowa',
'config-welcome' => '=== Sprawdzenie środowiska instalacji ===
-Wykonywane są podstawowe testy sprawdzające czy to środowisko jest odpowiednie dla instalacji MediaWiki.
-Jeśli potrzebujesz pomocy podczas instalacji załącz wyniki tych testów.',
+Teraz zostaną wykonane podstawowe testy sprawdzające czy to środowisko jest odpowiednie dla instalacji MediaWiki.
+Jeśli potrzebujesz pomocy podczas instalacji, załącz wyniki tych testów.',
'config-copyright' => "=== Prawa autorskie i warunki użytkowania ===
$1
@@ -14188,8 +15467,10 @@ Instalacja może się nie udać!",
'config-mod-security' => "''' Ostrzeżenie ''': Serwer sieci web ma włączone [http://modsecurity.org/ mod_security]. Jeśli niepoprawnie skonfigurowane, może być przyczyną problemów MediaWiki lub innego oprogramowania, które pozwala użytkownikom na wysyłanie dowolnej zawartości.
Sprawdź w [http://modsecurity.org/documentation/ dokumentacji mod_security] lub skontaktuj się z obsługa hosta, jeśli wystąpią losowe błędy.",
'config-diff3-bad' => 'Nie znaleziono GNU diff3.',
+ 'config-git' => 'Znaleziono oprogramowanie kontroli wersji Git: <code>$1</code>.',
+ 'config-git-bad' => 'Oprogramowanie systemu kontroli wersji Git nie zostało znalezione.',
'config-imagemagick' => 'Mamy zainstalowany ImageMagick <code>$1</code>, dzięki czemu będzie można pomniejszać załadowane grafiki.',
- 'config-gd' => 'Mamy wbudowaną bibliotekę graficzną GD, dzięki ceymu będzie można pomniejszać załadowane grafiki.',
+ 'config-gd' => 'Mamy wbudowaną bibliotekę graficzną GD, dzięki czemu będzie można pomniejszać załadowane grafiki.',
'config-no-scaling' => 'Nie można odnaleźć biblioteki GD lub ImageMagick. Nie będzie działać pomniejszanie załadowane grafiki.',
'config-no-uri' => "'''Błąd.''' Nie można określić aktualnego URI.
Instalacja została przerwana.",
@@ -14206,7 +15487,8 @@ Instalacja została przerwana.',
'config-using531' => 'MediaWiki nie może być używane z PHP $1 z powodu błędu dotyczącego referencyjnych argumentów funkcji <code>__call()</code>.
Uaktualnij do PHP 5.3.2 lub nowszego. Możesz również cofnąć wersję do PHP 5.3.0, aby naprawić ten błąd.
Instalacja została przerwana.',
- 'config-suhosin-max-value-length' => 'Jest zainstalowany Suhosin i ogranicza długość parametru GET do $1 bajtów. Komponent ResourceLoader w MediaWiki wykona obejście tego ograniczenia, ale kosztem wydajności. Jeśli to możliwe należy ustawić suhosin.get.max_value_length na 1024 lub wyższej w php.ini oraz ustawić $wgResourceLoaderMaxQueryLength w LocalSettings.php na tę samą wartość.',
+ 'config-suhosin-max-value-length' => 'Jest zainstalowany Suhosin i ogranicza długość parametru GET <code>length</code> do $1 bajtów. Komponent ResourceLoader w MediaWiki wykona obejście tego ograniczenia, ale kosztem wydajności.
+Jeśli to możliwe, należy ustawić <code>suhosin.get.max_value_length</code> na 1024 lub wyżej w <code>php.ini</code> oraz ustawić <code>$wgResourceLoaderMaxQueryLength</code> w <code>LocalSettings.php</code> na tę samą wartość.',
'config-db-type' => 'Typ bazy danych',
'config-db-host' => 'Adres serwera bazy danych',
'config-db-host-help' => 'Jeśli serwer bazy danych jest na innej maszynie, wprowadź jej nazwę domenową lub adres IP.
@@ -14278,7 +15560,6 @@ Zawiera ona nieopracowane dane użytkownika (adresy e-mail, zahaszowane hasła)
Warto rozważyć umieszczenie w bazie danych zupełnie gdzie indziej, na przykład w <code>/var/lib/mediawiki/yourwiki</code> .",
'config-oracle-def-ts' => 'Domyślna przestrzeń tabel',
'config-oracle-temp-ts' => 'Przestrzeń tabel tymczasowych',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki może współpracować z następującymi systemami baz danych:
$1
@@ -14288,18 +15569,16 @@ Poniżej wyświetlone są systemy baz danych gotowe do użycia. Jeżeli poniżej
'config-support-postgres' => '* $1 jest popularnym systemem baz danych, często stosowanym zamiast MySQL ([http://www.php.net/manual/en/pgsql.installation.php Zobacz, jak skompilować PHP ze wsparciem dla PostgreSQL]). Z powodu możliwości wystąpienia drobnych błędów, nie jest zalecana do wymagających wdrożeń.',
'config-support-sqlite' => '* $1 jest niewielkim systemem bazy danych, z którym MediaWiki bardzo dobrze współpracuje. ([http://www.php.net/manual/en/pdo.installation.php Jak skompilować PHP ze wsparciem dla SQLite], korzystając z PDO)',
'config-support-oracle' => '* $1 jest komercyjną profesjonalną bazą danych. ([http://www.php.net/manual/en/oci8.installation.php Jak skompilować PHP ze wsparciem dla OCI8])',
- 'config-support-ibm_db2' => '* $1 jest komercyjną zaawansowaną bazą danych.',
'config-header-mysql' => 'Ustawienia MySQL',
'config-header-postgres' => 'Ustawienia PostgreSQL',
'config-header-sqlite' => 'Ustawienia SQLite',
'config-header-oracle' => 'Ustawienia Oracle',
- 'config-header-ibm_db2' => 'ustawienia dla IBM DB2',
'config-invalid-db-type' => 'Nieprawidłowy typ bazy danych',
'config-missing-db-name' => 'Należy wpisać wartość w polu „Nazwa bazy danych”',
'config-missing-db-host' => 'Musisz wpisać wartość w polu „Serwer bazy danych”',
'config-missing-db-server-oracle' => 'Należy wpisać wartość w polu „Nazwa instancji bazy danych (TNS)”',
'config-invalid-db-server-oracle' => 'Nieprawidłowa nazwa instancji bazy danych (TNS) „$1”.
-W nazwie można użyć wyłącznie liter ASCII (a-z, A-Z), cyfr (0-9), podkreślenia (_) i kropek (.).',
+Użyj "TNS Name" lub "Easy Connect" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])',
'config-invalid-db-name' => 'Nieprawidłowa nazwa bazy danych „$1”.
Używaj wyłącznie liter ASCII (a-z, A-Z), cyfr (0-9), podkreślenia (_) lub znaku odejmowania (-).',
'config-invalid-db-prefix' => 'Nieprawidłowy prefiks bazy danych „$1”.
@@ -14356,7 +15635,7 @@ Jest to '''nie zalecane''', chyba że występują problemy z twoją wiki.",
Możesz wreszcie [$1 zacząć korzystać ze swojej wiki].',
'config-regenerate' => 'Ponowne generowanie LocalSettings.php →',
- 'config-show-table-status' => 'Zapytanie „SHOW TABLE STATUS” nie powiodło się!',
+ 'config-show-table-status' => 'Zapytanie „<code>SHOW TABLE STATUS</code>” nie powiodło się!',
'config-unknown-collation' => "'''Uwaga''' – bazy danych używa nierozpoznanej metody porównywania.",
'config-db-web-account' => 'Konto bazy danych dla dostępu przez WWW',
'config-db-web-help' => 'Wybierz nazwę użytkownika i hasło, z których korzystać będzie serwer WWW do łączenia się z serwerem baz danych, podczas zwykłej pracy z wiki.',
@@ -14374,6 +15653,12 @@ Konto, które wskazałeś tutaj musi już istnieć.',
Jeśli instalacja MySQL obsługuje InnoDB, jest wysoce zalecane, by to je wybrać.
Jeśli instalacja MySQL nie obsługuje InnoDB, być może nadszedł czas na jej uaktualnienie.",
+ 'config-mysql-only-myisam-dep' => "'''Ostrzeżenie:''' MyISAM jest jedynym dostępnym mechanizmem składowania dla MySQL, który nie jest zalecany do używania z MediaWiki, ponieważ:
+* słabo obsługuje współbieżność z powodu blokowania tabel
+* jest bardziej skłonny do uszkodzeń niż inne silniki
+* kod MediaWiki nie zawsze traktuje MyISAM jak powinien
+
+Twoja instalacja MySQL nie obsługuje InnoDB, być może jest to czas na aktualizację.",
'config-mysql-engine-help' => "'''InnoDB''' jest prawie zawsze najlepszą opcją, ponieważ posiada dobrą obsługę współbieżności.
'''MyISAM''' może być szybsze w instalacjach pojedynczego użytkownika lub tylko do odczytu.
@@ -14385,7 +15670,6 @@ Bazy danych MyISAM mają tendencję do ulegania uszkodzeniom częściej niż baz
Jest on bardziej wydajny niż tryb UTF-8 w MySQL i pozwala na używanie znaków pełnego zakresu Unicode.
W '''trybie UTF-8''', MySQL będzie znać zestaw znaków w jakim zakodowano dane, można też przedstawić i przekonwertuj je odpowiednio, ale nie pozwoli Ci przechowywać znaków spoza [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes podstawowej płaszczyzny wielojęzyczności].",
- 'config-ibm_db2-low-db-pagesize' => "Baza danych DB2 posiada domyślny obszar tabel z niewystarczającym rozmiarem strony. Wartość rozmiaru strony musi być równa lub powyżej '''32 K'''.",
'config-site-name' => 'Nazwa wiki',
'config-site-name-help' => 'Ten napis pojawi się w pasku tytułowym przeglądarki oraz w różnych innych miejscach.',
'config-site-name-blank' => 'Wprowadź nazwę witryny.',
@@ -14418,7 +15702,7 @@ Podaj inną nazwę.',
'config-admin-error-user' => 'Błąd wewnętrzny podczas tworzenia konta administratora o nazwie „<nowiki>$1</nowiki>”.',
'config-admin-error-password' => 'Wewnętrzny błąd podczas ustawiania hasła dla administratora „<nowiki>$1</nowiki>”: <pre>$2</pre>',
'config-admin-error-bademail' => 'Wpisałeś nieprawidłowy adres e‐mail',
- 'config-subscribe' => 'Zapisz się na [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce listę pocztową z ogłaszaniami o nowych wersjach].',
+ 'config-subscribe' => 'Zapisz się na [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce listę pocztową z ogłoszeniami o nowych wersjach].',
'config-subscribe-help' => 'Jest to lista o małej liczbie wiadomości, wykorzystywana do przesyłania informacji o udostępnieniu nowej wersji oraz istotnych sprawach dotyczących bezpieczeństwa.
Powinieneś zapisać się na tę listę i aktualizować zainstalowane oprogramowanie MediaWiki gdy pojawia się nowa wersja.',
'config-subscribe-noemail' => 'Próbowano subskrybować listę mailingową ogłoszeń wersji bez podania adresu e-mail.
@@ -14428,20 +15712,20 @@ Możesz pominąć pozostałe czynności konfiguracyjne i zainstalować wiki.',
'config-optional-continue' => 'Zadaj mi więcej pytań.',
'config-optional-skip' => 'Jestem już znudzony, po prostu zainstaluj wiki.',
'config-profile' => 'Profil uprawnień użytkowników',
- 'config-profile-wiki' => 'Tradycyjne wiki',
+ 'config-profile-wiki' => 'Otwarte wiki',
'config-profile-no-anon' => 'Wymagane utworzenie konta',
'config-profile-fishbowl' => 'Wyłącznie zatwierdzeni edytorzy',
'config-profile-private' => 'Prywatna wiki',
'config-profile-help' => "Strony typu wiki działają najlepiej, gdy pozwolisz je edytować tak wielu osobom, jak to możliwie.
-W MediaWiki, jest łatwe sprawdzenie ostatnich zmian i wycofać szkody, które są spowodowane przez naiwnych lub złośliwych użytkowników.
+W MediaWiki, można łatwo sprawdzić ostatnie zmiany i wycofać szkody, które są spowodowane przez naiwnych lub złośliwych użytkowników.
-Jednakże wielu uznało MediaWiki użytecznymi w różnorodnych rolach, a czasami nie jest łatwo przekonać wszystkich do korzyści ze sposobu działania wiki.
+Jednakże wielu uznało MediaWiki użytecznym w różnorodnych rolach, a czasami nie jest łatwo przekonać wszystkich do korzyści ze sposobu działania wiki. Masz więc wybór.
-Ustawienie '''{{int:config-profil-wiki}}''' pozwala każdemu na edycję, nawet bez logowania się.
-Wiki z '''{{int:config-profile-no-anon}}''' zawiera dodatkowe funkcje rozliczania się, ale może powstrzymać dorywczych współpracowników.
+Ustawienie '''{{int:config-profile-wiki}}''' pozwala każdemu na edycję, nawet bez logowania się.
+Wiki z '''{{int:config-profile-no-anon}}''' zawiera dodatkowe możliwości ale może powstrzymywać potencjalnych edytorów.
Scenariusz '''{{int:config-profile-fishbowl}}''' umożliwia zatwierdzonym użytkownikom edycję, ale wyświetlanie stron jest powszechnie dostępne, włącznie z historią.
-Ustawienie '''{{int:config-profile-private}}'' ' pozwala na wyświetlanie stron tylko zatwierdzonym użytkownikom, ta sama grupa może edytować.
+Ustawienie '''{{int:config-profile-private}}'' ' pozwala na wyświetlanie stron tylko zatwierdzonym użytkownikom, ta sama grupa może je edytować.
Bardziej skomplikowane konfiguracje uprawnień użytkowników są dostępne po zakończeniu instalacji, zobacz [//www.mediawiki.org/wiki/Manual:User_rights odpowiednią część podręcznika].",
'config-license' => 'Prawa autorskie i licencja',
@@ -14494,6 +15778,8 @@ Najlepiej, aby nie był on dostępny z internetu.',
'config-logo-help' => 'Domyślny motyw MediaWiki zawiera miejsce na logo wielkości 135 x 160 pikseli powyżej menu na pasku bocznym.
Prześlij obrazek o odpowiednim rozmiarze, a następnie wpisz jego URL tutaj.
+Możesz użyć <code>$wgStylePath</code> lub <code>$wgScriptPath</code> jeżeli twoje logo jest relatywne do tych ścieżek.
+
Jeśli nie chcesz logo, pozostaw to pole puste.',
'config-instantcommons' => 'Włącz Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] jest funkcją, która pozwala wiki używać obrazów, dźwięków i innych mediów znalezionych na witrynie [//commons.wikimedia.org/ Wikimedia Commons].
@@ -14527,8 +15813,8 @@ Jeśli nie znasz numeru portu, wartością domyślną jest 11211.',
Mogą one wymagać dodatkowych czynności konfiguracyjnych, ale można je teraz włączyć',
'config-install-alreadydone' => "'''Uwaga''' – wydaje się, że MediaWiki jest już zainstalowane, a obecnie próbujesz zainstalować je ponownie.
Przejdź do następnej strony.",
- 'config-install-begin' => 'Po naciśnięciu "{{int:config-continue}}", rozpocznie się instalacji MediaWiki.
-Jeśli nadal chcesz dokonać zmian, naciśnij wstecz.',
+ 'config-install-begin' => 'Po naciśnięciu "{{int:config-continue}}", rozpocznie się instalacja MediaWiki.
+Jeśli nadal chcesz dokonać zmian, naciśnij "{{int:config-back}}".',
'config-install-step-done' => 'gotowe',
'config-install-step-failed' => 'nieudane',
'config-install-extensions' => 'Włącznie z rozszerzeniami',
@@ -14570,35 +15856,40 @@ Tworzenie domyślnej listy pominięto.",
'config-install-extension-tables' => 'Tworzenie tabel dla aktywnych rozszerzeń',
'config-install-mainpage-failed' => 'Nie udało się wstawić strony głównej – $1',
'config-install-done' => "'''Gratulacje!'''
-Udało ci się zainstalować MediaWiki.
+Udało Ci się zainstalować MediaWiki.
Instalator wygenerował plik konfiguracyjny <code>LocalSettings.php</code>.
-Musisz go pobrać i umieścić go w korzeniu twojej instalacji wiki (tym samym katalogu co index.php). Pobieranie powinno zacząć się automatycznie.
+Musisz go pobrać i umieścić w katalogu głównym Twojej instalacji wiki (tym samym katalogu co index.php). Pobieranie powinno zacząć się automatycznie.
-Jeżeli pobieranie nie zostało zaproponowane, lub jeśli użytkownik anulował je, można ponownie uruchomić pobranie klikając poniższe łącze:
+Jeżeli pobieranie nie zostało zaproponowane lub jeśli użytkownik je anulował, można ponownie uruchomić pobranie klikając poniższe łącze:
$3
-'''Uwaga''': Jeśli tego nie zrobisz tego teraz, wygenerowany plik konfiguracyjny nie będzie już dostępny po zakończeniu instalacji.
+'''Uwaga''': Jeśli nie zrobisz tego teraz, wygenerowany plik konfiguracyjny nie będzie już dostępny po zakończeniu instalacji.
-Po załadowaniu pliku konfiguracyjnego możesz '''[ $2 wejść na wiki]'''.",
- 'config-download-localsettings' => 'Pobierz LocalSettings.php',
+Po załadowaniu pliku konfiguracyjnego możesz '''[$2 wejść na wiki]'''.",
+ 'config-download-localsettings' => 'Pobierz <code>LocalSettings.php</code>',
'config-help' => 'pomoc',
'config-nofile' => 'Nie udało się odnaleźć pliku "$1". Czy nie został usunięty?',
+ 'config-extension-link' => 'Czy wiesz, że twoja wiki obsługuje [//www.mediawiki.org/wiki/Manual:Extensions/pl rozszerzenia]?
+
+Możesz przejrzeć [//www.mediawiki.org/wiki/Category:Extensions_by_category rozszerzenia według kategorii] lub [//www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] aby zobaczyć pełną listę rozszerzeń.',
'mainpagetext' => "'''Instalacja MediaWiki powiodła się.'''",
'mainpagedocfooter' => 'Zobacz [//meta.wikimedia.org/wiki/Help:Contents przewodnik użytkownika] w celu uzyskania informacji o działaniu oprogramowania wiki.
== Na początek ==
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista ustawień konfiguracyjnych]
-* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki]',
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings/pl Lista ustawień konfiguracyjnych]
+* [//www.mediawiki.org/wiki/Manual:FAQ/pl MediaWiki FAQ]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]',
);
/** Piedmontese (Piemontèis)
* @author Borichèt
* @author Dragonòt
* @author Krinkle
+ * @author 아라
*/
$messages['pms'] = array(
'config-desc' => "L'instalador për mediaWiki",
@@ -14607,10 +15898,20 @@ $messages['pms'] = array(
'config-localsettings-upgrade' => "A l'é stàit trovà n'archivi <code>LocalSettings.php</code>.
Për agiorné cost'anstalassion, ch'a anserissa ël valor ëd <code>\$wgUpgradeKey</code> ant la casela sì-sota.
A la trovrà an LocalSetting.php.",
- 'config-localsettings-cli-upgrade' => "N'archivi LocalSettings.php a l'é stàit trovà.
-Për agiorné sta instalassion, për piasì fà anvece giré update.php",
+ 'config-localsettings-cli-upgrade' => "N'archivi <code>LocalSettings.php</code> a l'é stàit trovà.
+Për agiorné sta instalassion, për piasì fà anvece giré <code>update.php</code>",
'config-localsettings-key' => "Ciav d'agiornament:",
'config-localsettings-badkey' => "La ciav ch'it l'has dàit a l'é pa giusta.",
+ 'config-upgrade-key-missing' => "A l'é stàita trovà n'istalassion esistenta ëd MediaWiki.
+Për agiorné soa istalassion, për piasì ch'a buta la linia sì-sota al fond ëd sò <code>LocalSettings.php</code>:
+
+$1",
+ 'config-localsettings-incomplete' => "L'esistent <code>LocalSettings.php</code> a smija esse ancomplet.
+La variàbil $1 a l'é nen ampostà.
+Për piasì, ch'a modìfica <code>LocalSettings.php</code> ëd fasson che costa variàbil a sia ampostà, e ch'a sgnaca «{{int:Config-continue}}».",
+ 'config-localsettings-connection-error' => "A l'é ancapitaje n'eror an colegand-se a la base ëd dàit an dovrand j'ampostassion specificà an <code>LocalSettings.php</code> o <code>AdminSettings.php</code>. Për piasì, ch'a coregia cost'ampostassion e ch'a preuva torna.
+
+$1",
'config-session-error' => 'Eror an fasend parte la session: $1',
'config-session-expired' => "Ij sò dat ëd session a smijo scadù.
Le session a son configurà për na durà ëd $1.
@@ -14638,6 +15939,7 @@ Ch'a contròla sò php.ini e ch'as sigura che <code>session.save_path</code> a s
'config-page-releasenotes' => 'Nòte ëd publicassion',
'config-page-copying' => 'Copié',
'config-page-upgradedoc' => 'Agiorné',
+ 'config-page-existingwiki' => 'Wiki esistenta',
'config-help-restart' => "Veul-lo scancelé tùit ij dat salvà ch'a l'ha anserì e anandié torna ël process d'instalassion?",
'config-restart' => 'É!, felo torna parte',
'config-welcome' => "=== Contròj d'ambient ===
@@ -14666,13 +15968,21 @@ It peule instalé MediaWiki.",
'config-env-bad' => "L'ambient a l'é stàit controlà.
It peule pa instalé MediaWiki.",
'config-env-php' => "PHP $1 a l'é instalà.",
+ 'config-env-php-toolow' => "PHP $1 a l'é instalà.
+Ant tùit ij cas, MediaWiki a ciama PHP $2 o pi neuv.",
'config-unicode-using-utf8' => 'As deuvra utf8_normalize.so ëd Brion Vibber për la normalisassion Unicode.',
'config-unicode-using-intl' => "As deuvra l'[http://pecl.php.net/intl estension intl PECL] për la normalisassion Unicode.",
'config-unicode-pure-php-warning' => "'''Avis:''' L'[http://pecl.php.net/intl estension intl PECL] a l'é pa disponìbil për gestì la normalisassion Unicode, da già che l'implementassion an PHP pur a faliss për lentëssa.
S'a gestiss un sit a àut tràfich, a dovrìa lese cheicòs an sla [//www.mediawiki.org/wiki/Unicode_normalization_considerations normalisassion Unicode].",
'config-unicode-update-warning' => "'''Avis:''' La version instalà dlë spassiador ëd normalisassion Unicode a deuvra na version veja ëd la librarìa dël [http://site.icu-project.org/ proget ICU].
A dovrìa fé n'[//www.mediawiki.org/wiki/Unicode_normalization_considerations agiornament] s'a l'é anteressà a dovré Unicode.",
- 'config-no-db' => 'Impossìbil tové un pilòta ëd base ëd dàit bon!', # Fuzzy
+ 'config-no-db' => "Impossìbil trové un pilòta ëd base ëd dàit bon! A dev instalé un pilòta ëd base ëd dàit për PHP.
+Le sòrt ëd database ch'a ven-o a son apogià: $1.
+
+S'a l'é ansima a 'n servissi partagià, ch'a ciama a sò fornidor ëd servissi d'instalé un pilòta ëd base ëd dàit compatìbil.
+S'a l'é compilasse PHP chiel-midem, ch'a lo configura torna con un client ëd base ëd dàit abilità, për esempi an dovrand <code>./configure --with-mysql</code>.
+S'a l'ha instalà PHP dai pachèt Debian o Ubuntu, antlora a dev ëdcò istalé ël mòdul php5-mysql.",
+ 'config-outdated-sqlite' => "'''Avis''': chiel a l'ha SQLite $1, che a l'é pi vej che la version mìnima dont a-i é damanca $2. SQLite a sarà pa disponìbil.",
'config-no-fts3' => "'''Avis''': SQLite a l'é compilà sensa ël mòdul [//sqlite.org/fts3.html FTS3], le funsion d'arserca a saran pa disponìbij su cost motor.",
'config-register-globals' => "'''Avis: L'opsion <code>[http://php.net/register_globals register_globals]</code> ëd PHP a l'é abilità.'''
'''Ch'a la disabìlita s'a peul.'''
@@ -14696,14 +16006,19 @@ MediaWiki a l'ha da manca dle funsion an sto mòdul e a travajërà pa an costa
S'a fa giré mandrake, ch'a instala ël pachet php-xml.",
'config-pcre' => "A smija che ël mòdul d'apògg PCRE a sia mancant.
MediaWiki a l'ha da manca dle funsion d'espression regolar Perl-compatìbij për marcé.",
+ 'config-pcre-no-utf8' => "'''Fatal''': ël mòdul PCRE ëd PHP a smija esse compilà sensa l'apògg PCRE_UTF8.
+MediaWiki a ciama l'apògg d'UTF8 për marcé për da bin.",
'config-memory-raised' => "<code>memory_limit</code> ëd PHP a l'é $1, aussà a $2.",
'config-memory-bad' => "'''Avis:''' <code>memory_limit</code> ëd PHP a l'é $1.
Sossì a l'é probabilment tròp bass.
L'instalassion a peul falì!",
+ 'config-ctype' => "'''Fatal''': PHP a dev esse compilà con l'apògg për l'[http://www.php.net/manual/en/ctype.installation.php extension Ctype].",
'config-xcache' => "[http://xcache.lighttpd.net/ XCache] a l'é instalà",
'config-apc' => "[http://www.php.net/apc APC] a l'é instalà",
'config-wincache' => "[http://www.iis.net/download/WinCacheForPhp WinCache] a l'é instalà",
'config-no-cache' => "'''Avis:''' As treuva pa [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache]. Ël buté d'oget an memòria local a l'é pa abilità.",
+ 'config-mod-security' => "'''Avis''': Sò servent për l'aragnà a l'ha [http://modsecurity.org/ mod_security] abilità. Se mal configurà, a peul causé dij problema për MediaWiki o d'àutri programa ch'a përmëtto a j'utent dë spedì un contnù qualsëssìa.
+Ch'a fasa arferiment a la [http://modsecurity.org/documentation/ mod_security documentassion] o ch'a contata l'echip ëd sò servissi s'a-j rivo dj'eror casuaj.",
'config-diff3-bad' => 'GNU diff3 pa trovà.',
'config-imagemagick' => "Trovà ImageMagick: <code>$1</code>.
La miniaturisassion ëd figure a sarà abilità s'it abìlite le carie.",
@@ -14713,27 +16028,52 @@ La miniaturisassion ëd figure a sarà abilità s'a abìlita ij cariament.",
La miniaturisassion ëd figure a sarà disabilità.',
'config-no-uri' => "'''Eror:''' As peul pa determiné l'URI corenta.
Instalassion abortìa.",
+ 'config-no-cli-uri' => "'''Avis''': pa gnun --scriptpath specificà, a sarà dovrà ël predefinì: <code>$1</code>.",
+ 'config-using-server' => 'Utilisassion dël nòm ëd servent "<nowiki>$1</nowiki>".',
+ 'config-using-uri' => "Utilisassion ëd l'anliura ëd servent «<nowiki>$1$2</nowiki>».",
'config-uploads-not-safe' => "'''Avis:''' Sò dossié stàndard për carié <code>$1</code> a l'é vulneràbil a l'esecussion ëd qualsëssìa senari.
Bele che MediaWiki a contròla j'aspet ëd sicurëssa ëd tùit j'archivi carià, a l'é motobin arcomandà ëd [//www.mediawiki.org/wiki/Manual:Security#Upload_security saré ës përtus ëd sicurëssa] prima d'abilité ij cariament.",
+ 'config-no-cli-uploads-check' => "'''Avis:''' Toa cartela predefinìa për j-amportassion (<code>$1</code>) a l'é nen controlà a propòsit ëd la vulnerabilità
+d'esecussion ëd senari arbitrari durant l'istalassion CLI.",
+ 'config-brokenlibxml' => "Sò sistema a l'ha na combinassion ëd version PHP e libxml2 che a l'ha dij bigat e a peul provoché la corussion ëd dat ëstërmà an MediaWiki e d'àutre aplicassion për l'aragnà.
+Ch'a agiorna a PHP 5.2.9 o pi neuv e libxml2 2.7.3 o pi neuv ([//bugs.php.net/bug.php?id=45996 bug filed with PHP]).
+Istalassion abortìa.",
+ 'config-using531' => "MediaWiki a peul pa esse dovrà con PHP $1 a motiv d'un bigat ch'a ìmplica ij paràmetr d'arferiment a <code>__call()</code>.
+Ch'a agiorna a PHP 5.3.2 o pi neuv, o ch'a torna andré a PHP 5.3.0 për arzòlve ës problema.
+Istalassion abortìa.",
+ 'config-suhosin-max-value-length' => 'Suhosin a l\'é instalà e a lìmita la longheur dël paràmetr GET a $1 byte. Ël component ResourceLoader ëd MediaWiki a travajerà an rispetand ës lìmit, ma sòn a degraderà le prestassion. Se possìbil, a dovrìa amposté suhosin.get.max_value_lenght a 1024 o pi àut an <code>php.ini</code>, e amposté <code>$wgResourceLoaderMaxQueryLength</code> al midem valor an LocalSettings.php .', # Fuzzy
'config-db-type' => 'Sòrt ëd base ëd dàit:',
'config-db-host' => 'Ospitant ëd la base ëd dàit:',
- 'config-db-host-help' => "Se sò servent ëd base ëd dàit a l'é su un servent diferent, ch'a anseriss ambelessì ël nòm dl'ospitant o l'adrëssa IP.
+ 'config-db-host-help' => "Se sò servent ëd base ëd dàit a l'é su un servent diferent, ch'a anserissa ambelessì ël nòm dl'ospitant o l'adrëssa IP.
S'a deuvra n'ospitalità partagià, sò fornidor d'ospitalità a dovrìa deje ël nòm dl'ospitant giust ant soa documentassion.
-Se a anstala su un servent Windows e a deuvra MySQL, dovré \"localhost\" a podrìa funsioné nen com nòm dël servent. S'a marcia nen, ch'a preuva \"127.0.0.1\" com adrëssa IP local.", # Fuzzy
+Se a anstala su un servent Windows e a deuvra MySQL, dovré «localhost» a podrìa funsioné nen com nòm dël servent. S'a marcia nen, ch'a preuva «127.0.0.1» com adrëssa IP local.
+
+S'a deuvra PostgresSQL, ch'a lassa sto camp bianch për coleghesse a travers un socket UNIX.",
'config-db-host-oracle' => 'TNS dla base ëd dàit:',
'config-db-host-oracle-help' => "Anserì un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nòm ëd conession local] bon; n'archivi tnsnames.ora a dev esse visìbil da costa anstalassion..<br />S'a deuvra le librarìe cliente 10g o pi neuve a peul ëdcò dovré ël métod ëd nominassion [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].",
'config-db-wiki-settings' => 'Identìfica sta wiki',
'config-db-name' => 'Nòm dla base ëd dàit:',
'config-db-name-help' => "Ch'a serna un nòm ch'a identìfica soa wiki.
-A dovrìa conten-e gnun ëspassi o tratin.
+A dovrìa conten-e gnun ëspassi.
-S'a deuvra n'ospitalità partagià, sò fornidor ëd l'ospitalità a-j darà un nòm ëd base ëd dàit specìfich da dovré, o a lassrà ch'a lo crea via un panel ëd contròl.", # Fuzzy
+S'a deuvra n'ospitalità partagià, sò fornidor ëd l'ospitalità a-j darà un nòm ëd base ëd dàit specìfich da dovré o a lassrà ch'a lo crea via un panel ëd contròl.",
'config-db-name-oracle' => 'Schema dla base ëd dàit:',
+ 'config-db-account-oracle-warn' => "A-i é tre possibilità mantnùe për istalé Oracle tanme terminal ëd base ëd dàit:
+
+S'a veul creé un cont ëd base ëd dàit com part dël process d'istalassion, për piasì ch'a fornissa un cont con ël ròl SYSDBA com cont ëd base ëd dàit për l'istalassion e ch'a specìfica le credensiaj vorsùe për ël cont d'acess an sl'aragnà, dësnò a peul ëdcò creé ël cont d'acess an sl'aragnà manualment e mach fornì col cont (se a l'ha ij përmess necessari për creé j'oget dë schema) o fornì doi cont diferent, un con ij privilegi ëd creé e un limità për l'acess an sla Ragnà.
+
+Ij senari për creé un cont con ij privilegi necessari a peul esse trovà ant la cartela «manutension/oracol/» ëd costa istalassion. Ch'a ten-a da ment che dovrand un cont limità a disabiliterà tute le funsion ëd manutension con ël cont predefinì.",
'config-db-install-account' => "Cont d'utent për l'instalassion.",
'config-db-username' => "Nòm d'utent dla base ëd dàit:",
'config-db-password' => 'Ciav dla base ëd dàit:',
+ 'config-db-password-empty' => "Për piasì, ch'a anserissa na ciav për ël neuv utent ëd base ëd dàit: $1.
+Con tut ch'a sia possìbil creé d'utent sensa ciav, a l'é pa na ròba sigura.",
+ 'config-db-install-username' => "Ch'a nserissa lë stranòm che a sarà dovrà për coleghesse a la base ëd dàit durant ël process d'istalassion.
+Cost-sì a l'é nen lë stranòm dël cont MediaWiki; a l'é lë stranòm për soa base ëd dàit.",
+ 'config-db-install-password' => "Ch'a anserissa la ciav che a sarà dovrà për coleghesse a la base ëd dàit durant ël process d'istalassion.
+Costa-sì a l'é nen la ciav dël cont MediaWiki; a l'é la ciav për soa base ëd dàit.",
'config-db-install-help' => "Ch'a anserissa lë stranòm d'utent e la ciav che a saran dovrà për coleghesse a la base ëd dàit durant ël process d'instalassion.",
'config-db-account-lock' => "Dovré ij midem stranòm d'utent e ciav durant j'operassion normaj",
'config-db-wiki-account' => "Cont d'utent për j'operassion normaj",
@@ -14741,9 +16081,9 @@ S'a deuvra n'ospitalità partagià, sò fornidor ëd l'ospitalità a-j darà un
S'ël cont a esist pa, e ël cont d'instalassion a l'ha ij privilegi ch'a-i van, sto cont utent a sarà creà con ij privilegi mìnin për fé marcé la wiki.",
'config-db-prefix' => 'Prefiss dle tàule dla base ëd dàit:',
'config-db-prefix-help' => "S'a l'ha dabzògn ëd partagé na base ëd dàit an tra vàire wiki, o tra MediaWiki e n'àutra aplicassion dl'aragnà, a peul serne ëd gionté un prefiss a tùit ij nòm ëd le tàule për evité ëd conflit.
-Ch'a deuvra ni dë spassi ni ëd tratin.
+Ch'a deuvra pa dë spassi.
-Cost camp a l'é lassà normalment veuid.", # Fuzzy
+Cost camp a l'é lassà normalment veuid.",
'config-db-charset' => 'Ansema dij caràter dla base ëd dàit',
'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binari',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
@@ -14756,8 +16096,9 @@ An '''manera UTF-8''', MySQL a arconòss an che ansema ëd caràter a son ij sò
'config-mysql-old' => "A-i é da manca ëd MySQL $1 o pi recent, chiel a l'ha $2.",
'config-db-port' => 'Porta dla base ëd dàit:',
'config-db-schema' => 'Schema për MediaWiki',
- 'config-db-schema-help' => "Jë schema sì-dzora a son normalment giust.
-Ch'a-j cangia mach s'a sa ch'a n'ha da manca.", # Fuzzy
+ 'config-db-schema-help' => "Lë schema sì-sota a l'é ëd sòlit giust.
+Ch'a lo cangia mach s'a sa ch'a n'ha da manca.",
+ 'config-pg-test-error' => "Impossìbil coleghesse a la base ëd dàit '''$1'''; $2",
'config-sqlite-dir' => 'Dossié dij dat SQLite:',
'config-sqlite-dir-help' => "SQLite a memorisa tùit ij dat ant n'archivi ùnich.
@@ -14777,7 +16118,7 @@ $1
S'a vëd pa listà sì-sota ël sistema ëd base ëd dàit ch'a preuva a dovré, antlora va andaré a j'istrussion dl'anliura sì-dzora për abilité ël manteniment.",
'config-support-mysql' => "* $1 e l'é l'obietiv primari për MediaWiki e a l'é mej mantnù ([http://www.php.net/manual/en/mysql.installation.php com compilé PHP con ël manteniment MySQL])",
- 'config-support-postgres' => "* $1 e l'é un sistema ëd base ëd dàit popolar a sorgiss duverta com alternativa a MySQL ([http://www.php.net/manual/en/pgsql.installation.php com compilé PHP con ël manteniment ëd PostgreSQL])", # Fuzzy
+ 'config-support-postgres' => "* $1 e l'é un sistema ëd base ëd dàit popolar a sorgiss duverta com alternativa a MySQL ([http://www.php.net/manual/en/pgsql.installation.php com compilé PHP con ël manteniment ëd PostgreSQL]). A peulo ess-ie chèich cit bigat, e a l'é nen arcomandà ëd dovrelo an n'ambient ëd produssion.",
'config-support-sqlite' => "* $1 e l'é un sistema ëd base ëd dàit leger che a l'é motobin bin mantnù ([http://www.php.net/manual/en/pdo.installation.php com compilé PHP con ël manteniment ëd SQLite], a deuvra PDO)",
'config-support-oracle' => "* $1 a l'é na base ëd dàit comersial për j'amprèise. ([http://www.php.net/manual/en/oci8.installation.php Com compilé PHP con ël manteniment OCI8])",
'config-header-mysql' => 'Ampostassion MySQL',
@@ -14786,18 +16127,21 @@ S'a vëd pa listà sì-sota ël sistema ëd base ëd dàit ch'a preuva a dovré,
'config-header-oracle' => 'Ampostassion Oracle',
'config-invalid-db-type' => 'Sòrt ëd ëd base ëd dàit pa bon-a',
'config-missing-db-name' => 'A dev buteje un valor për "Nòm ëd la base ëd dàit"',
+ 'config-missing-db-host' => 'A dev buteje un valor për "l\'òspit ëd la base ëd dàit"',
'config-missing-db-server-oracle' => 'A dev buteje un valor për "TNS ëd la base ëd dat"',
'config-invalid-db-server-oracle' => 'TNS ëd la base ëd dat pa bon "$1".
Dovré mach dle litre ASCII (a-z, A-Z), nùmer (0-9), sotlignadure (_) e pontin (.).',
'config-invalid-db-name' => 'Nòm ëd la base ëd dàit pa bon "$1".
-Dovré mach litre ASCII (a-z, A-Z), nùmer (0-9) e sotlignadure (_).', # Fuzzy
+Dovré mach litre ASCII (a-z, A-Z), nùmer (0-9), sotlignadure (_) e tratin (-).',
'config-invalid-db-prefix' => 'Prefiss dla base ëd dàit pa bon "$1".
-Dovré mach litre ASCII (a-z, A-Z), nùmer (0-9) e sotlignadure (_).', # Fuzzy
+Dovré mach litre ASCII (a-z, A-Z), nùmer (0-9), sotlignadure (_) e tratin (-).',
'config-connection-error' => "$1.
Controla l'ospitant, lë stranòm d'utent e la ciav sì-sota e prové torna.",
'config-invalid-schema' => 'Schema pa bon për MediaWiki "$1".
Dovré mach litre ASCII (a-z, A-Z), nùmer (0-9) e sotlignadure (_).',
+ 'config-db-sys-create-oracle' => "L'istalador a arconòss mach ij cont SYSDBA durant la creassion d'un cont neuv.",
+ 'config-db-sys-user-exists-oracle' => 'Ël cont utent "$1" a esist già. SYSDBA a peul mach esse dovrà për creé un cont neuv!',
'config-postgres-old' => "A-i é da manca ëd PostgreSQL $1 o pi recent, chiel a l'ha $2.",
'config-sqlite-name-help' => "Serne un nòm ch'a identìfica soa wiki.
Dovré nì dë spassi nì ëd tratin.
@@ -14837,8 +16181,11 @@ Adess a peule [$1 ancaminé a dovré soa wiki].
S'a veul generé torna sò archivi <code>LocalSettings.php</code>, ch'a sgnaca ël boton sì-sota.
Sòn a l'è '''pa arcomandà''' gavà ch'a rancontra dij problema con soa wiki.",
+ 'config-upgrade-done-no-regenerate' => 'Agiornament complet.
+
+It peule adess [$1 ancaminé a dovré toa wiki].',
'config-regenerate' => 'Generé torna LocalSettings.php →',
- 'config-show-table-status' => 'Arcesta SHOW TABLE STATUS falìa!',
+ 'config-show-table-status' => 'Arcesta <code>SHOW TABLE STATUS</code> falìa!',
'config-unknown-collation' => "'''Avis:''' La base ëd dàit a deuvra na classificassion pa arconossùa.",
'config-db-web-account' => "Cont dla base ëd dàit për l'acess a l'aragnà",
'config-db-web-help' => "Ch'a selession-a lë stranòm d'utent e la ciav che ël servent ëd l'aragnà a dovrërà për coleghesse al servent dle base ëd dàit, durant j'operassion ordinarie dla wiki.",
@@ -14849,6 +16196,13 @@ Sòn a l'è '''pa arcomandà''' gavà ch'a rancontra dij problema con soa wiki."
'config-mysql-engine' => 'Motor ëd memorisassion:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-myisam-dep' => "'''Avis''': A l'ha selessionà MyISAM com motor ëd memorisassion për MySQL, che a l'é pa arcomandà da dovré con MediaWiki, përchè:
+* a sopòrta a pen-a la contemporanità për via ëd saradure ëd tàula
+* a l'é pi soget a la corussion che j'àutri motor
+* ël còdes bas ëd MediaWiki pa sempe a gestiss MyISAM com a dovrìa
+
+Se soa istalassion MySQL a manten InnoDB, a l'é fortement arcomandà ch'a serna pitòst col-lì.
+Se soa istalassion MySQL a manten nen InnoDB, a peul esse ch'a sia ël moment ëd n'agiornament.",
'config-mysql-engine-help' => "'''InnoDB''' a l'é scasi sempe la mej opsion, da già ch'a l'ha un bon manteniment dla concorensa.
'''MyISAM''' a peul esse pi lest an instalassion për n'utent sol o mach an letura.
@@ -14873,6 +16227,8 @@ Tùit ij tìtoj ëd pàgina ant cost ëspassi nominal a parto con un sert prefis
Tradissionalment, sto prefiss a l'é derivà dal nòm ëd la wiki, ma a peul pa conten-e caràter ëd pontegiatura coma \"#\" o \":\".",
'config-ns-invalid' => 'Lë spassi nominal specificà "<nowiki>$1</nowiki>" a l\'é pa bon.
Specìfica në spassi nominal ëd proget diferent.',
+ 'config-ns-conflict' => 'Lë spassi nominal specificà "<nowiki>$1</nowiki>" a và contra në spassi nominal predefinì ëd MediaWiki.
+Specìfica në spassi nominal ëd proget diferent.',
'config-admin-box' => "Cont ëd l'Aministrator",
'config-admin-name' => 'Tò nòm:',
'config-admin-password' => 'Ciav:',
@@ -14886,29 +16242,32 @@ Specìfica un nòm utent diferent.',
'config-admin-password-same' => "La ciav a dev nen esse l'istessa ëd lë stranòm d'utent.",
'config-admin-password-mismatch' => "Le doe ciav che a l'ha scrivù a son diferente antra 'd lor.",
'config-admin-email' => 'Adrëssa ëd pòsta eletrònica:',
- 'config-admin-email-help' => "Ch'a anserissa ambelessì n'adrëssa ëd pòsta eletrònica për përmëtt-je d'arsèive ëd mëssagi da d'àutri utent an sla wiki, riamposté soa ciav, e esse anformà ëd camgiament a le pàgine ch'a ten sot-euj.", # Fuzzy
+ 'config-admin-email-help' => "Ch'a anserissa ambelessì n'adrëssa ëd pòsta eletrònica për përmëtt-je d'arsèive ëd mëssagi da d'àutri utent an sla wiki, riamposté soa ciav, e esse anformà dle modìfiche a le pàgine ch'a ten sot-euj. A peule lassé ës camp veuid.",
'config-admin-error-user' => 'Eror antern an creand n\'aministrator con lë stranòm "<nowiki>$1</nowiki>".',
'config-admin-error-password' => 'Eror antern an ampostand na ciav për l\'admin "<nowiki>$1</nowiki>": <pre>$2</pre>',
+ 'config-admin-error-bademail' => "A l'ha butà n'adrëssa ëd pòsta eletrònica pa bon-a.",
'config-subscribe' => "Ch'a sot-scriva la [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce lista ëd discussion ëd j'anonsi ëd publicassion].",
'config-subscribe-help' => "Costa a l'é na lista ëd discussion a bass tràfich dovrà për j'anonsi ëd publicassion, comprèis d'amportant anonsi ëd sicurëssa.
A dovrìa sot-ëscrivla e agiorné soa instalassion mediaWiki quand che ëd version neuve a rivo.",
+ 'config-subscribe-noemail' => "A l'ha provà a abonesse a la lista ëd difusion dij comunicà sensa dé n'adrëssa ëd pòsta eletrònica.
+Për piasì, ch'a fornissa n'adrëssa ëd pòsta eletrònica s'a veul abonesse a la lista ëd pòsta.",
'config-almost-done' => "A l'ha bele che fàit!
A peul adess sauté la configurassion rimanenta e instalé dlongh la wiki.",
'config-optional-continue' => "Ciameme d'àutre chestion.",
'config-optional-skip' => 'I son già anojà, instala mach la wiki.',
'config-profile' => "Profil dij drit d'utent:",
- 'config-profile-wiki' => 'Wiki tradissional',
+ 'config-profile-wiki' => 'Wiki duverta',
'config-profile-no-anon' => 'A venta creé un cont',
'config-profile-fishbowl' => 'Mach editor autorisà',
'config-profile-private' => 'Wiki privà',
'config-profile-help' => "Le wiki a marcio mej quand ch'a lassa che pì përsone possìbij a-j modìfico.
-An MediaWiki, a l'é bel fé revisioné ij cambi recent, e buté andré minca dann che a sia fàit da utent noviss o malissios.
+An MediaWiki, a l'é bel fé revisioné j'ùltime modìfiche, e buté andré qualsëssìa dann che a sia fàit da dj'utent noviss o malissios.
An tùit ij cas, an tanti a l'han trovà che MediaWiki a sia ùtil ant na gran varietà ëd manere, e dle vire a l'é pa bel fé convince cheidun dij vantagi dla wiki.
Parèj a l'ha doe possibilità.
-Un '''{{int:config-profile-wiki}}''' a përmët a chicassìa ëd modifiché, bele sensa intré ant ël sistema.
-Na wiki con '''{{int:config-profile-no-anon}}''' a dà pì 'd contròl, ma a peul slontané dij contribudor casuaj.
+Ël model '''{{int:config-profile-wiki}}''' a përmët a chicassìa ëd modifiché, bele sensa intré ant ël sistema.
+Na wiki con '''{{int:config-profile-no-anon}}''' a dà pì 'd contròl, ma a peul slontané dij contributor ocasionaj.
Ël senari '''{{int:config-profile-fishbowl}}''' a përmët a j'utent aprovà ëd modifiché, ma ël pùblich a peul vëdde le pàgine, comprèisa la stòria.
Un '''{{int:config-profile-private}}''' a përmët mach a j'utent aprovà ëd vëdde le pàgine, con la midema partìa ch'a peul modifiché.
@@ -14917,7 +16276,10 @@ Configurassion ëd drit d'utent pi complicà a son disponìbij apress l'instalas
'config-license' => "Drit d'autor e licensa",
'config-license-none' => 'Gnun-a licensa an nòta an bass',
'config-license-cc-by-sa' => 'Creative Commons atribussion an part uguaj',
+ 'config-license-cc-by' => 'Creative Commons Attribution',
'config-license-cc-by-nc-sa' => 'Creative Commons atribussion nen comersial an part uguaj',
+ 'config-license-cc-0' => 'Creative Commons Zero (domini pùblich)',
+ 'config-license-gfdl' => 'Licensa GNU Free Documentation 1.3 o pi neuva',
'config-license-pd' => 'Domini Pùblich',
'config-license-cc-choose' => 'Selessioné na licensa Creative Commons përsonalisà',
'config-license-help' => "Vàire wiki pùbliche a buto tute le contribussion sota na [http://freedomdefined.org/Definition licensa lìbera]. Sòn a giuta a creé un sens d'apartenensa a la comunità e a ancoragia ëd contribussion ëd longa durà.
@@ -14925,8 +16287,9 @@ A l'é generalment nen necessari për na wiki privà o d'asienda.
S'a veul podèj dovré dij test da Wikipedia, e a veul che Wikipedia a aceta dij test copià da soa wiki, a dovrìa serne '''Creative Commons Attribution Share Alike'''.
-La GNU Free Documentation License a l'era la veja licensa dont sota a-i era Wikipedia.
-A l'é anco' na licensa bon-a, an tùit ij cas, sta licensa a l'ha chèich funsion ch'a rendo difìcij l'utilisassion e l'antërpretassion.", # Fuzzy
+Wikipedia prima a dovrava la GNU Free Documentation License.
+La GDFL a l'é anco' na licensa bon-a, ma a l'é malfé da capila.
+A l'é ëdcò mal fé riutilisé dël contnù licensià sota la GDFL.",
'config-email-settings' => 'Ampostassion ëd pòsta eletrònica',
'config-enable-email' => 'Abilité ij mëssagi ëd pòsta eletrònica an surtìa',
'config-enable-email-help' => "S'a veul che la pòsta eletrònica a marcia, j'[http://www.php.net/manual/en/mail.configuration.php ampostassion ëd pòsta eletrònica PHP] a devo esse configurà për da bin.
@@ -14956,7 +16319,7 @@ Peui ch'a abìlita costa opsion.",
'config-upload-deleted-help' => "ch'a serna un dossié andova goerné j'archivi scancelà.
Idealment, sòn a dovrìa pa esse acessìbil an sl'aragnà.",
'config-logo' => 'Anliura dla marca:',
- 'config-logo-help' => "La pel dë stàndard ëd MediaWiki a comprend lë spassi për na marca ëd 135x160 pontin ant ël canton an àut a snista.
+ 'config-logo-help' => "La pel dë stàndard ëd MediaWiki a comprend lë spassi për na marca ëd 135x160 pontin dzora la lista dla bara lateral.
Ch'a dëscaria na figura ëd la dimension aproprià, e ch'a anserissa l'anliura ambelessì.
S'a veul gnun-e marche, ch'a lassa ës camp bianch.", # Fuzzy
@@ -14978,21 +16341,45 @@ Ij sit da mesan a gròss a son motobin ancoragià a abilité sòn, e ij sit cit
'config-cache-memcached' => "Dovré Memcached (a ciama n'ampostassion e na configurassion adissionaj)",
'config-memcached-servers' => 'Servent Memcached:',
'config-memcached-help' => "Lista d'adrësse IP da dovré për Memcached.
-A dovrìa esse separà con dle vìrgole e specifiché la pòrta da dovré (për esempi: 127.0.0.1:11211, 192.168.1.25:11211).", # Fuzzy
+A dovrìa specifichene un-a për linia e specifiché la pòrta da dovré. Për esempi:
+127.0.0.1:11211
+192.168.1.25:11211",
+ 'config-memcache-needservers' => "A l'ha selessionà Memcached com soa sòrt ëd memorisassion local ma a l'ha specificà gnun servent.",
+ 'config-memcache-badip' => "It l'ha anserì n'adrëssa IP pa bon-a për Memcached: $1.",
+ 'config-memcache-noport' => "A l'ha pa specificà na pòrta da dovré për ël servent Memcached: $1.
+S'a conòsse nen la pòrta, cola predefinìa a l'é 11211.",
+ 'config-memcache-badport' => 'Ij nùmer ëd pòrta ëd Memcached a dovrìo esse tra $1 e $2.',
'config-extensions' => 'Estension',
'config-extensions-help' => "J'estension listà dì-sota a son ëstàite trovà ant sò dossié <code>./extensions</code>.
A peulo avèj da manca ëd configurassion adissionaj, ma a peul abiliteje adess",
'config-install-alreadydone' => "'''Avis''' A smija ch'a l'abie già instalà MediaWiki e ch'a preuva a instalelo torna.
Për piasì, ch'a vada a la pàgina ch'a-i ven.",
+ 'config-install-begin' => "An sgnacand su «{{int:config-continue}}», a anandiërà l'istalassion ëd MediaWiki.
+S'a veul anco' fé dle modìfiche, ch'a sgnaca su «{{int:config-back}}».",
'config-install-step-done' => 'fàit',
'config-install-step-failed' => 'falì',
'config-install-extensions' => "Comprende j'estension",
'config-install-database' => 'Creassion ëd la base ëd dàit',
+ 'config-install-schema' => 'Creassion dë schema',
+ 'config-install-pg-schema-not-exist' => 'Lë schema postgreSQL a esist pa.',
'config-install-pg-schema-failed' => 'Creassion dle tàule falìa.
Sigurte che l\'utent "$1" a peussa scrive lë schema "$2".',
+ 'config-install-pg-commit' => 'Salvé ij cambi.',
+ 'config-install-pg-plpgsql' => 'Contròl dël langagi PL/pgSQL',
+ 'config-pg-no-plpgsql' => 'A dev istalé ël langage PL/pgSQL ant la base ëd dàit $1',
+ 'config-pg-no-create-privs' => "Ël cont ch'a l'ha specificà për l'istalassion a l'ha pa basta 'd privilegi për creé un cont.",
+ 'config-pg-not-in-role' => "Ël cont ch'a l'ha specificà për l'utent ëd la ragnà a esist già.
+Ël cont ch'a l'has specificà për l'istalassion a l'é pa un superutent e a l'é pa un mémber dla partìa dj'utent dla Ragnà, parèj a peul pa creé dj'oget ch'a apartenent a l'utent dla Ragnà.
+
+MediaWiki al moment a ciama che le tàule a sia possedùe da n'utent dla Ragnà. Për piasì, ch'a specìfica n'àutr nòm ëd cont dla Ragnà, o ch'a sgnaca ansima a \"andré\" e ch'a specìfica n'utent ch'a l'ha ij privilegi ch'a basto për l'anstalassion.",
'config-install-user' => "Creassion ëd n'utent ëd la base ëd dàit",
+ 'config-install-user-alreadyexists' => 'L\'utent "$1" a esist già',
+ 'config-install-user-create-failed' => "Faliment ant la creassion ëd l'utent «$1»: $2",
'config-install-user-grant-failed' => 'Falì a dé ij përmess a l\'utent "$1": $2',
+ 'config-install-user-missing' => 'L\'utent specificà "$1" a esist pa.',
+ 'config-install-user-missing-create' => "L'utent specificà «$1» a esist pa.
+Për piasì, ch'a selession-a la casela «cont da creé» sì-sota s'a veul creelo.",
'config-install-tables' => 'Creassion dle tàule',
'config-install-tables-exist' => "'''Avis''': A smija che le tàule ëd mediaWiki a esisto già.
Sauté la creassion.",
@@ -15001,26 +16388,41 @@ Sauté la creassion.",
'config-install-interwiki-list' => "As peul pa trovesse l'archivi <code>interwiki.list</code>.",
'config-install-interwiki-exists' => "'''Avis''': La tàula interwiki a smija ch'a l'abia già dj'element.
Për stàndard, la lista a sarà sautà.",
- 'config-install-keys' => 'Generassion ëd la ciav segreta', # Fuzzy
+ 'config-install-stats' => 'Inissialisassion dle statìstiche',
+ 'config-install-keys' => 'Generassion ëd le ciav segrete',
+ 'config-insecure-keys' => "'''Avis:''' {{PLURAL:$2|Na ciav sigura|Dle ciav sigure}} ($1) generà durant l'istalassion {{PLURAL:$2|a l'é|a son}} pa completament sigure. Ch'a consìdera ëd modifiche{{PLURAL:$2|la|je}} manualment.",
'config-install-sysop' => "Creassion dël cont ëd l'utent aministrator",
+ 'config-install-subscribe-fail' => 'As peul pa sot-scrivse mediawiki-announce: $1',
+ 'config-install-subscribe-notpossible' => "cURL a l'é pa istalà e allow_url_fopen a l'é pa disponìbil.",
+ 'config-install-mainpage' => 'Creassion ëd la pàgina prinsipal con un contnù predefinì',
+ 'config-install-extension-tables' => "Creassion ëd tàule për j'estension abilità",
+ 'config-install-mainpage-failed' => 'As peul pa inserisse la pàgina prinsipal: $1',
'config-install-done' => "'''Congratulassion!'''
A l'ha instalà për da bin mediaWiki.
L'instalador a l'ha generà n'archivi <code>LocalSettings.php</code>.
A conten tuta soa configurassion.
-A dovrà [$1 dëscarielo] e butelo ant la bas ëd l'instalassion ëd soa wiki (ël midem dossié d'index.php).
+A dovrà dëscarielo e butelo ant la bas ëd l'instalassion ëd soa wiki (ël midem dossié d'index.php). La dëscaria a dovrìa esse ancaminà automaticament.
+
+Se la dëscaria a l'é pa disponìbil, o s'a l'ha scancelala, a peul torna ancaminé la dëscaria an sgnacand an sla liura sì-sota:
+
+$3
+
'''Nòta''': S'a lo fa nen adess, cost archivi ëd configurassion generà a sarà pa disponìbil për chiel pi tard s'a chita l'instalassion sensa dëscarielo.
-Quand che a l'é stàit fàit, a peul '''[$2 intré an soa wiki]'''.", # Fuzzy
+Quand che a l'é stàit fàit, a peul '''[$2 intré an soa wiki]'''.",
+ 'config-download-localsettings' => 'Dëscarié <code>LocalSettings.php</code>',
'config-help' => 'agiut',
+ 'config-nofile' => "L'archivi «$1» as treuva nen. A l'é stàit ëscancelà?",
'mainpagetext' => "'''MediaWiki a l'é staita anstalà a la përfession.'''",
'mainpagedocfooter' => "Che a varda la [//meta.wikimedia.org/wiki/Help:Contents User's Guide] për avèj dj'anformassion ant sël coma dovré ël programa dla wiki.
== Për anandiesse a travajé ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista dij paràmeter ëd configurassion]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki Chestion frequente]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista ëd discussion an sla distribussion ëd MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista ëd discussion an sla distribussion ëd MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localisa MediaWiki për toa lenga]",
);
/** Pontic (Ποντιακά)
@@ -15040,7 +16442,7 @@ $messages['prg'] = array(
== En pagaūseņu ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]', # Fuzzy
);
/** Pashto (پښتو)
@@ -15054,7 +16456,7 @@ $messages['ps'] = array(
'config-page-welcome' => 'مېډياويکي ته ښه راغلاست!',
'config-page-name' => 'نوم',
'config-page-options' => 'خوښنې',
- 'config-page-install' => 'لګول',
+ 'config-page-install' => 'لگول',
'config-page-complete' => 'بشپړ!',
'config-env-php' => 'د $1 PHP نصب شو.',
'config-db-type' => 'د توکبنسټ ډول:',
@@ -15067,20 +16469,19 @@ $messages['ps'] = array(
'config-header-postgres' => 'د PostgreSQL امستنې',
'config-header-sqlite' => 'د SQLite امستنې',
'config-header-oracle' => 'د اورېکل امستنې',
- 'config-header-ibm_db2' => 'د IBM DB2 امستنې',
'config-sqlite-readonly' => 'د <code>$1</code> دوتنه د ليکلو وړ نه ده.',
'config-sqlite-cant-create-db' => 'د توکبنسټ دوتنه <code>$1</code> جوړه نه شوه.',
'config-site-name' => 'د ويکي نوم:',
'config-site-name-blank' => 'د وېبځي نوم وليکۍ.',
'config-project-namespace' => 'د پروژې نوم-تشيال:',
'config-ns-generic' => 'پروژه',
- 'config-admin-box' => 'د پازوال ګڼون',
+ 'config-admin-box' => 'د پازوال گڼون',
'config-admin-name' => 'ستاسې نوم:',
'config-admin-password' => 'پټنوم:',
'config-admin-password-confirm' => 'پټنوم يو ځل بيا:',
'config-admin-email' => 'برېښليک پته:',
- 'config-profile-wiki' => 'دوديزه ويکي',
- 'config-license-pd' => 'ټولګړی شپول',
+ 'config-profile-wiki' => 'پرانيستې ويکي',
+ 'config-license-pd' => 'ټولگړی شپول',
'config-email-settings' => 'د برېښليک امستنې',
'config-install-step-done' => 'ترسره شو',
'config-install-tables' => 'لښتيالونه جوړول',
@@ -15091,36 +16492,40 @@ $messages['ps'] = array(
== پيلول ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings د امستنو د سازونې لړليک]
* [//www.mediawiki.org/wiki/Manual:FAQ د ميډياويکي ډېرځليزې پوښتنې]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce د مېډياويکي د برېښليکونو لړليک]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce د مېډياويکي د برېښليکونو لړليک]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources خپلې ژبې لپاره MediaWiki ځايتابول]',
);
/** Portuguese (português)
* @author Crazymadlover
* @author Hamilton Abreu
+ * @author Luckas
* @author Mormegil
* @author Platonides
* @author SandroHc
* @author Waldir
+ * @author 아라
+ * @author 555
*/
$messages['pt'] = array(
'config-desc' => 'O instalador do MediaWiki',
'config-title' => 'Instalação MediaWiki $1',
'config-information' => 'Informação',
'config-localsettings-upgrade' => 'Foi detectado um ficheiro <code>LocalSettings.php</code>.
-Para actualizar esta instalação, por favor introduza o valor de <code>$wgUpgradeKey</code> na caixa abaixo.
-Encontra este valor no LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Foi detectada a existência de um ficheiro LocalSettings.php.
-Para actualizar esta instalação execute o update.php, por favor.',
- 'config-localsettings-key' => 'Chave de actualização:',
+Para atualizar esta instalação, por favor introduza o valor de <code>$wgUpgradeKey</code> na caixa abaixo.
+Encontra este valor no <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Foi detectada a existência de um ficheiro <code>LocalSettings.php</code>.
+Para atualizar esta instalação execute o <code>update.php</code>, por favor.',
+ 'config-localsettings-key' => 'Chave de atualização:',
'config-localsettings-badkey' => 'A chave que forneceu está incorreta.',
'config-upgrade-key-missing' => 'Foi detectada uma instalação existente do MediaWiki.
-Para actualizar esta instalação, por favor coloque a seguinte linha no final do seu LocalSettings.php:
+Para atualizar esta instalação, por favor coloque a seguinte linha no final do seu <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'O ficheiro LocalSettings.php existente parece estar incompleto.
+ 'config-localsettings-incomplete' => 'O ficheiro <code>LocalSettings.php</code> existente parece estar incompleto.
A variável $1 não está definida.
-Por favor defina esta variável no LocalSettings.php e clique "Continuar".',
- 'config-localsettings-connection-error' => 'Ocorreu um erro ao ligar à base de dados usando as configurações especificadas no LocalSettings.php ou AdminSettings.php. Por favor corrija essas configurações e tente novamente.
+Por favor defina esta variável no <code>LocalSettings.php</code> e clique "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Ocorreu um erro ao ligar à base de dados usando as configurações especificadas no <code>LocalSettings.php</code> ou <code>AdminSettings.php</code>. Por favor corrija essas configurações e tente novamente.
$1',
'config-session-error' => 'Erro ao iniciar a sessão: $1',
@@ -15129,17 +16534,17 @@ As sessões estão configuradas para uma duração de $1.
Pode aumentar esta duração configurando <code>session.gc_maxlifetime</code> no php.ini.
Reinicie o processo de instalação.',
'config-no-session' => 'Os seus dados de sessão foram perdidos!
-Verifique o seu php.ini e certifique-se de que em <code>session.save_path</code> está definido um directório apropriado.',
+Verifique o seu php.ini e certifique-se de que em <code>session.save_path</code> está definido um diretório apropriado.',
'config-your-language' => 'A sua língua:',
- 'config-your-language-help' => 'Seleccione a língua que será usada durante o processo de instalação.',
+ 'config-your-language-help' => 'Selecione a língua que será usada durante o processo de instalação.',
'config-wiki-language' => 'Língua da wiki:',
- 'config-wiki-language-help' => 'Seleccione a língua que será predominante na wiki.',
+ 'config-wiki-language-help' => 'Selecione a língua que será predominante na wiki.',
'config-back' => '← Voltar',
'config-continue' => 'Continuar →',
'config-page-language' => 'Língua',
'config-page-welcome' => 'Bem-vindo(a) ao MediaWiki!',
'config-page-dbconnect' => 'Ligar à base de dados',
- 'config-page-upgrade' => 'Actualizar a instalação existente',
+ 'config-page-upgrade' => 'Atualizar a instalação existente',
'config-page-dbsettings' => 'Configurações da base de dados',
'config-page-name' => 'Nome',
'config-page-options' => 'Opções',
@@ -15149,13 +16554,13 @@ Verifique o seu php.ini e certifique-se de que em <code>session.save_path</code>
'config-page-readme' => 'Leia-me',
'config-page-releasenotes' => 'Notas de lançamento',
'config-page-copying' => 'A copiar',
- 'config-page-upgradedoc' => 'A actualizar',
+ 'config-page-upgradedoc' => 'Atualizando',
'config-page-existingwiki' => 'Wiki existente',
'config-help-restart' => 'Deseja limpar todos os dados gravados que introduziu e reiniciar o processo de instalação?',
'config-restart' => 'Sim, reiniciar',
'config-welcome' => '=== Verificações do ambiente ===
São realizadas verificações básicas para determinar se este ambiente é apropriado para instalação do MediaWiki.
-Se necessitar de pedir ajuda durante a instalação, deve fornecer os resultados destas verificações.',
+Se necessitar de pedir ajuda durante a instalação, deve fornecer os resultados destas verificações.', # Fuzzy
'config-copyright' => "=== Direitos de autor e Condições de uso ===
$1
@@ -15182,35 +16587,35 @@ Não pode instalar o MediaWiki.',
'config-env-php' => 'O PHP $1 está instalado.',
'config-env-php-toolow' => 'O PHP $1 está instalado.
No entanto, o MediaWiki requer o PHP $2 ou superior.',
- 'config-unicode-using-utf8' => 'A usar o utf8_normalize.so, por Brian Viper, para a normalização Unicode.',
+ 'config-unicode-using-utf8' => 'A usar o utf8_normalize.so, por Brion Vibber, para a normalização Unicode.',
'config-unicode-using-intl' => 'A usar a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.',
- 'config-unicode-pure-php-warning' => "'''Aviso''': A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efectuar a normalização Unicode. Irá recorrer-se à implementação em PHP puro, que é mais lenta.
+ 'config-unicode-pure-php-warning' => "'''Aviso''': A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode. Irá recorrer-se à implementação em PHP puro, que é mais lenta.
Se o seu site tem alto volume de tráfego, devia informar-se um pouco sobre a [//www.mediawiki.org/wiki/Unicode_normalization_considerations/pt normalização Unicode].",
- 'config-unicode-update-warning' => "'''Aviso''': A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://site.icu-project.org/ projecto ICU].
-Devia [//www.mediawiki.org/wiki/Unicode_normalization_considerations actualizá-la] se tem quaisquer preocupações sobre o uso do Unicode.",
+ 'config-unicode-update-warning' => "'''Aviso''': A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://site.icu-project.org/ projeto ICU].
+Devia [//www.mediawiki.org/wiki/Unicode_normalization_considerations atualizá-la] se tem quaisquer preocupações sobre o uso do Unicode.",
'config-no-db' => "Não foi possível encontrar um controlador ''(driver)'' apropriado para a base de dados! Precisa de instalar um controlador para o PHP. São aceites os seguintes tipos de base de dados: $1.
Se usa alojamento partilhado, peça ao fornecedor do alojamento para instalar um controlador apropriado.
-Se foi você quem compilou o PHP, reconfigure-o com um cliente de base de dados activado; por exemplo, usando <code>./configure --with-mysql</code>.
+Se foi você quem compilou o PHP, reconfigure-o com um cliente de base de dados ativado; por exemplo, usando <code>./configure --with-mysql</code>.
Se instalou o PHP a partir de um pacote Debian ou Ubuntu, então precisa de instalar também o módulo php5-mysql.",
'config-outdated-sqlite' => "'''Aviso''': Tem a versão $1 do SQLite, que é anterior à versão mínima necessária, a $2. O SQLite não estará disponível.",
'config-no-fts3' => "'''Aviso''': O SQLite foi compilado sem o módulo [//sqlite.org/fts3.html FTS3]; as funcionalidades de pesquisa não estarão disponíveis nesta instalação.",
- 'config-register-globals' => "'''Aviso: A opção <code>[http://php.net/register_globals register_globals]</code> do PHP está activada.'''
-'''Desactive-a, se puder.'''
+ 'config-register-globals' => "'''Aviso: A opção <code>[http://php.net/register_globals register_globals]</code> do PHP está ativada.'''
+'''Desative-a, se puder.'''
O MediaWiki funciona mesmo assim, mas o seu servidor está exposto a potenciais vulnerabilidades de segurança.",
- 'config-magic-quotes-runtime' => "'''Fatal: A opção [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] está activada!'''
+ 'config-magic-quotes-runtime' => "'''Fatal: A opção [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] está ativada!'''
Esta opção causa corrupção dos dados de entrada, de uma forma imprevisível.
-Não pode instalar ou usar o MediaWiki a menos que esta opção seja desactivada.",
- 'config-magic-quotes-sybase' => "'''Fatal: A opção [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] está activada!'''
+Não pode instalar ou usar o MediaWiki a menos que esta opção seja desativada.",
+ 'config-magic-quotes-sybase' => "'''Fatal: A opção [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] está ativada!'''
Esta opção causa corrupção dos dados de entrada, de uma forma imprevisível.
-Não pode instalar ou usar o MediaWiki a menos que esta opção seja desactivada.",
- 'config-mbstring' => "'''Fatal: A opção [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] está activada!'''
+Não pode instalar ou usar o MediaWiki a menos que esta opção seja desativada.",
+ 'config-mbstring' => "'''Fatal: A opção [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] está ativada!'''
Esta opção causa erros e pode corromper os dados de uma forma imprevisível.
-Não pode instalar ou usar o MediaWiki a menos que esta opção seja desactivada.",
- 'config-ze1' => "'''Fatal: A opção [http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode] está activada!'''
+Não pode instalar ou usar o MediaWiki a menos que esta opção seja desativada.",
+ 'config-ze1' => "'''Fatal: A opção [http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode] está ativada!'''
Esta opção causa problemas significativos no MediaWiki.
-Não pode instalar ou usar o MediaWiki a menos que esta opção seja desactivada.",
- 'config-safe-mode' => "'''Aviso:''' O [http://www.php.net/features.safe-mode safe mode] do PHP está activo.
+Não pode instalar ou usar o MediaWiki a menos que esta opção seja desativada.",
+ 'config-safe-mode' => "'''Aviso:''' O [http://www.php.net/features.safe-mode safe mode] do PHP está ativo.
Este modo pode causar problemas, especialmente no upload de ficheiros e no suporte a <code>math</code>.",
'config-xml-bad' => 'Falta o módulo XML do PHP.
O MediaWiki necessita de funções deste módulo e não funcionará com esta configuração.
@@ -15218,7 +16623,7 @@ Se está a executar o Mandrake, instale o pacote php-xml.',
'config-pcre' => 'Parece faltar o módulo de suporte PCRE.
Para funcionar, o MediaWiki necessita das funções de expressões regulares compatíveis com Perl.',
'config-pcre-no-utf8' => "'''Fatal''': O módulo PCRE do PHP parece ter sido compilado sem suporte PCRE_UTF8.
-O MediaWiki necessita do suporte UTF-8 para funcionar correctamente.",
+O MediaWiki necessita do suporte UTF-8 para funcionar corretamente.",
'config-memory-raised' => 'A configuração <code>memory_limit</code> do PHP era $1; foi aumentada para $2.',
'config-memory-bad' => "'''Aviso:''' A configuração <code>memory_limit</code> do PHP é $1.
Isto é provavelmente demasiado baixo.
@@ -15228,31 +16633,31 @@ A instalação poderá falhar!",
'config-apc' => '[http://www.php.net/apc APC] instalada',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] instalada',
'config-no-cache' => "'''Aviso:''' Não foi possível encontrar: [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache], nem [http://www.iis.net/download/WinCacheForPhp WinCache].
-A cache de objectos não será activada.",
+A cache de objetos não está ativada.",
'config-mod-security' => "'''Aviso''': O seu servidor de internet tem o [http://modsecurity.org/ mod_security] ativado. Se este estiver mal configurado, pode causar problemas ao MediaWiki ou a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.
Consulte a [http://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.",
'config-diff3-bad' => 'O GNU diff3 não foi encontrado.',
'config-imagemagick' => 'Foi encontrado o ImageMagick: <code>$1</code>.
-Se possibilitar uploads, a miniaturização de imagens será activada.',
+Se possibilitar uploads, a miniaturização de imagens será ativada.',
'config-gd' => 'Foi encontrada a biblioteca gráfica GD.
-Se possibilitar uploads, a miniaturização de imagens será activada.',
+Se possibilitar uploads, a miniaturização de imagens será ativada.',
'config-no-scaling' => 'Não foi encontrada a biblioteca gráfica GD nem o ImageMagick.
-A miniaturização de imagens será desactivada.',
- 'config-no-uri' => "'''Erro:''' Não foi possível determinar a URI actual.
+A miniaturização de imagens será desativada.',
+ 'config-no-uri' => "'''Erro:''' Não foi possível determinar a URI atual.
A instalação foi abortada.",
'config-no-cli-uri' => "'''Aviso''': Não foi especificado um --scriptpath; por omissão, será usado: <code>$1</code>.",
'config-using-server' => 'Será usado o nome do servidor "<nowiki>$1</nowiki>".',
'config-using-uri' => 'Será usada a URL do servidor "<nowiki>$1$2</nowiki>".',
- 'config-uploads-not-safe' => "'''Aviso:''' O directório por omissão para uploads <code>$1</code>, está vulnerável à execução arbitrária de scripts.
+ 'config-uploads-not-safe' => "'''Aviso:''' O diretório por omissão para uploads <code>$1</code>, está vulnerável à execução arbitrária de scripts.
Embora o MediaWiki verifique a existência de ameaças de segurança em todos os ficheiros enviados, é altamente recomendado que [//www.mediawiki.org/wiki/Manual:Security#Upload_security vede esta vulnerabilidade de segurança] antes de possibilitar uploads.",
- 'config-no-cli-uploads-check' => "'''Aviso:''' O directório por omissão para uploads, <code>\$1</code>, não é verificado para determinar se é vulnerável à execução de código arbitrário durante a instalação por CLI (\"Command-line Interface\").",
+ 'config-no-cli-uploads-check' => "'''Aviso:''' O diretório por omissão para uploads, <code>\$1</code>, não é verificado para determinar se é vulnerável à execução de código arbitrário durante a instalação por CLI (\"Command-line Interface\").",
'config-brokenlibxml' => 'O seu sistema tem uma combinação de versões de PHP e libxml2 conhecida por ser problemática, podendo causar corrupção de dados no MediaWiki e outras aplicações da internet.
-Actualize para o PHP versão 5.2.9 ou posterior e libxml2 versão 2.7.3 ou posterior ([//bugs.php.net/bug.php?id=45996 incidência reportada no PHP]).
+Atualize para o PHP versão 5.2.9 ou posterior e libxml2 versão 2.7.3 ou posterior ([//bugs.php.net/bug.php?id=45996 incidência reportada no PHP]).
Instalação interrompida.',
'config-using531' => 'O MediaWiki não pode ser usado com o PHP $1 devido a um problema que envolve parâmetros de referência para <code>__call()</code>.
-Para resolver este problema, actualize o PHP para a versão 5.3.2 ou posterior, ou reverta-o para a 5.3.0.
+Para resolver este problema, atualize o PHP para a versão 5.3.2 ou posterior, ou reverta-o para a 5.3.0.
Instalação interrompida.',
- 'config-suhosin-max-value-length' => 'O Suhosin está instalado e limita a $1 bytes o comprimento do parâmetro GET. O componente ResourceLoader do MediaWiki pode tornear este limite, mas prejudicando o desempenho. Se lhe for possível, deve atribuir o valor 1024 ou maior ao parâmetro suhosin.get.max_value_length no ficheiro php.ini, e definir o mesmo valor para $wgResourceLoaderMaxQueryLength no ficheiro LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'O Suhosin está instalado e limita a $1 bytes o comprimento do parâmetro GET. O componente ResourceLoader do MediaWiki pode tornear este limite, mas prejudicando o desempenho. Se lhe for possível, deve atribuir o valor 1024 ou maior ao parâmetro <code>suhosin.get.max_value_length</code> no ficheiro <code>php.ini</code>, e definir o mesmo valor para <code>$wgResourceLoaderMaxQueryLength</code> no ficheiro LocalSettings.php.', # Fuzzy
'config-db-type' => 'Tipo da base de dados:',
'config-db-host' => 'Servidor da base de dados:',
'config-db-host-help' => 'Se a base de dados estiver num servidor separado, introduza aqui o nome ou o endereço IP desse servidor.
@@ -15273,9 +16678,9 @@ Se estiver a usar um servidor partilhado, o fornecedor do alojamento deve poder
'config-db-name-oracle' => "Esquema ''(schema)'' da base de dados:",
'config-db-account-oracle-warn' => "Há três cenários suportados na instalação do servidor de base de dados Oracle:
-Se pretende criar a conta de acesso pela internet na base de dados durante o processo de instalação, forneça como conta para a instalação uma conta com o papel de SYSDBA na base de dados e especifique as credenciais desejadas para a conta de acesso pela internet. Se não pretende criar a conta de acesso pela internet durante a instalação, pode criá-la manualmente e fornecer só essa conta para a instalação (se ela tiver as permissões necessárias para criar os objectos do esquema ''(schema)''). A terceira alternativa é fornecer duas contas diferentes; uma com privilégios de criação e outra com privilégios limitados para o acesso pela internet.
+Se pretende criar a conta de acesso pela internet na base de dados durante o processo de instalação, forneça como conta para a instalação uma conta com o papel de SYSDBA na base de dados e especifique as credenciais desejadas para a conta de acesso pela internet. Se não pretende criar a conta de acesso pela internet durante a instalação, pode criá-la manualmente e fornecer só essa conta para a instalação (se ela tiver as permissões necessárias para criar os objetos do esquema ''(schema)''). A terceira alternativa é fornecer duas contas diferentes; uma com privilégios de criação e outra com privilégios limitados para o acesso pela internet.
-Existe um script para criação de uma conta com os privilégios necessários no directório \"maintenance/oracle/\" desta instalação. Mantenha em mente que usar uma conta com privilégios limitados impossibilita todas as operações de manutenção com a conta padrão.",
+Existe um script para criação de uma conta com os privilégios necessários no diretório \"maintenance/oracle/\" desta instalação. Mantenha em mente que usar uma conta com privilégios limitados impossibilita todas as operações de manutenção com a conta padrão.",
'config-db-install-account' => 'Conta do utilizador para a instalação',
'config-db-username' => 'Nome do utilizador da base de dados:',
'config-db-password' => 'Palavra-chave do utilizador da base de dados:',
@@ -15306,18 +16711,18 @@ mas não lhe permitirá armazenar caracteres acima do [//en.wikipedia.org/wiki/M
'config-mysql-old' => 'É necessário o MySQL $1 ou posterior; tem a versão $2.',
'config-db-port' => 'Porta da base de dados:',
'config-db-schema' => "Esquema ''(schema)'' do MediaWiki",
- 'config-db-schema-help' => 'Normalmente, este esquema ("schema") estará correcto.
+ 'config-db-schema-help' => 'Normalmente, este esquema estará correto.
Altere-o só se souber que precisa de o fazer.',
'config-pg-test-error' => "Não foi possível criar uma ligação à base de dados '''$1''': $2",
- 'config-sqlite-dir' => 'Directório de dados do SQLite:',
+ 'config-sqlite-dir' => 'Diretório de dados do SQLite:',
'config-sqlite-dir-help' => "O SQLite armazena todos os dados num único ficheiro.
-Durante a instalação, o servidor de internet precisa de ter permissão de escrita no directório que especificar.
+Durante a instalação, o servidor de internet precisa de ter permissão de escrita no diretório que especificar.
-Este directório '''não''' deve poder ser acedido directamente da internet, por isso está a ser colocado onde estão os seus ficheiros PHP.
+Este diretório '''não''' deve poder ser acedido diretamente da internet, por isso está a ser colocado onde estão os seus ficheiros PHP.
-Juntamente com o directório, o instalador irá criar um ficheiro <code>.htaccess</code>, mas se esta operação falhar é possível que alguém venha a ter acesso directo à base de dados.
-Isto inclui acesso aos dados dos utilizadores (endereços de correio electrónico, palavras-chave encriptadas), às revisões eliminadas e a outros dados de acesso restrito na wiki.
+Juntamente com o diretório, o instalador irá criar um ficheiro <code>.htaccess</code>, mas se esta operação falhar é possível que alguém venha a ter acesso direto à base de dados.
+Isto inclui acesso aos dados dos utilizadores (endereços de correio eletrónico, palavras-chave encriptadas), às revisões eliminadas e a outros dados de acesso restrito na wiki.
Considere colocar a base de dados num local completamente diferente, como, por exemplo, em <code>/var/lib/mediawiki/asuawiki</code>.",
'config-oracle-def-ts' => 'Tablespace padrão:',
@@ -15326,28 +16731,25 @@ Considere colocar a base de dados num local completamente diferente, como, por e
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'O MediaWiki suporta as seguintes plataformas de base de dados:
$1
-Se a plataforma que pretende usar não está listada abaixo, siga as instruções nos links acima para activar o suporte.',
+Se a plataforma que pretende usar não está listada abaixo, siga as instruções nos links acima para ativar o suporte.',
'config-support-mysql' => '* $1 é a plataforma primária do MediaWiki e a melhor suportada ([http://www.php.net/manual/en/mysql.installation.php como compilar PHP com suporte MySQL])',
'config-support-postgres' => '* $1 é uma plataforma de base de dados comum, de fonte aberta, alternativa ao MySQL ([http://www.php.net/manual/en/pgsql.installation.php como compilar PHP com suporte PostgreSQL]). Poderão existir alguns pequenos problemas e não é recomendado o seu uso em ambientes de exploração/produção.',
'config-support-sqlite' => '* $1 é uma plataforma de base de dados ligeira muito bem suportada. ([http://www.php.net/manual/en/pdo.installation.php Como compilar PHP com suporte SQLite], usa PDO)',
'config-support-oracle' => '* $1 é uma base de dados de uma empresa comercial. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])',
- 'config-support-ibm_db2' => '* $1 é uma base de dados empresarial.',
'config-header-mysql' => 'Definições MySQL',
'config-header-postgres' => 'Definições PostgreSQL',
'config-header-sqlite' => 'Definições SQLite',
'config-header-oracle' => 'Definições Oracle',
- 'config-header-ibm_db2' => 'Configurações da IBM DB2',
'config-invalid-db-type' => 'O tipo de base de dados é inválido',
'config-missing-db-name' => 'Tem de introduzir um valor para "Nome da base de dados"',
'config-missing-db-host' => 'Tem de introduzir um valor para "Servidor da base de dados"',
'config-missing-db-server-oracle' => 'Tem de introduzir um valor para "TNS da base de dados"',
'config-invalid-db-server-oracle' => 'O TNS da base de dados, "$1", é inválido.
-Use só letras (a-z, A-Z), algarismos (0-9), sublinhados (_) e pontos (.) dos caracteres ASCII.',
+Use só letras (a-z, A-Z), algarismos (0-9), sublinhados (_) e pontos (.) dos caracteres ASCII.', # Fuzzy
'config-invalid-db-name' => 'O nome da base de dados, "$1", é inválido.
Use só letras (a-z, A-Z), algarismos (0-9), sublinhados (_) e hífens (-) dos caracteres ASCII.',
'config-invalid-db-prefix' => 'O prefixo da base de dados, "$1", é inválido.
@@ -15363,51 +16765,51 @@ Use só letras (a-z, A-Z), algarismos (0-9) e sublinhados (_) dos caracteres ASC
'config-sqlite-name-help' => 'Escolha o nome que identificará a sua wiki.
Não use espaços ou hífens.
Este nome será usado como nome do ficheiro de dados do SQLite.',
- 'config-sqlite-parent-unwritable-group' => 'Não é possível criar o directório de dados <code><nowiki>$1</nowiki></code>, porque o servidor de internet não tem permissão de escrita no directório que o contém <code><nowiki>$2</nowiki></code>.
+ 'config-sqlite-parent-unwritable-group' => 'Não é possível criar o diretório de dados <code><nowiki>$1</nowiki></code>, porque o servidor de internet não tem permissão de escrita no diretório que o contém <code><nowiki>$2</nowiki></code>.
O instalador determinou em que nome de utilizador o seu servidor de internet está a correr.
-Para continuar, configure o directório <code><nowiki>$3</nowiki></code> para poder ser escrito por este utilizador.
+Para continuar, configure o diretório <code><nowiki>$3</nowiki></code> para poder ser escrito por este utilizador.
Para fazê-lo em sistemas Unix ou Linux, use:
<pre>cd $2
mkdir $3
chgrp $4 $3
chmod g+w $3</pre>',
- 'config-sqlite-parent-unwritable-nogroup' => 'Não é possível criar o directório de dados <code><nowiki>$1</nowiki></code>, porque o servidor de internet não tem permissão de escrita no directório que o contém <code><nowiki>$2</nowiki></code>.
+ 'config-sqlite-parent-unwritable-nogroup' => 'Não é possível criar o diretório de dados <code><nowiki>$1</nowiki></code>, porque o servidor de internet não tem permissão de escrita no diretório que o contém <code><nowiki>$2</nowiki></code>.
Não foi possível determinar em que nome de utilizador o seu servidor de internet está a correr.
-Para continuar, configure o directório <code><nowiki>$3</nowiki></code> para que este possa ser globalmente escrito por esse utilizador (e por outros!).
+Para continuar, configure o diretório <code><nowiki>$3</nowiki></code> para que este possa ser globalmente escrito por esse utilizador (e por outros!).
Para fazê-lo em sistemas Unix ou Linux, use:
<pre>cd $2
mkdir $3
chmod a+w $3</pre>',
- 'config-sqlite-mkdir-error' => 'Ocorreu um erro ao criar o directório de dados "$1".
+ 'config-sqlite-mkdir-error' => 'Ocorreu um erro ao criar o diretório de dados "$1".
Verifique a localização e tente novamente.',
- 'config-sqlite-dir-unwritable' => 'Não foi possível escrever no directório "$1".
+ 'config-sqlite-dir-unwritable' => 'Não foi possível escrever no diretório "$1".
Altere as permissões para que ele possa ser escrito pelo servidor de internet e tente novamente.',
'config-sqlite-connection-error' => '$1.
-Verifique o directório de dados e o nome da base de dados abaixo e tente novamente.',
+Verifique o diretório de dados e o nome da base de dados abaixo e tente novamente.',
'config-sqlite-readonly' => 'Não é possivel escrever no ficheiro <code>$1</code>.',
'config-sqlite-cant-create-db' => 'Não foi possível criar o ficheiro da base de dados <code>$1</code>.',
'config-sqlite-fts3-downgrade' => 'O PHP não tem suporte FTS3; a reverter o esquema das tabelas para o anterior',
'config-can-upgrade' => "Esta base de dados contém tabelas do MediaWiki.
-Para actualizá-las para o MediaWiki $1, clique '''Continuar'''.",
- 'config-upgrade-done' => "Actualização terminada.
+Para atualizá-las para o MediaWiki $1, clique '''Continuar'''.",
+ 'config-upgrade-done' => "Atualização terminada.
Agora pode [$1 começar a usar a sua wiki].
Se quiser regenerar o seu ficheiro <code>LocalSettings.php</code>, clique o botão abaixo.
Esta operação '''não é recomendada''' a menos que esteja a ter problemas com a sua wiki.",
- 'config-upgrade-done-no-regenerate' => 'Actualização terminada.
+ 'config-upgrade-done-no-regenerate' => 'Atualização terminada.
Agora pode [$1 começar a usar a sua wiki].',
'config-regenerate' => 'Regenerar o LocalSettings.php →',
- 'config-show-table-status' => 'A consulta SHOW TABLE STATUS falhou!',
+ 'config-show-table-status' => 'A consulta <code>SHOW TABLE STATUS</code> falhou!',
'config-unknown-collation' => "'''Aviso:''' A base de dados está a utilizar uma colação ''(collation)'' desconhecida.",
'config-db-web-account' => 'Conta na base de dados para acesso pela internet',
- 'config-db-web-help' => 'Seleccione o nome de utilizador e a palavra-chave que o servidor de internet irá utilizar para aceder ao servidor da base de dados, durante a operação normal da wiki.',
+ 'config-db-web-help' => 'Selecione o nome de utilizador e a palavra-chave que o servidor de internet irá utilizar para aceder ao servidor da base de dados, durante a operação normal da wiki.',
'config-db-web-account-same' => 'Usar a mesma conta usada na instalação',
'config-db-web-create' => 'Criar a conta se ainda não existir',
'config-db-web-no-create-privs' => 'A conta que especificou para a instalação não tem privilégios suficientes para criar uma conta.
@@ -15415,13 +16817,13 @@ A conta que especificar aqui já tem de existir.',
'config-mysql-engine' => 'Motor de armazenamento:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
- 'config-mysql-myisam-dep' => "'''Aviso''': Seleccionou o MyISAM para motor de armazenamento do MySQL, uma combinação desaconselhada para usar com o MediaWiki porque:
+ 'config-mysql-myisam-dep' => "'''Aviso''': Selecionou o MyISAM para motor de armazenamento do MySQL, uma combinação desaconselhada para usar com o MediaWiki porque:
* praticamente não permite acessos simultâneos, devido aos bloqueios de tabelas
-* o MyISAM é mais susceptível a perdas da integridade dos dados do que outros motores
+* o MyISAM é mais suscetível a perdas da integridade dos dados do que outros motores
* o código do MediaWiki não trabalha devidamente com o MyISAM
Se a sua instalação do MySQL suporta InnoDB, é altamente recomendado que o escolha em vez do MyISAM.
-Se não suporta o InnoDB, talvez esta seja uma boa altura para fazer a actualização para a versão mais recente do MySQL.",
+Se não suporta o InnoDB, talvez esta seja uma boa altura para fazer a atualização para a versão mais recente do MySQL.",
'config-mysql-engine-help' => "'''InnoDB''' é quase sempre a melhor opção, porque suporta bem acessos simultâneos ''(concurrency)''.
'''MyISAM''' pode ser mais rápido no modo de utilizador único ou em instalações somente para leitura.
@@ -15434,22 +16836,21 @@ Isto é mais eficiente do que o modo UTF-8 do MySQL e permite que sejam usados t
No modo '''UTF-8''', o MySQL saberá em que conjunto de caracteres os seus dados estão e pode apresentá-los e convertê-los da forma mais adequada,
mas não lhe permitirá armazenar caracteres acima do [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Plano Multilingue Básico].",
- 'config-ibm_db2-low-db-pagesize' => "A sua base de dados DB2 tem um tablespace padrão com um pagesize insuficiente. O pagesize tem de ser '''32K'' ou maior.",
'config-site-name' => 'Nome da wiki:',
'config-site-name-help' => 'Este nome aparecerá no título da janela do seu browser e em vários outros sítios.',
'config-site-name-blank' => 'Introduza o nome do site.',
- 'config-project-namespace' => 'Espaço nominal do projecto:',
- 'config-ns-generic' => 'Projecto',
+ 'config-project-namespace' => 'Espaço nominal do projeto:',
+ 'config-ns-generic' => 'Projeto',
'config-ns-site-name' => 'O mesmo que o nome da wiki: $1',
'config-ns-other' => 'Outro (especifique)',
'config-ns-other-default' => 'AMinhaWiki',
- 'config-project-namespace-help' => 'Seguindo o exemplo da Wikipedia, muitas wikis mantêm as páginas das suas normas e políticas, separadas das páginas de conteúdo, num "\'\'\'espaço nominal do projecto\'\'\'".
+ 'config-project-namespace-help' => 'Seguindo o exemplo da Wikipedia, muitas wikis mantêm as páginas das suas normas e políticas, separadas das páginas de conteúdo, num "\'\'\'espaço nominal do projeto\'\'\'".
Todos os nomes das páginas neste espaço nominal começam com um determinado prefixo, que pode especificar aqui.
Tradicionalmente, este prefixo deriva do nome da wiki, mas não pode conter caracteres de pontuação, como "#" ou ":".',
'config-ns-invalid' => 'O espaço nominal especificado "<nowiki>$1</nowiki>" é inválido.
-Introduza um espaço nominal de projecto diferente.',
+Introduza um espaço nominal de projeto diferente.',
'config-ns-conflict' => 'O espaço nominal que especificou, "<nowiki>$1</nowiki>", cria um conflito com um dos espaços nominais padrão do MediaWiki.
-Especifique um espaço nominal do projecto diferente.',
+Especifique um espaço nominal do projeto diferente.',
'config-admin-box' => 'Conta de administrador',
'config-admin-name' => 'O seu nome:',
'config-admin-password' => 'Palavra-chave:',
@@ -15469,7 +16870,7 @@ Introduza um nome de utilizador diferente.',
'config-admin-error-bademail' => 'Introduziu um correio electrónico inválido',
'config-subscribe' => 'Subscreva a [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce lista de divulgação de anúncios de lançamento].',
'config-subscribe-help' => 'Esta é uma lista de divulgação de baixo volume para anúncios de lançamento de versões novas, incluindo anúncios de segurança importantes.
-Deve subscrevê-la e actualizar a sua instalação MediaWiki quando são lançadas versões novas.',
+Deve subscrevê-la e atualizar a sua instalação MediaWiki quando são lançadas versões novas.',
'config-subscribe-noemail' => 'Tentou subscrever a lista de divulgação dos anúncios de novas versões, sem fornecer um endereço de correio electrónico.
Para subscrever esta lista de divulgação tem de fornecer um endereço de correio electrónico.',
'config-almost-done' => 'Está quase a terminar!
@@ -15477,7 +16878,7 @@ Agora pode saltar as configurações restantes e instalar já a wiki.',
'config-optional-continue' => 'Faz-me mais perguntas.',
'config-optional-skip' => 'Já estou aborrecido, instala lá a wiki.',
'config-profile' => 'Perfil de permissões:',
- 'config-profile-wiki' => 'Wiki tradicional',
+ 'config-profile-wiki' => 'Wiki tradicional', # Fuzzy
'config-profile-no-anon' => 'Criação de conta exigida',
'config-profile-fishbowl' => 'Somente utilizadores autorizados',
'config-profile-private' => 'Wiki privada',
@@ -15493,7 +16894,7 @@ Uma wiki com '''{{int:config-profile-no-anon}}''' atribui mais responsabilidade,
Um cenário '''{{int:config-profile-fishbowl}}''' permite que os utilizadores aprovados editem, mas que o público visione as páginas, incluindo o historial das mesmas.
Uma '''{{int:config-profile-private}}''' só permite que os utilizadores aprovados visionem as páginas e as editem.
-Após a instalação, estarão disponíveis mais configurações de privilégios. Consulte [//www.mediawiki.org/wiki/Manual:User_rights a entrada relevante no Manual].",
+Após a instalação, estarão disponíveis mais configurações de privilégios. Consulte [//www.mediawiki.org/wiki/Manual:User_rights a entrada relevante no Manual].", # Fuzzy
'config-license' => 'Direitos de autor e licença:',
'config-license-none' => 'Sem rodapé com a licença',
'config-license-cc-by-sa' => 'Creative Commons - Atribuição - Partilha nos Mesmos Termos',
@@ -15502,7 +16903,7 @@ Após a instalação, estarão disponíveis mais configurações de privilégios
'config-license-cc-0' => 'Creative Commons Zero (Domínio Público)',
'config-license-gfdl' => 'GNU Free Documentation License 1.3 ou posterior',
'config-license-pd' => 'Domínio Público',
- 'config-license-cc-choose' => 'Seleccione uma licença personalizada da Creative Commons',
+ 'config-license-cc-choose' => 'Selecione uma licença personalizada Creative Commons',
'config-license-help' => 'Muitas wikis de acesso público licenciam todas as colaborações com uma [http://freedomdefined.org/Definition licença livre].
Isto ajuda a criar um sentido de propriedade da comunidade e encoraja as colaborações a longo prazo.
Tal não é geralmente necessário nas wikis privadas ou corporativas.
@@ -15513,19 +16914,19 @@ A licença anterior da Wikipédia era a licença GNU Free Documentation License.
A GFDL é uma licença válida, mas de difícil compreensão.
Também é difícil reutilizar conteúdos licenciados com a GFDL.',
'config-email-settings' => 'Definições do correio electrónico',
- 'config-enable-email' => 'Activar mensagens electrónicas de saída',
- 'config-enable-email-help' => 'Se quer que o correio electrónico funcione, as [http://www.php.net/manual/en/mail.configuration.php definições de correio electrónico do PHP] têm de estar configuradas correctamente.
-Se não pretende viabilizar qualquer funcionalidade de correio electrónico, pode desactivá-lo aqui.',
- 'config-email-user' => 'Activar mensagens electrónicas entre utilizadores',
- 'config-email-user-help' => 'Permitir que todos os utilizadores troquem entre si mensagens de correio electrónico, se tiverem activado esta funcionalidade nas suas preferências.',
- 'config-email-usertalk' => 'Activar notificações de alterações à página de discussão dos utilizadores',
- 'config-email-usertalk-help' => 'Permitir que os utilizadores recebam notificações de alterações à sua página de discussão, se tiverem activado esta funcionalidade nas suas preferências.',
- 'config-email-watchlist' => 'Activar notificação de alterações às páginas vigiadas',
- 'config-email-watchlist-help' => 'Permitir que os utilizadores recebam notificações de alterações às suas páginas vigiadas, se tiverem activado esta funcionalidade nas suas preferências.',
- 'config-email-auth' => 'Activar autenticação do correio electrónico',
- 'config-email-auth-help' => "Se esta opção for activada, os utilizadores têm de confirmar o seu endereço de correio electrónico usando um link que lhes é enviado sempre que o definirem ou alterarem.
-Só os endereços de correio electrónico autenticados podem receber mensagens electrónicas dos outros utilizadores ou alterar as mensagens de notificação.
-É '''recomendado''' que esta opção seja activada nas wikis de acesso público para impedir o uso abusivo das funcionalidades de correio electrónico.",
+ 'config-enable-email' => 'Ativar mensagens eletrónicas de saída',
+ 'config-enable-email-help' => 'Se quer que o correio eletrónico funcione, as [http://www.php.net/manual/en/mail.configuration.php definições de correio eletrónico do PHP] têm de estar configuradas corretamente.
+Se não pretende viabilizar qualquer funcionalidade de correio eletrónico, pode desativá-lo aqui.',
+ 'config-email-user' => 'Ativar mensagens eletrónicas entre utilizadores',
+ 'config-email-user-help' => 'Permitir que todos os utilizadores troquem entre si mensagens de correio eletrónico, se tiverem ativado esta funcionalidade nas suas preferências.',
+ 'config-email-usertalk' => 'Ativar notificações de alterações à página de discussão dos utilizadores',
+ 'config-email-usertalk-help' => 'Permitir que os utilizadores recebam notificações de alterações à sua página de discussão, se tiverem ativado esta funcionalidade nas suas preferências.',
+ 'config-email-watchlist' => 'Ativar notificação de alterações às páginas vigiadas',
+ 'config-email-watchlist-help' => 'Permitir que os utilizadores recebam notificações de alterações às suas páginas vigiadas, se tiverem ativado esta funcionalidade nas suas preferências.',
+ 'config-email-auth' => 'Ativar autenticação do correio eletrónico',
+ 'config-email-auth-help' => "Se esta opção for ativada, os utilizadores têm de confirmar o seu endereço de correio eletrónico usando um link que lhes é enviado sempre que o definirem ou alterarem.
+Só os endereços de correio eletrónico autenticados podem receber mensagens eletrónicas dos outros utilizadores ou alterar as mensagens de notificação.
+É '''recomendado''' que esta opção seja ativada nas wikis de acesso público para impedir o uso abusivo das funcionalidades de correio eletrónico.",
'config-email-sender' => 'Endereço de correio electrónico de retorno:',
'config-email-sender-help' => 'Introduza o endereço de correio electrónico que será usado como endereço de retorno nas mensagens electrónicas de saída.
É para este endereço que serão enviadas as mensagens que não podem ser entregues.
@@ -15533,19 +16934,19 @@ Muitos servidores de correio electrónico exigem que pelo menos a parte do nome
'config-upload-settings' => 'Upload de imagens e ficheiros',
'config-upload-enable' => 'Possibilitar o upload de ficheiros',
'config-upload-help' => 'O upload de ficheiros expõe o seu servidor a riscos de segurança.
-Para mais informações, leia a [//www.mediawiki.org/wiki/Manual:Security secção sobre segurança] do Manual Técnico.
+Para mais informações, leia a [//www.mediawiki.org/wiki/Manual:Security seção sobre segurança] do Manual Técnico.
-Para permitir o upload de ficheiros, altere as permissões do subdirectório <code>images</code> no directório de raiz do MediaWik para que o servidor de internet possa escrever nele.
-Depois active esta opção.',
- 'config-upload-deleted' => 'Directório para os ficheiros apagados:',
- 'config-upload-deleted-help' => 'Escolha um directório onde serão arquivados os ficheiros apagados.
-O ideal é que este directório não possa ser directamente acedido a partir da internet.',
+Para permitir o upload de ficheiros, altere as permissões do subdiretório <code>images</code> no diretório de raiz do MediaWiki para que o servidor de internet possa escrever nele.
+Depois ative esta opção.',
+ 'config-upload-deleted' => 'Diretório para os ficheiros apagados:',
+ 'config-upload-deleted-help' => 'Escolha um diretório onde serão arquivados os ficheiros apagados.
+O ideal é que este diretório não possa ser diretamente acedido a partir da internet.',
'config-logo' => 'URL do logótipo:',
'config-logo-help' => 'O tema padrão do MediaWiki inclui espaço para um logótipo de 135x160 pixels acima do menu da barra lateral.
Coloque na wiki uma imagem com estas dimensões e introduza aqui a URL dessa imagem.
-Se não pretende usar um logótipo, deixe este campo em branco.',
- 'config-instantcommons' => 'Activar a funcionalidade Instant Commons',
+Se não pretende usar um logótipo, deixe este campo em branco.', # Fuzzy
+ 'config-instantcommons' => 'Ativar Instant Commons',
'config-instantcommons-help' => 'O [//www.mediawiki.org/wiki/InstantCommons Instant Commons] é uma funcionalidade que permite que as wikis usem imagens, áudio e outros ficheiros multimédia disponíveis no site [//commons.wikimedia.org/ Wikimedia Commons].
Para poder usá-los, o MediaWiki necessita de acesso à internet.
@@ -15555,30 +16956,30 @@ Introduza o nome da licença manualmente.',
'config-cc-again' => 'Escolha outra vez...',
'config-cc-not-chosen' => 'Escolha a licença da Creative Commons que pretende e clique "continuar".',
'config-advanced-settings' => 'Configuração avançada',
- 'config-cache-options' => 'Definições da cache de objectos:',
- 'config-cache-help' => 'A cache de objectos é usada para melhorar o desempenho do MediaWiki. Armazena dados usados com frequência.
-Sites de tamanho médio ou grande são altamente encorajados a activar esta funcionalidade e os sites pequenos também terão alguns benefícios em fazê-lo.',
+ 'config-cache-options' => 'Configuração da cache de objetos:',
+ 'config-cache-help' => 'A cache de objetos é usada para melhorar o desempenho do MediaWiki. Armazena dados usados com frequência.
+Sites de tamanho médio ou grande são altamente encorajados a ativar esta funcionalidade e os sites pequenos também terão alguns benefícios em fazê-lo.',
'config-cache-none' => 'Sem cache (não é removida nenhuma funcionalidade, mas a velocidade de operação pode ser afectada nas wikis grandes)',
- 'config-cache-accel' => 'Cache de objectos do PHP (APC, XCache ou WinCache)',
+ 'config-cache-accel' => 'Cache de objetos do PHP (APC, XCache ou WinCache)',
'config-cache-memcached' => 'Usar Memcached (requer instalação e configurações adicionais)',
'config-memcached-servers' => 'Servidores Memcached:',
'config-memcached-help' => 'Lista de endereços IP que serão usados para o Memcached.
Deve-se colocar um por linha e indicar a porta a utilizar. Por exemplo:
127.0.0.1:11211
192.168.1.25:1234',
- 'config-memcache-needservers' => 'Seleccionou o Memcached como tipo de chache, mas não especificou nenhum servidor.',
+ 'config-memcache-needservers' => 'Selecionou o Memcached como tipo de chache, mas não especificou nenhum servidor.',
'config-memcache-badip' => 'Introduziu um endereço IP inválido para o Memcached: $1.',
'config-memcache-noport' => 'Não especificou a porta a usar para o servidor Memcached: $1.
Se não sabe qual é a porta, a predefinida é a 11211.',
'config-memcache-badport' => 'Os números das portas do Memcached devem estar entre $1 e $2.',
'config-extensions' => 'Extensões',
- 'config-extensions-help' => 'Foi detectada a existência das extensões listadas acima, no seu directório <code>./extensions</code>.
+ 'config-extensions-help' => 'Foi detectada a existência das extensões listadas acima, no seu diretório <code>./extensions</code>.
-Estas talvez necessitem de configurações adicionais, mas pode activá-las agora',
+Estas talvez necessitem de configurações adicionais, mas pode ativá-las agora',
'config-install-alreadydone' => "'''Aviso:''' Parece que já instalou o MediaWiki e está a tentar instalá-lo novamente.
Passe para a próxima página, por favor.",
'config-install-begin' => 'Ao clicar "{{int:config-continue}}", vai iniciar a instalação do MediaWiki.
-Se quiser fazer mais alterações, clique Voltar.',
+Se quiser fazer mais alterações, clique Voltar.', # Fuzzy
'config-install-step-done' => 'terminado',
'config-install-step-failed' => 'falhou',
'config-install-extensions' => 'A incluir as extensões',
@@ -15592,7 +16993,7 @@ Certifique-se de que o utilizador "$1" pode escrever no esquema \'\'(schema)\'\'
'config-pg-no-plpgsql' => 'É preciso instalar a linguagem PL/pgSQL na base de dados $1',
'config-pg-no-create-privs' => 'A conta que especificou para a instalação não tem privilégios suficientes para criar uma conta.',
'config-pg-not-in-role' => 'A conta que especificou para o utilizador da internet já existe.
-A conta que especificou para a instalação não é a de um super-utilizador e não pertence ao grupo de utilizadores de acesso pela internet, por isso não pode criar objectos que pertencem ao utilizador da internet.
+A conta que especificou para a instalação não é a de um super-utilizador e não pertence ao grupo de utilizadores de acesso pela internet, por isso não pode criar objetos que pertencem ao utilizador da internet.
O MediaWiki necessita que as tabelas pertençam ao utilizador da internet. Especifique outra conta de internet, ou clique "voltar" e especifique um utilizador com os privilégios necessários para a instalação.',
'config-install-user' => 'A criar o utilizador da base de dados',
@@ -15601,7 +17002,7 @@ O MediaWiki necessita que as tabelas pertençam ao utilizador da internet. Espec
'config-install-user-grant-failed' => 'A atribuição das permissões ao utilizador "$1" falhou: $2',
'config-install-user-missing' => 'O utilizador especificado, "$1", não existe.',
'config-install-user-missing-create' => 'O utilizador especificado, "$1", não existe.
-Marque a caixa de selecção "criar conta" abaixo se pretende criá-la, por favor.',
+Marque a caixa de seleção "criar conta" abaixo se pretende criá-la, por favor.',
'config-install-tables' => 'A criar as tabelas',
'config-install-tables-exist' => "'''Aviso''': As tabelas do MediaWiki parecem já existir.
A criação das tabelas será saltada.",
@@ -15617,7 +17018,7 @@ O preenchimento padrão desta tabela será saltado.",
'config-install-subscribe-fail' => 'Não foi possível subscrever a lista mediawiki-announce: $1',
'config-install-subscribe-notpossible' => 'cURL não está instalado e allow_url_fopen não está disponível.',
'config-install-mainpage' => 'A criar a página principal com o conteúdo padrão.',
- 'config-install-extension-tables' => 'A criar as tabelas das extensões activadas',
+ 'config-install-extension-tables' => 'A criar as tabelas das extensões ativadas',
'config-install-mainpage-failed' => 'Não foi possível inserir a página principal: $1',
'config-install-done' => "'''Parabéns!'''
Terminou a instalação do MediaWiki.
@@ -15625,7 +17026,7 @@ Terminou a instalação do MediaWiki.
O instalador gerou um ficheiro <code>LocalSettings.php</code>.
Este ficheiro contém todas as configurações.
-Precisa de fazer o download do ficheiro e colocá-lo no directório de raiz da sua instalação (o mesmo directório onde está o ficheiro index.php). Este download deverá ter sido iniciado automaticamente.
+Precisa de fazer o download do ficheiro e colocá-lo no diretório de raiz da sua instalação (o mesmo diretório onde está o ficheiro index.php). Este download deverá ter sido iniciado automaticamente.
Se o download não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando o link abaixo:
@@ -15634,7 +17035,7 @@ $3
'''Nota''': Se não fizer isto agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.
Depois de terminar o passo anterior, pode '''[$2 entrar na wiki]'''.",
- 'config-download-localsettings' => 'Download do LocalSettings.php',
+ 'config-download-localsettings' => 'Download do <code>LocalSettings.php</code>',
'config-help' => 'ajuda',
'config-nofile' => 'Não foi possível encontrar o ficheiro "$1". Terá sido apagado?',
'mainpagetext' => "'''MediaWiki instalado com sucesso.'''",
@@ -15644,12 +17045,14 @@ Depois de terminar o passo anterior, pode '''[$2 entrar na wiki]'''.",
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de opções de configuração]
* [//www.mediawiki.org/wiki/Manual:FAQ Perguntas e respostas frequentes sobre o MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Subscreva a lista de divulgação de novas versões do MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Subscreva a lista de divulgação de novas versões do MediaWiki]', # Fuzzy
);
/** Brazilian Portuguese (português do Brasil)
+ * @author Cainamarques
* @author Giro720
* @author Gustavo
+ * @author Luckas
* @author Marcionunes
* @author 555
*/
@@ -15659,15 +17062,21 @@ $messages['pt-br'] = array(
'config-information' => 'Informações',
'config-localsettings-upgrade' => 'Foi detectada a existência do arquivo <code>LocalSettings.php</code>.
Para atualizar esta instalação, insira no box abaixo o valor de <code>$wgUpgradeKey</code>.
-Essa informação pode ser encontrada no arquivo LocalSettings.php',
- 'config-localsettings-cli-upgrade' => 'Foi detectada a existência do arquivo <code>LocalSettings.php</code>.
-Esta instalação deverá ser atualizada através do <code>update.php</code>',
+Essa informação pode ser encontrada no arquivo <code>LocalSettings.php</code>',
+ 'config-localsettings-cli-upgrade' => 'Foi detectada a existência do arquivo <code><code>LocalSettings.php</code></code>.
+Atualize esta instalação executando o arquivo <code>update.php</code>',
'config-localsettings-key' => 'Chave de atualização:',
'config-localsettings-badkey' => 'A chave fornecida está incorreta.',
'config-upgrade-key-missing' => 'Foi detectada uma instalação existente do MediaWiki.
-Para atualizar esta instalação, por favor, coloque a seguinte linha na parte inferior do seu LocalSettings.php:
+Para atualizar esta instalação, insira a seguinte linha na parte inferior do seu <code>LocalSettings.php</code>:
+
+$1',
+ 'config-localsettings-incomplete' => 'O arquivo <code>LocalSettings.php</code> parece incompleto.
+A variável $1 não está definida.
+Altere seu <code>LocalSettings.php</code> com a definição dessa variável e clique em "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Ocorreu um erro ao conectar ao banco de dados através das configurações presentes ou no <code>LocalSettings.php</code> ou no <code>AdminSettings.php</code>. Corrija essas configurações e tente novamente.
-$ 1', # Fuzzy
+$1',
'config-session-error' => 'Erro ao iniciar a sessão: $1',
'config-session-expired' => 'Os seus dados de sessão parecem ter expirado.
As sessões estão configuradas para uma duração de $1.
@@ -15675,31 +17084,32 @@ Você pode aumentar esta duração configurando <code>session.gc_maxlifetime</co
Reinicie o processo de instalação.',
'config-no-session' => 'Os seus dados de sessão foram perdidos!
Verifique o seu php.ini e certifique-se de que em <code>session.save_path</code> está definido um diretório apropriado.',
- 'config-your-language' => 'A sua língua:',
- 'config-your-language-help' => 'Selecione a língua que será usada durante o processo de instalação.',
- 'config-wiki-language' => 'Língua da wiki:',
- 'config-wiki-language-help' => 'Selecione a língua que será predominante na wiki.',
+ 'config-your-language' => 'Seu idioma:',
+ 'config-your-language-help' => 'Selecione o idioma que será usado durante o processo de instalação.',
+ 'config-wiki-language' => 'Idioma do wiki:',
+ 'config-wiki-language-help' => 'Selecione o idioma em que o wiki será predominantemente escrito.',
'config-back' => '← Voltar',
'config-continue' => 'Continuar →',
- 'config-page-language' => 'Língua',
+ 'config-page-language' => 'Idioma',
'config-page-welcome' => 'Bem-vindo(a) ao MediaWiki!',
- 'config-page-dbconnect' => 'Ligar à base de dados',
+ 'config-page-dbconnect' => 'Conectar ao banco de dados',
'config-page-upgrade' => 'Atualizar a instalação existente',
- 'config-page-dbsettings' => 'Configurações da base de dados',
+ 'config-page-dbsettings' => 'Configurações do banco de dados',
'config-page-name' => 'Nome',
'config-page-options' => 'Opções',
'config-page-install' => 'Instalar',
- 'config-page-complete' => 'Terminado!',
+ 'config-page-complete' => 'Concluído!',
'config-page-restart' => 'Reiniciar a instalação',
'config-page-readme' => 'Leia-me',
'config-page-releasenotes' => 'Notas de lançamento',
'config-page-copying' => 'Copiando',
'config-page-upgradedoc' => 'Atualizando',
+ 'config-page-existingwiki' => 'Wiki existente',
'config-help-restart' => 'Deseja limpar todos os dados salvos que você introduziu e reiniciar o processo de instalação?',
'config-restart' => 'Sim, reiniciar',
- 'config-welcome' => '=== Verificações do ambiente ===
-São realizadas verificações básicas para determinar se este ambiente é apropriado para instalação do MediaWiki.
-Você deverá fornecer os resultados destas verificações se você precisar de ajuda durante a instalação.',
+ 'config-welcome' => '=== Verificações de ambiente ===
+São realizadas verificações básicas para determinar se este ambiente é apropriado para a instalação do MediaWiki.
+Lembre-se de incluir estas informações se for procurar por suporte para a conclusão da instalação.',
'config-copyright' => "=== Direitos autorais e Termos de uso ===
$1
@@ -15710,28 +17120,52 @@ Este programa é distribuído na esperança de que seja útil, mas '''sem qualqu
Consulte a licença GNU General Public License para mais detalhes.
Em conjunto com este programa você deve ter recebido <doclink href=Copying>uma cópia da licença GNU General Public License</doclink>; se não a recebeu, peça-a por escrito para Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [http://www.gnu.org/copyleft/gpl.html leia-a na internet].",
- 'config-sidebar' => '* [//www.mediawiki.org/wiki/MediaWiki/pt Página principal do MediaWiki]
-* [//www.mediawiki.org/wiki/Help:Contents/pt Ajuda]
-* [//www.mediawiki.org/wiki/Manual:Contents/pt Manual técnico]
-* [//www.mediawiki.org/wiki/Manual:FAQ FAQ]', # Fuzzy
+ 'config-sidebar' => '* [//www.mediawiki.org/wiki/MediaWiki Página principal do MediaWiki]
+* [//www.mediawiki.org/wiki/Help:Contents Manual de uso]
+* [//www.mediawiki.org/wiki/Manual:Contents Manual administrativo]
+* [//www.mediawiki.org/wiki/Manual:FAQ FAQ]
+----
+* <doclink href=Readme>Leia-me</doclink>
+* <doclink href=ReleaseNotes>Notas de lançamento</doclink>
+* <doclink href=Copying>Licença</doclink>
+* <doclink href=UpgradeDoc>Como fazer upgrade</doclink>',
'config-env-good' => 'O ambiente foi verificado.
Você pode instalar o MediaWiki.',
'config-env-bad' => 'O ambiente foi verificado.
Você não pode instalar o MediaWiki.',
'config-env-php' => 'O PHP $1 está instalado.',
- 'config-unicode-using-utf8' => 'A usar o utf8_normalize.so, de Brian Viper, para a normalização Unicode.',
+ 'config-unicode-using-utf8' => 'Usando o utf8_normalize.so, de Brion Vibber, para a normalização Unicode.',
'config-unicode-using-intl' => 'Usando a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.',
- 'config-unicode-pure-php-warning' => "'''Aviso''': A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode.
-Se o seu site tem um alto volume de tráfego, devia informar-se um pouco sobre a [//www.mediawiki.org/wiki/Unicode_normalization_considerations normalização Unicode].", # Fuzzy
- 'config-no-db' => 'Não foi possível encontrar um driver de banco de dados adequado!', # Fuzzy
+ 'config-unicode-pure-php-warning' => "'''Aviso''': A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode sendo usada, em seu lugar, a lenta implementação de PHP puro.
+Se o seu site tem um alto volume de tráfego, informe-se sobre a [//www.mediawiki.org/wiki/Unicode_normalization_considerations normalização Unicode].",
+ 'config-no-db' => 'Não foi possível encontrar um driver de banco de dados adequado! É necessário instalar um driver de banco de dados para o PHP.
+São suportados os seguintes tipos de bancos de dados: $1.
+
+Se estiver em uma hospedagem partilhada, peça à sua empresa de hospedagem para instalar um driver de banco de dados adequado.
+Se você mesmo tiver compilado o PHP, reconfigure-o com um cliente de banco de dados ativado usando, por exemplo, <code>./configure --with-mysql</code>.
+Se você instalou o PHP a partir de um pacote do Debian ou do Ubuntu, instale também o módulo php5-mysql.',
'config-no-fts3' => "' ' 'Aviso' ' ': O SQLite foi compilado sem o módulo [//sqlite.org/fts3.html FTS3]; as funcionalidades de pesquisa não estarão disponíveis nesta instalação.",
'config-register-globals' => "' ' 'Aviso: A opção <code>[http://php.net/register_globals register_globals]</code> do PHP está ativada.'''
' ' 'Desative-a, se puder.'''
O MediaWiki funcionará mesmo assim, mas o seu servidor ficará exposto a potenciais vulnerabilidades de segurança.",
- 'config-logo-help' => 'O tema padrão do MediaWiki inclui espaço para um logotipo de 135x160 pixels no canto superior esquerdo.
-Faça o upload de uma imagem com estas dimensões e introduza aqui a URL dessa imagem.
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binary',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-binary' => 'Binary',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-ns-generic' => 'Projeto',
+ 'config-admin-box' => 'Conta de administrador',
+ 'config-admin-name' => 'Seu nome:',
+ 'config-admin-password' => 'Senha:',
+ 'config-license-pd' => 'Domínio público',
+ 'config-logo-help' => 'Faça o upload de uma imagem de tamanho adequado e insira seu URL aqui.
-Se você não pretende usar um logotipo, deixe este campo em branco.', # Fuzzy
+Você pode usar <code>$wgStylePath</code> ou <code>$wgScriptPath</code> se o seu logotipo for associado a esses diretórios.',
+ 'config-advanced-settings' => 'Configuração avançada',
+ 'config-extensions' => 'Extensões',
+ 'config-install-step-done' => 'feito',
+ 'config-help' => 'ajuda',
'mainpagetext' => "'''MediaWiki instalado com sucesso.'''",
'mainpagedocfooter' => 'Consulte o [//meta.wikimedia.org/wiki/Help:Contents Manual de Usuário] para informações de como usar o software wiki.
@@ -15739,20 +17173,34 @@ Se você não pretende usar um logotipo, deixe este campo em branco.', # Fuzzy
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista de opções de configuração]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ do MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussão com avisos de novas versões do MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussão com avisos de novas versões do MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Traduza o MediaWiki para seu idioma]',
);
/** Quechua (Runa Simi)
+ * @author AlimanRuna
*/
$messages['qu'] = array(
+ 'config-desc' => 'MediaWiki tiyachiq',
+ 'config-title' => 'MediaWiki $1 tiyachiy',
+ 'config-information' => 'Willay',
+ 'config-your-language' => 'Rimayniyki:',
+ 'config-wiki-language' => 'Wiki rimay:',
+ 'config-back' => '← Ñawpaqman',
+ 'config-extensions' => "Mast'ariykuna",
+ 'config-install-step-done' => 'rurasqañam',
+ 'config-install-step-failed' => 'manam aypasqachu',
+ 'config-help' => 'yanapay',
+ 'config-nofile' => '"$1" sutiyuq willañiqiqa manam tarisqachu. Qullusqachu?',
'mainpagetext' => "'''MediaWiki nisqa llamp'u kaqqa aypaylla takyachisqañam.'''",
'mainpagedocfooter' => "Wiki llamp'u kaqmanta willasunaykipaqqa [//meta.wikimedia.org/wiki/Help:Contents Ruraqpaq yanapana] ''(User's Guide)'' sutiyuq p'anqata qhaway.
== Qallarichkaspa ==
-* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
-* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Kunphigurasyun churanamanta sutisuyu]
+* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki nisqamanta pasaq tapuykuna]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki kachaykuy e-chaski sutisuyu]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources MediaWiki nisqata qampa rimaykiman t'ikray]",
);
/** Romagnol (Rumagnôl)
@@ -15771,14 +17219,20 @@ $messages['rm'] = array(
== Cumenzar ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Glista da las opziuns per la configuraziun]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Glista da mail da MediaWiki cun annunzias da novas versiuns]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Glista da mail da MediaWiki cun annunzias da novas versiuns]", # Fuzzy
);
/** Romanian (română)
+ * @author Firilacroco
* @author Minisarm
* @author Stelistcristi
*/
$messages['ro'] = array(
+ 'config-desc' => 'Programul de instalare pentru MediaWiki',
+ 'config-title' => 'Instalarea MediaWiki $1',
+ 'config-information' => 'Informații',
+ 'config-localsettings-key' => 'Cheie de actualizare:',
+ 'config-localsettings-badkey' => 'Cheia furnizată este incorectă.',
'config-session-error' => 'Eroare la pornirea sesiunii: $1',
'config-your-language' => 'Limba ta:',
'config-your-language-help' => 'Alege o limbă pentru a o utiliza în timpul procesului de instalare.',
@@ -15794,46 +17248,174 @@ $messages['ro'] = array(
'config-page-name' => 'Nume',
'config-page-options' => 'Opţiuni',
'config-page-install' => 'Instalare',
+ 'config-page-complete' => 'Finalizat!',
'config-page-restart' => 'Reporneşte instalarea',
'config-page-readme' => 'Citeşte-mă',
'config-page-releasenotes' => 'Note de lansare',
+ 'config-page-copying' => 'Copiere',
+ 'config-page-upgradedoc' => 'Actualizare',
+ 'config-page-existingwiki' => 'Wiki existent',
+ 'config-restart' => 'Da, repornește.',
+ 'config-env-php' => 'PHP $1 este instalat.',
+ 'config-env-php-toolow' => 'PHP $1 este instalat.
+Totuși, MediaWiki necesită PHP $2 sau mai nou.',
'config-db-type' => 'Tipul bazei de date:',
'config-db-host' => 'Gazdă bază de date:',
+ 'config-db-host-oracle' => 'Baza de date TNS:',
+ 'config-db-wiki-settings' => 'Identificați acest wiki',
+ 'config-db-name' => 'Numele bazei de date:',
+ 'config-db-name-oracle' => 'Schema bazei de date:',
+ 'config-db-username' => 'Nume de utilizator pentru baza de date:',
+ 'config-db-password' => 'Parola bazei de date:',
+ 'config-db-prefix' => 'Prefixul tabelelor din baza de date:',
+ 'config-db-charset' => 'Setul de caractere al bazei de date',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binar',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-db-port' => 'Portul bazei de date:',
+ 'config-db-schema' => 'Schema pentru MediaWiki:',
+ 'config-sqlite-dir' => 'Director de date SQLite:',
+ 'config-oracle-def-ts' => 'Spațiu de stocare („tablespace”) implicit:',
+ 'config-oracle-temp-ts' => 'Spațiu de stocare („tablespace”) temporar:',
'config-header-mysql' => 'Setările MySQL',
+ 'config-header-postgres' => 'Setări PostgreSQL',
'config-header-sqlite' => 'Setări SQLite',
'config-header-oracle' => 'Setări Oracle',
+ 'config-invalid-db-type' => 'Tip de bază de date incorect',
'config-missing-db-name' => 'Trebuie să introduci o valoare pentru „Numele bazei de date”',
+ 'config-connection-error' => '$1.
+
+Verificați gazda, numele de utilizator și parola și reîncercați.',
+ 'config-upgrade-done-no-regenerate' => 'Actualizare completă.
+
+Acum puteți [$1 începe să vă folosiți wikiul].',
+ 'config-regenerate' => 'Regenerare LocalSettings.php →',
+ 'config-unknown-collation' => 'AVERTISMENT: Baza de date folosește o colaționare nerecunoscută.',
+ 'config-db-web-account' => 'Contul bazei de date pentru accesul web.',
+ 'config-db-web-create' => 'Creați contul dacă nu există deja',
'config-mysql-engine' => 'Motor de stocare:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-charset' => 'Setul de caractere al bazei de date:',
+ 'config-mysql-binary' => 'Binar',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-site-name' => 'Numele wikiului:',
'config-site-name-blank' => 'Introduceți un nume pentru sit.',
+ 'config-project-namespace' => 'Spațiul de nume al proiectului:',
'config-ns-generic' => 'Proiect',
+ 'config-ns-site-name' => 'Același nume ca al wikiului: $1',
+ 'config-ns-other' => 'Altul (specificați)',
+ 'config-ns-other-default' => 'MyWiki',
+ 'config-admin-box' => 'Cont de administrator',
+ 'config-admin-name' => 'Numele dumneavoastră:',
'config-admin-password' => 'Parolă:',
+ 'config-admin-password-confirm' => 'Parola, din nou:',
+ 'config-admin-password-blank' => 'Introduceți o parolă pentru contul de administrator.',
+ 'config-admin-password-same' => 'Parola trebuie să difere de numele de utilizator.',
+ 'config-admin-password-mismatch' => 'Cele două parole introduse nu corespund.',
+ 'config-admin-email' => 'Adresa de e-mail:',
+ 'config-admin-error-bademail' => 'Ați introdus o adresă de e-mail incorectă.',
+ 'config-almost-done' => 'Sunteți aproape gata!
+Puteți sări peste configurarea rămasă și să instalați wikiul chiar acum.',
'config-optional-continue' => 'Adresează-mi mai multe întrebări.',
'config-optional-skip' => 'Sunt deja plictisit, doar instalează wikiul.',
- 'config-profile-wiki' => 'Wiki tradițional',
+ 'config-profile' => 'Profilul drepturilor de utilizator:',
+ 'config-profile-wiki' => 'Wiki tradițional', # Fuzzy
+ 'config-profile-no-anon' => 'Crearea de cont este necesară',
+ 'config-profile-fishbowl' => 'Doar editorii autorizați',
'config-profile-private' => 'Wiki privat',
+ 'config-license' => 'Drepturi de autor și licență:',
+ 'config-license-none' => 'Fără licență în subsolul paginii',
+ 'config-license-cc-by-sa' => 'Creative Commons Atribuire și distribuire în condiții identice',
+ 'config-license-cc-by' => 'Creative Commons Atribuire',
+ 'config-license-cc-by-nc-sa' => 'Creative Commons Atribuire, necomercial și distribuire în condiții identice',
+ 'config-license-cc-0' => 'Creative Commons Zero (domeniu public)',
+ 'config-license-gfdl' => 'Licența GNU pentru Documentare Liberă 1.3 sau ulterioară',
+ 'config-license-pd' => 'Domeniu public',
+ 'config-license-cc-choose' => 'Alegeți o licență Creative Commons personalizată',
'config-email-settings' => 'Setări pentru e-mail',
+ 'config-email-usertalk' => 'Activați notificările pentru pagina de discuții a utilizatorului',
+ 'config-upload-deleted' => 'Director pentru fișierele șterse:',
+ 'config-logo' => 'Adresa URL a siglei:',
+ 'config-cc-again' => 'Alegeți din nou...',
+ 'config-advanced-settings' => 'Configurare avansată',
+ 'config-cache-options' => 'Parametrii pentru stocarea temporară a obiectelor:',
+ 'config-extensions' => 'Extensii',
'config-install-step-done' => 'realizat',
+ 'config-install-step-failed' => 'eșuat',
+ 'config-install-extensions' => 'Se includ extensiile',
+ 'config-install-database' => 'Se creează baza de date',
+ 'config-install-schema' => 'Se creează schema',
+ 'config-install-pg-schema-not-exist' => 'Schema PostgreSQL nu există.',
+ 'config-install-pg-commit' => 'Se validează modificările',
+ 'config-install-user' => 'Se creează utilizatorul pentru baza de date',
+ 'config-install-user-alreadyexists' => 'Utilizatorul „$1” există deja',
+ 'config-install-user-create-failed' => 'Crearea utilizatorului „$1” a eșuat: $2',
+ 'config-install-tables' => 'Se creează tabelele',
+ 'config-install-stats' => 'Se inițializează statisticile',
+ 'config-install-keys' => 'Se generează cheile secrete',
+ 'config-install-sysop' => 'Se creează contul de administrator',
+ 'config-install-mainpage-failed' => 'Nu s-a putut insera pagina principală: $1',
+ 'config-download-localsettings' => 'Descarcă <code>LocalSettings.php</code>',
+ 'config-help' => 'ajutor',
'mainpagetext' => "'''Programul Wiki a fost instalat cu succes.'''",
'mainpagedocfooter' => 'Consultați [//meta.wikimedia.org/wiki/Help:Contents Ghidul utilizatorului (en)] pentru informații despre utilizarea software-ului wiki.
== Primii pași ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista parametrilor configurabili (en)]
* [//www.mediawiki.org/wiki/Manual:FAQ Întrebări frecvente despre MediaWiki (en)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discuții a MediaWiki (en)]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discuții a MediaWiki (en)]', # Fuzzy
);
/** tarandíne (tarandíne)
* @author Joetaras
*/
$messages['roa-tara'] = array(
+ 'config-desc' => "'U 'nstallatore de MediaUicchi",
+ 'config-title' => 'Installazzione de MediaUicchi $1',
+ 'config-information' => "'Mbormaziune",
+ 'config-localsettings-key' => 'Chiave de aggiornamende:',
+ 'config-localsettings-badkey' => "'A chiave ca è date non g'è corrette.",
+ 'config-session-error' => "Errore facenne accumenzà 'a sessione: $1",
+ 'config-your-language' => "'A lènga toje:",
+ 'config-your-language-help' => "Scacchie 'na lènghe da ausà duranne 'u processe de installazzione:",
+ 'config-wiki-language' => 'Lènga de Uicchi:',
+ 'config-back' => '← Rrète',
+ 'config-continue' => 'Condinue →',
+ 'config-page-language' => 'Lènghe',
+ 'config-page-welcome' => "Bovègne jndr'à MediaUicchi!",
+ 'config-page-dbconnect' => "Collegate a 'u database",
+ 'config-page-upgrade' => "Aggiorne l'installazzione esistende",
+ 'config-page-dbsettings' => "'Mbostaziune d'u database",
+ 'config-page-name' => 'Nome',
+ 'config-page-options' => 'Opziune',
+ 'config-page-install' => 'Installe',
+ 'config-page-complete' => 'Combletate!',
+ 'config-page-restart' => "Riavvie l'installazzione",
+ 'config-page-readme' => 'Liggeme',
+ 'config-page-releasenotes' => 'Note de rilasce',
+ 'config-page-copying' => 'Stoche a copie',
+ 'config-page-upgradedoc' => 'Aggiornamende',
+ 'config-page-existingwiki' => 'Uicchi esistende',
+ 'config-db-type' => 'Tipe de database:',
'config-db-charset' => "'Nzieme de carattere d'u database",
'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binary',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
'config-charset-mysql4' => 'MySQL 4.0 backwards-compatible UTF-8',
+ 'config-db-port' => "Porte d'u database:",
+ 'config-db-schema' => 'Scheme pe MediaUicchi:',
+ 'config-type-mysql' => 'MySQL',
+ 'config-type-postgres' => 'PostgreSQL',
+ 'config-type-sqlite' => 'SQLite',
+ 'config-type-oracle' => 'Oracle',
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-admin-email' => 'Indirizze e-mail:',
'config-install-step-done' => 'fatte',
'config-install-step-failed' => 'fallite',
+ 'config-install-extensions' => "'Ngludenne le estenziune",
+ 'config-install-database' => "Stoche a 'mboste l'archivije",
+ 'config-install-schema' => "Stoche a creje 'u scheme",
+ 'config-install-pg-schema-not-exist' => "'U scheme PostgreSQL non g'esiste.",
'config-help' => 'ajute',
'mainpagetext' => "'''MediaUicchi ha state 'nstallete.'''",
'mainpagedocfooter' => "Vè vide [//meta.wikimedia.org/wiki/Help:Contents User's Guide] pe l'mbormaziune sus a cumme s'ause 'u softuer wiki.
@@ -15841,7 +17423,8 @@ $messages['roa-tara'] = array(
== Pe accumenzà ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste pe le configuraziune]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ de MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste d'a poste de MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Elenghe d'a poste de MediaUicchi]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Localizzazzione de MediaUicchi pa lènga toje]",
);
/** Russian (русский)
@@ -15849,13 +17432,16 @@ $messages['roa-tara'] = array(
* @author DCamer
* @author Eleferen
* @author Express2000
+ * @author KPu3uC B Poccuu
* @author Kaganer
* @author Krinkle
* @author Lockal
* @author MaxSem
+ * @author Okras
* @author Yuriy Apostol
* @author Александр Сигачёв
* @author Сrower
+ * @author 아라
*/
$messages['ru'] = array(
'config-desc' => 'Инсталлятор MediaWiki',
@@ -15863,19 +17449,19 @@ $messages['ru'] = array(
'config-information' => 'Информация',
'config-localsettings-upgrade' => 'Обнаружен файл <code>LocalSettings.php</code>.
Для обновления этой установки, пожалуйста, введите значение <code>$wgUpgradeKey</code>.
-Его можно найти в файле LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Обнаружен файл LocalSettings.php.
-Для обновления этой установки, пожалуйста, запустите update.php',
+Его можно найти в файле <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Обнаружен файл <code>LocalSettings.php</code>.
+Для обновления этой установки, пожалуйста, запустите <code>update.php</code>',
'config-localsettings-key' => 'Ключ обновления:',
'config-localsettings-badkey' => 'Вы указали неправильный ключ',
'config-upgrade-key-missing' => 'Обнаружена существующая установленная копия MediaWiki.
-Чтобы обновить обнаруженную установку, пожалуйста, добавьте следующую строку в конец вашего файла LocalSettings.php:
+Чтобы обновить обнаруженную установку, пожалуйста, добавьте следующую строку в конец вашего файла <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Похоже, что существующий файл LocalSettings.php не является полными.
+ 'config-localsettings-incomplete' => 'Похоже, что существующий файл <code>LocalSettings.php</code> не является полными.
Не установлена переменная $1.
-Пожалуйста, измените LocalSettings.php так, чтобы значение этой переменной было задано, затем нажмите «Продолжить».',
- 'config-localsettings-connection-error' => 'Произошла ошибка при подключении к базе данных с помощью настроек, указанных в LocalSettings.php или AdminSettings.php. Пожалуйста, исправьте эти настройки и повторите попытку.
+Пожалуйста, измените <code>LocalSettings.php</code> так, чтобы значение этой переменной было задано, затем нажмите «{{int:Config-continue}}».',
+ 'config-localsettings-connection-error' => 'Произошла ошибка при подключении к базе данных с помощью настроек, указанных в <code>LocalSettings.php</code> или <code>AdminSettings.php</code>. Пожалуйста, исправьте эти настройки и повторите попытку.
$1',
'config-session-error' => 'Ошибка при запуске сессии: $1',
@@ -15909,8 +17495,8 @@ $1',
'config-help-restart' => 'Вы хотите удалить все сохранённые данные, которые вы ввели, и запустить процесс установки заново?',
'config-restart' => 'Да, начать заново',
'config-welcome' => '=== Проверка окружения ===
-Проводятся базовые проверки с целью определить, подходит ли данная система для установки MediaWiki.
-Укажите результаты этих проверок при обращении за помощью с установкой.',
+Будут проведены базовые проверки с целью определить, подходит ли данная система для установки MediaWiki.
+Не забудьте включить эту информацию, если вам потребуется помощь для завершения установки.',
'config-copyright' => "=== Авторские права и условия ===
$1
@@ -15977,6 +17563,11 @@ MediaWiki требует поддержки UTF-8 для корректной р
'config-memory-bad' => "'''Внимание:''' размер PHP <code>memory_limit</code> составляет $1.
Вероятно, этого слишком мало.
Установка может потерпеть неудачу!",
+ 'config-ctype' => "'''Фатальная ошибка:''' PHP должен быть скомпилирован с поддержкой [http://www.php.net/manual/ru/ctype.installation.php расширения Ctype].",
+ 'config-json' => "'''Фатальная ошибка:''' PHP был скомпилирован без поддержка JSON.
+Вам необходимо установить либо расширение PHP JSON, либо расширение [http://pecl.php.net/package/jsonc PECL jsonc] перед установкой MediaWiki.
+* PHP-расширение входит в состав Red Hat Enterprise Linux (CentOS) 5 и 6, хотя должна быть включено в <code>/etc/php.ini</code> или <code>/etc/php.d/json.ini</code>.
+* Некоторые дистрибутивы Linux, выпущенные после мая 2013 года, не включают расширение PHP, вместо того, чтобы упаковывать расширение PECL как <code>php5-json</code> или <code>php-pecl-jsonc</code>.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] установлен',
'config-apc' => '[http://www.php.net/apc APC] установлен',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] установлен',
@@ -15985,6 +17576,8 @@ MediaWiki требует поддержки UTF-8 для корректной р
'config-mod-security' => "'''Внимание''': на вашем веб-сервере включен [http://modsecurity.org/ mod_security]. При неправильной настройке он может вызывать проблемы для MediaWiki или другого ПО, позволяющего пользователям отправлять на сервер произвольный текст.
Обратитесь к [http://modsecurity.org/documentation/ документации mod_security] или в поддержку вашего хостера, если при работе возникают непонятные ошибки.",
'config-diff3-bad' => 'GNU diff3 не найден.',
+ 'config-git' => 'Найдена система контроля версий Git: <code>$1</code>.',
+ 'config-git-bad' => 'Программное обеспечение по управлению версиями Git не найдено.',
'config-imagemagick' => 'Обнаружен ImageMagick: <code>$1</code>.
Возможно отображение миниатюр изображений, если вы разрешите закачки файлов.',
'config-gd' => 'Найдена встроенная графическая библиотека GD.
@@ -16006,7 +17599,7 @@ MediaWiki требует поддержки UTF-8 для корректной р
'config-using531' => 'PHP $1 не совместим с MediaWiki из-за ошибки с параметрами-ссылками при вызовах <code>__call()</code>.
Обновитесь до PHP 5.3.2 и выше, или откатитесь до PHP 5.3.0, чтобы избежать этой проблемы.
Установка прервана.',
- 'config-suhosin-max-value-length' => 'Suhosin установлен и ограничивает длину параметра GET до $1 байт. Компонент MediaWiki ResourceLoader будет обходить это ограничение, но это снизит производительность. Если это возможно, следует установить suhosin.get.max_value_length 1024 или выше в php.ini, а также установить для $wgResourceLoaderMaxQueryLength такое же значение в LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Suhosin установлен и ограничивает параметр GET <code>length</code> до $1 байт. Компонент MediaWiki ResourceLoader будет обходить это ограничение, но это снизит производительность. Если это возможно, следует установить <code>suhosin.get.max_value_length</code> в значение 1024 или выше в <code>php.ini</code>, а также установить для <code>$wgResourceLoaderMaxQueryLength</code> такое же значение в LocalSettings.php.',
'config-db-type' => 'Тип базы данных:',
'config-db-host' => 'Хост базы данных:',
'config-db-host-help' => 'Если сервер базы данных находится на другом сервере, введите здесь его имя хоста или IP-адрес.
@@ -16082,7 +17675,6 @@ MediaWiki требует поддержки UTF-8 для корректной р
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki поддерживает следующие СУБД:
$1
@@ -16092,18 +17684,16 @@ $1
'config-support-postgres' => '* $1 — популярная открытая СУБД, альтернатива MySQL ([http://www.php.net/manual/en/pgsql.installation.php инструкция, как собрать PHP с поддержкой PostgreSQL]). Могут встречаться небольшие неисправленные ошибки, не рекомендуется для использования в рабочей системе.',
'config-support-sqlite' => '* $1 — это легковесная система баз данных, имеющая очень хорошую поддержку. ([http://www.php.net/manual/en/pdo.installation.php инструкция, как собрать PHP с поддержкой SQLite], работающей посредством PDO)',
'config-support-oracle' => '* $1 — это коммерческая база данных масштаба предприятия. ([http://www.php.net/manual/en/oci8.installation.php Как собрать PHP с поддержкой OCI8])',
- 'config-support-ibm_db2' => '$1 — коммерческая база данных масштаба предприятия.',
'config-header-mysql' => 'Настройки MySQL',
'config-header-postgres' => 'Настройки PostgreSQL',
'config-header-sqlite' => 'Настройки SQLite',
'config-header-oracle' => 'Настройки Oracle',
- 'config-header-ibm_db2' => 'Настройки IBM DB2',
'config-invalid-db-type' => 'Неверный тип базы данных',
'config-missing-db-name' => 'Вы должны ввести значение параметра «Имя базы данных»',
'config-missing-db-host' => 'Необходимо ввести значение параметра «Сервер базы данных»',
'config-missing-db-server-oracle' => 'Вы должны заполнить поле «TNS базы данных»',
- 'config-invalid-db-server-oracle' => 'Неверное имя TNS базы данных «$1».
-Используйте только символы ASCII (a-z, A-Z), цифры (0-9), знаки подчёркивания (_) и точки (.).',
+ 'config-invalid-db-server-oracle' => 'Неверное TNS базы данных «$1».
+Используйте либо «TNS Name», либо строку «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методы наименования Oracle])',
'config-invalid-db-name' => 'Неверное имя базы данных «$1».
Используйте только ASCII-символы (a-z, A-Z), цифры (0-9), знак подчёркивания (_) и дефис(-).',
'config-invalid-db-prefix' => 'Неверный префикс базы данных «$1».
@@ -16160,7 +17750,7 @@ chmod a+w $3</pre>',
Теперь вы можете [$1 начать работу с вики].',
'config-regenerate' => 'Создать LocalSettings.php заново →',
- 'config-show-table-status' => 'Запрос «SHOW TABLE STATUS» не выполнен!',
+ 'config-show-table-status' => 'Запрос «<code>SHOW TABLE STATUS</code>» не выполнен!',
'config-unknown-collation' => "'''Внимание:''' База данных использует нераспознанные правила сортировки.",
'config-db-web-account' => 'Учётная запись для доступа к базе данных из веб-сервера',
'config-db-web-help' => 'Выберите имя пользователя и пароль, которые веб-сервер будет использовать для подключения к серверу базы данных при обычной работе вики.',
@@ -16178,6 +17768,12 @@ chmod a+w $3</pre>',
Если ваша установка MySQL поддерживает InnoDB, настоятельно рекомендуется выбрать этот механизм.
Если ваша установка MySQL не поддерживает InnoDB, возможно, настало время обновиться.",
+ 'config-mysql-only-myisam-dep' => "'''Предупреждение:''' MyISAM является единственной доступной системой хранения данных для MySQL, которая, однако, не рекомендуется для использования с MediaWiki, потому что:
+ * он слабо поддерживает параллелизм из-за блокировки таблиц
+ * она больше других систем подвержена повреждению
+ * кодовая база MediaWiki не всегда обрабатывает MyISAM так, как следует
+
+Ваша MySQL не поддерживает InnoDB, так что, возможно, настало время для обновления.",
'config-mysql-engine-help' => "'''InnoDB''' почти всегда предпочтительнее, так как он лучше справляется с параллельным доступом.
'''MyISAM''' может оказаться быстрее для вики с одним пользователем или с минимальным количеством поступающих правок, однако базы данных на нём портятся чаще, чем на InnoDB.",
@@ -16188,7 +17784,6 @@ chmod a+w $3</pre>',
Это более эффективно, чем ''UTF-8 режим'' MySQL, и позволяет использовать полный набор символов Unicode.
В '''режиме UTF-8''' MySQL будет знать в какой кодировке находятся Ваши данные и может отображать и преобразовывать их соответствующим образом, но это не позволит вам хранить символы выше [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Базовой Многоязыковой Плоскости].",
- 'config-ibm_db2-low-db-pagesize' => "В вашей базе данных DB2 по умолчанию задано табличное пространство с недостаточным размером страницы. Размер страницы должен быть не менее '''32K'''.",
'config-site-name' => 'Название вики:',
'config-site-name-help' => 'Название будет отображаться в заголовке окна браузера и в некоторых других местах вики.',
'config-site-name-blank' => 'Введите название сайта.',
@@ -16231,7 +17826,7 @@ chmod a+w $3</pre>',
'config-optional-continue' => 'Произвести тонкую настройку',
'config-optional-skip' => 'Хватит, установить вики',
'config-profile' => 'Профиль прав прользователей:',
- 'config-profile-wiki' => 'Традиционная вики',
+ 'config-profile-wiki' => 'Открытая вики',
'config-profile-no-anon' => 'Требуется создание учётной записи',
'config-profile-fishbowl' => 'Только для авторизованных редакторов',
'config-profile-private' => 'Закрытая вики',
@@ -16241,7 +17836,7 @@ chmod a+w $3</pre>',
Однако, движок MediaWiki можно использовать и иными способами, и не далеко не всех удаётся убедить в преимуществах открытой вики-работы.
Так что в вас есть выбор.
-Конфигурация '''«{{int:config-profile-wiki}}»''' позволяет всем править страницы даже не регистрируясь на сайте. Конфигурация '''{{int:config-profile-no-anon}}''' обеспечивает дополнительный учёт, но может отсечь случайных участников.
+Модель '''«{{int:config-profile-wiki}}»''' позволяет всем править страницы даже не регистрируясь на сайте. Конфигурация '''{{int:config-profile-no-anon}}''' обеспечивает дополнительный учёт, но может отсечь случайных участников.
Сценарий '''«{{int:config-profile-fishbowl}}»''' разрешает редактирование только определённым участникам, но общедоступным остаётся просмотр страниц, в том числе просмотр истории изменения. В режиме '''«{{int:config-profile-private}}»''' просмотр страниц разрешён только определённым пользователям, какая-то их часть может иметь также права на редактирование.
@@ -16295,6 +17890,8 @@ GFDL может быть использована, но она сложна дл
'config-logo-help' => 'Стандартная тема оформления MediaWiki содержит над боковой панелью пространство для логотипа размером 135x160 пикселей.
Загрузите изображение соответствующего размера, и введите его URL здесь.
+Вы можете использовать <code>$wgStylePath</code> или <code>$wgScriptPath</code>, если ваш логотип находится относительно к этим путям.
+
Если вам не нужен логотип, оставьте это поле пустым.',
'config-instantcommons' => 'Включить Instant Commons',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] — это функция, позволяющая использовать изображения, звуки и другие медиафайлы с Викисклада ([//commons.wikimedia.org/ Wikimedia Commons]).
@@ -16329,7 +17926,7 @@ GFDL может быть использована, но она сложна дл
'config-install-alreadydone' => "'''Предупреждение:''' Вы, кажется, уже устанавливали MediaWiki и пытаетесь произвести повторную установку.
Пожалуйста, перейдите на следующую страницу.",
'config-install-begin' => 'Нажав «{{int:config-continue}}», вы начнёте установку MediaWiki.
-Если вы хотите внести изменения, нажмите «Назад».',
+Если вы хотите внести изменения, нажмите «{{int:config-back}}».',
'config-install-step-done' => 'выполнено',
'config-install-step-failed' => 'не удалось',
'config-install-extensions' => 'В том числе расширения',
@@ -16385,16 +17982,20 @@ $3
'''Примечание''': Если вы не сделаете этого сейчас, то сгенерированный файл конфигурации не будет доступен вам в дальнейшем, если вы выйдете из установки, не скачивая его.
По окончании действий, описанных выше, вы сможете '''[$2 войти в вашу вики]'''.",
- 'config-download-localsettings' => 'Загрузить LocalSettings.php',
+ 'config-download-localsettings' => 'Загрузить <code>LocalSettings.php</code>',
'config-help' => 'справка',
'config-nofile' => 'Файл "$1" не удается найти. Он был удален?',
+ 'config-extension-link' => 'Знаете ли вы, что ваш вики-проект поддерживает [//www.mediawiki.org/wiki/Manual:Extensions расширения]?
+
+Вы можете просмотреть [//www.mediawiki.org/wiki/Category:Extensions_by_category расширения по категориям] или [//www.mediawiki.org/wiki/Extension_Matrix матрицу расширений], чтобы увидеть их полный список.',
'mainpagetext' => "'''Вики-движок «MediaWiki» успешно установлен.'''",
'mainpagedocfooter' => 'Информацию по работе с этой вики можно найти в [//meta.wikimedia.org/wiki/%D0%9F%D0%BE%D0%BC%D0%BE%D1%89%D1%8C:%D0%A1%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D0%BD%D0%B8%D0%B5 справочном руководстве].
== Некоторые полезные ресурсы ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Список возможных настроек];
* [//www.mediawiki.org/wiki/Manual:FAQ Часто задаваемые вопросы и ответы по MediaWiki];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Рассылка уведомлений о выходе новых версий MediaWiki].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Рассылка уведомлений о выходе новых версий MediaWiki].
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Перевод MediaWiki на свой язык]',
);
/** Rusyn (русиньскый)
@@ -16408,7 +18009,7 @@ $messages['rue'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Наставлїня конфіґурації]
* [//www.mediawiki.org/wiki/Manual:FAQ Часты вопросы о MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Розосыланя повідомлїнь про новы верзії MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Розосыланя повідомлїнь про новы верзії MediaWiki]', # Fuzzy
);
/** Sanskrit (संस्कृतम्)
@@ -16428,7 +18029,7 @@ $messages['sah'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Конфигурация уларытыытын параметрдара]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki релизтарын почтовай испииһэгэ]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki релизтарын почтовай испииһэгэ]', # Fuzzy
);
/** Sardinian (sardu)
@@ -16447,7 +18048,7 @@ $messages['scn'] = array(
== P'accuminzari ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Alencu di mpustazzioni di cunfigurazzioni]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list dî rilassi di MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list dî rilassi di MediaWiki]", # Fuzzy
);
/** Scots (Scots)
@@ -16460,7 +18061,7 @@ $messages['sco'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settins leet]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki releese mailin leet]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki releese mailin leet]", # Fuzzy
);
/** Sassaresu (Sassaresu)
@@ -16474,7 +18075,7 @@ Li sighenti cullegamenti so in linga ingrese:
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Impusthazioni di cunfigurazioni]
* [//www.mediawiki.org/wiki/Manual:FAQ Prigonti friquenti i MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list annùnzii MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list annùnzii MediaWiki]", # Fuzzy
);
/** Cmique Itom (Cmique Itom)
@@ -16493,7 +18094,7 @@ $messages['sh'] = array(
== Početak ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista postavki]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki najčešće postavljana pitanja]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista E-Mail adresa MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista E-Mail adresa MediaWiki]', # Fuzzy
);
/** Tachelhit (Tašlḥiyt/ⵜⴰⵛⵍⵃⵉⵜ)
@@ -16506,7 +18107,7 @@ $messages['shi'] = array(
== Izwir d MediaWiki ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Umuɣ n iɣwwarn n usgadda ]
* [//www.mediawiki.org/wiki/Manual:FAQ/fr Isqqsitn f MidyWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Umuɣ n imsgdaln f imbḍitn n MidyaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Umuɣ n imsgdaln f imbḍitn n MidyaWiki]', # Fuzzy
);
/** Sinhala (සිංහල)
@@ -16514,62 +18115,144 @@ $messages['shi'] = array(
* @author පසිඳු කාවින්ද
*/
$messages['si'] = array(
+ 'config-desc' => 'මාධ්‍යවිකි සඳහා වූ ස්ථාපකය',
+ 'config-title' => 'මාධ්‍යවිකි $1 ස්ථාපනය',
'config-information' => 'තොරතුරු',
+ 'config-localsettings-key' => 'උසස්කිරීම් යතුර:',
+ 'config-localsettings-badkey' => 'ඔබ ඉදිරිපත් කෙරූ යතුර වැරදිය.',
+ 'config-session-error' => 'සැසිය ඇරඹීමේ දෝෂය: $1',
'config-your-language' => 'ඔබේ භාෂාව:',
'config-wiki-language' => 'විකි භාෂාව:',
'config-back' => '← ආපසු',
'config-continue' => 'ඉදිරියට →',
'config-page-language' => 'භාෂාව',
'config-page-welcome' => 'මාධ්‍යවිකි වෙත පිළිගනිමු!',
+ 'config-page-dbconnect' => 'දත්ත සංචිතයට සම්බන්ධ කරන්න',
+ 'config-page-upgrade' => 'පවත්නා ස්ථාපනය උසස් කරන්න',
'config-page-dbsettings' => 'දත්ත සංචිත සැකසුම්',
'config-page-name' => 'නම',
'config-page-options' => 'විකල්ප',
'config-page-install' => 'ස්ථාපනය',
'config-page-complete' => 'සම්පූර්ණයි!',
+ 'config-page-restart' => 'ස්ථාපනය යළි අරඹන්න',
'config-page-readme' => 'මාව කියවන්න',
'config-page-releasenotes' => 'නිකුතු සටහන්',
'config-page-copying' => 'පිටපත් කරමින්',
+ 'config-page-upgradedoc' => 'උසස් කරමින්',
+ 'config-page-existingwiki' => 'පවත්නා විකිය',
+ 'config-env-php' => 'PHP $1 ස්ථාපිතයි.',
+ 'config-db-type' => 'දත්ත සංචිත වර්ගය:',
+ 'config-db-host' => 'දත්ත සංචිත ධාරක:',
+ 'config-db-wiki-settings' => 'මෙම විකිය හඳුනා ගන්න',
'config-db-name' => 'දත්ත සංචිතයේ නම:',
+ 'config-db-name-oracle' => 'දත්ත සංචිත සංක්ෂිප්ත නිරූපණය:',
+ 'config-db-install-account' => 'ස්ථාපනය සඳහා පරිශීලක ගිණුම',
+ 'config-db-username' => 'දත්ත සංචිතයේ පරිශීලක නාමය:',
+ 'config-db-password' => 'දත්ත සංචිතයේ මුරපදය:',
+ 'config-db-wiki-account' => 'සාමාන්‍ය ක්‍රියාකාරිත්වය සඳහා පරිශීලක ගිණුම',
+ 'config-db-prefix' => 'දත්ත සංචිත වගු උපසර්ගය:',
+ 'config-db-charset' => 'දත්ත සංචිත අක්ෂර කට්ටලය',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 ද්විමය',
'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-charset-mysql4' => 'MySQL 4.0 ආපස්සට-ගැළපෙන UTF-8',
'config-db-port' => 'දත්ත සංචිතයේ කවුළුව:',
- 'config-type-ibm_db2' => 'IBM DB2',
+ 'config-db-schema' => 'මාධ්‍යවිකි සඳහා සංක්ෂිප්ත නිරූපණය:',
+ 'config-pg-test-error' => "'''$1''' දත්ත සංචිතය වෙත සම්බන්ධ විය නොහැක: $2",
+ 'config-sqlite-dir' => 'SQLite දත්ත නාමවලිය:',
+ 'config-oracle-def-ts' => 'සාමාන්‍ය වගු අවකාශය:',
+ 'config-oracle-temp-ts' => 'තාවකාලික වගු අවකාශය:',
'config-header-mysql' => 'MySQL සැකසුම්',
'config-header-postgres' => 'PostgreSQL සැකසුම්',
'config-header-sqlite' => 'SQLite සැකසුම්',
'config-header-oracle' => 'ඔරකල් සැකසුම්',
- 'config-header-ibm_db2' => 'IBM DB2 සැකසුම්',
+ 'config-invalid-db-type' => 'වලංගු නොවන දත්ත සංචිත වර්ගය',
+ 'config-missing-db-name' => '"දත්ත සංචිත නාමය" සඳහා ඔබ විසින් අගයක් දිය යුතු වේ',
+ 'config-missing-db-host' => '"දත්ත සංචිත ධාරකය" සඳහා ඔබ විසින් අගයක් දිය යුතු වේ',
+ 'config-missing-db-server-oracle' => '"දත්ත සංචිත TNS" සඳහා ඔබ විසින් අගයක් දිය යුතු වේ',
+ 'config-regenerate' => 'නැවත ජනිත කරන්න LocalSettings.php →',
+ 'config-db-web-account' => 'ජාල ප්‍රවේශනය සඳහා දත්ත සංචිත ගිණුම',
+ 'config-mysql-engine' => 'ආචයන එන්ජිම:',
'config-mysql-innodb' => 'InnoDB',
'config-mysql-myisam' => 'MyISAM',
'config-mysql-binary' => 'ද්විමය',
'config-mysql-utf8' => 'UTF-8',
+ 'config-site-name' => 'විකියෙහි නම:',
'config-site-name-blank' => 'අඩවි නාමයක් යොදන්න.',
'config-project-namespace' => 'ව්‍යාපෘතියේ නාමඅවකාශය:',
'config-ns-generic' => 'ව්‍යාපෘතිය',
+ 'config-ns-site-name' => 'විකියෙහි නම ලෙසම: $1',
'config-ns-other' => 'වෙනත් (විශේෂණය කරන්න)',
'config-ns-other-default' => 'මගේවිකිය',
'config-admin-box' => 'පරිපාලක ගිණුම',
'config-admin-name' => 'ඔබේ නම:',
'config-admin-password' => 'මුරපදය:',
'config-admin-password-confirm' => 'මුරපදය නැවතත්:',
+ 'config-admin-name-blank' => 'පරිපාලක පරිශීලක නාමය යොදන්න.',
+ 'config-admin-password-blank' => 'පරිපාලක ගිණුම සඳහා මුරපදය යොදන්න.',
+ 'config-admin-password-same' => 'මුරපදය හා පරිශීලක නාමය එක සමාන නොවිය යුතුය.',
+ 'config-admin-password-mismatch' => 'ඔබ ඇතුළු කල මුරපද දෙක නොගැලපේ.',
'config-admin-email' => 'විද්‍යුත්-තැපැල් ලිපිනය:',
- 'config-profile-wiki' => 'සාම්ප්‍රදායික විකිය',
+ 'config-admin-error-bademail' => 'ඔබ විසින් වලංගු නොවන විද්‍යුත්-ලිපිනයක් යොදා ඇත.',
+ 'config-optional-continue' => 'මගෙන් තව ප්‍රශ්ණ අහන්න.',
+ 'config-optional-skip' => 'මම දැනටමත් කම්මැලි වී ඇත, විකිය ස්ථාපනය කරන්න.',
+ 'config-profile' => 'පරිශීලක හිමිකම් පැතිකඩ:',
+ 'config-profile-wiki' => 'සාම්ප්‍රදායික විකිය', # Fuzzy
'config-profile-no-anon' => 'ගිණුම් තැනීම අවශ්‍යයි',
+ 'config-profile-fishbowl' => 'අවසරලත් සංස්කාරකවරුන් පමණි',
'config-profile-private' => 'පුද්ගලික විකිය',
+ 'config-license' => 'කතුහිමිකම සහ බලපත්‍රය:',
+ 'config-license-none' => 'බලපත්‍ර පාද තලයක් නොමැත',
+ 'config-license-cc-by-sa' => 'නිර්මාණාත්මක පොදුජන ආරෝපණය හුවමාරුවට සමානව',
+ 'config-license-cc-by' => 'නිර්මාණාත්මක පොදුජන ආරෝපණය',
+ 'config-license-cc-by-nc-sa' => 'නිර්මාණාත්මක පොදුජන ආරෝපණය වාණිජ්‍ය-නොවන හුවමාරුවට සමානව',
'config-license-pd' => 'පොදු වසම',
'config-email-settings' => 'විද්‍යුත්-තැපැල් සැකසුම්',
+ 'config-enable-email' => 'පිටතට යොමු වූ විද්‍යුත්-තැපෑල සක්‍රිය කරන්න',
+ 'config-email-user' => 'පරිශීලක-වෙත-පරිශීලක විද්‍යුත්-තැපෑල සක්‍රිය කරන්න',
+ 'config-email-usertalk' => 'පරිශීලක කතාබහ පිටු නිවේදනය සක්‍රිය කරන්න',
+ 'config-email-watchlist' => 'මුරලැයිස්තු නිවේදනය සක්‍රිය කරන්න',
+ 'config-email-auth' => 'විද්‍යුත්-තැපැල් සහතික කිරීම සක්‍රිය කරන්න',
+ 'config-email-sender' => 'ප්‍රත්‍යාගමන විද්‍යුත්-තැපැල් ලිපිනය:',
+ 'config-upload-settings' => 'පින්තූර සහ ගොනු උඩුගත කිරීම්',
+ 'config-upload-enable' => 'ගොනු උඩුගත කිරීම් සක්‍රිය කරන්න',
'config-upload-deleted' => 'මැකූ ගොනු සඳහා නාමාවලිය:',
+ 'config-logo' => 'ලාංඡනයේ URL:',
+ 'config-instantcommons' => 'ක්ෂණික කොමන්ස් සක්‍රිය කරන්න',
+ 'config-cc-again' => 'නැවත ඇහිඳගන්න...',
+ 'config-advanced-settings' => 'උසස් වින්‍යාසගතකෙරුම',
+ 'config-cache-options' => 'වස්තු කෑෂය සඳහා සැකසුම්:',
+ 'config-memcached-servers' => 'මතකකෑෂිත සර්වරයන්:',
'config-extensions' => 'විස්තීර්ණ',
'config-install-step-done' => 'සිදුකලා',
'config-install-step-failed' => 'අසාර්ථකයි',
+ 'config-install-extensions' => 'විස්තීර්ණ අඩංගු කරමින්',
+ 'config-install-database' => 'දත්ත සංචිතය සකසමින්',
+ 'config-install-schema' => 'සංක්ෂිප්ත නිරූපණය තනමින්',
+ 'config-install-pg-schema-not-exist' => 'PostgreSQL සංක්ෂිප්ත නිරූපණය නොපවතියි.',
+ 'config-install-pg-commit' => 'වෙනස්කම් ප්‍රයාපනය කරමින්',
+ 'config-install-pg-plpgsql' => 'PL/pgSQL භාෂාව සඳහා පරික්ෂා කරමින්',
+ 'config-install-user' => 'දත්ත සංචිත පරිශීලක තනමින්',
+ 'config-install-user-alreadyexists' => '"$1" පරිශීලක දැනටමත් පවතී',
+ 'config-install-user-create-failed' => '"$1" පරිශීලක තැනීම අසාර්ථකයි: $2',
+ 'config-install-user-missing' => 'විශේෂණය කෙරූ "$1" පරිශීලකයා නොපවතියි.',
'config-install-tables' => 'වගු තනමින්',
+ 'config-install-interwiki' => 'සාමාන්‍ය අන්තර්විකි වගුව ගහනය කරමින්',
+ 'config-install-interwiki-list' => '<code>interwiki.list</code> ගොනුව කියවිය නොහැක.',
+ 'config-install-stats' => 'සංඛ්‍යානය අරඹමින්',
+ 'config-install-keys' => 'රහස් යතුරු ජනිත කරමින්',
+ 'config-install-sysop' => 'පරිපාලක පරිශීලක ගිණුම තනමින්',
+ 'config-install-mainpage' => 'සාමාන්‍ය අන්තර්ගතය සමඟින් ප්‍රධාන පිටුව තනමින්',
+ 'config-install-mainpage-failed' => 'ප්‍රධාන පිටුව ඇතුල් කල නොහැක: $1',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code> බාගන්න',
'config-help' => 'උදව්',
+ 'config-nofile' => '"$1" ගොනුව සොයාගත නොහැක. එක මැකිලා ගියාවත්ද?',
'mainpagetext' => "'''මීඩියාවිකි සාර්ථක ලෙස ස්ථාපනය කරන ලදි.'''",
'mainpagedocfooter' => 'විකි මෘදුකාංග භාවිතා කිරීම පිළිබඳ තොරතුරු සඳහා [//meta.wikimedia.org/wiki/Help:Contents පරිශීලකයන් සඳහා නියමුව] හදාරන්න.
== ඇරඹුම ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings වින්‍යාස සැකසුම්]
* [//www.mediawiki.org/wiki/Manual:FAQ මීඩියාවිකි නිති-විමසන-පැන]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce මීඩියාවිකි නිකුතුව තැපැල් ලැයිස්තුව]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce මීඩියාවිකි නිකුතුව තැපැල් ලැයිස්තුව]', # Fuzzy
);
/** Slovak (slovenčina)
@@ -16581,7 +18264,7 @@ $messages['sk'] = array(
'config-back' => '← Späť',
'config-continue' => 'Pokračovať →',
'config-page-language' => 'Jazyk',
- 'config-download-localsettings' => 'Stiahnuť LocalSettings.php',
+ 'config-download-localsettings' => 'Stiahnuť <code>LocalSettings.php</code>',
'config-nofile' => 'Súbor "$1" sa nenašiel. Bol zmazaný?',
'mainpagetext' => "'''Softvér MediaWiki bol úspešne nainštalovaný.'''",
'mainpagedocfooter' => 'Informácie ako používať wiki softvér nájdete v [//meta.wikimedia.org/wiki/Help:Contents Používateľskej príručke].
@@ -16590,7 +18273,7 @@ $messages['sk'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Zoznam konfiguračných nastavení]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce mailing list nových verzií MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce mailing list nových verzií MediaWiki]', # Fuzzy
);
/** Slovenian (slovenščina)
@@ -16601,12 +18284,12 @@ $messages['sl'] = array(
'config-desc' => 'Namestitveni program za MediaWiki',
'config-title' => 'Namestitev MediaWiki $1',
'config-information' => 'Informacije',
- 'config-localsettings-cli-upgrade' => 'Zaznana je bila datoteka LocalSettings.php.
-Za nadgradnjo te namestitve zaženite update.php',
+ 'config-localsettings-cli-upgrade' => 'Zaznana je bila datoteka <code>LocalSettings.php</code>.
+Za nadgradnjo te namestitve zaženite <code>update.php</code>',
'config-localsettings-key' => 'Nadgraditveni ključ:',
'config-localsettings-badkey' => 'Naveden ključ je napačen.',
'config-upgrade-key-missing' => 'Zaznana je bila obstoječa namestitev MediaWiki.
-Za nadgradnjo te namestitve vstavite naslednjo vrstico na dno vaše LocalSettings.php:
+Za nadgradnjo te namestitve vstavite naslednjo vrstico na dno vaše <code>LocalSettings.php</code>:
$1',
'config-session-error' => 'Napaka pri začenjanju seje: $1',
@@ -16640,8 +18323,8 @@ Preverite vaš php.ini in se prepričajte, da je <code>session.save_path</code>
'config-help-restart' => 'Želite počistiti vse shranjene podatke, ki ste jih vnesti, in ponovno začeti s postopkom namestitve?',
'config-restart' => 'Da, ponovno zaženi',
'config-welcome' => '=== Pregledi okolja ===
-Izvedeni so osnovni pregledi, da vidimo, če je okolje primerno za namestitev MediaWiki.
-Če med namestitvijo potrebujete pomoč, posredujte tudi rezultate teh pregledov.',
+Izvedli bomo osnovne preglede, da vidimo, če je okolje primerno za namestitev MediaWiki.
+Posredujte rezultate teh pregledov, če med namestitvijo potrebujete pomoč.',
'config-sidebar' => '* [//www.mediawiki.org Domača stran MediaWiki]
* [//www.mediawiki.org/wiki/Help:Contents Vodnik za uporabnike]
* [//www.mediawiki.org/wiki/Manual:Contents Vodnik za administratorje]
@@ -16684,7 +18367,6 @@ Vendar pa MediaWiki zahteva PHP $2 ali višji.',
'config-db-schema-help' => 'Ta shema je po navadi v redu.
Spremenite jo samo, če veste, da jo morate.',
'config-sqlite-dir' => 'Mapa podatkov SQLite:',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki podpira naslednje sisteme zbirk podatkov:
$1
@@ -16694,13 +18376,12 @@ $1
'config-header-postgres' => 'Nastavitve PostgreSQL',
'config-header-sqlite' => 'Nastavitve SQLite',
'config-header-oracle' => 'Nastavitve Oracle',
- 'config-header-ibm_db2' => 'Nastavitve IBM DB2',
'config-invalid-db-type' => 'Neveljavna vrsta zbirke podatkov',
'config-missing-db-name' => 'Vnesti morate vrednost za »Ime zbirke podatkov«',
'config-missing-db-host' => 'Vnesti morate vrednost za »Gostitelj zbirke podatkov«',
'config-missing-db-server-oracle' => 'Vnesti morate vrednost za »TNS zbirke podatkov«',
'config-invalid-db-server-oracle' => 'Neveljaven TNS zbirke podatkov »$1«.
-Uporabljajte samo črke ASCII (a-z, A-Z), številke (0-9), podčrtaje (_) in pike (.).',
+Uporabite ali "ime TNS" ali niz "Easy Connect" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Načini poimenovanja Oracle])',
'config-invalid-db-name' => 'Neveljavno ime zbirke podatkov »$1«.
Uporabljajte samo črke ASCII (a-z, A-Z), številke (0-9), podčrtaje (_) in vezaje (-).',
'config-invalid-db-prefix' => 'Neveljavna predpona zbirke podatkov »$1«.
@@ -16718,7 +18399,7 @@ Preverite mapo podatkov in ime zbirke podatkov spodaj ter poskusite znova.',
Sedaj lahko [$1 začnete uporabljati vaš wiki].',
'config-regenerate' => 'Ponovno ustvari LocalSettings.php →',
- 'config-show-table-status' => 'Poizvedba SHOW TABLE STATUS ni uspela!',
+ 'config-show-table-status' => 'Poizvedba <code>SHOW TABLE STATUS</code> ni uspela!',
'config-unknown-collation' => "'''Opozorilo:''' Zbirke podatkov uporablja neprepoznano razvrščanje znakov.",
'config-db-web-account' => 'Račun zbirke podatkov za spletni dostop',
'config-db-web-account-same' => 'Uporabi enak račun kot za namestitev',
@@ -16759,11 +18440,11 @@ Določite drugo uporabniško ime.',
'config-admin-error-bademail' => 'Vnesli ste neveljaven e-poštni naslov.',
'config-subscribe' => 'Naročite se na [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce poštni seznam obvestil o izdajah].',
'config-almost-done' => 'Skoraj ste že končali!
-Sedaj lahko preskočite preostalo konfiguriranje in zdaj namestite wiki.',
+Preostalo konfiguriranje lahko zdaj preskočite in wiki takoj namestite.',
'config-optional-continue' => 'Zastavi mi več vprašanj.',
'config-optional-skip' => 'Se že dolgočasim; samo namesti wiki.',
'config-profile' => 'Profil uporabniških pravic:',
- 'config-profile-wiki' => 'Klasičen wiki',
+ 'config-profile-wiki' => 'Odprti wiki',
'config-profile-no-anon' => 'Zahtevano je ustvarjanje računa',
'config-profile-fishbowl' => 'Samo pooblaščeni urejevalci',
'config-profile-private' => 'Zasebni wiki',
@@ -16802,11 +18483,16 @@ Vnesite ime dovoljenja ročno.',
'config-install-pg-schema-not-exist' => 'Shema PostgreSQL ne obstaja.',
'config-install-user-alreadyexists' => 'Uporabnik »$1« že obstaja',
'config-install-tables' => 'Ustvarjanje tabel',
- 'config-download-localsettings' => 'Prenesi LocalSettings.php',
+ 'config-download-localsettings' => 'Prenesi <code>LocalSettings.php</code>',
'config-help' => 'pomoč',
'mainpagetext' => "'''Programje MediaWiki je bilo uspešno nameščeno.'''",
- 'mainpagedocfooter' => 'Za uporabo in pomoč pri nastavitvi, prosimo, preglejte [//meta.wikimedia.org/wiki/MediaWiki_localisation dokumentacijo za prilagajanje vmesnika]
-in [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Uporabniški priročnik].',
+ 'mainpagedocfooter' => 'Oglejte si [//meta.wikimedia.org/wiki/Help:Contents Uporabniški priročnik] za informacije o uporabi programja wiki.
+
+== Kako začeti ==
+* [//www.mediawiki.org/wiki/Manual:Configuration_settings Seznam konfiguracijskih nastavitev]
+* [//www.mediawiki.org/wiki/Manual:FAQ Poogsto zastavljena vprašanja MediaWiki]
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Poštni seznam izdaj MediaWiki]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Prevedite MediaWiki v svoj jezik]',
);
/** Lower Silesian (Schläsch)
@@ -16820,7 +18506,7 @@ $messages['sli'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Liste der Konfigurationsvariablen]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki-FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]', # Fuzzy
);
/** Somali (Soomaaliga)
@@ -16832,7 +18518,7 @@ $messages['so'] = array(
== Bilaaw ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Albanian (shqip)
@@ -16844,7 +18530,7 @@ $messages['sq'] = array(
== Sa për fillim==
* [//www.mediawiki.org/wiki/Help:Configuration_settings Parazgjedhjet e MediaWiki-t]
* [//www.mediawiki.org/wiki/Help:FAQ Pyetjet e shpeshta rreth MediaWiki-t]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Njoftime rreth MediaWiki-t]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Njoftime rreth MediaWiki-t]', # Fuzzy
);
/** Serbian (Cyrillic script) (српски (ћирилица)‎)
@@ -16890,7 +18576,7 @@ $messages['sr-ec'] = array(
== Увод ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Помоћ у вези са подешавањима]
* [//www.mediawiki.org/wiki/Manual:FAQ Често постављена питања]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописна листа о издањима Медијавикија]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописна листа о издањима Медијавикија]', # Fuzzy
);
/** Serbian (Latin script) (srpski (latinica)‎)
@@ -16934,7 +18620,7 @@ Proverite Vaš php.ini i obezbedite da je <code>session.save_path</code> postavl
== Za početak ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Pomoć u vezi sa podešavanjima]
* [//www.mediawiki.org/wiki/Manual:FAQ Najčešće postavljena pitanja]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mejling lista o izdanjima MedijaVikija]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mejling lista o izdanjima MedijaVikija]', # Fuzzy
);
/** Sranan Tongo (Sranantongo)
@@ -16947,7 +18633,7 @@ $messages['srn'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Den seti]
* [//www.mediawiki.org/wiki/Manual:FAQ Sani di ben aksi furu (FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Boskopu grupu gi nyun meki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Boskopu grupu gi nyun meki]', # Fuzzy
);
/** Swati (SiSwati)
@@ -16961,17 +18647,18 @@ $messages['ss'] = array(
*/
$messages['stq'] = array(
'mainpagetext' => "'''Ju MediaWiki Software wuude mäd Ärfoulch installierd.'''",
- 'mainpagedocfooter' => 'Sjuch ju [//meta.wikimedia.org/wiki/MediaWiki_localization Dokumentation tou de Anpaasenge fon dän Benutseruurfläche] un dät [//meta.wikimedia.org/wiki/Help:Contents Benutserhondbouk] foar Hälpe tou ju Benutsenge un Konfiguration.',
+ 'mainpagedocfooter' => 'Sjuch ju [//meta.wikimedia.org/wiki/MediaWiki_localization Dokumentation tou de Anpaasenge fon dän Benutseruurfläche] un dät [//meta.wikimedia.org/wiki/Help:Contents Benutserhondbouk] foar Hälpe tou ju Benutsenge un Konfiguration.', # Fuzzy
);
/** Sundanese (Basa Sunda)
*/
$messages['su'] = array(
'mainpagetext' => "'''''Software'' MediaWiki geus diinstal.'''",
- 'mainpagedocfooter' => "Mangga tingal ''[//meta.wikimedia.org/wiki/MediaWiki_localisation documentation on customizing the interface]'' jeung [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Tungtunan Pamaké] pikeun pitulung maké jeung konfigurasi.",
+ 'mainpagedocfooter' => "Mangga tingal ''[//meta.wikimedia.org/wiki/MediaWiki_localisation documentation on customizing the interface]'' jeung [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Tungtunan Pamaké] pikeun pitulung maké jeung konfigurasi.", # Fuzzy
);
/** Swedish (svenska)
+ * @author Jopparn
* @author Skalman
* @author WikiPhoenix
*/
@@ -16979,8 +18666,20 @@ $messages['sv'] = array(
'config-desc' => 'Installationsprogram för MediaWiki',
'config-title' => 'Installation av MediaWiki $1',
'config-information' => 'Information',
+ 'config-localsettings-upgrade' => 'A <code>LocalSettings.php</code>-fil har upptäckts.
+För att uppgradera den här installationen, vänligen ange värdet för <code>$wgUpgradeKey</code> i rutan nedan.
+Du hittar den i <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'En <code>LocalSettings.php</code>-fil har upptäckts.
+För att uppgradera denna installation, kör <code>update.php</code> istället',
'config-localsettings-key' => 'Uppgraderingsnyckel:',
'config-localsettings-badkey' => 'Nyckeln du angav är inkorrekt.',
+ 'config-upgrade-key-missing' => 'En nuvarande installerade av MediaWiki har upptäckts.
+För att uppgradera installationen, lägg till följande rad i slutet av din <code>LocalSettings.php</code>:
+
+$1',
+ 'config-localsettings-incomplete' => 'De befintliga <code>LocalSettings.php</code> verkar vara ofullständig.
+Variabeln $1 är inte inställd.
+Ändra <code>LocalSettings.php</code> så att denna variabel är inställd och klicka på "{{int:Config-continue}}".',
'config-session-error' => 'Fel vid uppstart av session: $1',
'config-your-language' => 'Ditt språk:',
'config-your-language-help' => 'Välj ett språk som ska användas under installationen.',
@@ -17021,19 +18720,50 @@ Du kan inte installera MediaWiki.',
'config-env-php' => 'PHP $1 är installerad.',
'config-env-php-toolow' => 'PHP $1 är installerad.
MediaWiki kräver PHP $2 eller högre.',
+ 'config-outdated-sqlite' => "'''Varning:''' du har SQLite $1, vilket är lägre än minimikravet version $2. SQLite kommer inte att vara tillgänglig.",
+ 'config-register-globals' => "'''Varning: PHP:s <code>[http://php.net/register_globals register_globals]</code> tillval är aktiverat.'''
+'''Inaktivera den om du kan.'''
+MediaWiki kommer att fungera, men din server exponeras för potentiella säkerhetsluckor.",
+ 'config-safe-mode' => "''' Varning:''' PHP:s [http://www.php.net/features.safe-mode felsäkert läge] är aktivt.
+Det kan orsaka problem, särskilt om du använder filöverföringar och <code>math</code>-stöd.",
+ 'config-xml-bad' => 'PHP:s XML-modul saknas.
+MediaWiki kräver funktioner i denna modul och kommer inte att fungera i den här konfigurationen.
+Om du kör Mandrake, installera php-xml-paketet.',
+ 'config-memory-raised' => 'PHPs <code>memory_limit</code> är $1, ökar till $2.',
+ 'config-memory-bad' => "''' Varning:''' PHP:s <code>memory_limit</code> är $1.
+Detta är förmodligen för lågt.
+Installationen kan misslyckas!",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] är installerad',
'config-apc' => '[http://www.php.net/apc APC] är installerad',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] är installerad',
'config-diff3-bad' => 'GNU diff3 hittades inte.',
+ 'config-git-bad' => 'Git-versionen av kontrollmjukvaran hittades inte.',
+ 'config-no-uri' => "'''Fel:''' Kunde inte fastställa det nuvarande URI:et.
+Installationen avbröts.",
+ 'config-no-cli-uri' => "'''Varning:''' inget --scriptpath är anget, med standarden: <code>$1</code> .",
'config-using-server' => 'Använder servernamn "<nowiki>$1</nowiki>".',
'config-using-uri' => 'Använder server-URL "<nowiki>$1$2</nowiki>".',
+ 'config-no-cli-uploads-check' => "'''Varning:''' Din standardkatalog för uppladdningar (<code>$1</code>) inte är kontrollerad för sårbarhet från godtyckliga skriptkörning under CLI-installationen.",
+ 'config-db-type' => 'Databastyp:',
+ 'config-db-host' => 'Databasvärd:',
'config-db-wiki-settings' => 'Identifiera denna wiki',
'config-db-name' => 'Databasnamn:',
'config-db-name-oracle' => 'Databasschema:',
'config-db-install-account' => 'Användarkonto för installation',
'config-db-username' => 'Databas-användarnamn:',
'config-db-password' => 'Databas-lösenord:',
+ 'config-db-password-empty' => 'Ange ett lösenord för den nya databasanvändaren: $1.
+Även om det kan vara möjligt att skapa användare utan lösenord är det inte säkert.',
+ 'config-db-account-lock' => 'Använda samma användarnamn och lösenord under normal drift',
+ 'config-db-wiki-account' => 'Användarkonto för normal drift',
+ 'config-db-prefix' => 'Prefix för tabellerna i databasen:',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binär',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-charset-mysql4' => 'MySQL 4.0 bakåtkompatibel UTF-8',
+ 'config-db-port' => 'Databasport:',
'config-db-schema' => 'Schema för MediaWiki',
+ 'config-pg-test-error' => "Kan inte ansluta till databas '''$1''': $2",
+ 'config-sqlite-dir' => 'SQLite data-katalog:',
'config-header-mysql' => 'MySQL-inställningar',
'config-header-postgres' => 'PostgreSQL-inställningar',
'config-header-sqlite' => 'SQLite-inställningar',
@@ -17041,6 +18771,7 @@ MediaWiki kräver PHP $2 eller högre.',
'config-invalid-db-type' => 'Ogiltig databastyp',
'config-missing-db-name' => 'Du måste ange ett värde för "Databasnamn"',
'config-missing-db-host' => 'Du måste ange ett värde för "Databasvärd"',
+ 'config-missing-db-server-oracle' => 'Du måste ange ett värde för "Databas TNS"',
'config-invalid-db-name' => '"$1" är ett ogiltigt databasnamn.
Använd bara ASCII-bokstäver (a-z, A-Z), siffror (0-9), understreck (_) och bindestreck (-).',
'config-invalid-db-prefix' => '"$1" är ett ogiltigt databasprefix.
@@ -17050,8 +18781,17 @@ Använd bara ASCII-bokstäver (a-z, A-Z), siffror (0-9), understreck (_) och bin
Kontrollera värden, användarnamnet och lösenordet nedan och försök igen',
'config-invalid-schema' => '"$1" är ett ogiltigt schema för MediaWiki.
Använd bara ASCII-bokstäver (a-z, A-Z), siffror (0-9), understreck (_) och bindestreck (-).',
+ 'config-db-sys-create-oracle' => 'Installationsprogrammet stöder endast med ett SYSDBA-konto för att skapa ett nytt konto.',
'config-db-sys-user-exists-oracle' => 'Användarkontot "$1" finns redan. SYSDBA kan endast användas för att skapa ett nytt konto!',
'config-postgres-old' => 'PostgreSQL $1 eller senare krävs, du har $2.',
+ 'config-sqlite-name-help' => 'Välja ett namn som identifierar din wiki.
+Använd inte mellanslag eller bindestreck.
+Detta kommer att användas för SQLite-data filnamnet.',
+ 'config-sqlite-readonly' => 'Filen <code>$1</code> är inte skrivbar.',
+ 'config-sqlite-cant-create-db' => 'Kunde inte skapa databasfilen <code>$1</code>.',
+ 'config-sqlite-fts3-downgrade' => 'PHP saknar stöd för FTS3, nedgraderar tabeller',
+ 'config-can-upgrade' => "Det finns MediaWiki-tabeller i den här databasen.
+För att uppgradera dem till MediaWiki $1, klicka på '''Fortsätt'''.",
'config-upgrade-done' => "Uppgraderingen slutfördes.
Du kan nu [$1 börja använda din wiki].
@@ -17061,10 +18801,23 @@ Detta '''rekommenderas inte''' om du har problem med din wiki.",
'config-upgrade-done-no-regenerate' => 'Uppgraderingen slutfördes.
Du kan nu [$1 börja använda din wiki].',
+ 'config-regenerate' => 'Återskapa LocalSettings.php →',
+ 'config-db-web-account' => 'Databaskonto för webbaccess',
+ 'config-db-web-account-same' => 'Använd samma konto som för installation',
+ 'config-db-web-create' => 'Skapa kontot om det inte redan finns',
+ 'config-mysql-engine' => 'Lagringsmotor:',
+ 'config-mysql-binary' => 'Binär',
'config-mysql-utf8' => 'UTF-8',
'config-site-name' => 'Namnet på wikin:',
'config-site-name-blank' => 'Ange ett sidnamn.',
'config-ns-generic' => 'Projekt',
+ 'config-ns-site-name' => 'Samma som wikinamnet: $1',
+ 'config-ns-other' => 'Annan (specificera)',
+ 'config-ns-invalid' => 'Den angivna namnrymden "<nowiki>$1</nowiki>" är ogiltig.
+Ange ett annat namnrymd för projektet.',
+ 'config-ns-conflict' => 'Den angivna namnrymden "<nowiki>$1</nowiki>" står i konflikt med en standardnamnrymd för MediaWiki.
+Ange ett annat namnrymd för projektet.',
+ 'config-admin-box' => 'Administratörskonto',
'config-admin-name' => 'Ditt namn:',
'config-admin-password' => 'Lösenord:',
'config-admin-password-confirm' => 'Lösenord igen:',
@@ -17074,26 +18827,105 @@ Detta är namnet du kommer att använda för att logga in på wikin.',
'config-admin-name-invalid' => 'Det angivna användarnamnet "<nowiki>$1</nowiki>" är ogiltigt.
Ange ett annat användarnamn.',
'config-admin-password-blank' => 'Ange ett lösenord för administratörskontot.',
+ 'config-admin-password-same' => 'Lösenordet får inte vara samma som användarnamnet.',
+ 'config-admin-password-mismatch' => 'De två lösenord du uppgett överensstämmer inte med varandra.',
'config-admin-email' => 'E-postadress:',
+ 'config-admin-error-user' => 'Internt fel när du skapar en administratör med namnet "<nowiki>$1</nowiki>".',
+ 'config-admin-error-password' => 'Internt fel när du väljer ett lösenord för administratören "<nowiki>$1</nowiki>": <pre>$2</pre>',
'config-admin-error-bademail' => 'Du har angivit en felaktigt e-postadress.',
+ 'config-subscribe' => 'Prenumerera på [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce e-postlistan för tillkännagivanden].',
+ 'config-almost-done' => 'Du är nästan färdig!
+Du kan nu hoppa över återstående konfigurationer och installera wikin nu.',
'config-optional-continue' => 'Ställ fler frågor till mig.',
+ 'config-optional-skip' => 'Jag är redan uttråkad, bara installera wiki.',
+ 'config-profile-wiki' => 'Öppen wiki',
+ 'config-profile-no-anon' => 'Kontoskapande krävs',
+ 'config-profile-fishbowl' => 'Endast auktoriserade redigerare',
'config-profile-private' => 'Privat wiki',
'config-license' => 'Upphovsrätt och licens:',
+ 'config-license-none' => 'Ingen licenssidfot',
+ 'config-license-cc-by-sa' => 'Creative Commons Erkännande Dela Lika',
+ 'config-license-cc-by' => 'Creative Commons Erkännande',
+ 'config-license-cc-by-nc-sa' => 'Creative Commons Erkännande Icke-Kommersiell Dela Lika',
+ 'config-license-cc-0' => 'Creative Commons Zero (allmän egendom)',
+ 'config-license-gfdl' => 'GNU Free Documentation License 1.3 eller senare',
'config-license-pd' => 'Allmän egendom',
+ 'config-license-cc-choose' => 'Välj en anpassad Creative Commons-licens',
'config-email-settings' => 'E-postinställningar',
+ 'config-enable-email' => 'Aktivera utgående e-post',
+ 'config-email-user' => 'Aktivera e-post mellan användare',
+ 'config-email-user-help' => 'Tillåta alla användare att skicka mail till varandra om de har aktiverat det i deras preferenser.',
+ 'config-email-usertalk' => 'Aktivera underrättelse för användardiskussionssidan',
+ 'config-email-watchlist' => 'Aktivera meddelanden för bevakningslistan',
+ 'config-email-auth' => 'Aktivera autentisering via e-post',
+ 'config-upload-settings' => 'Bild- och filuppladdningar',
+ 'config-upload-enable' => 'Aktivera filöverföringar',
+ 'config-upload-deleted' => 'Mapp för raderade filer:',
+ 'config-logo' => 'Logotyp-URL:',
+ 'config-instantcommons' => 'Aktivera Instant Commons',
+ 'config-cc-error' => 'Creative Commons-licens-väljaren gav inget resultat.
+Ange licensnamnet manuellt.',
+ 'config-cc-again' => 'Välj igen...',
+ 'config-cc-not-chosen' => 'Välj vilken Creative Commons-licens du vill ha och klicka på "gå vidare".',
+ 'config-advanced-settings' => 'Avancerad konfiguration',
+ 'config-extensions' => 'Tillägg',
+ 'config-install-alreadydone' => "''' Varning:''' Du verkar redan ha installerat MediaWiki och försöker installera det igen.
+Vänligen fortsätt till nästa sida.",
+ 'config-install-begin' => 'Genom att trycka på "{{int:config-continue}}", påbörjar du installationen av MediaWiki.
+Om du fortfarande vill göra ändringar trycker du på "{{int:config-back}}".',
'config-install-step-done' => 'klar',
'config-install-step-failed' => 'misslyckades',
+ 'config-install-database' => 'Konfigurerar databas',
+ 'config-install-schema' => 'Skapar schema',
+ 'config-pg-no-plpgsql' => 'Du måste installera språket PL/pgSQL i databasen $1',
+ 'config-install-user' => 'Skapar databasanvändare',
+ 'config-install-user-alreadyexists' => 'Användaren "$1" finns redan',
+ 'config-install-user-create-failed' => 'Misslyckades att skapa användare "$1": $2',
+ 'config-install-user-grant-failed' => 'Beviljandet av behörighet till användaren "$1" misslyckades: $2',
+ 'config-install-user-missing' => 'Den angivna användaren "$1" existerar inte.',
+ 'config-install-user-missing-create' => 'Den angivna användaren "$1" existerar inte.
+Vänligen klicka på kryssrutan "skapa konto" nedan om du vill skapa den.',
+ 'config-install-tables' => 'Skapar tabeller',
+ 'config-install-tables-exist' => "''' Varning:''' MediaWiki-tabeller verkar redan finnas.
+Hoppar över skapandet.",
+ 'config-install-tables-failed' => "''' Fel:''' Skapandet av tabell misslyckades med följande fel: $1",
+ 'config-install-interwiki' => 'Lägger till standardtabell för interwiki',
+ 'config-install-interwiki-list' => 'Kunde inte läsa filen <code>interwiki.list</code>.',
+ 'config-install-stats' => 'Initierar statistik',
+ 'config-install-keys' => 'Genererar hemliga nycklar',
'config-insecure-keys' => "'''Varning:''' {{PLURAL:$2|En säkerhetsnyckel|Säkerhetsnycklar}} ($1) som generades under installationen är inte helt {{PLURAL:$2|säker|säkra}} . Överväg att ändra {{PLURAL:$2|den|dem}} manuellt.",
- 'config-download-localsettings' => 'Ladda ned LocalSettings.php',
+ 'config-install-sysop' => 'Skapar administratörskonto',
+ 'config-install-subscribe-fail' => 'Det gick inte att prenumerera på mediawiki-announce: $1',
+ 'config-install-subscribe-notpossible' => 'cURL är inte installerad och allow_url_fopen är inte tillgänglig.',
+ 'config-install-mainpage' => 'Skapa huvudsida med standardinnehåll',
+ 'config-install-extension-tables' => 'Skapar tabeller för aktiverade tillägg',
+ 'config-install-mainpage-failed' => 'Kunde inte infoga huvudsidan: $1',
+ 'config-install-done' => "'''Grattis!'''
+Du har installerat MediaWiki.
+
+Installationsprogrammet har genererat filen <code>LocalSettings.php</code>.
+Det innehåller alla dina konfigurationer.
+
+Du kommer att behöva ladda ned den och placera den i botten av din wiki-installation (samma mapp som index.php). Nedladdningen borde ha startats automatiskt.
+
+Om ingen nedladdning erbjöds, eller om du har avbrutit det kan du starta om nedladdningen genom att klicka på länken nedan:
+
+$3
+
+'''OBS''': Om du inte gör detta nu, kommer denna genererade konfigurationsfil inte vara tillgänglig för dig senare om du avslutar installationen utan att ladda ned den.
+
+När det är klart, kan du '''[$2 gå in på din wiki]'''.",
+ 'config-download-localsettings' => 'Ladda ned <code>LocalSettings.php</code>',
'config-help' => 'hjälp',
+ 'config-nofile' => 'Filen "$1" kunde inte hittas. Har den tagits bort?',
'mainpagetext' => "'''MediaWiki har installerats utan problem.'''",
'mainpagedocfooter' => 'Information om hur wiki-programvaran används finns i [//meta.wikimedia.org/wiki/Help:Contents användarguiden].
== Att komma igång ==
-
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista över konfigurationsinställningar]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mail list]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mail list]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Lokalisera MediaWiki för ditt språk]',
);
/** Swahili (Kiswahili)
@@ -17106,7 +18938,7 @@ $messages['sw'] = array(
== Msaada wa kianzio ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Orodha ya mipangilio ya msingi]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ ya MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Orodha ya utoaji wa habari za MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Orodha ya utoaji wa habari za MediaWiki]', # Fuzzy
);
/** Silesian (ślůnski)
@@ -17119,7 +18951,7 @@ $messages['szl'] = array(
== Na sztart ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lista sztalowań konfiguracyje]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komuńikaty uo nowych wersyjach MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komuńikaty uo nowych wersyjach MediaWiki]', # Fuzzy
);
/** Tamil (தமிழ்)
@@ -17193,7 +19025,7 @@ $messages['ta'] = array(
'config-optional-continue' => 'என்னை இன்னும் அதிகமாக வினவு.',
'config-optional-skip' => 'நான் ஏற்கனவே சோர்வடைந்துள்ளேன், விக்கியை மட்டும் உருவாக்கு.',
'config-profile' => 'பயனர் உரிமைகள் சுயவிவரம்:',
- 'config-profile-wiki' => 'பாரம்பரிய விக்கி',
+ 'config-profile-wiki' => 'பாரம்பரிய விக்கி', # Fuzzy
'config-profile-no-anon' => 'கணக்கு உருவாக்குதல் அவசியம்',
'config-profile-private' => 'தனியார் விக்கி',
'config-license' => 'பதிப்புரிமை மற்றும் உரிமம்:',
@@ -17214,7 +19046,7 @@ $messages['ta'] = array(
'config-install-tables' => 'வரிசைப் பட்டியல்களை உருவாக்குகிறது',
'config-install-mainpage' => 'இயல்புநிலை உள்ளடக்கத்துடன் முதற்பக்கத்தை உருவாக்குகிறது',
'config-install-extension-tables' => 'செயற்படுத்தப்பட்ட நீட்சிகளுக்கு வரிசைப் பட்டியல்களை உருவாக்குகிறது',
- 'config-download-localsettings' => 'LocalSettings.phpஐத் தரவிறக்கவும்',
+ 'config-download-localsettings' => '<code>LocalSettings.php</code>ஐத் தரவிறக்கவும்',
'config-help' => 'உதவி',
'mainpagetext' => "'''விக்கி மென்பொருள் வெற்றிகரமாக உள்ளிடப்பட்டது.'''",
'mainpagedocfooter' => 'விக்கி மென்பொருளைப் பயன்படுத்துவது தொடர்பாக [//meta.wikimedia.org/wiki/Help:Contents பயனர் வழிகாட்டியைப்] பார்க்க.
@@ -17223,7 +19055,7 @@ $messages['ta'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings அமைப்புக்களை மாற்றம் செய்தல்]
* [//www.mediawiki.org/wiki/Manual:FAQ மிடியாவிக்கி பொதுவான கேள்விகள்]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce மீடியாவிக்கி வெளியீடு மின்னஞ்சல் பட்டியல்]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce மீடியாவிக்கி வெளியீடு மின்னஞ்சல் பட்டியல்]', # Fuzzy
);
/** Tulu (ತುಳು)
@@ -17236,7 +19068,7 @@ $messages['tcy'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ ಮೀಡಿಯವಿಕಿ FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]', # Fuzzy
);
/** Telugu (తెలుగు)
@@ -17282,7 +19114,7 @@ $messages['te'] = array(
'config-admin-password-confirm' => 'సంకేతపదం మళ్ళీ:',
'config-admin-email' => 'ఈ-మెయిలు చిరునామా:',
'config-optional-continue' => 'నన్ను మరిన్ని ప్రశ్నలు అడుగు.',
- 'config-profile-wiki' => 'సంప్రదాయ వికీ',
+ 'config-profile-wiki' => 'సంప్రదాయ వికీ', # Fuzzy
'config-profile-no-anon' => 'ఖాతా సృష్టింపు తప్పనిసరి',
'config-profile-private' => 'అంతరంగిక వికీ',
'config-license' => 'కాపీహక్కులు మరియు లైసెన్సు:',
@@ -17300,7 +19132,7 @@ $messages['te'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings మీడియావికీ పనితీరు, అమరిక మార్చుకునేందుకు వీలుకల్పించే చిహ్నాల జాబితా]
* [//www.mediawiki.org/wiki/Manual:FAQ మీడియావికీపై తరుచుగా అడిగే ప్రశ్నలు]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce మీడియావికీ సాఫ్టువేరు కొత్త వెర్షను విడుదలల గురించి తెలిపే మెయిలింగు లిస్టు]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce మీడియావికీ సాఫ్టువేరు కొత్త వెర్షను విడుదలల గురించి తెలిపే మెయిలింగు లిస్టు]', # Fuzzy
);
/** Tetum (tetun)
@@ -17321,7 +19153,7 @@ $messages['tg-cyrl'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Феҳристи танзимоти пайгирбандӣ]
* [//www.mediawiki.org/wiki/Manual:FAQ Пурсишҳои МедиаВики]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Феҳристи ройномаҳои нусхаҳои МедиаВики]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Феҳристи ройномаҳои нусхаҳои МедиаВики]', # Fuzzy
);
/** Tajik (Latin script) (tojikī)
@@ -17335,7 +19167,7 @@ $messages['tg-latn'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Fehristi tanzimoti pajgirbandī]
* [//www.mediawiki.org/wiki/Manual:FAQ Pursişhoi MediaViki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Fehristi rojnomahoi nusxahoi MediaViki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Fehristi rojnomahoi nusxahoi MediaViki]', # Fuzzy
);
/** Thai (ไทย)
@@ -17349,7 +19181,7 @@ $messages['th'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings รายการการปรับแต่งระบบ] (ภาษาอังกฤษ)
* [//www.mediawiki.org/wiki/Manual:FAQ คำถามที่ถามบ่อยในมีเดียวิกิ] (ภาษาอังกฤษ)
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce เมลลิงลิสต์ของมีเดียวิกิ]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce เมลลิงลิสต์ของมีเดียวิกิ]', # Fuzzy
);
/** Turkmen (Türkmençe)
@@ -17362,12 +19194,13 @@ $messages['tk'] = array(
== Öwrenjeler ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Konfigurasiýa sazlamalary]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki SSS]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-poçta sanawy]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-poçta sanawy]', # Fuzzy
);
/** Tagalog (Tagalog)
* @author AnakngAraw
* @author Sky Harbor
+ * @author 아라
*/
$messages['tl'] = array(
'config-desc' => 'Ang tagapagluklok para sa MediaWiki',
@@ -17375,20 +19208,20 @@ $messages['tl'] = array(
'config-information' => 'Kabatiran',
'config-localsettings-upgrade' => 'Napansin ang isang talaksang <code>LocalSettings.php</code>.
Upang maitaas ang uri ng pagluluklok na ito, paki ipasok ang halaga ng <code>$wgUpgradeKey</code> sa loob ng kahong nasa ibaba.
-Matatagpuan mo ito sa loob ng LocalSettings.php.',
- 'config-localsettings-cli-upgrade' => 'Napansin ang isang talaksan ng LocalSettings.php.
-Upang isapanahon ang pagtatalagang ito, mangyaring patakbuhin sa halip ang update.php',
+Matatagpuan mo ito sa loob ng <code>LocalSettings.php</code>.',
+ 'config-localsettings-cli-upgrade' => 'Napansin ang isang talaksan ng <code>LocalSettings.php</code>.
+Upang isapanahon ang pagtatalagang ito, mangyaring patakbuhin sa halip ang <code>update.php</code>',
'config-localsettings-key' => 'Susi ng pagsasapanahon:',
'config-localsettings-badkey' => 'Hindi tama ang susing ibinigay mo.',
'config-upgrade-key-missing' => 'Napansin ang isang umiiral na pagtatalaga ng MediaWiki.
-Upang isapanahon ang katalagahang ito, mangyaring ilagay ang sumusunod na guhit sa ilalim ng iyong LocalSettings.php:
+Upang isapanahon ang katalagahang ito, mangyaring ilagay ang sumusunod na guhit sa ilalim ng iyong <code>LocalSettings.php</code>:
$1',
- 'config-localsettings-incomplete' => 'Lumilitaw na hindi pa buo ang umiiral na LocalSettings.php.
+ 'config-localsettings-incomplete' => 'Lumilitaw na hindi pa buo ang umiiral na <code>LocalSettings.php</code>.
Ang pabagu-bagong $1 ay hindi nakatakda.
-Mangyaring baguhin ang LocalSettings.php upang ang maitakda ang pagpapabagu-bagong ito, at pindutin ang "Magpatuloy".',
- 'config-localsettings-connection-error' => 'Isang kamalian ang nakatagpo noong kumakabit sa kalipunan ng dato na ginagamit ang tinukoy na mga katakdaan sa loob ng LocalSettings.php o
-AdminSettings.php. Paki kumpunihin ang mga katakdaang ito at subukang muli.
+Mangyaring baguhin ang <code>LocalSettings.php</code> upang ang maitakda ang pagpapabagu-bagong ito, at pindutin ang "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Isang kamalian ang nakatagpo noong kumakabit sa kalipunan ng dato na ginagamit ang tinukoy na mga katakdaan sa loob ng <code>LocalSettings.php</code> o
+<code>AdminSettings.php</code>. Paki kumpunihin ang mga katakdaang ito at subukang muli.
$1',
'config-session-error' => 'Kamalian sa pagsisimula ng sesyon: $1',
@@ -17517,7 +19350,7 @@ Pinigilan ang pag-iinstala.",
'config-brokenlibxml' => "Ang sistema mo ay mayroong isang pagsasama ng mga bersiyon ng PHP at libxml2 na maaaring masurot at maaaring makapagsanhi ng pagkasira ng datong nakakubli sa loob ng MediaWiki at iba pang mga aplikasyon ng sangkasaputan.
Magtaas ng uri upang maging PHP 5.2.9 o mas lalong huli at libxml2 2.7.3 o mas lalong huli ([//bugs.php.net/bug.php?id=45996 isinalansan ang surot o ''bug'' na mayroong PHP]). Binigo ang pagluluklok.",
'config-using531' => 'Hindi maaaring gamitin ang MediaWiki na kapiling ang PHP na $1 dahil sa isang surot na kinasasangkutan ng mga parametrong pangsangguni sa <code>__call()</code>. Magtaas ng uri upang maging PHP 5.3.2 o mas mataas, o magbaba ng uri upang maging PHP 5.3.0 upang malutas ito. Binigo ang pagluluklok.',
- 'config-suhosin-max-value-length' => 'Nakaluklok ang Suhosin at hinahanggahan ang haba ng parametro ng GET sa $1 mga byte. Ang sangkap na ResourceLoader ng MediaWiki ay gagana sa paligid ng hangganang ito, subalit pasasamain nito ang pagganap. Kung talagang maaari, dapat mong itakda ang suhosin.get.max_value_length upang maging 1024 o mas mataas sa loob ng php.ini, at itakda ang $wgResourceLoaderMaxQueryLength sa katulad na halaga sa loob ng LocalSettings.php.',
+ 'config-suhosin-max-value-length' => 'Nakaluklok ang Suhosin at hinahanggahan ang haba ng parametro ng GET sa $1 mga byte. Ang sangkap na ResourceLoader ng MediaWiki ay gagana sa paligid ng hangganang ito, subalit pasasamain nito ang pagganap. Kung talagang maaari, dapat mong itakda ang <code>suhosin.get.max_value_length</code> upang maging 1024 o mas mataas sa loob ng <code>php.ini</code>, at itakda ang <code>$wgResourceLoaderMaxQueryLength</code> sa katulad na halaga sa loob ng LocalSettings.php.', # Fuzzy
'config-db-type' => 'Uri ng kalipunan ng datos:',
'config-db-host' => 'Tagapagpasinaya ng kalipunan ng datos:',
'config-db-host-help' => 'Kung ang iyong tagapaghain ng kalipunan ng dato ay nasa ibabaw ng isang ibang tagapaghain, ipasok ang pangalan ng tagapagpasinaya o tirahan ng IP dito.
@@ -17592,7 +19425,6 @@ Isaalang-alang ang paglalagay na magkakasama ang kalipunan ng dato sa ibang luga
'config-type-postgres' => 'PostgreSQL',
'config-type-sqlite' => 'SQLite',
'config-type-oracle' => 'Oracle',
- 'config-type-ibm_db2' => 'DB2 ng IBM',
'config-support-info' => 'Sinusuportahan ng MediaWiki ang sumusunod na mga sistema ng kalipunan ng dato:
$1
@@ -17602,12 +19434,10 @@ Kung hindi mo makita ang sistema ng kalipunan ng dato na sinusubukan mong gamiti
'config-support-postgres' => '* Ang $1 ay isang bantog na sistema ng kalipunan ng dato na bukas ang pinagmulan na panghalili sa MySQL ([http://www.php.net/manual/en/pgsql.installation.php paano magtipon ng PHP na mayroong suporta ng PostgreSQL]). Maaaring mayroong ilang hindi pangunahing mga surot na natitira pa, at hindi iminumungkahi para gamitin sa loob ng isang kapaligiran ng produksiyon.',
'config-support-sqlite' => 'Ang $1 ay isang magaan ang timbang na sistema ng kalipunan ng dato na sinusuportahan nang napaka mainam. ([http://www.php.net/manual/en/pdo.installation.php Paano magtipon ng PHP na mayroong suporta ng SQLite], gumagamit ng PDO)',
'config-support-oracle' => '* Ang $1 ay isang kalipunan ng dato ng kasigasigang pangkalakal. ([http://www.php.net/manual/en/oci8.installation.php Paano magtipunan ng PHP na mayroong suporta ng OCI8])',
- 'config-support-ibm_db2' => '* Ang $1 ay isang kalipunan ng dato ng kasigasigang pangkalakal.',
'config-header-mysql' => 'Mga katakdaan ng MySQL',
'config-header-postgres' => 'Mga katakdaan ng PostgreSQL',
'config-header-sqlite' => 'Mga katakdaan ng SQLite',
'config-header-oracle' => 'Mga katakdaan ng Oracle',
- 'config-header-ibm_db2' => 'Mga katakdaan ng DB2 ng IBM',
'config-invalid-db-type' => 'Hindi tanggap na uri ng kalipunan ng dato',
'config-missing-db-name' => 'Dapat kang magpasok ng isang halaga para sa "Pangalan ng kalipunan ng dato"',
'config-missing-db-host' => 'Dapat kang magpasok ng isang halaga para sa "Tagapagpasinaya ng kalipunan ng dato"',
@@ -17670,7 +19500,7 @@ Kung nais mong muling likhain ang iyong talaksang <code>LocalSettings.php</code>
Maaari ka na ngayong [$1 magsimula sa paggamit ng wiki mo].',
'config-regenerate' => 'Muling likhain ang LocalSettings.php →',
- 'config-show-table-status' => 'Nabigo ang pagtatanong na IPAKITA ANG KALAGAYAN NG TALAHANAYAN!',
+ 'config-show-table-status' => 'Nabigo ang pagtatanong na IPAKITA ANG KALAGAYAN NG TALAHANAYAN!', # Fuzzy
'config-unknown-collation' => "'''Babala:''' Ang kalipunan ng dato ay gumagagamit ng hindi nakikilalang pag-iipon.",
'config-db-web-account' => 'Akawnt ng kalipunan ng dato para sa pagpunta sa web',
'config-db-web-help' => 'Piliin ang pangalan ng tagagamit at hudyat na gagamitin ng tagapaghain ng web upang umugnay sa tagapaghain ng kalipunan ng dato, habang nasa pangkaraniwang pagtakbo ng wiki.',
@@ -17699,7 +19529,6 @@ May gawi ang mga kalipunan ng dato ng MyISAM na masira nang mas madalas kaysa sa
Mas kapaki-pakinabang ito kaysa sa gawi na UTF-8 ng MySQL, at nagpapahintulot sa iyo upang magamit ang buong kasaklawan ng mga panitik ng Unikodigo.
Sa ''gawi na UTF-8''', malalaman ng MySQL kung sa anong pangkat ng panitik nakapaloob ang iyong dato, at angkop na makakapagharap at makapapagpalit nito, subalit hindi ka nito papayagan na mag-imbak ng mga panitik na nasa itaas ng [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane] o Saligang Tapyas na Pangmaramihang Wika.",
- 'config-ibm_db2-low-db-pagesize' => "Ang kalipunan mo ng dato na DB2 ay mayroong isang likas na nakatakdang puwang ng talahanayan na mayroong hindi sapat na sukat ng pahina. Ang sukat ng pahina ay dapat na maging '''32K''' o mas mataas.",
'config-site-name' => 'Pangalan ng wiki:',
'config-site-name-help' => "Lilitaw ito sa bareta ng pamagat ng pantingin-tingin at sa samu't saring ibang mga lugar.",
'config-site-name-blank' => 'Magpasok ng isang pangalan ng sityo.',
@@ -17740,7 +19569,7 @@ Maaari mo ngayong laktawan ang natitira pang pag-aayos at iluklok na ang wiki ng
'config-optional-continue' => 'Magtanong sa akin ng marami pang mga tanong.',
'config-optional-skip' => 'Naiinip na ako, basta iluklok na lang ang wiki.',
'config-profile' => 'Balangkas ng mga karapatan ng tagagamit:',
- 'config-profile-wiki' => 'Tradisyonal na wiki',
+ 'config-profile-wiki' => 'Tradisyonal na wiki', # Fuzzy
'config-profile-no-anon' => 'Kailangan ang paglikha ng akawnt',
'config-profile-fishbowl' => 'Pinahintulutang mga patnugot lamang',
'config-profile-private' => 'Pribadong wiki',
@@ -17756,7 +19585,7 @@ Ang isang wiki na mayroong '''{{int:config-profile-no-anon}}''' ay nagbibigay ng
Ang tagpo na '''{{int:config-profile-fishbowl}}''' ay nagpapahintulot lamang sa pinayagang mga tagagamit na makatingin ng mga pahina, na kapiling ang pangkat na pinayagang makapamatnugot.
Ang isang '''{{int:config-profile-private}}''' ay nagpapahintulot lamang sa pinayagang mga tagagamit na makatingin ng mga pahina, na kapiling ang pangkat na pinayagang makapamatnugot.
-Ang mas masasalimuot na mga kaayusan ng mga karapatan ng tagagamit ay makukuha pagkaraan ng pagluluklok, tingnan ang [//www.mediawiki.org/wiki/Manual:User_rights may kaugnayang kinamay na lahok].",
+Ang mas masasalimuot na mga kaayusan ng mga karapatan ng tagagamit ay makukuha pagkaraan ng pagluluklok, tingnan ang [//www.mediawiki.org/wiki/Manual:User_rights may kaugnayang kinamay na lahok].", # Fuzzy
'config-license' => 'Karapatang-ari at lisensiya:',
'config-license-none' => 'Walang talababa ng lisensiya',
'config-license-cc-by-sa' => 'Malikhaing Pangkaraniwang Pagtukoy Pamamahaging Magkatulad',
@@ -17807,7 +19636,7 @@ Ideyal na dapat itong hindi mapupuntahan mula sa web.',
'config-logo-help' => 'Ang likas na nakatakdang pabalat ng MediaWiki ay nagsasama ng puwang para sa isang logong 135x160 ang piksel na nasa itaas ng menu ng panggilid na bareta.
Magkargang papaitaas ng isang imahe na mayroong naaangkop na sukat, at ipasok dito ang URL.
-Kung ayaw mo ng isang logo, iwanang walang laman ang kahong ito.',
+Kung ayaw mo ng isang logo, iwanang walang laman ang kahong ito.', # Fuzzy
'config-instantcommons' => 'Paganahin ang Mga Pangkaraniwang Biglaan',
'config-instantcommons-help' => 'Ang [//www.mediawiki.org/wiki/InstantCommons Instant Commons] ay isang tampok na nagpapahintulot sa mga wiki upang gumamit ng mga imahe, mga tunog at iba pang mga midyang matatagpuan sa pook ng [//commons.wikimedia.org/ Wikimedia Commons].
Upang magawa ito, nangangailangan ang MediaWiki ng pagka nakakapunta sa Internet.
@@ -17841,7 +19670,7 @@ Maaaring mangailangan ang mga ito ng karagdagang kaayusan, subalit mapapagana mo
'config-install-alreadydone' => "'''Babala:''' Tila nailuklok mo na ang MediaWiki at tinatangka mong iluklok ito ulit.
Paki magpatuloy sa susunod na pahina.",
'config-install-begin' => 'Sa pamamagitan ng pagpindot sa "{{int:config-continue}}", sisimulan mo ang pagluluklok ng MediaWiki.
-Kung nais mo paring gumawa ng mga pagbabago, paki pindutin ang bumalik.',
+Kung nais mo paring gumawa ng mga pagbabago, paki pindutin ang bumalik.', # Fuzzy
'config-install-step-done' => 'nagawa na',
'config-install-step-failed' => 'nabigo',
'config-install-extensions' => 'Isinasama ang mga karugtong',
@@ -17897,7 +19726,7 @@ $3
'''Paunawa''': Kapag hindi mo ito ginawa ngayon, ang nagawang talaksang ito ng pagkakaayos ay hindi mo na makukuha mamaya kapag lumabas ka mula sa pagluluklok na hindi ikinakarga itong paibaba.
Kapag nagawa na iyan, maaari ka nang '''[$2 pumasok sa wiki mo]'''.",
- 'config-download-localsettings' => 'Ikargang paibaba ang LocalSettings.php',
+ 'config-download-localsettings' => 'Ikargang paibaba ang <code>LocalSettings.php</code>',
'config-help' => 'saklolo',
'config-nofile' => 'Hindi matagpuan ang talaksang "$1". Binura na ba ito?',
'mainpagetext' => "'''Matagumpay na ininstala ang MediaWiki.'''",
@@ -17907,7 +19736,7 @@ Kapag nagawa na iyan, maaari ka nang '''[$2 pumasok sa wiki mo]'''.",
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Tala ng mga nakatakdang kumpigurasyon]
* [//www.mediawiki.org/wiki/Manual:FAQ Mga malimit itanong sa MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Tala ng mga pinadadalhan ng liham ng MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Tala ng mga pinadadalhan ng liham ng MediaWiki]", # Fuzzy
);
/** толышә зывон (толышә зывон)
@@ -17926,7 +19755,7 @@ $messages['tr'] = array(
== Yeni Başlayanlar ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Yapılandırma ayarlarının listesi]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki SSS]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-posta listesi]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-posta listesi]', # Fuzzy
);
/** Tatar (Cyrillic script) (татарча)
@@ -17939,7 +19768,7 @@ $messages['tt-cyrl'] = array(
== Кайбер файдалы ресурслар ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Көйләнмәләр исемлеге (инг.)];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki турында еш бирелгән сораулар һәм җаваплар (инг.)];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki'ның яңа версияләре турында хәбәрләр яздырып алу].",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki'ның яңа версияләре турында хәбәрләр яздырып алу].", # Fuzzy
);
/** Tatar (Latin script) (tatarça)
@@ -17952,7 +19781,7 @@ $messages['tt-latn'] = array(
== Qayber faydalı resurslar ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Köylänmälär isemlege (ing.)];
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki turında yış birelgän sorawlar häm cawaplar (ing.)];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki'nıñ yaña versiäläre turında xäbärlär yazdırıp alu].",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki'nıñ yaña versiäläre turında xäbärlär yazdırıp alu].", # Fuzzy
);
/** Udmurt (удмурт)
@@ -17972,15 +19801,19 @@ $messages['ug-arab'] = array(
== دەسلەپكى ساۋات ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings سەپلىمە تەڭشەك تىزىملىكى]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki كۆپ ئۇچرايدىغان مەسىلىلەرگە جاۋاب]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki تارقاتقان ئېلخەت تىزىملىكى]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki تارقاتقان ئېلخەت تىزىملىكى]', # Fuzzy
);
/** Ukrainian (українська)
* @author AS
* @author Ahonc
* @author Alex Khimich
+ * @author Andriykopanytsia
+ * @author Base
* @author Diemon.ukr
+ * @author Ата
* @author Тест
+ * @author 아라
*/
$messages['uk'] = array(
'config-desc' => 'Інсталятор MediaWiki',
@@ -17989,14 +19822,26 @@ $messages['uk'] = array(
'config-localsettings-upgrade' => "'''Увага''': було виявлено файл <code>LocalSettings.php</code>.
Ваше програмне забезпечення може бути оновлено.
Будь-ласка, перемістіть файл <code>LocalSettings.php</code> в іншу безпечну директорію, а потім знову запустіть програму установки.",
- 'config-localsettings-cli-upgrade' => 'Виявлено файл LocalSettings.php.
-Щоб оновити наявну установку, запустіть update.php',
+ 'config-localsettings-cli-upgrade' => 'Виявлено файл <code>LocalSettings.php</code>.
+Щоб оновити наявну установку, запустіть <code>update.php</code>',
'config-localsettings-key' => 'Ключ оновлення:',
'config-localsettings-badkey' => 'Ви вказали неправильний ключ.',
'config-upgrade-key-missing' => 'Виявлено наявну установку MediaWiki.
-Для оновлення цієї установки, будь ласка, вставте такий рядок в кінець вашого LocalSettings.php:
+Для оновлення цієї установки, будь ласка, вставте такий рядок в кінець вашого <code>LocalSettings.php</code>:
+$1',
+ 'config-localsettings-incomplete' => 'Існуючий файл <code>LocalSettings.php</code> виявився неповним.
+Не вказано змінну $1.
+Будь ласка, змініть <code>LocalSettings.php</code> так, щоб цю змінну було задано, і натисніть "{{int:Config-continue}}".',
+ 'config-localsettings-connection-error' => 'Сталася помилка при підключення до бази даних з допомогою налаштувань на сторінці <code>LocalSettings.php</code> чи <code>AdminSettings.php</code>. Будь ласка, виплавте ці налаштування і спробуйте знову.
+
$1',
'config-session-error' => 'Помилка початку сесії: $1',
+ 'config-session-expired' => 'Час Вашої сесії минув.
+Задана тривалість сесії — $1.
+Ви можете збільшити її, змінивши <code>session.gc_maxlifetime</code> у php.ini.
+Перезапустіть процес встановлення.',
+ 'config-no-session' => 'Дані сесії було втрачено!
+Перевірте Ваш php.ini і переконайтесь, що <code>session.save_path</code> встановлено у відповідну папку.',
'config-your-language' => 'Ваша мова:',
'config-your-language-help' => 'Оберіть мову для використання в процесі установки.',
'config-wiki-language' => 'Мова для вікі:',
@@ -18017,80 +19862,521 @@ $1',
'config-page-releasenotes' => 'Інформація про версію',
'config-page-copying' => 'Копіювання',
'config-page-upgradedoc' => 'Оновлення',
+ 'config-page-existingwiki' => 'Існуюча вікі',
'config-help-restart' => 'Ви бажаєте видалити всі введені та збережені вами дані і запустити процес установки спочатку?',
'config-restart' => 'Так, перезапустити установку',
'config-welcome' => '=== Перевірка оточення ===
-Проводяться базові перевірки, щоб виявити, чи можлива установка MediaWiki у даній системі.
-Вкажіть результати цих перевірок при зверненні за допомогою під час установки.',
+Будуть проведені базові перевірки, щоб виявити, чи можлива установка MediaWiki у даній системі.
+Не забудьте включити цю інформацію, якщо ви звернетеся по підтримку, як завершити установку.',
+ 'config-copyright' => "=== Авторське право і умови ===
+
+$1
+
+Ця програма є вільним програмним забезпеченням; Ви можете розповсюджувати та/або змінювати її під ліцензією GNU General Public License, опублікованою Фондом вільного програмного забезпечення; версією 2 цієї ліцензії або будь-якою пізнішою на Ваш вибір.
+
+Ця програма поширюється з надією на те, що вона буде корисною, однак '''без жодних гарантій'''; навіть без неявної гарантії '''комерційної цінності''' або '''придатності для певних цілей'''.
+Див. GNU General Public License для детальної інформації.
+
+Ви повинні були отримати <doclink href=Copying>копію GNU General Public License</doclink> разом із цією програмою; якщо ж ні, зверніться до Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. або [http://www.gnu.org/copyleft/gpl.html ознайомтесь з нею онлайн].",
'config-sidebar' => '* [//www.mediawiki.org Сайт MediaWiki]
-* [//www.mediawiki.org/wiki/Help:Contents/uk Керівництво користувача]
-* [//www.mediawiki.org/wiki/Manual:Contents/uk Керівництво адміністратора]
-* [//www.mediawiki.org/wiki/Manual:FAQ/uk FAQ]', # Fuzzy
+* [//www.mediawiki.org/wiki/Help:Contents Посібник користувача]
+* [//www.mediawiki.org/wiki/Manual:Contents Посібник адміністратора]
+* [//www.mediawiki.org/wiki/Manual:FAQ FAQ]
+----
+* <doclink href=Readme>Read me</doclink>
+* <doclink href=ReleaseNotes>Інформація про випуск</doclink>
+* <doclink href=Copying>Ліцензія</doclink>
+* <doclink href=UpgradeDoc>Оновлення</doclink>',
'config-env-good' => 'Перевірку середовища успішно завершено.
Ви можете встановити MediaWiki.',
'config-env-bad' => 'Було проведено перевірку середовища. Ви не можете встановити MediaWiki.',
'config-env-php' => 'Встановлено версію PHP: $1.',
+ 'config-env-php-toolow' => 'Встановлено PHP $1.
+Натомість MediaWiki вимагає PHP $2 і вище.',
'config-unicode-using-utf8' => 'Використовувати utf8_normalize.so Брайона Віббера для нормалізації Юнікоду.',
'config-unicode-using-intl' => 'Використовувати [http://pecl.php.net/intl міжнародне розширення PECL] для нормалізації Юнікоду.',
'config-unicode-pure-php-warning' => "'''Увага''': [http://pecl.php.net/intl міжнародне розширення PECL] не може провести нормалізацію Юнікоду.
Якщо ваш сайт має високий трафік, вам варто почитати про [//www.mediawiki.org/wiki/Unicode_normalization_considerations нормалізацію Юнікоду].",
+ 'config-unicode-update-warning' => "'''Увага''': Встановлена версія обгортки нормалізації Юнікоду використовує стару версію бібліотеки [http://site.icu-project.org/ проекту ICU].
+Ви маєте [//www.mediawiki.org/wiki/Unicode_normalization_considerations оновити версію], якщо плануєте повноцінно використовувати Юнікод.",
'config-no-db' => 'Не вдалося знайти відповідний драйвер бази даних! Вам необхідно встановити драйвер бази даних для PHP. Підтримуються такі типи баз даних: $1.
Якщо ви користуєтесь віртуальним хостингом, попросіть вашого хостинг-провайдера інсталювати відповідний драйвер бази даних.
Якщо ви скомпілювали PHP самостійно, переналаштуйте його з включенням клієнта бази даних, наприклад за допомогою <code>./configure --with-mysql</code>.
Якщо установлено PHP з пакетів Debian або Ubuntu, тоді ви також повинні встановити php5-mysql модуль.',
+ 'config-outdated-sqlite' => "'''Увага''': у Вас встановлена версія SQLite $1, а це нижче, ніж мінімально необхідна версія $2. SQLite буде недоступним.",
+ 'config-no-fts3' => "'''Увага''': SQLite зібраний без [//sqlite.org/fts3.html модуля FTS3], функції пошуку не будуть працювати у цій системі.",
+ 'config-register-globals' => "'''Увага: Опція PHP <code>[http://php.net/register_globals register_globals]</code> увімкнена.'''
+'''Вимкніть її, якщо це можливо.'''
+MediaWiki буде працювати, але Ваш сервер буде більш вразливим до потенційного проникнення зовні.",
+ 'config-magic-quotes-runtime' => "'''Проблема: Опція PHP [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] увімкнена!'''
+Ця опція призводить до непередбачуваного пошкодження даних.
+Ви не можете встановити і використовувати MediaWiki, поки не буде вимкнено цю опцію.",
+ 'config-magic-quotes-sybase' => "'''Проблема: Опція PHP [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] увімкнена!'''
+Ця опція призводить до непередбачуваного пошкодження даних.
+Ви не можете встановити і використовувати MediaWiki, поки не буде вимкнено цю опцію.",
+ 'config-mbstring' => "'''Проблема: Опція PHP [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] увімкнена!'''
+Ця опція призводить до непередбачуваного пошкодження даних.
+Ви не можете встановити і використовувати MediaWiki, поки не буде вимкнено цю опцію.",
+ 'config-ze1' => "'''Проблема: Опція PHP [http://www.php.net/manual/en/ini.core.php zend.ze1_compatibility_mode] увімкнена!'''
+Ця опція призводить до непередбачуваного пошкодження даних.
+Ви не можете встановити і використовувати MediaWiki, поки не буде вимкнено цю опцію.",
+ 'config-safe-mode' => "'''Увага:''' Опція PHP [http://www.php.net/features.safe-mode «безпечний режим»] увімкнена.
+Це може спричинити проблеми, зокрема із завантаженням файлів та вставкою математичних формул.",
+ 'config-xml-bad' => 'XML-модуть PHP відсутній.
+MediaWiki необхідні його функції, без цього модуля вона працювати не буде.
+Якщо Ви використовуєте Mandrake, встановіть php-xml пакет.',
+ 'config-pcre' => 'Модуть підтримку PCRE не знайдено.
+Для роботи MediaWiki необхідна підтримка Perl-сумісних регулярних виразів.',
+ 'config-pcre-no-utf8' => "'''Помилка''': PCRE-модуть PHP, вочевидь, було зібрано без підтримки PCRE_UTF8.
+MediaWiki вимагає підтримку UTF-8 для коректної роботи.",
+ 'config-memory-raised' => "Обмеження пам'яті PHP (<code>memory_limit</code>) $1, піднято до $2.",
+ 'config-memory-bad' => "'''Увага:''' Розмір пам'яті PHP (<code>memory_limit</code>) становить $1.
+Імовірно, це замало.
+Встановлення може не вдатись!",
+ 'config-ctype' => "'''Помилка''': PHP має бути зібраним з підтримкою [http://www.php.net/manual/en/ctype.installation.php розширення Ctype].",
+ 'config-json' => "'''Fatal:''' PHP був скомпільований без підтримки JSON.
+Вам потрібно встановити або розширення PHP JSON або розширення[http://pecl.php.net/package/jsonc PECL jsonc] перед встановлення Медіавікі.
+* Розширення PHP включено у Red Hat Enterprise Linux (CentOS) 5 та 6, хоча має бути доступним у <code>/etc/php.ini</code> або <code>/etc/php.d/json.ini</code>.
+* Деякі дистрибутиви Лінукса, випущені після травня 2013, пропустили розширення PHP, натомість упакували розширення PECL як <code>php5-json</code> або <code>php-pecl-jsonc</code>.",
'config-xcache' => '[http://xcache.lighttpd.net/ XCache] встановлено',
'config-apc' => '[http://www.php.net/apc APC] встановлено',
'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] встановлено',
+ 'config-no-cache' => "'''Увага:''' Не вдалося знайти [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] чи [http://www.iis.net/download/WinCacheForPhp WinCache].
+Кешування об'єктів не ввімкнено.",
+ 'config-mod-security' => "'''Увага''': на Вашому веб-сервері увімкнено [http://modsecurity.org/ mod_security]. У разі неправильних налаштувать, він може викликати проблеми MediaWiki або іншого ПЗ, яке дозволяє користувачам надсилати довільний вміст.
+Зверніться до [http://modsecurity.org/documentation/ документації mod_security] або підтримки Вашого хостера, якщо під час роботи виникають незрозумілі помилки.",
+ 'config-diff3-bad' => 'GNU diff3 не знайдено.',
+ 'config-git' => 'Знайшов програму управління версіями Git: <code>$1</code>.',
+ 'config-git-bad' => 'Програму управління версіями Git не знайдено.',
+ 'config-imagemagick' => 'Виявлено ImageMagick: <code>$1</code>.
+Буде ввімкнуто відображення мініатюр, якщо ви дозволите завантаження файлів.',
+ 'config-gd' => 'Виявлено вбудовано графічну бібліотеку GD.
+Буде ввімкнуто відображення мініатюр, якщо ви дозволите завантаження файлів.',
+ 'config-no-scaling' => 'Не вдалося виявити бібліотеку GD чи ImageMagick.
+Відображення мініатюр буде вимкнено.',
+ 'config-no-uri' => "'''Помилка:''' Не вдалося визначити поточний URI.
+Встановлення перервано.",
+ 'config-no-cli-uri' => "'''Увага''': Не задано параметр --scriptpath, використовується за замовчуванням: <code>$1</code>.",
+ 'config-using-server' => 'Використовується ім\'я сервера "<nowiki>$1</nowiki>".',
+ 'config-using-uri' => 'Використовується URL сервера "<nowiki>$1$2</nowiki>".',
+ 'config-uploads-not-safe' => "'''Увага:''' Ваша типова папка для завантажень <code>$1</code> вразлива до виконання довільних скриптів.
+Хоча MediaWiki перевіряє усі завантажені файли на наявність загроз, наполегливо рекомендується [//www.mediawiki.org/wiki/Manual:Security#Upload_security закрити дану вразливість] перед тим, як дозволяти завантаження файлів.",
+ 'config-no-cli-uploads-check' => "'''Увага:''' Ваша типова папка для завантажень (<code>$1</code>) не перевірялась на вразливість до виконання довільних скриптів під час встановлення CLI.",
+ 'config-brokenlibxml' => 'У Вашій системі невдале поєднання версій PHP і libxml2, яке може спричинити пошкодження прихованих даних у MediaWiki та інших веб-застосунках.
+Оновіть PHP до версії 5.2.9 або пізнішої і libxml2 до 2.7.3 або пізнішої ([//bugs.php.net/bug.php?id=45996 відомості про помилку]).
+Встановлення перервано.',
+ 'config-using531' => 'MediaWiki не можна використовувати разом з PHP $1 через помилку з параметрами-посиланнями <code>__call()</code>.
+Оновіть PHP до версії 5.3.2 і вище або відкотіть до PHP 5.3.0 щоб уникнути цієї проблеми.
+Встановлення скасовано.',
+ 'config-suhosin-max-value-length' => 'Suhosin встановлено і обмежує параметра GET <code>length</code> до $1 байта. Компонент MediaWiki ResourceLoader буде обходити це обмеження, однак це зменшить продуктивність. Якщо це можливо, Вам варто встановити значення <code>suhosin.get.max_value_length</code> як 1024 і більше у <code>php.ini</code> і встановити таке ж значення <code>$wgResourceLoaderMaxQueryLength</code> у LocalSettings.php .',
'config-db-type' => 'Тип бази даних:',
'config-db-host' => 'Хост бази даних:',
+ 'config-db-host-help' => 'Якщо сервер бази даних знаходиться на іншому сервері, введіть тут ім\'я хосту і IP адресу.
+
+Якщо Ви використовуєте віртуальний хостинг, Ваш хостинг-провайдер має надати Вам правильне ім\'я хосту у його документації.
+
+Якщо у Вас сервер із Windows Ви використовуєте MySQL, параметр "localhost" може не працювати для імені сервера. Якщо не працює, використайте "127.0.0.1" як локальну IP-адресу.
+
+Якщо Ви використовуєте PostgreSQL, залиште це поле пустим, щоб під\'єднатись через сокет Unix.',
+ 'config-db-host-oracle' => 'TNS бази даних:',
+ 'config-db-host-oracle-help' => 'Введіть допустиме [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; файл tnsnames.ora має бути видимим для цієї інсталяції. <br />Якщо Ви використовуєте бібліотеки 10g чи новіші, можна також використовувати метод іменування [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].',
+ 'config-db-wiki-settings' => 'Ідентифікувати цю вікі',
'config-db-name' => 'Назва бази даних:',
+ 'config-db-name-help' => 'Виберіть назву, що ідентифікує Вашу вікі.
+Вона не повинна містити пробілів.
+
+Якщо Ви використовуєте віртуальний хостинг, Ваш хостинг-провайдер або надасть Вам конкретну назву бази даних, або дозволить створювати бази даних з допомогою панелі управління.',
+ 'config-db-name-oracle' => 'Схема бази даних:',
+ 'config-db-account-oracle-warn' => 'Є три підтримувані сценарії установки Oracle:
+
+Якщо Ви хочете створити обліковий запис бази даних у процесі встановлення, будь ласка, вкажіть обліковий запис ролі SYSDBA для установки і бажані повноваження для облікового запису з веб-доступом. В протилежному випадку Ви можете або створити обліковий запис з веб-доступом вручну і вказати тільки цей обліковий запис (якщо він має необхідні дозволи на створення об\'єктів-схем), або вказати два різні облікові записи, з яких в одного будуть права на створення, а в другого, обмеженого — права веб-доступу.
+
+Скрипт для створення облікового запису з необхідними повноваженнями можна знайти у папці "maintenance/oracle/" цієї інсталяції. Майте на увазі, що використання обмеженого облікового запису вимкне можливість використання технічного обслуговування з облікового запису за замовчуванням.',
+ 'config-db-install-account' => 'Обліковий запис користувача для встановлення',
+ 'config-db-username' => "Ім'я користувача бази даних:",
'config-db-password' => 'Пароль бази даних:',
+ 'config-db-password-empty' => 'Будь ласка, введіть пароль для нового користувача бази даних: $1.
+Хоча можна створювати користувачів без паролів, це не є безпечним.',
+ 'config-db-install-username' => "Введіть ім'я користувача, яке буде використано для підключення до бази даних під час процесу встановлення.
+Це не ім'я користувача облікового запису MediaWiki; це ім'я користувача для Вашої бази даних.",
+ 'config-db-install-password' => 'Введіть пароль, який буде використано для підключення до бази даних під час процесу встановлення.
+Це не пароль облікового запису MediaWiki; це пароль для Вашої бази даних.',
+ 'config-db-install-help' => "Введіть ім'я користувача і пароль, які буде використано для підключення до бази даних у процесі встановлення.",
+ 'config-db-account-lock' => "Використовувати ті ж ім'я користувача і пароль і для звичайної роботи",
+ 'config-db-wiki-account' => 'Обліковий запис користувача для звичайної роботи',
+ 'config-db-wiki-help' => "Введіть ім'я користувача і пароль, які будуть використовуватись для з'єднання з базою даних під час звичайної роботи.
+Якщо обліковий запис не існує, а в облікового запису інсталяції є достатні повноваження, цей обліковий запис користувача буде створено з мінімальними правами, що необхідні для роботи з вікі.",
+ 'config-db-prefix' => 'Префікс таблиць бази даних:',
+ 'config-db-prefix-help' => 'Якщо треба ділити одну базу даних між декількома вікі або між MediaWiki та іншим веб-застосунком, Ви можете додати префікс до усіх назв таблиць для уникнення конфліктів.
+Не використовуйте пробіли.
+
+Це поле зазвичай залишають пустим.',
'config-db-charset' => 'Кодування бази даних',
+ 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 binary',
+ 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8',
+ 'config-charset-mysql4' => 'MySQL 4.0 зворотно сумісна з UTF-8',
+ 'config-charset-help' => "'''Увага:''' Якщо Ви використовуєте '''зворотно сумісну UTF-8''' на MySQL 4.1+ і створюєте резервні копії бази даних з допомогою <code>mysqldump</code>, це може викривити усі не-ASCII символи, незворотно пошкодивши резервні копії!
+
+У '''бінарному режимі''' MediaWiki зберігає текст UTF-8 у базі даних з бінарними полями.
+Це більш ефективно, ніж UTF-8 режим MySQL, і дозволяє використовувати увесь набір символів Юнікоду.
+У '''режимі UTF-8''' MySQL буде знати, якого символу стосуються Ваші дані, і могтиме відображати та конвертувати їх належним чином,
+але не дозволятиме зберігати символи, що виходять за межі [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane].",
+ 'config-mysql-old' => 'Необхідна MySQL $1 або пізніша, а у Вас $2.',
'config-db-port' => 'Порт бази даних:',
+ 'config-db-schema' => 'Схема для MediaWiki',
+ 'config-db-schema-help' => 'Ця схема зазвичай працює добре.
+Змінюйте її тільки якщо знаєте, що Вам це потрібно.',
+ 'config-pg-test-error' => "Не вдається підключитися до бази даних '''$1''': $2",
+ 'config-sqlite-dir' => 'Папка даних SQLite:',
+ 'config-sqlite-dir-help' => "SQLite зберігає усі дані в єдиному файлі.
+
+Папка, яку Ви вказуєте, має бути доступна серверу для запису під час встановлення.
+
+Вона '''не''' повинна бути доступна через інтернет, тому ми і не поміщуємо її туди, де Ваші файли PHP.
+
+Інсталятор пропише у неї файл <code>.htaccess</code>, але якщо це не спрацює, хтось може отримати доступ до Вашої вихідної бази даних, яка містить вихідні дані користувача (адреси електронної пошти, хеші паролів), а також видалені версії та інші обмежені дані на вікі.
+
+За можливості розташуйте базу даних десь окремо, наприклад в <code>/var/lib/mediawiki/yourwiki</code>.",
+ 'config-oracle-def-ts' => 'Простір таблиць за замовчуванням:',
+ 'config-oracle-temp-ts' => 'Тимчасовий простір таблиць:',
+ 'config-support-info' => 'MediaWiki підтримує таки системи баз даних:
+
+$1
+
+Якщо Ви не бачите серед перерахованих систему баз даних, яку використовуєте, виконайте вказівки, вказані вище, щоб увімкнути підтримку.',
+ 'config-support-mysql' => '* $1 є основною для MediaWiki і найкраще підтримується ([http://www.php.net/manual/en/mysql.installation.php як зібрати PHP з допомогою MySQL])',
+ 'config-support-postgres' => '* $1 — популярна відкрита СУБД, альтернатива MySQL ([http://www.php.net/manual/en/pgsql.installation.php як зібрати PHP з допомогою PostgreSQL]). Можуть зустрічатись деякі невеликі невиправлені помилки, не рекомендується використовувати у робочій системі.',
+ 'config-support-sqlite' => '* $1 — легка система баз даних, яка дуже добре підтримується. ([http://www.php.net/manual/en/pdo.installation.php Як зібрати PHP з допомогою SQLite], що використовує PDO)',
+ 'config-support-oracle' => '* $1 — комерційна база даних масштабу підприємства. ([http://www.php.net/manual/en/oci8.installation.php Як зібрати PHP з підтримкою OCI8])',
+ 'config-header-mysql' => 'Налаштування MySQL',
+ 'config-header-postgres' => 'Налаштування PostgreSQL',
+ 'config-header-sqlite' => 'Налаштування SQLite',
+ 'config-header-oracle' => 'Налаштування Oracle',
'config-invalid-db-type' => 'Невірний тип бази даних',
+ 'config-missing-db-name' => "Ви повинні ввести значення параметру «Ім'я бази даних»",
+ 'config-missing-db-host' => 'Ви повинні ввести значення параметру «Хост бази даних»',
+ 'config-missing-db-server-oracle' => 'Ви повинні ввести значення параметру «TNS бази даних»',
+ 'config-invalid-db-server-oracle' => 'Неприпустиме TNS бази даних "$1".
+Використовуйте "TNS Name" або рядок "Easy Connect" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методи найменування Oracle])',
'config-invalid-db-name' => 'Неприпустима назва бази даних "$1".
Використовуйте тільки ASCII букви (a-z, A-Z), цифри (0-9), знаки підкреслення (_) і дефіси (-).',
'config-invalid-db-prefix' => 'Неприпустимий префікс бази даних "$1".
Використовуйте тільки ASCII букви (a-z, A-Z), цифри (0-9), знаки підкреслення (_) і дефіси (-).',
+ 'config-connection-error' => "$1.
+
+Перевірте хост, ім'я користувача та пароль і спробуйте ще раз.",
+ 'config-invalid-schema' => 'Неприпустима схема для MediaWiki "$1".
+Використовуйте тільки ASCII букви (a-z, A-Z), цифри (0-9) і знаки підкреслення(_).',
+ 'config-db-sys-create-oracle' => 'Інсталятор підтримує лише використання облікового запису SYSDBA для створення нового облікового запису.',
+ 'config-db-sys-user-exists-oracle' => 'Обліковий запис користувача "$1" уже існує. SYSDBA використовується лише для створення новий облікових записів!',
+ 'config-postgres-old' => 'Необхідна PostgreSQL $1 або пізніша, а у Вас $2.',
+ 'config-sqlite-name-help' => 'Виберіть назву, що ідентифікує Вашу вікі.
+Не використовуйте пробіли і дефіси.
+Це буде використовуватись у назві файлу даних SQLite.',
+ 'config-sqlite-parent-unwritable-group' => 'Не можна створити папку даних <code><nowiki>$1</nowiki></code>, оскільки батьківська папка <code><nowiki>$2</nowiki></code> не доступна веб-серверу для запису.
+
+Інсталятор виявив, під яким користувачем працює Ваш сервер.
+Зробіть папку <code><nowiki>$3</nowiki></code> доступною для запису, щоб продовжити.
+В ОС Unix/Linux виконайте:
+
+<pre>cd $2
+mkdir $3
+chgrp $4 $3
+chmod g+w $3</pre>',
+ 'config-sqlite-parent-unwritable-nogroup' => 'Не можна створити папку даних <code><nowiki>$1</nowiki></code>, оскільки батьківська папка <code><nowiki>$2</nowiki></code> не доступна веб-серверу для запису.
+
+Інсталятор не зміг виявити, під яким користувачем працює Ваш сервер.
+Зробіть папку <code><nowiki>$3</nowiki></code> доступною для запису серверу (і всім!) глобально, щоб продовжити.
+В ОС Unix/Linux виконайте:
+
+<pre>cd $2
+mkdir $3
+chmod a+w $3</pre>',
+ 'config-sqlite-mkdir-error' => 'Помилка при створенні папки даних "$1".
+Перевірте розташування і спробуйте знову.',
+ 'config-sqlite-dir-unwritable' => 'Не можливо записати до папки "$1".
+Змініть налаштування доступу так, щоб веб-сервер міг писати до неї, і спробуйте ще раз.',
+ 'config-sqlite-connection-error' => '$1.
+
+Перевірте папку даних і назву бази даних нижче та спробуйте знову.',
+ 'config-sqlite-readonly' => 'Файл <code>$1</code> недоступний для запису.',
'config-sqlite-cant-create-db' => 'Не вдалося створити файл бази даних <code>$1</code>.',
+ 'config-sqlite-fts3-downgrade' => 'У PHP немає підтримки FTS3, скидаю таблиці',
+ 'config-can-upgrade' => "У цій базі даних є таблиці MediaWiki.
+Щоб оновити їх до MediaWiki $1, натисніть '''Продовжити'''.",
+ 'config-upgrade-done' => "Оновлення завершено.
+
+Ви можете зараз [$1 починати використовувати свою вікі].
+
+Якщо Ви хочете повторно згенерувати файл <code>LocalSettings.php</code>, натисніть на кнопку нижче.
+Це '''не рекомендується''', якщо тільки у Вас не виникли проблеми з Вашою вікі.",
+ 'config-upgrade-done-no-regenerate' => 'Оновлення завершено.
+
+Ви можете зараз [$1 починати використовувати свою вікі].',
+ 'config-regenerate' => 'Повторно згенерувати LocalSettings.php →',
+ 'config-show-table-status' => 'Запит <code>SHOW TABLE STATUS</code> не виконано!',
+ 'config-unknown-collation' => "'''Увага:''' База даних використовує нерозпізнане сортування.",
+ 'config-db-web-account' => 'Обліковий запис бази даних для інтернет-доступу',
+ 'config-db-web-help' => "Оберіть ім'я користувача і пароль, які веб-сервер буде використовувати для з'єднання із сервером бази даних під час звичайної роботи вікі.",
+ 'config-db-web-account-same' => 'Використати той же обліковий запис для встановлення',
'config-db-web-create' => 'Створити обліковий запис, якщо його ще не існує',
+ 'config-db-web-no-create-privs' => 'Обліковий запис, вказаний Вами для встановлення, не має достатніх повноважень для створення облікового запису.
+Обліковий запис, який Ви вказуєте тут, уже повинен існувати.',
+ 'config-mysql-engine' => 'Двигун бази даних:',
+ 'config-mysql-innodb' => 'InnoDB',
+ 'config-mysql-myisam' => 'MyISAM',
+ 'config-mysql-myisam-dep' => "'''Увага''': Ви обрали MyISAM для зберігання даних MySQL, що не рекомендовано для роботи з MediaWiki, оскільки:
+* він слабко підтримує паралелізм через блокування таблиць
+* він більш схильний до ушкоджень, ніж інші двигуни
+* база коду MediaWiki не завжди працює з MyISAM так, як мала б.
+
+Якщо Ваша інсталяція MySQL підтримує InnoDB, дуже рекомендується вибрати цей двигун.
+Якщо Ваша інсталяція MySQL не підтримує InnoDB, можливо настав час її оновити.",
+ 'config-mysql-only-myisam-dep' => '"\'Зауваження:"\' MyISAM є єдиним механізмом для зберігання MySQL, який не рекомендується для використання з MediaWiki, оскільки:
+* слабо підтримує паралелізм через блокування таблиць
+* більш схильний до пошкоджень, ніж інші двигуни
+* код MediaWiki не завжди розглядає MyISAM, як повинен
+
+Твоє встановлення MySQL не підтримує InnoDB, можливо, потрібно оновити.',
+ 'config-mysql-engine-help' => "'''InnoDB''' є завжди кращим вибором, оскільки краще підтримує паралельний доступ.
+
+'''MyISAM''' може бути швидшим для одного користувача або в інсталяціях read-only.
+Бази даних MyISAM схильні псуватись частіше, ніж бази InnoDB.",
'config-mysql-charset' => 'Кодування бази даних:',
'config-mysql-binary' => 'Двійкове',
+ 'config-mysql-utf8' => 'UTF-8',
+ 'config-mysql-charset-help' => "У '''бінарному режимі''' MediaWiki зберігає текст UTF-8 у базі даних з бінарними полями.
+Це більш ефективно, ніж UTF-8 режим MySQL, і дозволяє використовувати увесь набір символів Юнікоду.
+
+У '''режимі UTF-8''' MySQL буде знати, якого символу стосуються Ваші дані, і могтиме відображати та конвертувати їх належним чином, але не дозволятиме зберігати символи, що виходять за межі [//en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane].",
'config-site-name' => 'Назва вікі:',
+ 'config-site-name-help' => 'Це буде відображатись у заголовку вікна браузера та у деяких інших місцях.',
'config-site-name-blank' => 'Введіть назву сайту.',
'config-project-namespace' => 'Простір назв проекту:',
'config-ns-generic' => 'Проект',
+ 'config-ns-site-name' => 'Те ж саме, що й назва вікі: $1',
+ 'config-ns-other' => 'Інше (вкажіть)',
+ 'config-ns-other-default' => 'MyWiki',
+ 'config-project-namespace-help' => 'За прикладом Вікіпедії, чимало вікі тримають свої сторінки правил окремо від сторінок основного вмісту, у "\'\'\'просторі імен проекту\'\'\'".
+Усі назви сторінок у цьому просторі імен починаються з певного префікса, який Ви можете вказати тут.
+Традиційно цей префікс виводиться з назви вікі, але не може містити знаки пунктуація, як-то "#" чи ":".',
+ 'config-ns-invalid' => 'Вказаний простір імен "<nowiki>$1</nowiki>" не припустимий.
+Вкажіть інший простір імен проекту.',
+ 'config-ns-conflict' => 'Вказаний простір імен "<nowiki>$1</nowiki>" конфліктує зі стандартним простором імен MediaWiki.
+Вкажіть інший простір імен проекту.',
+ 'config-admin-box' => 'Обліковий запис адміністратора',
'config-admin-name' => "Ваше ім'я:",
'config-admin-password' => 'Пароль:',
'config-admin-password-confirm' => 'Пароль ще раз:',
+ 'config-admin-help' => 'Введіть бажане ім\'я користувача тут, наприклад "Павло НЛО".
+Це ім\'я ви будете використовувати про вході у вікі.',
+ 'config-admin-name-blank' => "Введіть ім'я користувача адміністратора.",
+ 'config-admin-name-invalid' => 'Вказане ім\'я користувача "<nowiki>$1</nowiki>" не припустиме.
+Вкажіть інше ім\'я користувача.',
+ 'config-admin-password-blank' => 'Введіть пароль до облікового запису адміністратора.',
+ 'config-admin-password-same' => "Пароль не може бути таким же, як ім'я користувача.",
'config-admin-password-mismatch' => 'Два введені вами паролі не збігаються.',
'config-admin-email' => 'Адреса електронної пошти:',
+ 'config-admin-email-help' => 'Введіть адресу електронної пошти, щоб мати змогу отримувати електронну пошту від інших користувачів у вікі, відновити пароль і отримувати повідомлення про зміни, внесені до сторінок у Вашому списку спостереження. Ви можете залишити це поле пустим.',
+ 'config-admin-error-user' => 'Внутрішня помилка під час створення адміністратора з ім\'ям "<nowiki>$1</nowiki>".',
+ 'config-admin-error-password' => 'Внутрішня помилка під час встановлення пароля для адміністратора "<nowiki>$1</nowiki>":<pre>$2</pre>',
+ 'config-admin-error-bademail' => 'Ви ввели недопустиму адресу електронної пошти.',
+ 'config-subscribe' => 'Підписатися на [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce розсилку анонсів нових версій MediaWiki].',
+ 'config-subscribe-help' => "Це список розсилки з малим обсягом повідомлень, що використовується для анонсування релізів, а також важливих повідомлень про безпеку.
+Вам варто підписати і оновлювати інсталяцію MediaWiki, коли з'являтимуться нові версії.",
+ 'config-subscribe-noemail' => 'Ви намагались підписатись на розсилку анонсів релізів, не вказавши адреси електронної пошти.
+Будь ласка, вкажіть адресу електронної пошти, якщо хочете підписатись на розсилку.',
+ 'config-almost-done' => 'Майже готово!
+Ви можете зараз пропустити налаштування, що залишилось, і встановити вікі прямо зараз.',
+ 'config-optional-continue' => 'Запитуйте ще.',
+ 'config-optional-skip' => 'Це вже втомлює, просто встановити вікі.',
+ 'config-profile' => 'Профіль прав користувача:',
+ 'config-profile-wiki' => 'Відкрита вікі',
+ 'config-profile-no-anon' => 'Необхідно створити обліковий запис',
+ 'config-profile-fishbowl' => 'Тільки для авторизованих редакторів',
+ 'config-profile-private' => 'Приватна вікі',
+ 'config-profile-help' => "Вікі краще працюють, коли Ви дозволяєте їх редагувати якомога ширшому колу людей.
+У MediaWiki легко переглядати останні зміни і відкочувати будь-яку шкоду, спричинену недосвідченими або зловмисними користувачами.
+
+Одначе, MediaWiki може бути корисна по-різному, й інколи важко переконати у вигідності відкритої вікі-роботи.
+Тож у Вас є вибір.
+
+Модель '''{{int:config-profile-wiki}}''' дозволяє редагувати будь-кому, навіть без входження в систему.
+Вікі з вимогою \"'''{{int:config-profile-no-anon}}'''\" дає певний облік, але може відвернути випадкових дописувачів.
+Спосіб \"'''{{int:config-profile-fishbowl}}'''\" дозволяє редагувати підтвердженим користувачам, а переглядати сторінки і історію можуть усі.
+'''{{int:config-profile-private}}''' дозволяє переглядати сторінки і редагувати лише підтвердженим користувачам.
+
+Детальніші конфігурації прав користувачів доступні після встановлення, див. [//www.mediawiki.org/wiki/Manual:User_rights відповідний розділ посібника].",
'config-license' => 'Авторські права і ліцензія:',
+ 'config-license-none' => 'Без ліцензії у нижньому колонтитулі',
+ 'config-license-cc-by-sa' => 'Creative Commons Attribution Share Alike',
+ 'config-license-cc-by' => 'Creative Commons Attribution',
'config-license-cc-by-nc-sa' => 'Creative Commons Attribution Non-Commercial Share Alike',
+ 'config-license-cc-0' => 'Creative Commons Zero (Суспільне надбання)',
+ 'config-license-gfdl' => 'GNU Free Documentation License 1.3 або пізніша',
+ 'config-license-pd' => 'Суспільне надбання (Public Domain)',
+ 'config-license-cc-choose' => 'Виберіть одну з ліцензій Creative Commons',
+ 'config-license-help' => "Чимало загальнодоступних вікі публікують увесь свій вміст під [http://freedomdefined.org/Definition вільною ліцензією]. Це розвиває відчуття спільної власності і заохочує довготривалу участь. У загальному випадку для приватної чи корпоративної вікі у цьому немає необхідності.
+
+Якщо Ви хочете мати змогу використовувати текст з Вікіпедії і дати Вікіпедії змогу використовувати текст, скопійований з Вашої вікі, вам необхідно обрати '''Creative Commons Attribution Share Alike'''.
+
+Раніше Вікіпедія використовувала GNU Free Documentation License.
+GFDL — допустима ліцензія, але у ній важко розібратися, а контент під GFDL важко використовувати повторно.",
'config-email-settings' => 'Налаштування електронної пошти',
+ 'config-enable-email' => 'Увімкнути вихідну електронну пошту',
+ 'config-enable-email-help' => 'Якщо Ви хочете, що електронна пошта працювала, необхідно виставити коректні [http://www.php.net/manual/en/mail.configuration.php налаштування пошти у PHP].
+Якщо Вам не потрібні жодні можливості електронної пошти у вікі, можете тут їх відключити.',
+ 'config-email-user' => 'Увімкнути електронну пошту користувач-користувачеві',
+ 'config-email-user-help' => 'Дозволити усім користувачам надсилати один одному електронну пошту, якщо вони увімкнули цю можливість у своїх налаштуваннях.',
+ 'config-email-usertalk' => 'Увімкнути сповіщення про повідомлення на сторінці обговорення користувача',
+ 'config-email-usertalk-help' => 'Дозволити користувачам отримувати сповіщення про зміни на своїй сторінці обговорення, якщо вони увімкнули цю можливість у своїх налаштуваннях.',
+ 'config-email-watchlist' => 'Увімкнути сповіщення про зміни у списку спостереження',
+ 'config-email-watchlist-help' => 'Дозволити користувачам отримувати сповіщення про сторінки з їхнього списку спостереження, якщо вони увімкнули цю можливість у своїх налаштуваннях.',
+ 'config-email-auth' => 'Увімкнути автентифікацію через електронну пошту',
+ 'config-email-auth-help' => "Якщо ця опція увімкнена, користувачам треба підтвердити свою адресу електронної пошти з допомогою надісланого їм посилання, коли вони встановлюють чи змінюють її.
+Тільки автентифіковані адреси електронної пошти отримують листи від інших користувачів або змінювати поштові сповіщення.
+Увімкнення цієї опції '''рекомендується''' загальнодоступним вікі через можливі зловживання функціями електронної пошти.",
+ 'config-email-sender' => 'Зворотна адреса електронної пошти:',
+ 'config-email-sender-help' => "Введіть адресу електронної пошти, що буде використовуватись як зворотна адреса для вихідної пошти.
+На неї будуть надсилатись відмови.
+Чимало поштових серверів вимагають, щоб принаймні доменне ім'я було допустимим.",
+ 'config-upload-settings' => 'Завантаження зображень і файлів',
'config-upload-enable' => 'Дозволити завантаження файлів',
+ 'config-upload-help' => 'Завантаження файлів підставляє Ваш сервер під потенційні загрози.
+Детальнішу інформацію можна почитати у посібнику, [//www.mediawiki.org/wiki/Manual:Security розділ про безпеку].
+
+Щоб дозволити завантаження файлів, змініть режим підпапки <code>images</code> у кореневій папці MediaWiki так, щоб сервер міг у неї записувати.
+Потім увімкніть цю опцію.',
'config-upload-deleted' => 'Каталог для вилучених файлів:',
+ 'config-upload-deleted-help' => 'Оберіть папку для архівації видалених файлів.
+В ідеалі, вона не має бути доступною через інтернет.',
+ 'config-logo' => 'URL логотипу:',
+ 'config-logo-help' => 'Стандартна схема оформлення MediaWiki містить вільне для логотипу місце над бічною панеллю розміром 135x160 пікселів.
+
+Завантажте зображення відповідного розміру і введіть тут його URL.
+
+Ви можете використати <code>$wgStylePath</code> або <code>$wgScriptPath</code>, якщо ваш логотип пов\'язаний з цими шляхами.
+
+Якщо Вам не потрібен логотип, залиште це поле пустим.',
+ 'config-instantcommons' => 'Увімкнути Instant Commons',
+ 'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons Instant Commons] це функція, що дозволяє вікі використовувати зображення, звуки та інші медіа, розміщені на [//commons.wikimedia.org/ Вікісховищі].
+Для цього MediaWiki необхідний доступ до інтернету.
+
+Додаткову інформацію стосовно цієї функції, включаючи інструкції, як її увімкнути у вікі, відмінних від Вікісховища, дивіться у [//mediawiki.org/wiki/Manual:$wgForeignFileRepos посібнику].',
+ 'config-cc-error' => 'Механізм вибору ліцензії Creative Commons не дав результатів.
+Введіть назву ліцензії вручну.',
'config-cc-again' => 'Виберіть знову ...',
+ 'config-cc-not-chosen' => 'Оберіть, яку ліцензію Creative Commons Ви хочете використовувати, і натисніть "продовжити".',
+ 'config-advanced-settings' => 'Розширені налаштування',
+ 'config-cache-options' => "Налаштування кешування об'єктів:",
+ 'config-cache-help' => "Кешування об'єктів використовується для покращення швидкодії MediaWiki методом кешування часто використовуваних даних.
+Заохочується увімкнення цієї можливості для середніх і великих сайтів, малі сайти також можуть відчути її перевагу.",
+ 'config-cache-none' => 'Без кешування (жодні функції не втрачаються, але впливає на швидкодію великих вікі-сайтів)',
+ 'config-cache-accel' => "PHP кешування об'єктів (APC, XCache чи WinCache)",
+ 'config-cache-memcached' => 'Використовувати Memcached (вимагає додаткової установки і налаштування)',
+ 'config-memcached-servers' => 'Сервери Memcached:',
+ 'config-memcached-help' => 'Список IP-адрес, що викоритовує Memcached.
+Вкажіть по одному в рядку, разом з портами. Наприклад:
+ 127.0.0.1:11211
+ 192.168.1.25:1234',
+ 'config-memcache-needservers' => 'Ви обрали тип кешування Memcached, але не вказали ніяких серверів.',
+ 'config-memcache-badip' => 'Ви ввели недопустиму IP-адресу для Memcached: $1.',
+ 'config-memcache-noport' => 'Ви не вказали порт для сервера Memcached: $1.
+Якщо Ви його не знаєте, за замовчуванням використовується 11211.',
+ 'config-memcache-badport' => 'Номери портів Memcached повинні лежати в межах від $1 до $2.',
'config-extensions' => 'Розширення',
+ 'config-extensions-help' => 'Розширення, перераховані вище, були знайдені у папці <code>./extensions</code>.
+
+Вони можуть потребувати додаткових налаштувань, але Ви можете увімкнути їх зараз.',
+ 'config-install-alreadydone' => "'''Увага:''' Здається, Ви вже встановлювали MediaWiki і зараз намагаєтесь встановити її знову.
+Будь ласка, перейдіть на наступну сторінку.",
+ 'config-install-begin' => 'Натискаючи "{{int:config-continue}}", Ви розпочинаєте встановлення MediaWiki.
+Якщо Ви все ще хочете внести зміни, натисніть "{{int:config-back}}".',
'config-install-step-done' => 'виконано',
'config-install-step-failed' => 'не вдалося',
+ 'config-install-extensions' => 'У тому числі розширення',
+ 'config-install-database' => 'Налаштування бази даних',
+ 'config-install-schema' => 'Створення схеми',
+ 'config-install-pg-schema-not-exist' => 'Схеми PostgreSQL не існує.',
+ 'config-install-pg-schema-failed' => 'Не вдалось створити таблиці.
+Переконайтесь, що користувач "$1" може писати до схеми "$2".',
+ 'config-install-pg-commit' => 'Внесення змін',
+ 'config-install-pg-plpgsql' => 'Перевірка мови PL/pgSQL',
+ 'config-pg-no-plpgsql' => 'Вам необхідно встановити мову PL/pgSQL у базі даних $1',
+ 'config-pg-no-create-privs' => 'Обліковий запис, вказаний для встановлення, має недостатньо прав для створення облікового запису.',
+ 'config-pg-not-in-role' => 'Обліковий запис, який Ви вказали для веб-користувача, уже існує.
+Обліковий запис, який Ви вказали для встановлення не є суперюзером і не відноситься до ролі веб-користувача, тому неможливо створити об\'єкти, що належать веб-користувачеві.
+
+У даний час MediaWiki вимагає, щоб усі таблиці належали веб-користувачу. Будь ласка, вкажіть інше ім\'я облікового запису або натисніть "Назад" та вкажіть користувача з достатніми правами.',
+ 'config-install-user' => 'Створення користувача бази даних',
+ 'config-install-user-alreadyexists' => 'Користувач "$1" уже існує',
+ 'config-install-user-create-failed' => 'Не вдалося створити користувача "$1": $2',
+ 'config-install-user-grant-failed' => 'Не вдалося надати права користувачеві "$1": $2',
+ 'config-install-user-missing' => 'Зазначеного користувача "$1" не існує.',
+ 'config-install-user-missing-create' => 'Зазначеного користувача "$1" не існує.
+Будь ласка, поставте галочку "Створити обліковий запис", якщо хочете його створити.',
+ 'config-install-tables' => 'Створення таблиць',
+ 'config-install-tables-exist' => "'''Увага''': Таблиці MediaWiki уже, здається, існують.
+Пропуск створення.",
+ 'config-install-tables-failed' => "'''Помилка''': Не вдалося створити таблицю внаслідок такої помилки: $1",
+ 'config-install-interwiki' => 'Заповнення таблиці інтервікі значеннями за замовчуванням',
'config-install-interwiki-list' => 'Не вдалося знайти файл <code>interwiki.list</code>.',
+ 'config-install-interwiki-exists' => "'''Увага''': Таблиця інтервікі уже, здається, має записи.
+Створення стандартного списку пропускається.",
+ 'config-install-stats' => 'Ініціалізація статистики',
+ 'config-install-keys' => 'Генерація секретних ключів',
+ 'config-insecure-keys' => "'''Увага:''' {{PLURAL:$2|Секретний ключ|Секретні ключі}} ($1), {{PLURAL:$2|згенерований в процесі встановлення, недостатньо надійний|згенеровані в процесі встановлення, недостатньо надійні}}. Розгляньте можливість {{PLURAL:$2|його|їх}} заміни вручку.",
+ 'config-install-sysop' => 'Створення облікового запису адміністратора',
+ 'config-install-subscribe-fail' => 'Не можливо підписатись на mediawiki-announce: $1',
+ 'config-install-subscribe-notpossible' => 'cURL не встановлено і опція allow_url_fopen не доступна.',
+ 'config-install-mainpage' => 'Створення головної сторінки із вмістом за замовчуванням',
+ 'config-install-extension-tables' => 'Створення таблиць для увімкнених розширень',
+ 'config-install-mainpage-failed' => 'Не вдається вставити головну сторінку: $1',
+ 'config-install-done' => "'''Вітаємо!'''
+Ви успішно встановили MediaWiki.
+
+Інсталятор згенерував файл <code>LocalSettings.php</code>, який містить усі Ваші налаштування.
+
+Вам необхідно завантажити його і помістити у кореневу папку Вашої вікі (туди ж, де index.php). Завантаження мало початись автоматично.
+
+Якщо завантаження не почалось або Ви його скасували, можете заново його почати, натиснувши на посилання внизу:
+
+$3
+
+'''Примітка''': Якщо Ви не зробите цього зараз, цей файл не буде доступним пізніше, коли Ви вийдете з встановлення, не скачавши його.
+
+Після виконання дій, описаних вище, Ви зможете '''[$2 увійти у свою вікі]'''.",
+ 'config-download-localsettings' => 'Завантажити <code>LocalSettings.php</code>',
'config-help' => 'допомога',
+ 'config-nofile' => 'Файл "$1" не знайдено. Його видалено?',
+ 'config-extension-link' => 'Чи знаєте ви, що ваше вікі підтримує [//www.mediawiki.org/wiki/Manual:Extensions розширення]?
+
+Ви можете переглядати [//www.mediawiki.org/wiki/Category:Extensions_by_category розширення по категорії] або в [//www.mediawiki.org/wiki/Extension_Matrix матрицю розширень] щоб побачити повний список розширень.',
'mainpagetext' => 'Програмне забезпечення «MediaWiki» успішно встановлене.',
- 'mainpagedocfooter' => 'Інформацію про роботу з цією вікі можна знайти в [//meta.wikimedia.org/wiki/%D0%9F%D0%BE%D0%BC%D0%BE%D1%89%D1%8C:%D0%A1%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D0%BD%D0%B8%D0%B5 посібнику користувача].
+ 'mainpagedocfooter' => 'Інформацію про роботу з цією вікі можна знайти в [//meta.wikimedia.org/wiki/Help:Contents посібнику користувача].
== Деякі корисні ресурси ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Список налаштувань];
* [//www.mediawiki.org/wiki/Manual:FAQ Часті питання з приводу MediaWiki];
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Розсилка повідомлень про появу нових версій MediaWiki].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Розсилка повідомлень про появу нових версій MediaWiki];
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources Локалізуйте MediaWiki своєю мовою]',
);
/** Urdu (اردو)
+ * @author Noor2020
* @author පසිඳු කාවින්ද
*/
$messages['ur'] = array(
'config-information' => 'معلومات',
+ 'config-git' => 'Git ورژن کنٹرول مصنع لطیف ملا: <code>$1</code> ۔',
+ 'config-git-bad' => 'GIT ورژن کنٹرول مصنع لطیف نہيں ملا ۔',
+ 'config-mysql-only-myisam-dep' => "' ' تنبیہ: ' '[[MyISAM|مائ اسام]] واحد دستیاب 'ذخیرہ جاتی انجن' ہے جو مائی ایس کیو ایل کے لیے ہے ، جو کہ ناموزوں ہے میڈیا وکی کے لیے ،کیوں کہ :
+* یہ ہموار قطاروں کی سہولت بمشکل فراہم کرتا ہے
+* یہ دوسرے انجنوں کے مقابلے زیادہ بگڑ جاتا ہے
+* میڈیا وکی کوڈ بیس ہمیشہ سنبھال نہيں پاتا مائی اسام کو ۔
+
+آپ کا مائی ایس کیو ایل کا نصب ہمیشہ اننو ڈی بی کی سہولت نہيں دے سکتا ، ہو سکتا ہے یہ مزید ترقیاتی کام چاہے", # Fuzzy
'config-profile-fishbowl' => 'صرف مجاز ایڈیٹرز',
'config-license-pd' => 'پبلک ڈومین',
'config-email-settings' => 'ای میل کی ترتیبات',
@@ -18116,8 +20402,10 @@ $messages['ur'] = array(
);
/** Uzbek (oʻzbekcha)
+ * @author Sociologist
*/
$messages['uz'] = array(
+ 'config-admin-password-blank' => 'Administrator hisob yozuvi uchun maxfiy soʻz kiriting.',
'mainpagetext' => "'''MediaWiki muvaffaqiyatli o'rnatildi.'''",
'mainpagedocfooter' => "Wiki dasturini ishlatish haqida ma'lumot olish uchun [//meta.wikimedia.org/wiki/Help:Contents Foydalanuvchi qo'llanmasi] sahifasiga murojaat qiling.
@@ -18125,7 +20413,7 @@ $messages['uz'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Moslamalar ro'yxati]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki haqida ko'p so'raladigan savollar]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki yangi versiyasi chiqqanda xabar berish ro'yxati]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki yangi versiyasi chiqqanda xabar berish ro'yxati]", # Fuzzy
);
/** vèneto (vèneto)
@@ -18141,7 +20429,7 @@ I seguenti cołegamenti i xé en łengua inglese:
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Inpostasion de configurasion]
* [//www.mediawiki.org/wiki/Manual:FAQ Domande frequenti so MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list anunsi MediaWiki]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list anunsi MediaWiki]", # Fuzzy
);
/** Veps (vepsän kel’)
@@ -18154,7 +20442,7 @@ $messages['vep'] = array(
== Erased tarbhaižed resursad ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Järgendusiden nimikirjutez]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce počtnimikirjutez]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce počtnimikirjutez]', # Fuzzy
);
/** Vietnamese (Tiếng Việt)
@@ -18176,7 +20464,7 @@ $messages['vi'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Danh sách các thiết lập cấu hình]
* [//www.mediawiki.org/wiki/Manual:FAQ Các câu hỏi thường gặp MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Danh sách gửi thư về việc phát hành MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Danh sách gửi thư về việc phát hành MediaWiki]', # Fuzzy
);
/** Volapük (Volapük)
@@ -18189,7 +20477,7 @@ $messages['vo'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Parametalised]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki: SSP]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Potalised tefü fomams nulik ela MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Potalised tefü fomams nulik ela MediaWiki]', # Fuzzy
);
/** Võro (Võro)
@@ -18200,7 +20488,7 @@ $messages['vro'] = array(
* [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide MediaWiki pruukmisoppus (inglüse keelen)].
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Säädmiisi oppus (inglüse keelen)]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki kõgõ küsütümbäq küsümiseq (inglüse keelen)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-postilist, minka andas teedäq MediaWiki vahtsist kujõst].',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-postilist, minka andas teedäq MediaWiki vahtsist kujõst].', # Fuzzy
);
/** Walloon (walon)
@@ -18220,7 +20508,7 @@ $messages['war'] = array(
== Ha pagtikang==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Wolof (Wolof)
@@ -18233,7 +20521,7 @@ $messages['wo'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Limu jumtukaayi kocc-koccal gi]
* [//www.mediawiki.org/wiki/Manual:FAQ FAQ MediaWiki]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Limu waxtaan ci liy-génn ci MediaWiki]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Limu waxtaan ci liy-génn ci MediaWiki]', # Fuzzy
);
/** Wu (吴语)
@@ -18246,7 +20534,7 @@ $messages['wuu'] = array(
== 入门 ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings MediaWiki 配置设置列表]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki 常见问题解答]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 发布邮件列表]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 发布邮件列表]', # Fuzzy
);
/** Kalmyk (хальмг)
@@ -18259,7 +20547,7 @@ $messages['xal'] = array(
== Туста заавр ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Көгүдә бүрткл]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki туск ЮмБи]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki шинҗллһнә бүрткл]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki шинҗллһнә бүрткл]', # Fuzzy
);
/** Yiddish (ייִדיש)
@@ -18267,21 +20555,36 @@ $messages['xal'] = array(
* @author පසිඳු කාවින්ද
*/
$messages['yi'] = array(
+ 'config-desc' => 'דער אינסטאלירער פאר מעדיעוויקי',
+ 'config-title' => 'מעדיעוויקי $1 אינסטאלירונג',
+ 'config-information' => 'אינפֿארמאציע',
+ 'config-wiki-language' => 'ווקי שפראך:',
'config-back' => '→ צוריק',
+ 'config-continue' => 'פֿארזעצן ←',
'config-page-language' => 'שפראַך',
'config-page-name' => 'נאָמען',
'config-page-options' => 'ברירות',
+ 'config-db-type' => 'דאטנבאזע טיפ:',
+ 'config-db-name' => 'דאטנבאזע נאָמען:',
+ 'config-project-namespace' => 'פראיעקט נאָמענטייל:',
+ 'config-ns-generic' => 'פראיעקט',
'config-admin-name' => 'אײַער נאָמען:',
'config-admin-password' => 'פאַסווארט:',
+ 'config-admin-password-mismatch' => 'די צוויי פאסוועטרט איר האט איינגעגעבן שטימען נישט.',
'config-admin-email' => 'בליצפּאָסט אַדרעס:',
+ 'config-install-tables' => 'שאפן טאבעלעס',
+ 'config-install-tables-exist' => "'''ווארענונג''': זעט אויס אז די מעדיעוויקי טאבעלעס עקזיסטירן שוין.
+איבערהיפן שאפֿן.",
+ 'config-download-localsettings' => 'אראפלאדן <code>LocalSettings.php</code>',
'config-help' => 'הילף',
+ 'config-nofile' => 'מ\'האט נישט געקענט טרעפן די טעקע "$1". צי האט מען זי אויסגעמעקט?',
'mainpagetext' => "'''מעדיעוויקי אינסטאלירט מיט דערפאלג.'''",
'mainpagedocfooter' => "גיט זיך אן עצה מיט [//meta.wikimedia.org/wiki/Help:Contents באניצער'ס וועגווײַזער] פֿאר אינפֿארמאציע וויאזוי זיך באנוצן מיט וויקי ווייכוואַרג.
== נוצליכע וועבלינקען פֿאַר אנהייבערס ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings רשימה פון קאנפֿיגוראציעס]
* [//www.mediawiki.org/wiki/Manual:FAQ אפֿט געפֿרעגטע שאלות]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce מעדיעוויקי באפֿרײַאונג פאסטליסטע]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce מעדיעוויקי באפֿרײַאונג פאסטליסטע]* [//www.mediawiki.org/wiki/Localisation#Translation_resources איבערזעצן מעדיעוויקי אין אײַער שפראך]",
);
/** Yoruba (Yorùbá)
@@ -18294,7 +20597,7 @@ $messages['yo'] = array(
== Láti bẹ̀rẹ̀ ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Configuration settings list]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki FAQ]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]", # Fuzzy
);
/** Cantonese (粵語)
@@ -18306,7 +20609,7 @@ $messages['yue'] = array(
==開始使用==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings 配置設定清單](英)
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki 常見問題](英)
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 發佈郵件名單](英)',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 發佈郵件名單](英)', # Fuzzy
);
/** Zeeuws (Zeêuws)
@@ -18319,30 +20622,36 @@ $messages['zea'] = array(
* [//www.mediawiki.org/wiki/Manual:Configuration_settings Lieste mie instelliengen]
* [//www.mediawiki.org/wiki/Manual:FAQ Veehestelde vraehen (FAQ)]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailienglieste voe ankondigiengen van nieuwe versies]",
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailienglieste voe ankondigiengen van nieuwe versies]", # Fuzzy
);
/** Simplified Chinese (中文(简体)‎)
+ * @author Anthony Fok
+ * @author Cwek
* @author Hydra
* @author Hzy980512
* @author Liangent
+ * @author Makecat
* @author PhiLiP
* @author Xiaomingyan
+ * @author Yfdyh000
+ * @author 乌拉跨氪
* @author 阿pp
+ * @author 아라
*/
$messages['zh-hans'] = array(
'config-desc' => 'MediaWiki安装程序',
'config-title' => 'MediaWiki $1配置',
'config-information' => '信息',
- 'config-localsettings-upgrade' => '已检测到<code>LocalSettings.php</code>文件。要升级该配置,请在下面的框中输入<code>$wgUpgradeKey</code>的值。您可以在LocalSettings.php中找到它。',
- 'config-localsettings-cli-upgrade' => '已检测到LocalSettings.php文件。要升级该配置,请直接运行update.php。',
+ 'config-localsettings-upgrade' => '已检测到<code>LocalSettings.php</code>文件。要升级该配置,请在下面的框中输入<code>$wgUpgradeKey</code>的值。您可以在<code>LocalSettings.php</code>中找到它。',
+ 'config-localsettings-cli-upgrade' => '已检测到<code>LocalSettings.php</code>文件。要升级该配置,请直接运行<code>update.php</code>。',
'config-localsettings-key' => '升级密钥:',
'config-localsettings-badkey' => '您提供的密钥不正确。',
- 'config-upgrade-key-missing' => '检测到MediaWiki的配置已经存在。若要升级该配置,请将下面一行文本添加到LocalSettings.php的底部:
+ 'config-upgrade-key-missing' => '检测到MediaWiki的配置已经存在。若要升级该配置,请将下面一行文本添加到<code>LocalSettings.php</code>的底部:
$1',
- 'config-localsettings-incomplete' => '当前的LocalSettings.php可能并不完整,因为变量$1没有设置。请在LocalSettings.php设置该变量,并单击“继续”。',
- 'config-localsettings-connection-error' => '在使用LocalSettings.php或AdminSettings.php中指定的设置连接数据库时发生错误。请修复相应设置并重试。
+ 'config-localsettings-incomplete' => '当前的<code>LocalSettings.php</code>可能并不完整,因为变量$1没有设置。请在<code>LocalSettings.php</code>设置该变量,并单击“{{int:Config-continue}}”。',
+ 'config-localsettings-connection-error' => '在使用<code>LocalSettings.php</code>或<code>AdminSettings.php</code>中指定的设置连接数据库时发生错误。请修复相应设置并重试。
$1',
'config-session-error' => '启动会话出错:$1',
@@ -18397,7 +20706,8 @@ $1',
'config-env-php-toolow' => '已安装PHP $1;但是,MediaWiki需要PHP $2或更高版本。',
'config-unicode-using-utf8' => '使用Brion Vibber的utf8_normalize.so实现Unicode正常化。',
'config-unicode-using-intl' => '使用[http://pecl.php.net/intl intl PECL扩展]实现Unicode正常化。',
- 'config-unicode-pure-php-warning' => "'''警告''':[http://pecl.php.net/intl intl PECL扩展]无法处理Unicode正常化,故只能退而采用运行较慢的纯PHP实现的方法。如果您运行着一个高流量的站点,请参阅[//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode正常化]一文。",
+ 'config-unicode-pure-php-warning' => "'''警告:'''因为尚未安装 [http://pecl.php.net/intl intl PECL 扩展]以处理 Unicode 正常化,故只能退而采用运行较慢的纯 PHP 实现的方法。
+如果您运行着一个高流量的站点,请参阅 [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode 正常化]一文。",
'config-unicode-update-warning' => "'''警告''':Unicode正常化封装器的已安装版本使用了旧版本的[http://site.icu-project.org/ ICU项目]库。如果您需要使用Unicode,请将其[//www.mediawiki.org/wiki/Unicode_normalization_considerations 升级]。",
'config-no-db' => '找不到合适的数据库驱动!您需要为PHP安装数据库驱动。目前支持以下数据库:$1。
@@ -18427,6 +20737,8 @@ $1',
Object caching is not enabled.",
'config-mod-security' => "'''警告''':您的服务器已启动[http://modsecurity.org/ mod_security]。若其配置错误, 会导致MediaWiki和其他软件的错误并允许用户任意发布内容。如果您遇到任何错误,请查阅[http://modsecurity.org/documentation/ mod_security文档]或联系您的客服。",
'config-diff3-bad' => '找不到GNU diff3。',
+ 'config-git' => '发现Git版本控制软件:<code>$1</code>',
+ 'config-git-bad' => 'Git版本控制软件未找到。',
'config-imagemagick' => '已找到ImageMagick:<code>$1</code>。如果你启用了上传功能,缩略图功能也将被启用。',
'config-gd' => '已找到内建的GD图形库。如果你启用了上传功能,缩略图功能也将被启用。',
'config-no-scaling' => '找不到GD库或ImageMagick。缩略图功能将不可用。',
@@ -18438,7 +20750,7 @@ Object caching is not enabled.",
'config-no-cli-uploads-check' => "'''警告''':在CLI安装过程中,没有对您的默认上传目录(<code>$1</code>)进行执行任意脚本的漏洞检查。",
'config-brokenlibxml' => '您的系统安装的PHP和libxml2版本组合存在故障,并可能在MediaWiki和其他web应用程序中造成隐藏的数据损坏。请将PHP升级到5.2.9或以上,libxml2升级到2.7.3或以上([//bugs.php.net/bug.php?id=45996 PHP的故障报告])。安装已中断。',
'config-using531' => '由于函数<code>__call()</code>的引用参数存在故障,PHP $1和MediaWiki无法兼容。请升级到PHP 5.3.2或更高版本,或降级到PHP 5.3.0以修复该问题。安装已中断。',
- 'config-suhosin-max-value-length' => 'Suhosin已经安装并将GET请求的参数长度限制在$1字节。MediaWiki的ResourceLoader部件可以在此限制下正常工作,但其性能会被降低。如果可能,请在php.ini中将suhosin.get.max_value_length设为1024或更高值,并在LocalSettings.php中将$wgResourceLoaderMaxQueryLength设为同一值。',
+ 'config-suhosin-max-value-length' => 'Suhosin已经安装并将GET请求的参数长度限制在$1字节。MediaWiki的ResourceLoader部件可以在此限制下正常工作,但其性能会被降低。如果可能,请在<code>php.ini</code>中将<code>suhosin.get.max_value_length</code>设为1024或更高值,并在LocalSettings.php中将<code>$wgResourceLoaderMaxQueryLength</code>设为同一值。',
'config-db-type' => '数据库类型:',
'config-db-host' => '数据库主机:',
'config-db-host-help' => '如果您的数据库在别的服务器上,请在这里输入它的域名或IP地址。
@@ -18501,7 +20813,6 @@ Object caching is not enabled.",
请考虑将数据库统一放置在某处,如<code>/var/lib/mediawiki/yourwiki</code>下。",
'config-oracle-def-ts' => '默认表空间:',
'config-oracle-temp-ts' => '临时表空间:',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki支持以下数据库系统:
$1
@@ -18511,12 +20822,10 @@ $1
'config-support-postgres' => '* $1是一种流行的开源数据库系统,可作为MySQL的替代([http://www.php.net/manual/en/pgsql.installation.php 如何将对PostgreSQL的支持编译进PHP中])。本程序中可能依然存在一些小而明显的错误,因此并不建议在生产环境中使用该数据库系统。',
'config-support-sqlite' => '* $1是一种轻量级的数据库系统,能被良好地支持。([http://www.php.net/manual/en/pdo.installation.php 如何将对SQLite的支持编译进PHP中],须使用PDO)',
'config-support-oracle' => '* $1是一种商用企业级的数据库。([http://www.php.net/manual/en/oci8.installation.php 如何将对OCI8的支持编译进PHP中])',
- 'config-support-ibm_db2' => '* $1是一种商用企业级数据库。',
'config-header-mysql' => 'MySQL设置',
'config-header-postgres' => 'PostgreSQL设置',
'config-header-sqlite' => 'SQLite设置',
'config-header-oracle' => 'Oracle设置',
- 'config-header-ibm_db2' => 'IBM DB2设置',
'config-invalid-db-type' => '无效的数据库类型',
'config-missing-db-name' => '您必须为“数据库名称”输入内容',
'config-missing-db-host' => '您必须为“数据库主机”输入内容',
@@ -18565,7 +20874,7 @@ chmod a+w $3</pre>',
现在您可以[$1 开始使用您的wiki]了。',
'config-regenerate' => '重新生成LocalSettings.php →',
- 'config-show-table-status' => 'SHOW TABLE STATUS语句执行失败!',
+ 'config-show-table-status' => '<code>SHOW TABLE STATUS</code>语句执行失败!',
'config-unknown-collation' => "'''警告:'''数据库使用了无法识别的整理。",
'config-db-web-account' => '供网页访问使用的数据库帐号',
'config-db-web-help' => '请指定在wiki执行普通操作时,网页服务器用于连接数据库服务器的用户名和密码。',
@@ -18582,6 +20891,12 @@ chmod a+w $3</pre>',
如果您的MySQL程序支持InnoDB,我们高度推荐您使用该引擎替代MyISAM。
如果您的MySQL程序不支持InnoDB,请考虑升级。",
+ 'config-mysql-only-myisam-dep' => "''''警告:'''MyISAM是MySQL唯一可用的存储引擎,但不适合用于MediaWiki,是由于:
+*由于只支持表级锁定,几乎不支持并发。
+*它比其他引擎更容易损坏。
+*MediaWiki代码不能总是按照预设地操作MyISAM。
+
+你的MySQL不支持InnoDB,是时候升级了。",
'config-mysql-engine-help' => "'''InnoDB'''通常是最佳选项,因为它对并发操作有着良好的支持。
'''MyISAM'''在单用户或只读环境下可能会有更快的性能表现。但MyISAM数据库出错的概率一般要大于InnoDB数据库。",
@@ -18591,7 +20906,6 @@ chmod a+w $3</pre>',
'config-mysql-charset-help' => "在'''二进制模式'''下,MediaWiki会将UTF-8编码的文本存于数据库的二进制字段中。相对于MySQL的UTF-8模式,这种方法效率更高,并允许您使用全范围的Unicode字符。
在'''UTF-8模式'''下,MySQL将知道您数据使用的字符集,并能适当地提供和转换内容。但这样做您将无法在数据库中存储[//zh.wikipedia.org/wiki/基本多文种平面 基本多文种平面]以外的字符。",
- 'config-ibm_db2-low-db-pagesize' => "您的DB2数据库默认表空间的页长(pagesize)不足。至少需要'''32K'''或更大的页长。",
'config-site-name' => 'Wiki的名称:',
'config-site-name-help' => '填入的内容会出现在浏览器的标题栏以及其他多处位置中。',
'config-site-name-blank' => '输入网站的名称。',
@@ -18625,7 +20939,7 @@ chmod a+w $3</pre>',
'config-optional-continue' => '多问我一些问题吧。',
'config-optional-skip' => '我已经不耐烦了,赶紧安装我的wiki。',
'config-profile' => '用户权限配置:',
- 'config-profile-wiki' => '传统wiki',
+ 'config-profile-wiki' => '开放的wiki',
'config-profile-no-anon' => '需要注册帐号',
'config-profile-fishbowl' => '编辑受限',
'config-profile-private' => '非公开wiki',
@@ -18675,6 +20989,8 @@ GNU自由文档许可证是维基百科曾经使用过的许可证,并迄今
'config-logo' => '标志URL:',
'config-logo-help' => '在MediaWiki的默认外观中,左侧栏菜单之上有一块135x160像素的标志区。请上传一幅相应大小的图像,并在此输入URL。
+你可以用<code>$wgStylePath</code>或<code>$wgScriptPath</code>来表示相对于这些位置的路径。
+
如果您不希望使用标志,请将本处留空。',
'config-instantcommons' => '启用即时共享资源',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons 即时共享资源]可以让wiki使用来自[//commons.wikimedia.org/ 维基共享资源]网站的图像、音频和其他媒体文件。要启用该功能,MediaWiki必须能够访问互联网。
@@ -18702,7 +21018,7 @@ GNU自由文档许可证是维基百科曾经使用过的许可证,并迄今
您可能要对它们进行额外的配置,但您现在可以启用它们。',
'config-install-alreadydone' => "'''警告:'''您似乎已经安装了MediaWiki,并试图重新安装它。请前往下一个页面。",
- 'config-install-begin' => '点击“{{int:config-continue}}”后,您将开始安装MediaWiki。如果您还想对配置作一些修改,请点击后退。',
+ 'config-install-begin' => '点击“{{int:config-continue}}”后,您将开始安装MediaWiki。如果您还想对配置作一些修改,请点击“{{int:config-back}}”。',
'config-install-step-done' => '完成',
'config-install-step-failed' => '失败',
'config-install-extensions' => '正在启用扩展',
@@ -18753,35 +21069,41 @@ $3
'''注意''':如果您现在不完成本步骤,而是没有下载便退出了安装过程,此后您将无法获得自动生成的配置文件。
当本步骤完成后,您可以 '''[$2 进入您的wiki]'''。",
- 'config-download-localsettings' => '下载LocalSettings.php',
+ 'config-download-localsettings' => '下载<code>LocalSettings.php</code>',
'config-help' => '帮助',
+ 'config-nofile' => '找不到文件“$1”。它是否已被删除?',
'mainpagetext' => "'''已成功安装MediaWiki。'''",
'mainpagedocfooter' => '请查阅[//meta.wikimedia.org/wiki/Help:Contents 用户指南]以获取使用本wiki软件的信息!
== 入门 ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings MediaWiki配置设置列表]
* [//www.mediawiki.org/wiki/Manual:FAQ/zh-hans MediaWiki常见问题]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki发布邮件列表]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki发布邮件列表]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources 本地化MediaWiki到您的语言]',
);
/** Traditional Chinese (中文(繁體)‎)
+ * @author Anthony Fok
* @author Hzy980512
+ * @author Justincheng12345
* @author Liangent
* @author Mark85296341
+ * @author Simon Shek
+ * @author 아라
*/
$messages['zh-hant'] = array(
'config-desc' => 'MediaWiki安裝程序',
'config-title' => 'MediaWiki $1配置',
'config-information' => '資訊',
- 'config-localsettings-upgrade' => '已檢測到<code>LocalSettings.php</code>文件。要升級該配置,請在下面的框中輸入<code>$wgUpgradeKey</code>的值。您可以在LocalSettings.php中找到它。',
- 'config-localsettings-cli-upgrade' => '已檢測到LocalSettings.php文件。要升級該配置,請直接執行update.php。',
+ 'config-localsettings-upgrade' => '已檢測到<code>LocalSettings.php</code>文件。要升級該配置,請在下面的框中輸入<code>$wgUpgradeKey</code>的值。您可以在<code>LocalSettings.php</code>中找到它。',
+ 'config-localsettings-cli-upgrade' => '已檢測到<code>LocalSettings.php</code>文件。要升級該配置,請直接執行<code>update.php</code>。',
'config-localsettings-key' => '升級密鑰:',
'config-localsettings-badkey' => '您提供的密鑰不正確。',
- 'config-upgrade-key-missing' => '檢測到MediaWiki的配置已經存在。若要升級該配置,請將下面一行文本添加到LocalSettings.php的底部:
+ 'config-upgrade-key-missing' => '檢測到MediaWiki的配置已經存在。若要升級該配置,請將下面一行文本添加到<code>LocalSettings.php</code>的底部:
$1',
- 'config-localsettings-incomplete' => '當前的LocalSettings.php可能並不完整,因為變量$1沒有設置。請在LocalSettings.php設置該變量,並單擊“繼續”。',
- 'config-localsettings-connection-error' => '在使用LocalSettings.php或AdminSettings.php中指定的設置連接數據庫時發生錯誤。請修復相應設置並重試。
+ 'config-localsettings-incomplete' => '當前的<code>LocalSettings.php</code>可能並不完整,因為變量$1沒有設置。請在<code>LocalSettings.php</code>設置該變量,並單擊“{{int:Config-continue}}”。',
+ 'config-localsettings-connection-error' => '在使用<code>LocalSettings.php</code>或<code>AdminSettings.php</code>中指定的設置連接數據庫時發生錯誤。請修復相應設置並重試。
$1',
'config-session-error' => '啟動會話出錯:$1',
@@ -18832,11 +21154,11 @@ $1',
* <doclink href=UpgradeDoc>升級</doclink>',
'config-env-good' => '環境檢查已經完成。您可以安裝MediaWiki。',
'config-env-bad' => '環境檢查已經完成。您不能安裝MediaWiki。',
- 'config-env-php' => 'PHP $1已安裝。',
- 'config-env-php-toolow' => '已安裝PHP $1;但是,MediaWiki需要PHP $2或更高版本。',
- 'config-unicode-using-utf8' => '使用Brion Vibber的utf8_normalize.so實現Unicode正常化。',
- 'config-unicode-using-intl' => '使用[http://pecl.php.net/intl intl PECL擴展]實現Unicode正常化。',
- 'config-unicode-pure-php-warning' => "'''警告''':[http://pecl.php.net/intl intl PECL擴展]無法處理Unicode正常化,故只能退而採用運行較慢的純PHP實現的方法。如果您運行着一個高流量的站點,請參閱[//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode正常化]一文。",
+ 'config-env-php' => 'PHP $1 已安裝。',
+ 'config-env-php-toolow' => '已安裝 PHP $1;但是,MediaWiki 需要 PHP $2 或更高版本。',
+ 'config-unicode-using-utf8' => '將使用 Brion Vibber 的 utf8_normalize.so 以實作 Unicode 正規化。',
+ 'config-unicode-using-intl' => '將使用 [http://pecl.php.net/intl intl PECL 延伸函式庫]以實作 Unicode 正規化。',
+ 'config-unicode-pure-php-warning' => "'''警告:'''因為尚未安裝 [http://pecl.php.net/intl intl PECL 延伸函式庫]以處理 Unicode 正規化,故只能退而採用較慢的純 PHP 實作。如果您運行着一個高流量的網站,請參閱 [//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode 正規化]一文。",
'config-unicode-update-warning' => "'''警告''':Unicode正常化封裝器的已安裝版本使用了舊版本的[http://site.icu-project.org/ ICU項目]庫。如果您需要使用Unicode,請將其[//www.mediawiki.org/wiki/Unicode_normalization_considerations 升級]。",
'config-no-db' => '找不到合適的數據庫驅動!您需要為PHP安裝數據庫驅動。目前支持以下數據庫:$1。
@@ -18866,6 +21188,8 @@ $1',
Object caching is not enabled.",
'config-mod-security' => "'''警告''':您的服務器已啟動[http://modsecurity.org/ mod_security]。若其配置錯誤, 會導致MediaWiki和其他軟件的錯誤並允許用戶任意發布內容。如果您遇到任何錯誤,請查閱[http://modsecurity.org/documentation/ mod_security文檔]或聯繫您的客服。",
'config-diff3-bad' => '找不到GNU diff3。',
+ 'config-git' => '發現Git版本控制軟件:<code>$1</code>。',
+ 'config-git-bad' => '無法找到Git版本控制軟件。',
'config-imagemagick' => '已找到ImageMagick:<code>$1</code>。如果你啟用了上傳功能,縮略圖功能也將被啟用。',
'config-gd' => '已找到內建的GD圖形庫。如果你啟用了上傳功能,縮略圖功能也將被啟用。',
'config-no-scaling' => '找不到GD庫或ImageMagick。縮略圖功能將不可用。',
@@ -18877,7 +21201,7 @@ Object caching is not enabled.",
'config-no-cli-uploads-check' => "'''警告''':在CLI安裝過程中,沒有對您的默認上傳目錄(<code>$1</code>)進行執行任意腳本的漏洞檢查。",
'config-brokenlibxml' => '您的系統安裝的PHP和libxml2版本組合存在故障,並可能在MediaWiki和其他web應用程序中造成隱藏的數據損壞。請將PHP升級到5.2.9或以上,libxml2升級到2.7.3或以上([//bugs.php.net/bug.php?id=45996 PHP的故障報告])。安裝已中斷。',
'config-using531' => '由於函數<code>__call()</code>的引用參數存在故障,PHP $1和MediaWiki無法兼容。請升級到PHP 5.3.2或更高版本,或降級到PHP 5.3.0以修復該問題。安裝已中斷。',
- 'config-suhosin-max-value-length' => 'Suhosin已經安裝並將GET請求的參數長度限制在$1字節。MediaWiki的ResourceLoader部件可以在此限制下正常工作,但其性能會被降低。如果可能,請在php.ini中將suhosin.get.max_value_length設為1024或更高值,並在LocalSettings.php中將$wgResourceLoaderMaxQueryLength設為同一值。',
+ 'config-suhosin-max-value-length' => 'Suhosin已經安裝並將GET請求的參數長度限制在$1字節。MediaWiki的ResourceLoader部件可以在此限制下正常工作,但其性能會被降低。如果可能,請在<code>php.ini</code>中將<code>suhosin.get.max_value_length</code>設為1024或更高值,並在LocalSettings.php中將<code>$wgResourceLoaderMaxQueryLength</code>設為同一值。', # Fuzzy
'config-db-type' => '資料庫類型:',
'config-db-host' => '資料庫主機:',
'config-db-host-help' => '如果您的數據庫在別的服務器上,請在這裡輸入它的域名或IP地址。
@@ -18940,7 +21264,6 @@ Object caching is not enabled.",
請考慮將數據庫統一放置在某處,如<code>/var/lib/mediawiki/yourwiki</code>下。",
'config-oracle-def-ts' => '默認表空間:',
'config-oracle-temp-ts' => '臨時表空間:',
- 'config-type-ibm_db2' => 'IBM DB2',
'config-support-info' => 'MediaWiki支持以下數據庫系統:
$1
@@ -18950,17 +21273,16 @@ $1
'config-support-postgres' => '* $1是一種流行的開源數據庫系統,可作為MySQL的替代([http://www.php.net/manual/en/pgsql.installation.php 如何將對PostgreSQL的支持編譯進PHP中])。本程序中可能依然存在一些小而明顯的錯誤,因此並不建議在生產環境中使用該數據庫系統。',
'config-support-sqlite' => '* $1是一種輕量級的數據庫系統,能被良好地支持。([http://www.php.net/manual/en/pdo.installation.php 如何將對SQLite的支持編譯進PHP中],須使用PDO)',
'config-support-oracle' => '* $1是一種商用企業級的數據庫。([http://www.php.net/manual/en/oci8.installation.php 如何將對OCI8的支持編譯進PHP中])',
- 'config-support-ibm_db2' => '* $1是一種商用企業級數據庫。',
'config-header-mysql' => 'MySQL 的設定',
'config-header-postgres' => 'PostgreSQL設置',
'config-header-sqlite' => 'SQLite 的設定',
'config-header-oracle' => '甲骨文設定',
- 'config-header-ibm_db2' => 'IBM DB2設置',
'config-invalid-db-type' => '無效的資料庫類型',
'config-missing-db-name' => '您必須為“數據庫名稱”輸入內容',
'config-missing-db-host' => '您必須為“數據庫主機”輸入內容',
'config-missing-db-server-oracle' => '您必須為“數據庫透明網絡底層(TNS)”輸入內容',
- 'config-invalid-db-server-oracle' => '無效的數據庫TNS“$1”。請只使用ASCII字母(a-z、A-Z)、數字(0-9)、下劃線(_)和點號(.)。',
+ 'config-invalid-db-server-oracle' => '無效的數據庫TNS「$1」。
+請只使用「TNS Name」或「Easy Connect」 字串([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle命名法])',
'config-invalid-db-name' => '無效的數據庫名稱“$1”。請只使用ASCII字母(a-z、A-Z)、數字(0-9)、下劃線(_)和連字號(-)。',
'config-invalid-db-prefix' => '無效的數據庫前綴“$1”。請只使用ASCII字母(a-z、A-Z)、數字(0-9)、下劃線(_)和連字號(-)。',
'config-connection-error' => '$1。
@@ -19004,7 +21326,7 @@ chmod a+w $3</pre>',
現在您可以[$1 開始使用您的wiki]了。',
'config-regenerate' => '重新生成LocalSettings.php →',
- 'config-show-table-status' => '查詢SHOW TABLE STATUS失敗!',
+ 'config-show-table-status' => '查詢<code>SHOW TABLE STATUS</code>失敗!',
'config-unknown-collation' => "'''警告:'''數據庫使用了無法識別的整理。",
'config-db-web-account' => '供網頁訪問使用的數據庫帳號',
'config-db-web-help' => '請指定在wiki執行普通操作時,網頁服務器用於連接數據庫服務器的用戶名和密碼。',
@@ -19030,7 +21352,6 @@ chmod a+w $3</pre>',
'config-mysql-charset-help' => "在'''二進制模式'''下,MediaWiki會將UTF-8編碼的文本存於數據庫的二進制字段中。相對於MySQL的UTF-8模式,這種方法效率更高,並允許您使用全範圍的Unicode字符。
在'''UTF-8模式'''下,MySQL將知道您數據使用的字符集,並能適當地提供和轉換內容。但這樣做您將無法在數據庫中存儲[//zh.wikipedia.org/wiki/基本多文種平面 基本多文種平面]以外的字符。",
- 'config-ibm_db2-low-db-pagesize' => "您的DB2數據庫默認表空間的頁長(pagesize)不足。至少需要'''32K'''或更大的頁長。",
'config-site-name' => 'Wiki的名稱:',
'config-site-name-help' => '填入的內容會出現在瀏覽器的標題欄以及其他多處位置中。',
'config-site-name-blank' => '輸入站點名稱。',
@@ -19052,7 +21373,7 @@ chmod a+w $3</pre>',
'config-admin-password-blank' => '輸入管理員帳號密碼。',
'config-admin-password-same' => '密碼不能與使用者名稱相同。',
'config-admin-password-mismatch' => '兩次輸入的密碼並不相同。',
- 'config-admin-email' => 'E-mail 地址:',
+ 'config-admin-email' => '電郵地址:',
'config-admin-email-help' => '輸入電子郵件地址後,您可以收到此wiki上其他用戶發來的電子郵件,並能重置您的密碼,還可在監視列表中頁面被更改時收到郵件通知。您可以將此字段留空。',
'config-admin-error-user' => '在創建用戶名為“<nowiki>$1</nowiki>”的管理員帳號時發生內部錯誤。',
'config-admin-error-password' => '在為管理員“<nowiki>$1</nowiki>”設置密碼時發生內部錯誤:<pre>$2</pre>',
@@ -19064,7 +21385,7 @@ chmod a+w $3</pre>',
'config-optional-continue' => '多問我一些問題吧。',
'config-optional-skip' => '我已經不耐煩了,趕緊安裝我的wiki。',
'config-profile' => '用戶權限配置:',
- 'config-profile-wiki' => '傳統wiki',
+ 'config-profile-wiki' => '開放的wiki',
'config-profile-no-anon' => '需要註冊帳號',
'config-profile-fishbowl' => '編輯受限',
'config-profile-private' => '非公開wiki',
@@ -19076,7 +21397,7 @@ chmod a+w $3</pre>',
'''{{int:config-profile-fishbowl}}'''模式只允許獲批准的用戶編輯,但對公眾開放頁面瀏覽(包括歷史記錄)。'''{{int:config-profile-private}}'''則只允許獲批准的用戶瀏覽、編輯頁面。
-安裝完成後,您還可以對用戶權限進行更多、更複雜的配置,參見[//www.mediawiki.org/wiki/Manual:User_rights 相關的使用手冊]。",
+安裝完成後,您還可以對用戶權限進行更多、更複雜的配置,參見[//www.mediawiki.org/wiki/Manual:User_rights 相關的使用手冊]。", # Fuzzy
'config-license' => '版權和許可證:',
'config-license-none' => '頁腳無許可證',
'config-license-cc-by-sa' => '知識共享署名-相同方式分享',
@@ -19114,7 +21435,7 @@ GNU自由文檔許可證是維基百科曾經使用過的許可證,並迄今
'config-logo' => '標誌URL:',
'config-logo-help' => '在MediaWiki的默認外觀中,左側欄菜單之上有一塊135x160像素的標誌區。請上傳一幅相應大小的圖像,並在此輸入URL。
-如果您不希望使用標誌,請將本處留空。',
+如果您不希望使用標誌,請將本處留空。', # Fuzzy
'config-instantcommons' => '啟用即時共享資源',
'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons 即時共享資源]可以讓wiki使用來自[//commons.wikimedia.org/ 維基共享資源]網站的圖像、音頻和其他媒體文件。要啟用該功能,MediaWiki必須能夠訪問互聯網。
@@ -19141,7 +21462,7 @@ GNU自由文檔許可證是維基百科曾經使用過的許可證,並迄今
您可能要對它們進行額外的配置,但您現在可以啟用它們。',
'config-install-alreadydone' => "'''警告:'''您似乎已經安裝了MediaWiki,並試圖重新安裝它。請前往下一個頁面。",
- 'config-install-begin' => '點擊“{{int:config-continue}}”後,您將開始安裝MediaWiki。如果您還想對配置作一些修改,請點擊後退。',
+ 'config-install-begin' => '點擊“{{int:config-continue}}”後,您將開始安裝MediaWiki。如果您還想對配置作一些修改,請點擊後退。', # Fuzzy
'config-install-step-done' => '完成',
'config-install-step-failed' => '失敗',
'config-install-extensions' => '正在啟用擴展',
@@ -19192,7 +21513,7 @@ $3
'''注意''':如果您現在不完成本步驟,而是沒有下載便退出了安裝過程,此後您將無法獲得自動生成的配置文件。
當本步驟完成後,您可以 '''[$2 進入您的wiki]'''。",
- 'config-download-localsettings' => '下載LocalSettings.php',
+ 'config-download-localsettings' => '下載<code>LocalSettings.php</code>',
'config-help' => '說明',
'mainpagetext' => "'''已成功安裝MediaWiki。'''",
'mainpagedocfooter' => '請參閱[//meta.wikimedia.org/wiki/Help:Contents 用戶手冊]以獲得使用此wiki軟體的訊息!
@@ -19200,7 +21521,8 @@ $3
== 入門 ==
* [//www.mediawiki.org/wiki/Manual:Configuration_settings MediaWiki配置設定清單]
* [//www.mediawiki.org/wiki/Manual:FAQ MediaWiki常見問題解答]
-* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki發佈郵件清單]',
+* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki發佈郵件清單]
+* [//www.mediawiki.org/wiki/Localisation#Translation_resources MediaWiki界面本地化]',
);
/** Chinese (Hong Kong) (‪中文(香港)‬)
diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php
index ac5dbd74..62bb2ec4 100644
--- a/includes/installer/Installer.php
+++ b/includes/installer/Installer.php
@@ -47,6 +47,13 @@ abstract class Installer {
protected $settings;
/**
+ * List of detected DBs, access using getCompiledDBs().
+ *
+ * @var array
+ */
+ protected $compiledDBs;
+
+ /**
* Cached DB installer instances, access using getDBInstaller().
*
* @var array
@@ -88,7 +95,6 @@ abstract class Installer {
'postgres',
'oracle',
'sqlite',
- 'ibm_db2',
);
/**
@@ -115,6 +121,7 @@ abstract class Installer {
'envCheckModSecurity',
'envCheckDiff3',
'envCheckGraphics',
+ 'envCheckGit',
'envCheckServer',
'envCheckPath',
'envCheckExtension',
@@ -123,6 +130,7 @@ abstract class Installer {
'envCheckLibicu',
'envCheckSuhosinMaxValueLength',
'envCheckCtype',
+ 'envCheckJSON',
);
/**
@@ -148,8 +156,8 @@ abstract class Installer {
'wgDBtype',
'wgDiff3',
'wgImageMagickConvertCommand',
+ 'wgGitBin',
'IP',
- 'wgServer',
'wgScriptPath',
'wgScriptExtension',
'wgMetaNamespace',
@@ -174,7 +182,6 @@ abstract class Installer {
protected $internalDefaults = array(
'_UserLang' => 'en',
'_Environment' => false,
- '_CompiledDBs' => array(),
'_SafeMode' => false,
'_RaiseMemory' => false,
'_UpgradeDone' => false,
@@ -295,7 +302,8 @@ abstract class Installer {
/**
* URL to mediawiki-announce subscription
*/
- protected $mediaWikiAnnounceUrl = 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
+ protected $mediaWikiAnnounceUrl =
+ 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
/**
* Supported language codes for Mailman
@@ -313,19 +321,19 @@ abstract class Installer {
* output format such as HTML or text before being sent to the user.
* @param $msg
*/
- public abstract function showMessage( $msg /*, ... */ );
+ abstract public function showMessage( $msg /*, ... */ );
/**
* Same as showMessage(), but for displaying errors
* @param $msg
*/
- public abstract function showError( $msg /*, ... */ );
+ abstract public function showError( $msg /*, ... */ );
/**
* Show a message to the installing user by using a Status object
* @param $status Status
*/
- public abstract function showStatusMessage( Status $status );
+ abstract public function showStatusMessage( Status $status );
/**
* Constructor, always call this from child classes.
@@ -369,7 +377,7 @@ abstract class Installer {
}
}
}
- $this->setVar( '_CompiledDBs', $compiledDBs );
+ $this->compiledDBs = $compiledDBs;
$this->parserTitle = Title::newFromText( 'Installer' );
$this->parserOptions = new ParserOptions; // language will be wrong :(
@@ -400,7 +408,7 @@ abstract class Installer {
*/
public function doEnvironmentChecks() {
$phpVersion = phpversion();
- if( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
+ if ( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
$this->showMessage( 'config-env-php', $phpVersion );
$good = true;
} else {
@@ -408,7 +416,7 @@ abstract class Installer {
$good = false;
}
- if( $good ) {
+ if ( $good ) {
foreach ( $this->envChecks as $check ) {
$status = $this->$check();
if ( $status === false ) {
@@ -451,6 +459,15 @@ abstract class Installer {
}
/**
+ * Get a list of DBs supported by current PHP setup
+ *
+ * @return array
+ */
+ public function getCompiledDBs() {
+ return $this->compiledDBs;
+ }
+
+ /**
* Get an instance of DatabaseInstaller for the specified DB type.
*
* @param $type Mixed: DB installer for which is needed, false to use default.
@@ -465,7 +482,7 @@ abstract class Installer {
$type = strtolower( $type );
if ( !isset( $this->dbInstallers[$type] ) ) {
- $class = ucfirst( $type ). 'Installer';
+ $class = ucfirst( $type ) . 'Installer';
$this->dbInstallers[$type] = new $class( $this );
}
@@ -485,16 +502,17 @@ abstract class Installer {
$_lsExists = file_exists( "$IP/LocalSettings.php" );
wfRestoreWarnings();
- if( !$_lsExists ) {
+ if ( !$_lsExists ) {
return false;
}
- unset($_lsExists);
+ unset( $_lsExists );
- require( "$IP/includes/DefaultSettings.php" );
- require( "$IP/LocalSettings.php" );
+ require "$IP/includes/DefaultSettings.php";
+ require "$IP/LocalSettings.php";
if ( file_exists( "$IP/AdminSettings.php" ) ) {
- require( "$IP/AdminSettings.php" );
+ require "$IP/AdminSettings.php";
}
+
return get_defined_vars();
}
@@ -621,6 +639,7 @@ abstract class Installer {
'ss_users' => 0,
'ss_images' => 0 ),
__METHOD__, 'IGNORE' );
+
return Status::newGood();
}
@@ -644,19 +663,15 @@ abstract class Installer {
$allNames = array();
+ // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
+ // config-type-sqlite
foreach ( self::getDBTypes() as $name ) {
$allNames[] = wfMessage( "config-type-$name" )->text();
}
- // cache initially available databases to make sure that everything will be displayed correctly
- // after a refresh on env checks page
- $databases = $this->getVar( '_CompiledDBs-preFilter' );
- if ( !$databases ) {
- $databases = $this->getVar( '_CompiledDBs' );
- $this->setVar( '_CompiledDBs-preFilter', $databases );
- }
+ $databases = $this->getCompiledDBs();
- $databases = array_flip ( $databases );
+ $databases = array_flip( $databases );
foreach ( array_keys( $databases ) as $db ) {
$installer = $this->getDBInstaller( $db );
$status = $installer->checkPrerequisites();
@@ -670,10 +685,11 @@ abstract class Installer {
$databases = array_flip( $databases );
if ( !$databases ) {
$this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
+
// @todo FIXME: This only works for the web installer!
return false;
}
- $this->setVar( '_CompiledDBs', $databases );
+
return true;
}
@@ -681,7 +697,7 @@ abstract class Installer {
* Environment check for register_globals.
*/
protected function envCheckRegisterGlobals() {
- if( wfIniGetBool( 'register_globals' ) ) {
+ if ( wfIniGetBool( 'register_globals' ) ) {
$this->showMessage( 'config-register-globals' );
}
}
@@ -694,8 +710,10 @@ abstract class Installer {
$test = new PhpXmlBugTester();
if ( !$test->ok ) {
$this->showError( 'config-brokenlibxml' );
+
return false;
}
+
return true;
}
@@ -709,8 +727,10 @@ abstract class Installer {
$test->execute();
if ( !$test->ok ) {
$this->showError( 'config-using531', phpversion() );
+
return false;
}
+
return true;
}
@@ -719,10 +739,12 @@ abstract class Installer {
* @return bool
*/
protected function envCheckMagicQuotes() {
- if( wfIniGetBool( "magic_quotes_runtime" ) ) {
+ if ( wfIniGetBool( "magic_quotes_runtime" ) ) {
$this->showError( 'config-magic-quotes-runtime' );
+
return false;
}
+
return true;
}
@@ -733,8 +755,10 @@ abstract class Installer {
protected function envCheckMagicSybase() {
if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
$this->showError( 'config-magic-quotes-sybase' );
+
return false;
}
+
return true;
}
@@ -745,8 +769,10 @@ abstract class Installer {
protected function envCheckMbstring() {
if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
$this->showError( 'config-mbstring' );
+
return false;
}
+
return true;
}
@@ -757,8 +783,10 @@ abstract class Installer {
protected function envCheckZE1() {
if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
$this->showError( 'config-ze1' );
+
return false;
}
+
return true;
}
@@ -771,6 +799,7 @@ abstract class Installer {
$this->setVar( '_SafeMode', true );
$this->showMessage( 'config-safe-mode' );
}
+
return true;
}
@@ -781,8 +810,10 @@ abstract class Installer {
protected function envCheckXML() {
if ( !function_exists( "utf8_encode" ) ) {
$this->showError( 'config-xml-bad' );
+
return false;
}
+
return true;
}
@@ -797,6 +828,7 @@ abstract class Installer {
protected function envCheckPCRE() {
if ( !function_exists( 'preg_match' ) ) {
$this->showError( 'config-pcre' );
+
return false;
}
wfSuppressWarnings();
@@ -809,8 +841,10 @@ abstract class Installer {
wfRestoreWarnings();
if ( $regexd != '--' || $regexprop != '--' ) {
$this->showError( 'config-pcre-no-utf8' );
+
return false;
}
+
return true;
}
@@ -827,16 +861,17 @@ abstract class Installer {
$n = wfShorthandToInteger( $limit );
- if( $n < $this->minMemorySize * 1024 * 1024 ) {
+ if ( $n < $this->minMemorySize * 1024 * 1024 ) {
$newLimit = "{$this->minMemorySize}M";
- if( ini_set( "memory_limit", $newLimit ) === false ) {
+ if ( ini_set( "memory_limit", $newLimit ) === false ) {
$this->showMessage( 'config-memory-bad', $limit );
} else {
$this->showMessage( 'config-memory-raised', $limit, $newLimit );
$this->setVar( '_RaiseMemory', true );
}
}
+
return true;
}
@@ -869,6 +904,7 @@ abstract class Installer {
if ( self::apacheModulePresent( 'mod_security' ) ) {
$this->showMessage( 'config-mod-security' );
}
+
return true;
}
@@ -888,6 +924,7 @@ abstract class Installer {
$this->setVar( 'wgDiff3', false );
$this->showMessage( 'config-diff3-bad' );
}
+
return true;
}
@@ -897,19 +934,44 @@ abstract class Installer {
*/
protected function envCheckGraphics() {
$names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
- $convert = self::locateExecutableInDefaultPaths( $names, array( '$1 -version', 'ImageMagick' ) );
+ $versionInfo = array( '$1 -version', 'ImageMagick' );
+ $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
$this->setVar( 'wgImageMagickConvertCommand', '' );
if ( $convert ) {
$this->setVar( 'wgImageMagickConvertCommand', $convert );
$this->showMessage( 'config-imagemagick', $convert );
+
return true;
} elseif ( function_exists( 'imagejpeg' ) ) {
$this->showMessage( 'config-gd' );
-
} else {
$this->showMessage( 'config-no-scaling' );
}
+
+ return true;
+ }
+
+ /**
+ * Search for git.
+ *
+ * @since 1.22
+ * @return bool
+ */
+ protected function envCheckGit() {
+ $names = array( wfIsWindows() ? 'git.exe' : 'git' );
+ $versionInfo = array( '$1 --version', 'git version' );
+
+ $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
+
+ if ( $git ) {
+ $this->setVar( 'wgGitBin', $git );
+ $this->showMessage( 'config-git', $git );
+ } else {
+ $this->setVar( 'wgGitBin', false );
+ $this->showMessage( 'config-git-bad' );
+ }
+
return true;
}
@@ -918,8 +980,11 @@ abstract class Installer {
*/
protected function envCheckServer() {
$server = $this->envGetDefaultServer();
- $this->showMessage( 'config-using-server', $server );
- $this->setVar( 'wgServer', $server );
+ if ( $server !== null ) {
+ $this->showMessage( 'config-using-server', $server );
+ $this->setVar( 'wgServer', $server );
+ }
+
return true;
}
@@ -927,7 +992,7 @@ abstract class Installer {
* Helper function to be called from envCheckServer()
* @return String
*/
- protected abstract function envGetDefaultServer();
+ abstract protected function envGetDefaultServer();
/**
* Environment check for setting $IP and $wgScriptPath.
@@ -938,12 +1003,18 @@ abstract class Installer {
$IP = dirname( dirname( __DIR__ ) );
$this->setVar( 'IP', $IP );
- $this->showMessage( 'config-using-uri', $this->getVar( 'wgServer' ), $this->getVar( 'wgScriptPath' ) );
+ $this->showMessage(
+ 'config-using-uri',
+ $this->getVar( 'wgServer' ),
+ $this->getVar( 'wgScriptPath' )
+ );
+
return true;
}
/**
* Environment check for setting the preferred PHP file extension.
+ * @return bool
*/
protected function envCheckExtension() {
// @todo FIXME: Detect this properly
@@ -953,11 +1024,12 @@ abstract class Installer {
$ext = 'php';
}
$this->setVar( 'wgScriptExtension', ".$ext" );
+
return true;
}
/**
- * TODO: document
+ * Environment check for preferred locale in shell
* @return bool
*/
protected function envCheckShellLocale() {
@@ -976,7 +1048,7 @@ abstract class Installer {
return true;
}
- $lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
+ $lines = array_map( 'trim', explode( "\n", $lines ) );
$candidatesByLocale = array();
$candidatesByLang = array();
@@ -989,15 +1061,16 @@ abstract class Installer {
continue;
}
- list( $all, $lang, $territory, $charset, $modifier ) = $m;
+ list( , $lang, , , ) = $m;
$candidatesByLocale[$m[0]] = $m;
$candidatesByLang[$lang][] = $m;
}
# Try the current value of LANG.
- if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
+ if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
$this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
+
return true;
}
@@ -1006,6 +1079,7 @@ abstract class Installer {
foreach ( $commonLocales as $commonLocale ) {
if ( isset( $candidatesByLocale[$commonLocale] ) ) {
$this->setVar( 'wgShellLocale', $commonLocale );
+
return true;
}
}
@@ -1016,6 +1090,7 @@ abstract class Installer {
if ( isset( $candidatesByLang[$wikiLang] ) ) {
$m = reset( $candidatesByLang[$wikiLang] );
$this->setVar( 'wgShellLocale', $m[0] );
+
return true;
}
@@ -1023,6 +1098,7 @@ abstract class Installer {
if ( count( $candidatesByLocale ) ) {
$m = reset( $candidatesByLocale );
$this->setVar( 'wgShellLocale', $m[0] );
+
return true;
}
@@ -1031,7 +1107,7 @@ abstract class Installer {
}
/**
- * TODO: document
+ * Environment check for the permissions of the uploads directory
* @return bool
*/
protected function envCheckUploadsDirectory() {
@@ -1044,26 +1120,22 @@ abstract class Installer {
if ( !$safe ) {
$this->showMessage( 'config-uploads-not-safe', $dir );
}
+
return true;
}
/**
- * Checks if suhosin.get.max_value_length is set, and if so, sets
- * $wgResourceLoaderMaxQueryLength to that value in the generated
- * LocalSettings file
+ * Checks if suhosin.get.max_value_length is set, and if so generate
+ * a warning because it decreases ResourceLoader performance.
* @return bool
*/
protected function envCheckSuhosinMaxValueLength() {
$maxValueLength = ini_get( 'suhosin.get.max_value_length' );
- if ( $maxValueLength > 0 ) {
- if( $maxValueLength < 1024 ) {
- # Only warn if the value is below the sane 1024
- $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
- }
- } else {
- $maxValueLength = -1;
+ if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
+ // Only warn if the value is below the sane 1024
+ $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
}
- $this->setVar( 'wgResourceLoaderMaxQueryLength', $maxValueLength );
+
return true;
}
@@ -1073,24 +1145,23 @@ abstract class Installer {
* @return string
*/
protected function unicodeChar( $c ) {
- $c = hexdec($c);
- if ($c <= 0x7F) {
- return chr($c);
- } elseif ($c <= 0x7FF) {
- return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
- } elseif ($c <= 0xFFFF) {
- return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
- . chr(0x80 | $c & 0x3F);
- } elseif ($c <= 0x10FFFF) {
- return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
- . chr(0x80 | $c >> 6 & 0x3F)
- . chr(0x80 | $c & 0x3F);
+ $c = hexdec( $c );
+ if ( $c <= 0x7F ) {
+ return chr( $c );
+ } elseif ( $c <= 0x7FF ) {
+ return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
+ } elseif ( $c <= 0xFFFF ) {
+ return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F )
+ . chr( 0x80 | $c & 0x3F );
+ } elseif ( $c <= 0x10FFFF ) {
+ return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F )
+ . chr( 0x80 | $c >> 6 & 0x3F )
+ . chr( 0x80 | $c & 0x3F );
} else {
return false;
}
}
-
/**
* Check the libicu version
*/
@@ -1105,8 +1176,8 @@ abstract class Installer {
* Note that we use the hex representation to create the code
* points in order to avoid any Unicode-destroying during transit.
*/
- $not_normal_c = $this->unicodeChar("FA6C");
- $normal_c = $this->unicodeChar("242EE");
+ $not_normal_c = $this->unicodeChar( "FA6C" );
+ $normal_c = $this->unicodeChar( "242EE" );
$useNormalizer = 'php';
$needsUpdate = false;
@@ -1115,14 +1186,14 @@ abstract class Installer {
* We're going to prefer the pecl extension here unless
* utf8_normalize is more up to date.
*/
- if( $utf8 ) {
+ if ( $utf8 ) {
$useNormalizer = 'utf8';
$utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
if ( $utf8 !== $normal_c ) {
$needsUpdate = true;
}
}
- if( $intl ) {
+ if ( $intl ) {
$useNormalizer = 'intl';
$intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
if ( $intl !== $normal_c ) {
@@ -1130,12 +1201,13 @@ abstract class Installer {
}
}
- // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
- if( $useNormalizer === 'php' ) {
+ // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8',
+ // 'config-unicode-using-intl'
+ if ( $useNormalizer === 'php' ) {
$this->showMessage( 'config-unicode-pure-php-warning' );
} else {
$this->showMessage( 'config-unicode-using-' . $useNormalizer );
- if( $needsUpdate ) {
+ if ( $needsUpdate ) {
$this->showMessage( 'config-unicode-update-warning' );
}
}
@@ -1147,8 +1219,23 @@ abstract class Installer {
protected function envCheckCtype() {
if ( !function_exists( 'ctype_digit' ) ) {
$this->showError( 'config-ctype' );
+
return false;
}
+
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function envCheckJSON() {
+ if ( !function_exists( 'json_decode' ) ) {
+ $this->showError( 'config-json' );
+
+ return false;
+ }
+
return true;
}
@@ -1174,11 +1261,11 @@ abstract class Installer {
*
* Used only by environment checks.
*
- * @param $path String: path to search
- * @param $names Array of executable names
+ * @param string $path path to search
+ * @param array $names of executable names
* @param $versionInfo Boolean false or array with two members:
- * 0 => Command to run for version check, with $1 for the full executable name
- * 1 => String to compare the output with
+ * 0 => Command to run for version check, with $1 for the full executable name
+ * 1 => String to compare the output with
*
* If $versionInfo is not false, only executables with a version
* matching $versionInfo[1] will be returned.
@@ -1207,6 +1294,7 @@ abstract class Installer {
}
}
}
+
return false;
}
@@ -1218,12 +1306,13 @@ abstract class Installer {
* @return bool|string
*/
public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
- foreach( self::getPossibleBinPaths() as $path ) {
+ foreach ( self::getPossibleBinPaths() as $path ) {
$exe = self::locateExecutable( $path, $names, $versionInfo );
- if( $exe !== false ) {
+ if ( $exe !== false ) {
return $exe;
}
}
+
return false;
}
@@ -1257,8 +1346,7 @@ abstract class Installer {
try {
$text = Http::get( $url . $file, array( 'timeout' => 3 ) );
- }
- catch( MWException $e ) {
+ } catch ( MWException $e ) {
// Http::get throws with allow_url_fopen = false and no curl extension.
$text = null;
}
@@ -1266,6 +1354,7 @@ abstract class Installer {
if ( $text == 'exec' ) {
wfRestoreWarnings();
+
return $ext;
}
}
@@ -1279,7 +1368,7 @@ abstract class Installer {
/**
* Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
*
- * @param $moduleName String: Name of module to check.
+ * @param string $moduleName Name of module to check.
* @return bool
*/
public static function apacheModulePresent( $moduleName ) {
@@ -1290,6 +1379,7 @@ abstract class Installer {
ob_start();
phpinfo( INFO_MODULES );
$info = ob_get_clean();
+
return strpos( $info, $moduleName ) !== false;
}
@@ -1313,30 +1403,61 @@ abstract class Installer {
}
/**
+ * Load the extension credits for i18n strings. Very hacky for
+ * now, but I expect it only be used for 1.22.0 at the most.
+ */
+ public function getExtensionInfo( $file ) {
+ global $wgExtensionCredits, $wgVersion, $wgResourceModules;
+
+ $wgVersion = "1.22";
+ $wgResourceModules = array();
+ require_once $file ;
+ $e = array_values( $wgExtensionCredits );
+ if( $e ) {
+ $ext = array_values( $e[0] );
+ $wgExtensionCredits = array();
+ return $ext[0];
+ }
+ }
+
+ /**
* Finds extensions that follow the format /extensions/Name/Name.php,
* and returns an array containing the value for 'Name' for each found extension.
*
* @return array
*/
public function findExtensions() {
- if( $this->getVar( 'IP' ) === null ) {
- return false;
+ if ( $this->getVar( 'IP' ) === null ) {
+ return array();
}
- $exts = array();
$extDir = $this->getVar( 'IP' ) . '/extensions';
- $dh = opendir( $extDir );
+ if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
+ return array();
+ }
+ $dh = opendir( $extDir );
+ $exts = array();
while ( ( $file = readdir( $dh ) ) !== false ) {
- if( !is_dir( "$extDir/$file" ) ) {
+ if ( !is_dir( "$extDir/$file" ) ) {
continue;
}
- if( file_exists( "$extDir/$file/$file.php" ) ) {
- $exts[] = $file;
+
+ $extFile = "$extDir/$file/$file.php";
+ $extI18NFile = "$extDir/$file/$file.i18n.php";
+ if ( file_exists( $extFile ) ) {
+ if ( $info = $this->getExtensionInfo( $extFile ) ) {
+ $exts[$info['name']] = $info;
+
+ if ( file_exists( $extI18NFile ) ) {
+ global $wgExtensionMessagesFiles;
+ $wgExtensionMessagesFiles[$file] = $extI18NFile;
+ }
+ }
}
}
- natcasesort( $exts );
-
+ closedir( $dh );
+ uksort( $exts, 'strnatcasecmp' );
return $exts;
}
@@ -1361,10 +1482,11 @@ abstract class Installer {
global $wgAutoloadClasses;
$wgAutoloadClasses = array();
- require( "$IP/includes/DefaultSettings.php" );
+ require "$IP/includes/DefaultSettings.php";
- foreach( $exts as $e ) {
- require_once( "$IP/extensions/$e/$e.php" );
+ $extensions = $this->findExtensions();
+ foreach ( $exts as $e ) {
+ require_once $extensions[$e]['path'];
}
$hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
@@ -1391,29 +1513,29 @@ abstract class Installer {
*/
protected function getInstallSteps( DatabaseInstaller $installer ) {
$coreInstallSteps = array(
- array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
- array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
- array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
- array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
- array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
- array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
- array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
+ array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
+ array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
+ array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
+ array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
+ array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
+ array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
+ array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
);
// Build the array of install steps starting from the core install list,
// then adding any callbacks that wanted to attach after a given step
- foreach( $coreInstallSteps as $step ) {
+ foreach ( $coreInstallSteps as $step ) {
$this->installSteps[] = $step;
- if( isset( $this->extraInstallSteps[ $step['name'] ] ) ) {
+ if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
$this->installSteps = array_merge(
$this->installSteps,
- $this->extraInstallSteps[ $step['name'] ]
+ $this->extraInstallSteps[$step['name']]
);
}
}
// Prepend any steps that want to be at the beginning
- if( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
+ if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
$this->installSteps = array_merge(
$this->extraInstallSteps['BEGINNING'],
$this->installSteps
@@ -1421,7 +1543,7 @@ abstract class Installer {
}
// Extensions should always go first, chance to tie into hooks and such
- if( count( $this->getVar( '_Extensions' ) ) ) {
+ if ( count( $this->getVar( '_Extensions' ) ) ) {
array_unshift( $this->installSteps,
array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
);
@@ -1430,14 +1552,15 @@ abstract class Installer {
'callback' => array( $installer, 'createExtensionTables' )
);
}
+
return $this->installSteps;
}
/**
* Actually perform the installation.
*
- * @param $startCB Array A callback array for the beginning of each step
- * @param $endCB Array A callback array for the end of each step
+ * @param array $startCB A callback array for the beginning of each step
+ * @param array $endCB A callback array for the end of each step
*
* @return Array of Status objects
*/
@@ -1446,7 +1569,7 @@ abstract class Installer {
$installer = $this->getDBInstaller();
$installer->preInstall();
$steps = $this->getInstallSteps( $installer );
- foreach( $steps as $stepObj ) {
+ foreach ( $steps as $stepObj ) {
$name = $stepObj['name'];
call_user_func_array( $startCB, array( $name ) );
@@ -1459,13 +1582,14 @@ abstract class Installer {
// If we've hit some sort of fatal, we need to bail.
// Callback already had a chance to do output above.
- if( !$status->isOk() ) {
+ if ( !$status->isOk() ) {
break;
}
}
- if( $status->isOk() ) {
+ if ( $status->isOk() ) {
$this->setVar( '_InstallDone', true );
}
+
return $installResults;
}
@@ -1479,6 +1603,7 @@ abstract class Installer {
if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
$keys['wgUpgradeKey'] = 16;
}
+
return $this->doGenerateKeys( $keys );
}
@@ -1531,13 +1656,13 @@ abstract class Installer {
try {
$user->setPassword( $this->getVar( '_AdminPassword' ) );
- } catch( PasswordError $pwe ) {
+ } catch ( PasswordError $pwe ) {
return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
}
$user->addGroup( 'sysop' );
$user->addGroup( 'bureaucrat' );
- if( $this->getVar( '_AdminEmail' ) ) {
+ if ( $this->getVar( '_AdminEmail' ) ) {
$user->setEmail( $this->getVar( '_AdminEmail' ) );
}
$user->saveSettings();
@@ -1548,7 +1673,7 @@ abstract class Installer {
}
$status = Status::newGood();
- if( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
+ if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
$this->subscribeToMediaWikiAnnounce( $status );
}
@@ -1560,23 +1685,23 @@ abstract class Installer {
*/
private function subscribeToMediaWikiAnnounce( Status $s ) {
$params = array(
- 'email' => $this->getVar( '_AdminEmail' ),
+ 'email' => $this->getVar( '_AdminEmail' ),
'language' => 'en',
- 'digest' => 0
+ 'digest' => 0
);
// Mailman doesn't support as many languages as we do, so check to make
// sure their selected language is available
$myLang = $this->getVar( '_UserLang' );
- if( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
+ if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
$myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
$params['language'] = $myLang;
}
- if( MWHttpRequest::canMakeRequests() ) {
+ if ( MWHttpRequest::canMakeRequests() ) {
$res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
array( 'method' => 'POST', 'postData' => $params ) )->execute();
- if( !$res->isOK() ) {
+ if ( !$res->isOK() ) {
$s->warning( 'config-install-subscribe-fail', $res->getMessage() );
}
} else {
@@ -1594,14 +1719,18 @@ abstract class Installer {
$status = Status::newGood();
try {
$page = WikiPage::factory( Title::newMainPage() );
- $page->doEdit( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
- wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text(),
- '',
- EDIT_NEW,
- false,
- User::newFromName( 'MediaWiki default' )
+ $content = new WikitextContent(
+ wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
+ wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
);
- } catch (MWException $e) {
+
+ $page->doEditContent( $content,
+ '',
+ EDIT_NEW,
+ false,
+ User::newFromName( 'MediaWiki default' )
+ );
+ } catch ( MWException $e ) {
//using raw, because $wgShowExceptionDetails can not be set yet
$status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
}
@@ -1636,14 +1765,19 @@ abstract class Installer {
// Some of the environment checks make shell requests, remove limits
$GLOBALS['wgMaxShellMemory'] = 0;
+
+ // Don't bother embedding images into generated CSS, which is not cached
+ $GLOBALS['wgResourceLoaderLESSFunctions']['embeddable'] = function( $frame, $less ) {
+ return $less->toBool( false );
+ };
}
/**
* Add an installation step following the given step.
*
- * @param $callback Array A valid installation callback array, in this form:
+ * @param array $callback A valid installation callback array, in this form:
* array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
- * @param $findStep String the step to find. Omit to put the step at the beginning
+ * @param string $findStep the step to find. Omit to put the step at the beginning
*/
public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
$this->extraInstallSteps[$findStep][] = $callback;
diff --git a/includes/installer/LocalSettingsGenerator.php b/includes/installer/LocalSettingsGenerator.php
index bbc6b64e..53939826 100644
--- a/includes/installer/LocalSettingsGenerator.php
+++ b/includes/installer/LocalSettingsGenerator.php
@@ -62,25 +62,25 @@ class LocalSettingsGenerator {
'wgRightsText', 'wgMainCacheType', 'wgEnableUploads',
'wgMainCacheType', '_MemCachedServers', 'wgDBserver', 'wgDBuser',
'wgDBpassword', 'wgUseInstantCommons', 'wgUpgradeKey', 'wgDefaultSkin',
- 'wgMetaNamespace', 'wgResourceLoaderMaxQueryLength'
+ 'wgMetaNamespace', 'wgResourceLoaderMaxQueryLength', 'wgLogo',
),
$db->getGlobalNames()
);
- $unescaped = array( 'wgRightsIcon' );
+ $unescaped = array( 'wgRightsIcon', 'wgLogo' );
$boolItems = array(
'wgEnableEmail', 'wgEnableUserEmail', 'wgEnotifUserTalk',
'wgEnotifWatchlist', 'wgEmailAuthentication', 'wgEnableUploads', 'wgUseInstantCommons'
);
- foreach( $confItems as $c ) {
+ foreach ( $confItems as $c ) {
$val = $installer->getVar( $c );
- if( in_array( $c, $boolItems ) ) {
+ if ( in_array( $c, $boolItems ) ) {
$val = wfBoolToStr( $val );
}
- if ( !in_array( $c, $unescaped ) ) {
+ if ( !in_array( $c, $unescaped ) && $val !== null ) {
$val = self::escapePhpString( $val );
}
@@ -94,8 +94,8 @@ class LocalSettingsGenerator {
/**
* For $wgGroupPermissions, set a given ['group']['permission'] value.
- * @param $group String Group name
- * @param $rightsArr Array An array of permissions, in the form of:
+ * @param string $group Group name
+ * @param array $rightsArr An array of permissions, in the form of:
* array( 'right' => true, 'right2' => false )
*/
public function setGroupRights( $group, $rightsArr ) {
@@ -136,15 +136,22 @@ class LocalSettingsGenerator {
public function getText() {
$localSettings = $this->getDefaultText();
- if( count( $this->extensions ) ) {
+ if ( count( $this->extensions ) ) {
+ $extensions = $this->installer->findExtensions();
$localSettings .= "
# Enabled Extensions. Most extensions are enabled by including the base extension file here
# but check specific extension documentation for more details
# The following extensions were automatically enabled:\n";
- foreach( $this->extensions as $extName ) {
- $encExtName = self::escapePhpString( $extName );
- $localSettings .= "require_once( \"\$IP/extensions/$encExtName/$encExtName.php\" );\n";
+ $ip = $this->installer->getVar( 'IP' );
+ foreach ( $this->extensions as $ext) {
+ $path = str_replace( $ip, '', $extensions[$ext]['path'] );
+ $prefix = '';
+ if ( $path !== $extensions[$ext]['path'] ) {
+ $prefix = '$IP';
+ }
+ $path = $prefix . self::escapePhpString( $path );
+ $localSettings .= "require_once \"$path\";\n";
}
}
@@ -157,7 +164,7 @@ class LocalSettingsGenerator {
/**
* Write the generated LocalSettings to a file
*
- * @param $fileName String Full path to filename to write to
+ * @param string $fileName Full path to filename to write to
*/
public function writeFile( $fileName ) {
file_put_contents( $fileName, $this->getText() );
@@ -169,13 +176,13 @@ class LocalSettingsGenerator {
protected function buildMemcachedServerList() {
$servers = $this->values['_MemCachedServers'];
- if( !$servers ) {
+ if ( !$servers ) {
return 'array()';
} else {
$ret = 'array( ';
$servers = explode( ',', $servers );
- foreach( $servers as $srv ) {
+ foreach ( $servers as $srv ) {
$srv = trim( $srv );
$ret .= "'$srv', ";
}
@@ -188,33 +195,33 @@ class LocalSettingsGenerator {
* @return String
*/
protected function getDefaultText() {
- if( !$this->values['wgImageMagickConvertCommand'] ) {
+ if ( !$this->values['wgImageMagickConvertCommand'] ) {
$this->values['wgImageMagickConvertCommand'] = '/usr/bin/convert';
$magic = '#';
} else {
$magic = '';
}
- if( !$this->values['wgShellLocale'] ) {
+ if ( !$this->values['wgShellLocale'] ) {
$this->values['wgShellLocale'] = 'en_US.UTF-8';
$locale = '#';
} else {
$locale = '';
}
- //$rightsUrl = $this->values['wgRightsUrl'] ? '' : '#'; // TODO: Fixme, I'm unused!
+ //$rightsUrl = $this->values['wgRightsUrl'] ? '' : '#'; // @todo FIXME: I'm unused!
$hashedUploads = $this->safeMode ? '' : '#';
$metaNamespace = '';
- if( $this->values['wgMetaNamespace'] !== $this->values['wgSitename'] ) {
+ if ( $this->values['wgMetaNamespace'] !== $this->values['wgSitename'] ) {
$metaNamespace = "\$wgMetaNamespace = \"{$this->values['wgMetaNamespace']}\";\n";
}
$groupRights = '';
- if( $this->groupPermissions ) {
+ if ( $this->groupPermissions ) {
$groupRights .= "# The following permissions were set based on your choice in the installer\n";
- foreach( $this->groupPermissions as $group => $rightArr ) {
+ foreach ( $this->groupPermissions as $group => $rightArr ) {
$group = self::escapePhpString( $group );
- foreach( $rightArr as $right => $perm ) {
+ foreach ( $rightArr as $right => $perm ) {
$right = self::escapePhpString( $right );
$groupRights .= "\$wgGroupPermissions['$group']['$right'] = " .
wfBoolToStr( $perm ) . ";\n";
@@ -222,12 +229,18 @@ class LocalSettingsGenerator {
}
}
- switch( $this->values['wgMainCacheType'] ) {
+ $wgServerSetting = "";
+ if ( array_key_exists( 'wgServer', $this->values ) && $this->values['wgServer'] !== null ) {
+ $wgServerSetting = "\n## The protocol and server name to use in fully-qualified URLs\n";
+ $wgServerSetting .= "\$wgServer = \"{$this->values['wgServer']}\";\n";
+ }
+
+ switch ( $this->values['wgMainCacheType'] ) {
case 'anything':
case 'db':
case 'memcached':
case 'accel':
- $cacheType = 'CACHE_' . strtoupper( $this->values['wgMainCacheType']);
+ $cacheType = 'CACHE_' . strtoupper( $this->values['wgMainCacheType'] );
break;
case 'none':
default:
@@ -235,6 +248,7 @@ class LocalSettingsGenerator {
}
$mcservers = $this->buildMemcachedServerList();
+
return "<?php
# This file was automatically generated by the MediaWiki {$GLOBALS['wgVersion']}
# installer. If you make manual changes, please keep track in case you
@@ -255,59 +269,56 @@ if ( !defined( 'MEDIAWIKI' ) ) {
## Uncomment this to disable output compression
# \$wgDisableOutputCompression = true;
-\$wgSitename = \"{$this->values['wgSitename']}\";
+\$wgSitename = \"{$this->values['wgSitename']}\";
{$metaNamespace}
## 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
## (like /w/index.php/Page_title to /wiki/Page_title) please see:
## http://www.mediawiki.org/wiki/Manual:Short_URL
-\$wgScriptPath = \"{$this->values['wgScriptPath']}\";
-\$wgScriptExtension = \"{$this->values['wgScriptExtension']}\";
-
-## The protocol and server name to use in fully-qualified URLs
-\$wgServer = \"{$this->values['wgServer']}\";
-
+\$wgScriptPath = \"{$this->values['wgScriptPath']}\";
+\$wgScriptExtension = \"{$this->values['wgScriptExtension']}\";
+${wgServerSetting}
## The relative URL path to the skins directory
-\$wgStylePath = \"\$wgScriptPath/skins\";
+\$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\";
+\$wgLogo = \"{$this->values['wgLogo']}\";
## UPO means: this is also a user preference option
-\$wgEnableEmail = {$this->values['wgEnableEmail']};
-\$wgEnableUserEmail = {$this->values['wgEnableUserEmail']}; # UPO
+\$wgEnableEmail = {$this->values['wgEnableEmail']};
+\$wgEnableUserEmail = {$this->values['wgEnableUserEmail']}; # UPO
\$wgEmergencyContact = \"{$this->values['wgEmergencyContact']}\";
-\$wgPasswordSender = \"{$this->values['wgPasswordSender']}\";
+\$wgPasswordSender = \"{$this->values['wgPasswordSender']}\";
-\$wgEnotifUserTalk = {$this->values['wgEnotifUserTalk']}; # UPO
-\$wgEnotifWatchlist = {$this->values['wgEnotifWatchlist']}; # UPO
+\$wgEnotifUserTalk = {$this->values['wgEnotifUserTalk']}; # UPO
+\$wgEnotifWatchlist = {$this->values['wgEnotifWatchlist']}; # UPO
\$wgEmailAuthentication = {$this->values['wgEmailAuthentication']};
## Database settings
-\$wgDBtype = \"{$this->values['wgDBtype']}\";
-\$wgDBserver = \"{$this->values['wgDBserver']}\";
-\$wgDBname = \"{$this->values['wgDBname']}\";
-\$wgDBuser = \"{$this->values['wgDBuser']}\";
-\$wgDBpassword = \"{$this->values['wgDBpassword']}\";
+\$wgDBtype = \"{$this->values['wgDBtype']}\";
+\$wgDBserver = \"{$this->values['wgDBserver']}\";
+\$wgDBname = \"{$this->values['wgDBname']}\";
+\$wgDBuser = \"{$this->values['wgDBuser']}\";
+\$wgDBpassword = \"{$this->values['wgDBpassword']}\";
{$this->dbSettings}
## Shared memory settings
-\$wgMainCacheType = $cacheType;
+\$wgMainCacheType = $cacheType;
\$wgMemCachedServers = $mcservers;
## To enable image uploads, make sure the 'images' directory
## is writable, then set this to true:
-\$wgEnableUploads = {$this->values['wgEnableUploads']};
+\$wgEnableUploads = {$this->values['wgEnableUploads']};
{$magic}\$wgUseImageMagick = true;
{$magic}\$wgImageMagickConvertCommand = \"{$this->values['wgImageMagickConvertCommand']}\";
# InstantCommons allows wiki to use images from http://commons.wikimedia.org
-\$wgUseInstantCommons = {$this->values['wgUseInstantCommons']};
+\$wgUseInstantCommons = {$this->values['wgUseInstantCommons']};
## If you use ImageMagick (or any other shell command) on a
## Linux server, this will need to be set to the name of an
@@ -335,27 +346,20 @@ if ( !defined( 'MEDIAWIKI' ) ) {
\$wgUpgradeKey = \"{$this->values['wgUpgradeKey']}\";
## Default skin: you can change the default skin. Use the internal symbolic
-## names, ie 'standard', 'nostalgia', 'cologneblue', 'monobook', 'vector':
+## names, ie 'cologneblue', 'monobook', 'vector':
\$wgDefaultSkin = \"{$this->values['wgDefaultSkin']}\";
## 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.
\$wgRightsPage = \"\"; # Set to the title of a wiki page that describes your license/copyright
-\$wgRightsUrl = \"{$this->values['wgRightsUrl']}\";
+\$wgRightsUrl = \"{$this->values['wgRightsUrl']}\";
\$wgRightsText = \"{$this->values['wgRightsText']}\";
\$wgRightsIcon = \"{$this->values['wgRightsIcon']}\";
# Path to the GNU diff3 utility. Used for conflict resolution.
\$wgDiff3 = \"{$this->values['wgDiff3']}\";
-# Query string length limit for ResourceLoader. You should only set this if
-# your web server has a query string length limit (then set it to that limit),
-# or if you have suhosin.get.max_value_length set in php.ini (then set it to
-# that value)
-\$wgResourceLoaderMaxQueryLength = {$this->values['wgResourceLoaderMaxQueryLength']};
-
{$groupRights}";
}
-
}
diff --git a/includes/installer/MysqlInstaller.php b/includes/installer/MysqlInstaller.php
index f66f15f2..5f76972c 100644
--- a/includes/installer/MysqlInstaller.php
+++ b/includes/installer/MysqlInstaller.php
@@ -64,15 +64,11 @@ class MysqlInstaller extends DatabaseInstaller {
return 'mysql';
}
- public function __construct( $parent ) {
- parent::__construct( $parent );
- }
-
/**
* @return Bool
*/
public function isCompiled() {
- return self::checkExtension( 'mysql' );
+ return self::checkExtension( 'mysql' ) || self::checkExtension( 'mysqli' );
}
/**
@@ -86,12 +82,18 @@ class MysqlInstaller extends DatabaseInstaller {
* @return string
*/
public function getConnectForm() {
- return
- $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
+ return $this->getTextBox(
+ 'wgDBserver',
+ 'config-db-host',
+ array(),
+ $this->parent->getHelpBox( 'config-db-host-help' )
+ ) .
Html::openElement( 'fieldset' ) .
Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
- $this->getTextBox( 'wgDBname', 'config-db-name', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-name-help' ) ) .
- $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
+ $this->getTextBox( 'wgDBname', 'config-db-name', array( 'dir' => 'ltr' ),
+ $this->parent->getHelpBox( 'config-db-name-help' ) ) .
+ $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ),
+ $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
Html::closeElement( 'fieldset' ) .
$this->getInstallUserBox();
}
@@ -107,7 +109,7 @@ class MysqlInstaller extends DatabaseInstaller {
}
if ( !strlen( $newValues['wgDBname'] ) ) {
$status->fatal( 'config-missing-db-name' );
- } elseif ( !preg_match( '/^[a-z0-9_-]+$/i', $newValues['wgDBname'] ) ) {
+ } elseif ( !preg_match( '/^[a-z0-9+_-]+$/i', $newValues['wgDBname'] ) ) {
$status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
}
if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
@@ -148,19 +150,18 @@ class MysqlInstaller extends DatabaseInstaller {
public function openConnection() {
$status = Status::newGood();
try {
- $db = new DatabaseMysql(
- $this->getVar( 'wgDBserver' ),
- $this->getVar( '_InstallUser' ),
- $this->getVar( '_InstallPassword' ),
- false,
- false,
- 0,
- $this->getVar( 'wgDBprefix' )
- );
+ $db = DatabaseBase::factory( 'mysql', array(
+ 'host' => $this->getVar( 'wgDBserver' ),
+ 'user' => $this->getVar( '_InstallUser' ),
+ 'password' => $this->getVar( '_InstallPassword' ),
+ 'dbname' => false,
+ 'flags' => 0,
+ 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ) );
$status->value = $db;
} catch ( DBConnectionError $e ) {
$status->fatal( 'config-connection-error', $e->getMessage() );
}
+
return $status;
}
@@ -170,6 +171,7 @@ class MysqlInstaller extends DatabaseInstaller {
$status = $this->getConnection();
if ( !$status->isOK() ) {
$this->parent->showStatusError( $status );
+
return;
}
/**
@@ -243,6 +245,7 @@ class MysqlInstaller extends DatabaseInstaller {
}
}
$engines = array_intersect( $this->supportedEngines, $engines );
+
return $engines;
}
@@ -327,6 +330,7 @@ class MysqlInstaller extends DatabaseInstaller {
// Can't grant everything
return false;
}
+
return true;
}
@@ -350,17 +354,24 @@ class MysqlInstaller extends DatabaseInstaller {
$s .= Xml::openElement( 'div', array(
'id' => 'dbMyisamWarning'
- ));
- $s .= $this->parent->getWarningBox( wfMessage( 'config-mysql-myisam-dep' )->text() );
+ ) );
+ $myisamWarning = 'config-mysql-myisam-dep';
+ if ( count( $engines ) === 1 ) {
+ $myisamWarning = 'config-mysql-only-myisam-dep';
+ }
+ $s .= $this->parent->getWarningBox( wfMessage( $myisamWarning )->text() );
$s .= Xml::closeElement( 'div' );
- if( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) {
+ if ( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) {
$s .= Xml::openElement( 'script', array( 'type' => 'text/javascript' ) );
$s .= '$(\'#dbMyisamWarning\').hide();';
$s .= Xml::closeElement( 'script' );
}
if ( count( $engines ) >= 2 ) {
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-mysql-innodb, config-mysql-myisam
$s .= $this->getRadioSet( array(
'var' => '_MysqlEngine',
'label' => 'config-mysql-engine',
@@ -369,11 +380,14 @@ class MysqlInstaller extends DatabaseInstaller {
'itemAttribs' => array(
'MyISAM' => array(
'class' => 'showHideRadio',
- 'rel' => 'dbMyisamWarning'),
+ 'rel' => 'dbMyisamWarning'
+ ),
'InnoDB' => array(
'class' => 'hideShowRadio',
- 'rel' => 'dbMyisamWarning')
- )));
+ 'rel' => 'dbMyisamWarning'
+ )
+ )
+ ) );
$s .= $this->parent->getHelpBox( 'config-mysql-engine-help' );
}
@@ -385,12 +399,15 @@ class MysqlInstaller extends DatabaseInstaller {
// Do charset selector
if ( count( $charsets ) >= 2 ) {
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-mysql-binary, config-mysql-utf8
$s .= $this->getRadioSet( array(
'var' => '_MysqlCharset',
'label' => 'config-mysql-charset',
'itemLabelPrefix' => 'config-mysql-',
'values' => $charsets
- ));
+ ) );
$s .= $this->parent->getHelpBox( 'config-mysql-charset-help' );
}
@@ -419,15 +436,13 @@ class MysqlInstaller extends DatabaseInstaller {
if ( !$create ) {
// Test the web account
try {
- new DatabaseMysql(
- $this->getVar( 'wgDBserver' ),
- $this->getVar( 'wgDBuser' ),
- $this->getVar( 'wgDBpassword' ),
- false,
- false,
- 0,
- $this->getVar( 'wgDBprefix' )
- );
+ $db = DatabaseBase::factory( 'mysql', array(
+ 'host' => $this->getVar( 'wgDBserver' ),
+ 'user' => $this->getVar( 'wgDBuser' ),
+ 'password' => $this->getVar( 'wgDBpassword' ),
+ 'dbname' => false,
+ 'flags' => 0,
+ 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ) );
} catch ( DBConnectionError $e ) {
return Status::newFatal( 'config-connection-error', $e->getMessage() );
}
@@ -443,6 +458,7 @@ class MysqlInstaller extends DatabaseInstaller {
if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
$this->setVar( '_MysqlCharset', reset( $charsets ) );
}
+
return Status::newGood();
}
@@ -465,11 +481,15 @@ class MysqlInstaller extends DatabaseInstaller {
}
$conn = $status->value;
$dbName = $this->getVar( 'wgDBname' );
- if( !$conn->selectDB( $dbName ) ) {
- $conn->query( "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ), __METHOD__ );
+ if ( !$conn->selectDB( $dbName ) ) {
+ $conn->query(
+ "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8",
+ __METHOD__
+ );
$conn->selectDB( $dbName );
}
$this->setupSchemaVars();
+
return $status;
}
@@ -478,7 +498,7 @@ class MysqlInstaller extends DatabaseInstaller {
*/
public function setupUser() {
$dbUser = $this->getVar( 'wgDBuser' );
- if( $dbUser == $this->getVar( '_InstallUser' ) ) {
+ if ( $dbUser == $this->getVar( '_InstallUser' ) ) {
return Status::newGood();
}
$status = $this->getConnection();
@@ -496,15 +516,13 @@ class MysqlInstaller extends DatabaseInstaller {
if ( $this->getVar( '_CreateDBAccount' ) ) {
// Before we blindly try to create a user that already has access,
try { // first attempt to connect to the database
- new DatabaseMysql(
- $server,
- $dbUser,
- $password,
- false,
- false,
- 0,
- $this->getVar( 'wgDBprefix' )
- );
+ $db = DatabaseBase::factory( 'mysql', array(
+ 'host' => $server,
+ 'user' => $dbUser,
+ 'password' => $password,
+ 'dbname' => false,
+ 'flags' => 0,
+ 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ) );
$grantableNames[] = $this->buildFullUserName( $dbUser, $server );
$tryToCreate = false;
} catch ( DBConnectionError $e ) {
@@ -515,8 +533,9 @@ class MysqlInstaller extends DatabaseInstaller {
$tryToCreate = false;
}
- if( $tryToCreate ) {
- $createHostList = array($server,
+ if ( $tryToCreate ) {
+ $createHostList = array(
+ $server,
'localhost',
'localhost.localdomain',
'%'
@@ -525,16 +544,16 @@ class MysqlInstaller extends DatabaseInstaller {
$createHostList = array_unique( $createHostList );
$escPass = $this->db->addQuotes( $password );
- foreach( $createHostList as $host ) {
+ foreach ( $createHostList as $host ) {
$fullName = $this->buildFullUserName( $dbUser, $host );
- if( !$this->userDefinitelyExists( $dbUser, $host ) ) {
- try{
+ if ( !$this->userDefinitelyExists( $dbUser, $host ) ) {
+ try {
$this->db->begin( __METHOD__ );
$this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
$this->db->commit( __METHOD__ );
$grantableNames[] = $fullName;
- } catch( DBQueryError $dqe ) {
- if( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
+ } catch ( DBQueryError $dqe ) {
+ if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
// User (probably) already exists
$this->db->rollback( __METHOD__ );
$status->warning( 'config-install-user-alreadyexists', $dbUser );
@@ -557,12 +576,12 @@ class MysqlInstaller extends DatabaseInstaller {
// Try to grant to all the users we know exist or we were able to create
$dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
- foreach( $grantableNames as $name ) {
+ foreach ( $grantableNames as $name ) {
try {
$this->db->begin( __METHOD__ );
$this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
$this->db->commit( __METHOD__ );
- } catch( DBQueryError $dqe ) {
+ } catch ( DBQueryError $dqe ) {
$this->db->rollback( __METHOD__ );
$status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
}
@@ -573,8 +592,8 @@ class MysqlInstaller extends DatabaseInstaller {
/**
* Return a formal 'User'@'Host' username for use in queries
- * @param $name String Username, quotes will be added
- * @param $host String Hostname, quotes will be added
+ * @param string $name Username, quotes will be added
+ * @param string $host Hostname, quotes will be added
* @return String
*/
private function buildFullUserName( $name, $host ) {
@@ -584,19 +603,19 @@ class MysqlInstaller extends DatabaseInstaller {
/**
* Try to see if the user account exists. Our "superuser" may not have
* access to mysql.user, so false means "no" or "maybe"
- * @param $host String Hostname to check
- * @param $user String Username to check
+ * @param string $host Hostname to check
+ * @param string $user Username to check
* @return boolean
*/
private function userDefinitelyExists( $host, $user ) {
try {
$res = $this->db->selectRow( 'mysql.user', array( 'Host', 'User' ),
array( 'Host' => $host, 'User' => $user ), __METHOD__ );
+
return (bool)$res;
- } catch( DBQueryError $dqe ) {
+ } catch ( DBQueryError $dqe ) {
return false;
}
-
}
/**
@@ -613,6 +632,7 @@ class MysqlInstaller extends DatabaseInstaller {
if ( $this->getVar( '_MysqlCharset' ) !== null ) {
$options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
}
+
return implode( ', ', $options );
}
@@ -634,12 +654,12 @@ class MysqlInstaller extends DatabaseInstaller {
$dbmysql5 = wfBoolToStr( $this->getVar( 'wgDBmysql5', true ) );
$prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
$tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
- return
-"# MySQL specific settings
-\$wgDBprefix = \"{$prefix}\";
+
+ return "# MySQL specific settings
+\$wgDBprefix = \"{$prefix}\";
# MySQL table options to use during installation or update
-\$wgDBTableOptions = \"{$tblOpts}\";
+\$wgDBTableOptions = \"{$tblOpts}\";
# Experimental charset support for MySQL 5.0.
\$wgDBmysql5 = {$dbmysql5};";
diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php
index 49dff805..f348ecd4 100644
--- a/includes/installer/MysqlUpdater.php
+++ b/includes/installer/MysqlUpdater.php
@@ -32,187 +32,213 @@ class MysqlUpdater extends DatabaseUpdater {
protected function getCoreUpdateList() {
return array(
// 1.2
- array( 'addField', 'ipblocks', 'ipb_id', 'patch-ipblocks.sql' ),
- array( 'addField', 'ipblocks', 'ipb_expiry', 'patch-ipb_expiry.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_id', 'patch-ipblocks.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_expiry', 'patch-ipb_expiry.sql' ),
array( 'doInterwikiUpdate' ),
array( 'doIndexUpdate' ),
- array( 'addTable', 'hitcounter', 'patch-hitcounter.sql' ),
- array( 'addField', 'recentchanges', 'rc_type', 'patch-rc_type.sql' ),
+ array( 'addTable', 'hitcounter', 'patch-hitcounter.sql' ),
+ array( 'addField', 'recentchanges', 'rc_type', 'patch-rc_type.sql' ),
// 1.3
- array( 'addField', 'user', 'user_real_name', 'patch-user-realname.sql' ),
- array( 'addTable', 'querycache', 'patch-querycache.sql' ),
- array( 'addTable', 'objectcache', 'patch-objectcache.sql' ),
- array( 'addTable', 'categorylinks', 'patch-categorylinks.sql' ),
+ array( 'addField', 'user', 'user_real_name', 'patch-user-realname.sql' ),
+ array( 'addTable', 'querycache', 'patch-querycache.sql' ),
+ array( 'addTable', 'objectcache', 'patch-objectcache.sql' ),
+ array( 'addTable', 'categorylinks', 'patch-categorylinks.sql' ),
array( 'doOldLinksUpdate' ),
array( 'doFixAncientImagelinks' ),
- array( 'addField', 'recentchanges', 'rc_ip', 'patch-rc_ip.sql' ),
+ array( 'addField', 'recentchanges', 'rc_ip', 'patch-rc_ip.sql' ),
// 1.4
- array( 'addIndex', 'image', 'PRIMARY', 'patch-image_name_primary.sql' ),
- array( 'addField', 'recentchanges', 'rc_id', 'patch-rc_id.sql' ),
- array( 'addField', 'recentchanges', 'rc_patrolled', 'patch-rc-patrol.sql' ),
- array( 'addTable', 'logging', 'patch-logging.sql' ),
- array( 'addField', 'user', 'user_token', 'patch-user_token.sql' ),
- array( 'addField', 'watchlist', 'wl_notificationtimestamp', 'patch-email-notification.sql' ),
+ array( 'addIndex', 'image', 'PRIMARY', 'patch-image_name_primary.sql' ),
+ array( 'addField', 'recentchanges', 'rc_id', 'patch-rc_id.sql' ),
+ array( 'addField', 'recentchanges', 'rc_patrolled', 'patch-rc-patrol.sql' ),
+ array( 'addTable', 'logging', 'patch-logging.sql' ),
+ array( 'addField', 'user', 'user_token', 'patch-user_token.sql' ),
+ array( 'addField', 'watchlist', 'wl_notificationtimestamp', 'patch-email-notification.sql' ),
array( 'doWatchlistUpdate' ),
- array( 'dropField', 'user', 'user_emailauthenticationtimestamp', 'patch-email-authentication.sql' ),
+ array( 'dropField', 'user', 'user_emailauthenticationtimestamp',
+ 'patch-email-authentication.sql' ),
// 1.5
array( 'doSchemaRestructuring' ),
- array( 'addField', 'logging', 'log_params', 'patch-log_params.sql' ),
- array( 'checkBin', 'logging', 'log_title', 'patch-logging-title.sql', ),
- array( 'addField', 'archive', 'ar_rev_id', 'patch-archive-rev_id.sql' ),
- array( 'addField', 'page', 'page_len', 'patch-page_len.sql' ),
- array( 'dropField', 'revision', 'inverse_timestamp', 'patch-inverse_timestamp.sql' ),
- array( 'addField', 'revision', 'rev_text_id', 'patch-rev_text_id.sql' ),
- array( 'addField', 'revision', 'rev_deleted', 'patch-rev_deleted.sql' ),
- array( 'addField', 'image', 'img_width', 'patch-img_width.sql' ),
- array( 'addField', 'image', 'img_metadata', 'patch-img_metadata.sql' ),
- array( 'addField', 'user', 'user_email_token', 'patch-user_email_token.sql' ),
- array( 'addField', 'archive', 'ar_text_id', 'patch-archive-text_id.sql' ),
+ array( 'addField', 'logging', 'log_params', 'patch-log_params.sql' ),
+ array( 'checkBin', 'logging', 'log_title', 'patch-logging-title.sql', ),
+ array( 'addField', 'archive', 'ar_rev_id', 'patch-archive-rev_id.sql' ),
+ array( 'addField', 'page', 'page_len', 'patch-page_len.sql' ),
+ array( 'dropField', 'revision', 'inverse_timestamp', 'patch-inverse_timestamp.sql' ),
+ array( 'addField', 'revision', 'rev_text_id', 'patch-rev_text_id.sql' ),
+ array( 'addField', 'revision', 'rev_deleted', 'patch-rev_deleted.sql' ),
+ array( 'addField', 'image', 'img_width', 'patch-img_width.sql' ),
+ array( 'addField', 'image', 'img_metadata', 'patch-img_metadata.sql' ),
+ array( 'addField', 'user', 'user_email_token', 'patch-user_email_token.sql' ),
+ array( 'addField', 'archive', 'ar_text_id', 'patch-archive-text_id.sql' ),
array( 'doNamespaceSize' ),
- array( 'addField', 'image', 'img_media_type', 'patch-img_media_type.sql' ),
+ array( 'addField', 'image', 'img_media_type', 'patch-img_media_type.sql' ),
array( 'doPagelinksUpdate' ),
- array( 'dropField', 'image', 'img_type', 'patch-drop_img_type.sql' ),
+ array( 'dropField', 'image', 'img_type', 'patch-drop_img_type.sql' ),
array( 'doUserUniqueUpdate' ),
array( 'doUserGroupsUpdate' ),
- array( 'addField', 'site_stats', 'ss_total_pages', 'patch-ss_total_articles.sql' ),
- array( 'addTable', 'user_newtalk', 'patch-usernewtalk2.sql' ),
- array( 'addTable', 'transcache', 'patch-transcache.sql' ),
- array( 'addField', 'interwiki', 'iw_trans', 'patch-interwiki-trans.sql' ),
+ array( 'addField', 'site_stats', 'ss_total_pages', 'patch-ss_total_articles.sql' ),
+ array( 'addTable', 'user_newtalk', 'patch-usernewtalk2.sql' ),
+ array( 'addTable', 'transcache', 'patch-transcache.sql' ),
+ array( 'addField', 'interwiki', 'iw_trans', 'patch-interwiki-trans.sql' ),
// 1.6
array( 'doWatchlistNull' ),
- array( 'addIndex', 'logging', 'times', 'patch-logging-times-index.sql' ),
- array( 'addField', 'ipblocks', 'ipb_range_start', 'patch-ipb_range_start.sql' ),
+ array( 'addIndex', 'logging', 'times', 'patch-logging-times-index.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_range_start', 'patch-ipb_range_start.sql' ),
array( 'doPageRandomUpdate' ),
- array( 'addField', 'user', 'user_registration', 'patch-user_registration.sql' ),
+ array( 'addField', 'user', 'user_registration', 'patch-user_registration.sql' ),
array( 'doTemplatelinksUpdate' ),
- array( 'addTable', 'externallinks', 'patch-externallinks.sql' ),
- array( 'addTable', 'job', 'patch-job.sql' ),
- array( 'addField', 'site_stats', 'ss_images', 'patch-ss_images.sql' ),
- array( 'addTable', 'langlinks', 'patch-langlinks.sql' ),
- array( 'addTable', 'querycache_info', 'patch-querycacheinfo.sql' ),
- array( 'addTable', 'filearchive', 'patch-filearchive.sql' ),
- array( 'addField', 'ipblocks', 'ipb_anon_only', 'patch-ipb_anon_only.sql' ),
- array( 'addIndex', 'recentchanges', 'rc_ns_usertext', 'patch-recentchanges-utindex.sql' ),
- array( 'addIndex', 'recentchanges', 'rc_user_text', 'patch-rc_user_text-index.sql' ),
+ array( 'addTable', 'externallinks', 'patch-externallinks.sql' ),
+ array( 'addTable', 'job', 'patch-job.sql' ),
+ array( 'addField', 'site_stats', 'ss_images', 'patch-ss_images.sql' ),
+ array( 'addTable', 'langlinks', 'patch-langlinks.sql' ),
+ array( 'addTable', 'querycache_info', 'patch-querycacheinfo.sql' ),
+ array( 'addTable', 'filearchive', 'patch-filearchive.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_anon_only', 'patch-ipb_anon_only.sql' ),
+ array( 'addIndex', 'recentchanges', 'rc_ns_usertext', 'patch-recentchanges-utindex.sql' ),
+ array( 'addIndex', 'recentchanges', 'rc_user_text', 'patch-rc_user_text-index.sql' ),
// 1.9
- array( 'addField', 'user', 'user_newpass_time', 'patch-user_newpass_time.sql' ),
- array( 'addTable', 'redirect', 'patch-redirect.sql' ),
- array( 'addTable', 'querycachetwo', 'patch-querycachetwo.sql' ),
- array( 'addField', 'ipblocks', 'ipb_enable_autoblock', 'patch-ipb_optional_autoblock.sql' ),
+ array( 'addField', 'user', 'user_newpass_time', 'patch-user_newpass_time.sql' ),
+ array( 'addTable', 'redirect', 'patch-redirect.sql' ),
+ array( 'addTable', 'querycachetwo', 'patch-querycachetwo.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_enable_autoblock', 'patch-ipb_optional_autoblock.sql' ),
array( 'doBacklinkingIndicesUpdate' ),
- array( 'addField', 'recentchanges', 'rc_old_len', 'patch-rc_len.sql' ),
- array( 'addField', 'user', 'user_editcount', 'patch-user_editcount.sql' ),
+ array( 'addField', 'recentchanges', 'rc_old_len', 'patch-rc_len.sql' ),
+ array( 'addField', 'user', 'user_editcount', 'patch-user_editcount.sql' ),
// 1.10
array( 'doRestrictionsUpdate' ),
- array( 'addField', 'logging', 'log_id', 'patch-log_id.sql' ),
- array( 'addField', 'revision', 'rev_parent_id', 'patch-rev_parent_id.sql' ),
- array( 'addField', 'page_restrictions', 'pr_id', 'patch-page_restrictions_sortkey.sql' ),
- array( 'addField', 'revision', 'rev_len', 'patch-rev_len.sql' ),
- array( 'addField', 'recentchanges', 'rc_deleted', 'patch-rc_deleted.sql' ),
- array( 'addField', 'logging', 'log_deleted', 'patch-log_deleted.sql' ),
- array( 'addField', 'archive', 'ar_deleted', 'patch-ar_deleted.sql' ),
- array( 'addField', 'ipblocks', 'ipb_deleted', 'patch-ipb_deleted.sql' ),
- array( 'addField', 'filearchive', 'fa_deleted', 'patch-fa_deleted.sql' ),
- array( 'addField', 'archive', 'ar_len', 'patch-ar_len.sql' ),
+ array( 'addField', 'logging', 'log_id', 'patch-log_id.sql' ),
+ array( 'addField', 'revision', 'rev_parent_id', 'patch-rev_parent_id.sql' ),
+ array( 'addField', 'page_restrictions', 'pr_id', 'patch-page_restrictions_sortkey.sql' ),
+ array( 'addField', 'revision', 'rev_len', 'patch-rev_len.sql' ),
+ array( 'addField', 'recentchanges', 'rc_deleted', 'patch-rc_deleted.sql' ),
+ array( 'addField', 'logging', 'log_deleted', 'patch-log_deleted.sql' ),
+ array( 'addField', 'archive', 'ar_deleted', 'patch-ar_deleted.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_deleted', 'patch-ipb_deleted.sql' ),
+ array( 'addField', 'filearchive', 'fa_deleted', 'patch-fa_deleted.sql' ),
+ array( 'addField', 'archive', 'ar_len', 'patch-ar_len.sql' ),
// 1.11
- array( 'addField', 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ),
array( 'doCategorylinksIndicesUpdate' ),
- array( 'addField', 'oldimage', 'oi_metadata', 'patch-oi_metadata.sql' ),
- array( 'addIndex', 'archive', 'usertext_timestamp', 'patch-archive-user-index.sql' ),
- array( 'addIndex', 'image', 'img_usertext_timestamp', 'patch-image-user-index.sql' ),
- array( 'addIndex', 'oldimage', 'oi_usertext_timestamp', 'patch-oldimage-user-index.sql' ),
- array( 'addField', 'archive', 'ar_page_id', 'patch-archive-page_id.sql' ),
- array( 'addField', 'image', 'img_sha1', 'patch-img_sha1.sql' ),
+ array( 'addField', 'oldimage', 'oi_metadata', 'patch-oi_metadata.sql' ),
+ array( 'addIndex', 'archive', 'usertext_timestamp', 'patch-archive-user-index.sql' ),
+ array( 'addIndex', 'image', 'img_usertext_timestamp', 'patch-image-user-index.sql' ),
+ array( 'addIndex', 'oldimage', 'oi_usertext_timestamp', 'patch-oldimage-user-index.sql' ),
+ array( 'addField', 'archive', 'ar_page_id', 'patch-archive-page_id.sql' ),
+ array( 'addField', 'image', 'img_sha1', 'patch-img_sha1.sql' ),
// 1.12
- array( 'addTable', 'protected_titles', 'patch-protected_titles.sql' ),
+ array( 'addTable', 'protected_titles', 'patch-protected_titles.sql' ),
// 1.13
- array( 'addField', 'ipblocks', 'ipb_by_text', 'patch-ipb_by_text.sql' ),
- array( 'addTable', 'page_props', 'patch-page_props.sql' ),
- array( 'addTable', 'updatelog', 'patch-updatelog.sql' ),
- array( 'addTable', 'category', 'patch-category.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_by_text', 'patch-ipb_by_text.sql' ),
+ array( 'addTable', 'page_props', 'patch-page_props.sql' ),
+ array( 'addTable', 'updatelog', 'patch-updatelog.sql' ),
+ array( 'addTable', 'category', 'patch-category.sql' ),
array( 'doCategoryPopulation' ),
- array( 'addField', 'archive', 'ar_parent_id', 'patch-ar_parent_id.sql' ),
- array( 'addField', 'user_newtalk', 'user_last_timestamp', 'patch-user_last_timestamp.sql' ),
+ array( 'addField', 'archive', 'ar_parent_id', 'patch-ar_parent_id.sql' ),
+ array( 'addField', 'user_newtalk', 'user_last_timestamp', 'patch-user_last_timestamp.sql' ),
array( 'doPopulateParentId' ),
- array( 'checkBin', 'protected_titles', 'pt_title', 'patch-pt_title-encoding.sql', ),
+ array( 'checkBin', 'protected_titles', 'pt_title', 'patch-pt_title-encoding.sql', ),
array( 'doMaybeProfilingMemoryUpdate' ),
array( 'doFilearchiveIndicesUpdate' ),
// 1.14
- array( 'addField', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ),
+ array( 'addField', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ),
array( 'doActiveUsersInit' ),
- array( 'addField', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ),
// 1.15
array( 'doUniquePlTlIl' ),
- array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
- array( 'addTable', 'tag_summary', 'patch-change_tag.sql' ),
- array( 'addTable', 'valid_tag', 'patch-change_tag.sql' ),
+ array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
+ array( 'addTable', 'tag_summary', 'patch-tag_summary.sql' ),
+ array( 'addTable', 'valid_tag', 'patch-valid_tag.sql' ),
// 1.16
- array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
- array( 'addTable', 'log_search', 'patch-log_search.sql' ),
- array( 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ),
- array( 'doLogUsertextPopulation' ), # listed separately from the previous update because 1.16 was released without this update
+ array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
+ array( 'addTable', 'log_search', 'patch-log_search.sql' ),
+ array( 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ),
+ # listed separately from the previous update because 1.16 was released without this update
+ array( 'doLogUsertextPopulation' ),
array( 'doLogSearchPopulation' ),
- array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
- array( 'addTable', 'external_user', 'patch-external_user.sql' ),
- array( 'addIndex', 'log_search', 'ls_field_val', 'patch-log_search-rename-index.sql' ),
- array( 'addIndex', 'change_tag', 'change_tag_rc_tag', 'patch-change_tag-indexes.sql' ),
- array( 'addField', 'redirect', 'rd_interwiki', 'patch-rd_interwiki.sql' ),
+ array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
+ array( 'addIndex', 'log_search', 'ls_field_val', 'patch-log_search-rename-index.sql' ),
+ array( 'addIndex', 'change_tag', 'change_tag_rc_tag', 'patch-change_tag-indexes.sql' ),
+ array( 'addField', 'redirect', 'rd_interwiki', 'patch-rd_interwiki.sql' ),
array( 'doUpdateTranscacheField' ),
- array( 'renameEuWikiId' ),
array( 'doUpdateMimeMinorField' ),
// 1.17
- array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
- array( 'addIndex', 'iwlinks', 'iwl_prefix_title_from', 'patch-rename-iwl_prefix.sql' ),
- array( 'addField', 'updatelog', 'ul_value', 'patch-ul_value.sql' ),
- array( 'addField', 'interwiki', 'iw_api', 'patch-iw_api_and_wikiid.sql' ),
- array( 'dropIndex', 'iwlinks', 'iwl_prefix', 'patch-kill-iwl_prefix.sql' ),
- array( 'dropIndex', 'iwlinks', 'iwl_prefix_from_title', 'patch-kill-iwl_pft.sql' ),
- array( 'addField', 'categorylinks', 'cl_collation', 'patch-categorylinks-better-collation.sql' ),
+ array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
+ array( 'addIndex', 'iwlinks', 'iwl_prefix_title_from', 'patch-rename-iwl_prefix.sql' ),
+ array( 'addField', 'updatelog', 'ul_value', 'patch-ul_value.sql' ),
+ array( 'addField', 'interwiki', 'iw_api', 'patch-iw_api_and_wikiid.sql' ),
+ array( 'dropIndex', 'iwlinks', 'iwl_prefix', 'patch-kill-iwl_prefix.sql' ),
+ array( 'addField', 'categorylinks', 'cl_collation', 'patch-categorylinks-better-collation.sql' ),
array( 'doClFieldsUpdate' ),
array( 'doCollationUpdate' ),
- array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
- array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
- array( 'dropIndex', 'archive', 'ar_page_revid', 'patch-archive_kill_ar_page_revid.sql' ),
- array( 'addIndex', 'archive', 'ar_revid', 'patch-archive_ar_revid.sql' ),
+ array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
+ array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
+ array( 'dropIndex', 'archive', 'ar_page_revid', 'patch-archive_kill_ar_page_revid.sql' ),
+ array( 'addIndex', 'archive', 'ar_revid', 'patch-archive_ar_revid.sql' ),
array( 'doLangLinksLengthUpdate' ),
// 1.18
array( 'doUserNewTalkTimestampNotNull' ),
- array( 'addIndex', 'user', 'user_email', 'patch-user_email_index.sql' ),
+ array( 'addIndex', 'user', 'user_email', 'patch-user_email_index.sql' ),
array( 'modifyField', 'user_properties', 'up_property', 'patch-up_property.sql' ),
- array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
- array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql'),
+ array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
+ array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql' ),
// 1.19
- array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql'),
+ array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql' ),
+ array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ),
array( 'doMigrateUserOptions' ),
- array( 'dropField', 'user', 'user_options', 'patch-drop-user_options.sql' ),
- array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ),
- array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ),
- array( 'addIndex', 'page', 'page_redirect_namespace_len', 'patch-page_redirect_namespace_len.sql' ),
- array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
- array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ),
- array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ),
- array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
+ array( 'dropField', 'user', 'user_options', 'patch-drop-user_options.sql' ),
+ array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ),
+ array( 'addIndex', 'page', 'page_redirect_namespace_len',
+ 'patch-page_redirect_namespace_len.sql' ),
+ array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ),
+ array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ),
// 1.20
array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
- array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
- array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
- array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
+ array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
+ array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
+
+ // 1.21
+ array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ),
+ array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ),
+ array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ),
+ array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ),
+ array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ),
+ array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ),
+ array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
+ array( 'addTable', 'sites', 'patch-sites.sql' ),
+ array( 'addField', 'filearchive', 'fa_sha1', 'patch-fa_sha1.sql' ),
+ array( 'addField', 'job', 'job_token', 'patch-job_token.sql' ),
+ array( 'addField', 'job', 'job_attempts', 'patch-job_attempts.sql' ),
+ array( 'doEnableProfiling' ),
+ array( 'addField', 'uploadstash', 'us_props', 'patch-uploadstash-us_props.sql' ),
+ array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase-255.sql' ),
+ array( 'modifyField', 'user_former_groups', 'ufg_group',
+ 'patch-ufg_group-length-increase-255.sql' ),
+ array( 'addIndex', 'page_props', 'pp_propname_page',
+ 'patch-page_props-propname-page-index.sql' ),
+ array( 'addIndex', 'image', 'img_media_mime', 'patch-img_media_mime-index.sql' ),
+
+ // 1.22
+ array( 'doIwlinksIndexNonUnique' ),
+ array( 'addIndex', 'iwlinks', 'iwl_prefix_from_title',
+ 'patch-iwlinks-from-title-index.sql' ),
+ array( 'addField', 'archive', 'ar_id', 'patch-archive-ar_id.sql' ),
+ array( 'addField', 'externallinks', 'el_id', 'patch-externallinks-el_id.sql' ),
);
}
@@ -220,16 +246,17 @@ class MysqlUpdater extends DatabaseUpdater {
* 1.4 betas were missing the 'binary' marker from logging.log_title,
* which causes a collation mismatch error on joins in MySQL 4.1.
*
- * @param $table String: table name
- * @param $field String: field name to check
- * @param $patchFile String: path to the patch to correct the field
+ * @param string $table table name
+ * @param string $field field name to check
+ * @param string $patchFile path to the patch to correct the field
*/
protected function checkBin( $table, $field, $patchFile ) {
- $tableName = $this->db->tableName( $table );
- $res = $this->db->query( "SELECT $field FROM $tableName LIMIT 0", __METHOD__ );
- $flags = explode( ' ', mysql_field_flags( $res->result, 0 ) );
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
- if ( in_array( 'binary', $flags ) ) {
+ $fieldInfo = $this->db->fieldInfo( $table, $field );
+ if ( $fieldInfo->isBinary() ) {
$this->output( "...$table table has correct $field encoding.\n" );
} else {
$this->applyPatch( $patchFile, false, "Fixing $field encoding on $table table" );
@@ -239,22 +266,28 @@ class MysqlUpdater extends DatabaseUpdater {
/**
* Check whether an index contain a field
*
- * @param $table String: table name
- * @param $index String: index name to check
- * @param $field String: field that should be in the index
+ * @param string $table table name
+ * @param string $index index name to check
+ * @param string $field field that should be in the index
* @return Boolean
*/
protected function indexHasField( $table, $index, $field ) {
+ if ( !$this->doTable( $table ) ) {
+ return true;
+ }
+
$info = $this->db->indexInfo( $table, $index, __METHOD__ );
if ( $info ) {
foreach ( $info as $row ) {
if ( $row->Column_name == $field ) {
$this->output( "...index $index on table $table includes field $field.\n" );
+
return true;
}
}
}
$this->output( "...index $index on table $table has no field $field; added.\n" );
+
return false;
}
@@ -264,13 +297,22 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doInterwikiUpdate() {
global $IP;
+ if ( !$this->doTable( 'interwiki' ) ) {
+ return true;
+ }
+
if ( $this->db->tableExists( "interwiki", __METHOD__ ) ) {
$this->output( "...already have interwiki table\n" );
+
return;
}
$this->applyPatch( 'patch-interwiki.sql', false, 'Creating interwiki table' );
- $this->applyPatch( "$IP/maintenance/interwiki.sql", true, 'Adding default interwiki definitions' );
+ $this->applyPatch(
+ "$IP/maintenance/interwiki.sql",
+ true,
+ 'Adding default interwiki definitions'
+ );
}
/**
@@ -283,6 +325,7 @@ class MysqlUpdater extends DatabaseUpdater {
}
if ( $meta->isMultipleKey() ) {
$this->output( "...indexes seem up to 20031107 standards.\n" );
+
return;
}
@@ -298,11 +341,18 @@ class MysqlUpdater extends DatabaseUpdater {
$info = $this->db->fieldInfo( 'imagelinks', 'il_from' );
if ( !$info || $info->type() !== 'string' ) {
$this->output( "...il_from OK\n" );
+
return;
}
- if( $this->applyPatch( 'patch-fix-il_from.sql', false, "Fixing ancient broken imagelinks table." ) ) {
- $this->output("NOTE: you will have to run maintenance/refreshLinks.php after this." );
+ $applied = $this->applyPatch(
+ 'patch-fix-il_from.sql',
+ false,
+ 'Fixing ancient broken imagelinks table.'
+ );
+
+ if ( $applied ) {
+ $this->output( "NOTE: you will have to run maintenance/refreshLinks.php after this." );
}
}
@@ -311,9 +361,15 @@ class MysqlUpdater extends DatabaseUpdater {
*/
function doWatchlistUpdate() {
$talk = $this->db->selectField( 'watchlist', 'count(*)', 'wl_namespace & 1', __METHOD__ );
- $nontalk = $this->db->selectField( 'watchlist', 'count(*)', 'NOT (wl_namespace & 1)', __METHOD__ );
+ $nontalk = $this->db->selectField(
+ 'watchlist',
+ 'count(*)',
+ 'NOT (wl_namespace & 1)',
+ __METHOD__
+ );
if ( $talk == $nontalk ) {
$this->output( "...watchlist talk page rows already present.\n" );
+
return;
}
@@ -331,6 +387,7 @@ class MysqlUpdater extends DatabaseUpdater {
function doSchemaRestructuring() {
if ( $this->db->tableExists( 'page', __METHOD__ ) ) {
$this->output( "...page table already exists.\n" );
+
return;
}
@@ -338,10 +395,21 @@ class MysqlUpdater extends DatabaseUpdater {
$this->output( wfTimestamp( TS_DB ) );
$this->output( "......checking for duplicate entries.\n" );
- list ( $cur, $old, $page, $revision, $text ) = $this->db->tableNamesN( 'cur', 'old', 'page', 'revision', 'text' );
+ list( $cur, $old, $page, $revision, $text ) = $this->db->tableNamesN(
+ 'cur',
+ 'old',
+ 'page',
+ 'revision',
+ 'text'
+ );
- $rows = $this->db->query( "SELECT cur_title, cur_namespace, COUNT(cur_namespace) AS c
- FROM $cur GROUP BY cur_title, cur_namespace HAVING c>1", __METHOD__ );
+ $rows = $this->db->query( "
+ SELECT cur_title, cur_namespace, COUNT(cur_namespace) AS c
+ FROM $cur
+ GROUP BY cur_title, cur_namespace
+ HAVING c>1",
+ __METHOD__
+ );
if ( $rows->numRows() > 0 ) {
$this->output( wfTimestamp( TS_DB ) );
@@ -349,11 +417,16 @@ class MysqlUpdater extends DatabaseUpdater {
$this->output( sprintf( "<b> %-60s %3s %5s</b>\n", 'Title', 'NS', 'Count' ) );
$duplicate = array();
foreach ( $rows as $row ) {
- if ( ! isset( $duplicate[$row->cur_namespace] ) ) {
+ if ( !isset( $duplicate[$row->cur_namespace] ) ) {
$duplicate[$row->cur_namespace] = array();
}
+
$duplicate[$row->cur_namespace][] = $row->cur_title;
- $this->output( sprintf( " %-60s %3s %5s\n", $row->cur_title, $row->cur_namespace, $row->c ) );
+ $this->output( sprintf(
+ " %-60s %3s %5s\n",
+ $row->cur_title, $row->cur_namespace,
+ $row->c
+ ) );
}
$sql = "SELECT cur_title, cur_namespace, cur_id, cur_timestamp FROM $cur WHERE ";
$firstCond = true;
@@ -388,7 +461,7 @@ class MysqlUpdater extends DatabaseUpdater {
if ( $prev_title == $row->cur_title && $prev_namespace == $row->cur_namespace ) {
$deleteId[] = $row->cur_id;
}
- $prev_title = $row->cur_title;
+ $prev_title = $row->cur_title;
$prev_namespace = $row->cur_namespace;
}
$sql = "DELETE FROM $cur WHERE cur_id IN ( " . join( ',', $deleteId ) . ')';
@@ -438,7 +511,10 @@ class MysqlUpdater extends DatabaseUpdater {
$this->output( wfTimestamp( TS_DB ) );
$this->output( "......Locking tables.\n" );
- $this->db->query( "LOCK TABLES $page WRITE, $revision WRITE, $old WRITE, $cur WRITE", __METHOD__ );
+ $this->db->query(
+ "LOCK TABLES $page WRITE, $revision WRITE, $old WRITE, $cur WRITE",
+ __METHOD__
+ );
$maxold = intval( $this->db->selectField( 'old', 'max(old_id)', '', __METHOD__ ) );
$this->output( wfTimestamp( TS_DB ) );
@@ -459,27 +535,38 @@ class MysqlUpdater extends DatabaseUpdater {
$cur_text = 'cur_text';
$cur_flags = "''";
}
- $this->db->query( "INSERT INTO $old (old_namespace, old_title, old_text, old_comment, old_user, old_user_text,
- old_timestamp, old_minor_edit, old_flags)
- SELECT cur_namespace, cur_title, $cur_text, cur_comment, cur_user, cur_user_text, cur_timestamp, cur_minor_edit, $cur_flags
- FROM $cur", __METHOD__ );
+ $this->db->query(
+ "INSERT INTO $old (old_namespace, old_title, old_text, old_comment, old_user,
+ old_user_text, old_timestamp, old_minor_edit, old_flags)
+ SELECT cur_namespace, cur_title, $cur_text, cur_comment, cur_user, cur_user_text,
+ cur_timestamp, cur_minor_edit, $cur_flags
+ FROM $cur",
+ __METHOD__
+ );
$this->output( wfTimestamp( TS_DB ) );
$this->output( "......Setting up revision table.\n" );
- $this->db->query( "INSERT INTO $revision (rev_id, rev_page, rev_comment, rev_user, rev_user_text, rev_timestamp,
- rev_minor_edit)
+ $this->db->query(
+ "INSERT INTO $revision (rev_id, rev_page, rev_comment, rev_user,
+ rev_user_text, rev_timestamp, rev_minor_edit)
SELECT old_id, cur_id, old_comment, old_user, old_user_text,
old_timestamp, old_minor_edit
- FROM $old,$cur WHERE old_namespace=cur_namespace AND old_title=cur_title", __METHOD__ );
+ FROM $old,$cur WHERE old_namespace=cur_namespace AND old_title=cur_title",
+ __METHOD__
+ );
$this->output( wfTimestamp( TS_DB ) );
$this->output( "......Setting up page table.\n" );
- $this->db->query( "INSERT INTO $page (page_id, page_namespace, page_title, page_restrictions, page_counter,
- page_is_redirect, page_is_new, page_random, page_touched, page_latest, page_len)
- SELECT cur_id, cur_namespace, cur_title, cur_restrictions, cur_counter, cur_is_redirect, cur_is_new,
- cur_random, cur_touched, rev_id, LENGTH(cur_text)
+ $this->db->query(
+ "INSERT INTO $page (page_id, page_namespace, page_title,
+ page_restrictions, page_counter, page_is_redirect, page_is_new, page_random,
+ page_touched, page_latest, page_len)
+ SELECT cur_id, cur_namespace, cur_title, cur_restrictions, cur_counter,
+ cur_is_redirect, cur_is_new, cur_random, cur_touched, rev_id, LENGTH(cur_text)
FROM $cur,$revision
- WHERE cur_id=rev_page AND rev_timestamp=cur_timestamp AND rev_id > {$maxold}", __METHOD__ );
+ WHERE cur_id=rev_page AND rev_timestamp=cur_timestamp AND rev_id > {$maxold}",
+ __METHOD__
+ );
$this->output( wfTimestamp( TS_DB ) );
$this->output( "......Unlocking tables.\n" );
@@ -495,12 +582,12 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doNamespaceSize() {
$tables = array(
- 'page' => 'page',
- 'archive' => 'ar',
+ 'page' => 'page',
+ 'archive' => 'ar',
'recentchanges' => 'rc',
- 'watchlist' => 'wl',
- 'querycache' => 'qc',
- 'logging' => 'log',
+ 'watchlist' => 'wl',
+ 'querycache' => 'qc',
+ 'logging' => 'log',
);
foreach ( $tables as $table => $prefix ) {
$field = $prefix . '_namespace';
@@ -522,10 +609,15 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doPagelinksUpdate() {
if ( $this->db->tableExists( 'pagelinks', __METHOD__ ) ) {
$this->output( "...already have pagelinks table.\n" );
+
return;
}
- $this->applyPatch( 'patch-pagelinks.sql', false, "Converting links and brokenlinks tables to pagelinks" );
+ $this->applyPatch(
+ 'patch-pagelinks.sql',
+ false,
+ 'Converting links and brokenlinks tables to pagelinks'
+ );
global $wgContLang;
foreach ( MWNamespace::getCanonicalNamespaces() as $ns => $name ) {
@@ -552,9 +644,14 @@ class MysqlUpdater extends DatabaseUpdater {
}
protected function doUserUniqueUpdate() {
+ if ( !$this->doTable( 'user' ) ) {
+ return true;
+ }
+
$duper = new UserDupes( $this->db, array( $this, 'output' ) );
if ( $duper->hasUniqueIndex() ) {
$this->output( "...already have unique user_name index.\n" );
+
return;
}
@@ -565,24 +662,31 @@ class MysqlUpdater extends DatabaseUpdater {
}
protected function doUserGroupsUpdate() {
+ if ( !$this->doTable( 'user_groups' ) ) {
+ return true;
+ }
+
if ( $this->db->tableExists( 'user_groups', __METHOD__ ) ) {
$info = $this->db->fieldInfo( 'user_groups', 'ug_group' );
if ( $info->type() == 'int' ) {
$oldug = $this->db->tableName( 'user_groups' );
$newug = $this->db->tableName( 'user_groups_bogus' );
- $this->output( "user_groups table exists but is in bogus intermediate format. Renaming to $newug... " );
+ $this->output( "user_groups table exists but is in bogus intermediate " .
+ "format. Renaming to $newug... " );
$this->db->query( "ALTER TABLE $oldug RENAME TO $newug", __METHOD__ );
$this->output( "done.\n" );
$this->applyPatch( 'patch-user_groups.sql', false, "Re-adding fresh user_groups table" );
$this->output( "***\n" );
- $this->output( "*** WARNING: You will need to manually fix up user permissions in the user_groups\n" );
+ $this->output( "*** WARNING: You will need to manually fix up user " .
+ "permissions in the user_groups\n" );
$this->output( "*** table. Old 1.5 alpha versions did some pretty funky stuff...\n" );
$this->output( "***\n" );
} else {
$this->output( "...user_groups table exists and is in current format.\n" );
}
+
return;
}
@@ -590,11 +694,16 @@ class MysqlUpdater extends DatabaseUpdater {
if ( !$this->db->tableExists( 'user_rights', __METHOD__ ) ) {
if ( $this->db->fieldExists( 'user', 'user_rights', __METHOD__ ) ) {
- $this->db->applyPatch( 'patch-user_rights.sql', false, "Upgrading from a 1.3 or older database? Breaking out user_rights for conversion" );
+ $this->applyPatch(
+ 'patch-user_rights.sql',
+ false,
+ 'Upgrading from a 1.3 or older database? Breaking out user_rights for conversion'
+ );
} else {
$this->output( "*** WARNING: couldn't locate user_rights table or field for upgrade.\n" );
$this->output( "*** You may need to manually configure some sysops by manipulating\n" );
$this->output( "*** the user_groups table.\n" );
+
return;
}
}
@@ -613,7 +722,7 @@ class MysqlUpdater extends DatabaseUpdater {
foreach ( $groups as $group ) {
$this->db->insert( 'user_groups',
array(
- 'ug_user' => $row->ur_user,
+ 'ug_user' => $row->ur_user,
'ug_group' => $group ),
__METHOD__ );
}
@@ -627,12 +736,20 @@ class MysqlUpdater extends DatabaseUpdater {
*/
protected function doWatchlistNull() {
$info = $this->db->fieldInfo( 'watchlist', 'wl_notificationtimestamp' );
+ if ( !$info ) {
+ return;
+ }
if ( $info->isNullable() ) {
$this->output( "...wl_notificationtimestamp is already nullable.\n" );
+
return;
}
- $this->applyPatch( 'patch-watchlist-null.sql', false, "Making wl_notificationtimestamp nullable" );
+ $this->applyPatch(
+ 'patch-watchlist-null.sql',
+ false,
+ 'Making wl_notificationtimestamp nullable'
+ );
}
/**
@@ -645,7 +762,7 @@ class MysqlUpdater extends DatabaseUpdater {
$this->db->query( "UPDATE $page SET page_random = RAND() WHERE page_random = 0", __METHOD__ );
$rows = $this->db->affectedRows();
- if( $rows ) {
+ if ( $rows ) {
$this->output( "Set page_random to a random value on $rows rows where it was set to 0\n" );
} else {
$this->output( "...no page_random rows needed to be set\n" );
@@ -655,6 +772,7 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doTemplatelinksUpdate() {
if ( $this->db->tableExists( 'templatelinks', __METHOD__ ) ) {
$this->output( "...templatelinks table already exists\n" );
+
return;
}
@@ -678,7 +796,6 @@ class MysqlUpdater extends DatabaseUpdater {
'tl_title' => $row->pl_title,
), __METHOD__
);
-
}
} else {
// Fast update
@@ -692,14 +809,15 @@ class MysqlUpdater extends DatabaseUpdater {
), __METHOD__
);
}
- $this->output( "Done. Please run maintenance/refreshLinks.php for a more thorough templatelinks update.\n" );
+ $this->output( "Done. Please run maintenance/refreshLinks.php for a more " .
+ "thorough templatelinks update.\n" );
}
protected function doBacklinkingIndicesUpdate() {
if ( !$this->indexHasField( 'pagelinks', 'pl_namespace', 'pl_from' ) ||
!$this->indexHasField( 'templatelinks', 'tl_namespace', 'tl_from' ) ||
- !$this->indexHasField( 'imagelinks', 'il_to', 'il_from' ) )
- {
+ !$this->indexHasField( 'imagelinks', 'il_to', 'il_from' )
+ ) {
$this->applyPatch( 'patch-backlinkindexes.sql', false, "Updating backlinking indices" );
}
}
@@ -712,11 +830,20 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doRestrictionsUpdate() {
if ( $this->db->tableExists( 'page_restrictions', __METHOD__ ) ) {
$this->output( "...page_restrictions table already exists.\n" );
+
return;
}
- $this->applyPatch( 'patch-page_restrictions.sql', false, "Creating page_restrictions table (1/2)" );
- $this->applyPatch( 'patch-page_restrictions_sortkey.sql', false, "Creating page_restrictions table (2/2)" );
+ $this->applyPatch(
+ 'patch-page_restrictions.sql',
+ false,
+ 'Creating page_restrictions table (1/2)'
+ );
+ $this->applyPatch(
+ 'patch-page_restrictions_sortkey.sql',
+ false,
+ 'Creating page_restrictions table (2/2)'
+ );
$this->output( "done.\n" );
$this->output( "Migrating old restrictions to new table...\n" );
@@ -733,6 +860,7 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doCategoryPopulation() {
if ( $this->updateRowExists( 'populate category' ) ) {
$this->output( "...category table already populated.\n" );
+
return;
}
@@ -759,14 +887,36 @@ class MysqlUpdater extends DatabaseUpdater {
}
}
+ protected function doEnableProfiling() {
+ global $wgProfileToDatabase;
+
+ if ( !$this->doTable( 'profiling' ) ) {
+ return true;
+ }
+
+ if ( $wgProfileToDatabase === true && !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
+ $this->applyPatch( 'patch-profiling.sql', false, 'Add profiling table' );
+ }
+ }
+
protected function doMaybeProfilingMemoryUpdate() {
+ if ( !$this->doTable( 'profiling' ) ) {
+ return true;
+ }
+
if ( !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
- // Simply ignore
+ return true;
} elseif ( $this->db->fieldExists( 'profiling', 'pf_memory', __METHOD__ ) ) {
$this->output( "...profiling table has pf_memory field.\n" );
- } else {
- $this->applyPatch( 'patch-profiling-memory.sql', false, "Adding pf_memory field to table profiling" );
+
+ return true;
}
+
+ return $this->applyPatch(
+ 'patch-profiling-memory.sql',
+ false,
+ 'Adding pf_memory field to table profiling'
+ );
}
protected function doFilearchiveIndicesUpdate() {
@@ -774,43 +924,57 @@ class MysqlUpdater extends DatabaseUpdater {
if ( !$info ) {
$this->applyPatch( 'patch-filearchive-user-index.sql', false, "Updating filearchive indices" );
}
+
+ return true;
}
protected function doUniquePlTlIl() {
$info = $this->db->indexInfo( 'pagelinks', 'pl_namespace' );
if ( is_array( $info ) && !$info[0]->Non_unique ) {
$this->output( "...pl_namespace, tl_namespace, il_to indices are already UNIQUE.\n" );
- return;
- }
- $this->applyPatch( 'patch-pl-tl-il-unique.sql', false, "Making pl_namespace, tl_namespace and il_to indices UNIQUE" );
- }
+ return true;
+ }
+ if ( $this->skipSchema ) {
+ $this->output( "...skipping schema change (making pl_namespace, tl_namespace " .
+ "and il_to indices UNIQUE).\n" );
- protected function renameEuWikiId() {
- if ( $this->db->fieldExists( 'external_user', 'eu_local_id', __METHOD__ ) ) {
- $this->output( "...eu_wiki_id already renamed to eu_local_id.\n" );
- return;
+ return false;
}
- $this->applyPatch( 'patch-eu_local_id.sql', false, "Renaming eu_wiki_id -> eu_local_id" );
+ return $this->applyPatch(
+ 'patch-pl-tl-il-unique.sql',
+ false,
+ 'Making pl_namespace, tl_namespace and il_to indices UNIQUE'
+ );
}
protected function doUpdateMimeMinorField() {
if ( $this->updateRowExists( 'mime_minor_length' ) ) {
$this->output( "...*_mime_minor fields are already long enough.\n" );
+
return;
}
- $this->applyPatch( 'patch-mime_minor_length.sql', false, "Altering all *_mime_minor fields to 100 bytes in size" );
+ $this->applyPatch(
+ 'patch-mime_minor_length.sql',
+ false,
+ 'Altering all *_mime_minor fields to 100 bytes in size'
+ );
}
protected function doClFieldsUpdate() {
if ( $this->updateRowExists( 'cl_fields_update' ) ) {
$this->output( "...categorylinks up-to-date.\n" );
+
return;
}
- $this->applyPatch( 'patch-categorylinks-better-collation2.sql', false, 'Updating categorylinks (again)' );
+ $this->applyPatch(
+ 'patch-categorylinks-better-collation2.sql',
+ false,
+ 'Updating categorylinks (again)'
+ );
}
protected function doLangLinksLengthUpdate() {
@@ -819,19 +983,55 @@ class MysqlUpdater extends DatabaseUpdater {
$row = $this->db->fetchObject( $res );
if ( $row && $row->Type == "varbinary(10)" ) {
- $this->applyPatch( 'patch-langlinks-ll_lang-20.sql', false, 'Updating length of ll_lang in langlinks' );
+ $this->applyPatch(
+ 'patch-langlinks-ll_lang-20.sql',
+ false,
+ 'Updating length of ll_lang in langlinks'
+ );
} else {
$this->output( "...ll_lang is up-to-date.\n" );
}
}
protected function doUserNewTalkTimestampNotNull() {
+ if ( !$this->doTable( 'user_newtalk' ) ) {
+ return true;
+ }
+
$info = $this->db->fieldInfo( 'user_newtalk', 'user_last_timestamp' );
+ if ( $info === false ) {
+ return;
+ }
if ( $info->isNullable() ) {
$this->output( "...user_last_timestamp is already nullable.\n" );
+
return;
}
- $this->applyPatch( 'patch-user-newtalk-timestamp-null.sql', false, "Making user_last_timestamp nullable" );
+ $this->applyPatch(
+ 'patch-user-newtalk-timestamp-null.sql',
+ false,
+ 'Making user_last_timestamp nullable'
+ );
+ }
+
+ protected function doIwlinksIndexNonUnique() {
+ $info = $this->db->indexInfo( 'iwlinks', 'iwl_prefix_title_from' );
+ if ( is_array( $info ) && $info[0]->Non_unique ) {
+ $this->output( "...iwl_prefix_title_from index is already non-UNIQUE.\n" );
+
+ return true;
+ }
+ if ( $this->skipSchema ) {
+ $this->output( "...skipping schema change (making iwl_prefix_title_from index non-UNIQUE).\n" );
+
+ return false;
+ }
+
+ return $this->applyPatch(
+ 'patch-iwl_prefix_title_from-non-unique.sql',
+ false,
+ 'Making iwl_prefix_title_from index non-UNIQUE'
+ );
}
}
diff --git a/includes/installer/OracleInstaller.php b/includes/installer/OracleInstaller.php
index 72ec800d..8ce74f42 100644
--- a/includes/installer/OracleInstaller.php
+++ b/includes/installer/OracleInstaller.php
@@ -40,7 +40,7 @@ class OracleInstaller extends DatabaseInstaller {
protected $internalDefaults = array(
'_OracleDefTS' => 'USERS',
'_OracleTempTS' => 'TEMP',
- '_InstallUser' => 'SYSDBA',
+ '_InstallUser' => 'SYSTEM',
);
public $minimumVersion = '9.0.1'; // 9iR1
@@ -59,35 +59,51 @@ class OracleInstaller extends DatabaseInstaller {
if ( $this->getVar( 'wgDBserver' ) == 'localhost' ) {
$this->parent->setVar( 'wgDBserver', '' );
}
- return
- $this->getTextBox( 'wgDBserver', 'config-db-host-oracle', array(), $this->parent->getHelpBox( 'config-db-host-oracle-help' ) ) .
+
+ return $this->getTextBox(
+ 'wgDBserver',
+ 'config-db-host-oracle',
+ array(),
+ $this->parent->getHelpBox( 'config-db-host-oracle-help' )
+ ) .
Html::openElement( 'fieldset' ) .
Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
$this->getTextBox( 'wgDBprefix', 'config-db-prefix' ) .
$this->getTextBox( '_OracleDefTS', 'config-oracle-def-ts' ) .
- $this->getTextBox( '_OracleTempTS', 'config-oracle-temp-ts', array(), $this->parent->getHelpBox( 'config-db-oracle-help' ) ) .
+ $this->getTextBox(
+ '_OracleTempTS',
+ 'config-oracle-temp-ts',
+ array(),
+ $this->parent->getHelpBox( 'config-db-oracle-help' )
+ ) .
Html::closeElement( 'fieldset' ) .
- $this->parent->getWarningBox( wfMessage( 'config-db-account-oracle-warn' )->text() ).
- $this->getInstallUserBox().
+ $this->parent->getWarningBox( wfMessage( 'config-db-account-oracle-warn' )->text() ) .
+ $this->getInstallUserBox() .
$this->getWebUserBox();
}
public function submitInstallUserBox() {
parent::submitInstallUserBox();
$this->parent->setVar( '_InstallDBname', $this->getVar( '_InstallUser' ) );
+
return Status::newGood();
}
public function submitConnectForm() {
// Get variables from the request
- $newValues = $this->setVarsFromRequest( array( 'wgDBserver', 'wgDBprefix', 'wgDBuser', 'wgDBpassword' ) );
+ $newValues = $this->setVarsFromRequest( array(
+ 'wgDBserver',
+ 'wgDBprefix',
+ 'wgDBuser',
+ 'wgDBpassword'
+ ) );
$this->parent->setVar( 'wgDBname', $this->getVar( 'wgDBuser' ) );
// Validate them
$status = Status::newGood();
if ( !strlen( $newValues['wgDBserver'] ) ) {
$status->fatal( 'config-missing-db-server-oracle' );
- } elseif ( !preg_match( '/^[a-zA-Z0-9_\.]+$/', $newValues['wgDBserver'] ) ) {
+ } elseif ( !self::checkConnectStringFormat( $newValues['wgDBserver'] ) ) {
$status->fatal( 'config-invalid-db-server-oracle', $newValues['wgDBserver'] );
}
if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBprefix'] ) ) {
@@ -119,7 +135,7 @@ class OracleInstaller extends DatabaseInstaller {
return $status;
}
if ( !$this->getVar( '_CreateDBAccount' ) ) {
- $status->fatal('config-db-sys-create-oracle');
+ $status->fatal( 'config-db-sys-create-oracle' );
}
} else {
return $status;
@@ -163,6 +179,7 @@ class OracleInstaller extends DatabaseInstaller {
$this->connError = $e->db->lastErrno();
$status->fatal( 'config-connection-error', $e->getMessage() );
}
+
return $status;
}
@@ -182,6 +199,7 @@ class OracleInstaller extends DatabaseInstaller {
$this->connError = $e->db->lastErrno();
$status->fatal( 'config-connection-error', $e->getMessage() );
}
+
return $status;
}
@@ -190,6 +208,7 @@ class OracleInstaller extends DatabaseInstaller {
$this->parent->setVar( 'wgDBname', $this->getVar( 'wgDBuser' ) );
$retVal = parent::needsUpgrade();
$this->parent->setVar( 'wgDBname', $tempDBname );
+
return $retVal;
}
@@ -202,9 +221,9 @@ class OracleInstaller extends DatabaseInstaller {
$this->parent->addInstallStep( $callback, 'database' );
}
-
public function setupDatabase() {
$status = Status::newGood();
+
return $status;
}
@@ -241,7 +260,7 @@ class OracleInstaller extends DatabaseInstaller {
$status->fatal( 'config-db-sys-user-exists-oracle', $this->getVar( 'wgDBuser' ) );
}
- if ($status->isOK()) {
+ if ( $status->isOK() ) {
// user created or already existing, switching back to a normal connection
// as the new user has all needed privileges to setup the rest of the schema
// i will be using that user as _InstallUser from this point on
@@ -287,15 +306,38 @@ class OracleInstaller extends DatabaseInstaller {
foreach ( $varNames as $name ) {
$vars[$name] = $this->getVar( $name );
}
+
return $vars;
}
public function getLocalSettings() {
$prefix = $this->getVar( 'wgDBprefix' );
- return
-"# Oracle specific settings
-\$wgDBprefix = \"{$prefix}\";
+
+ return "# Oracle specific settings
+\$wgDBprefix = \"{$prefix}\";
";
}
+ /**
+ * Function checks the format of Oracle connect string
+ * The actual validity of the string is checked by attempting to connect
+ *
+ * Regex should be able to validate all connect string formats
+ * [//](host|tns_name)[:port][/service_name][:POOLED]
+ * http://www.orafaq.com/wiki/EZCONNECT
+ *
+ * @since 1.22
+ *
+ * @param string $connect_string
+ *
+ * @return bool Whether the connection string is valid.
+ */
+ public static function checkConnectStringFormat( $connect_string ) {
+ // @@codingStandardsIgnoreStart Long lines with regular expressions.
+ // @todo Very long regular expression. Make more readable?
+ $isValid = preg_match( '/^[[:alpha:]][\w\-]*(?:\.[[:alpha:]][\w\-]*){0,2}$/', $connect_string ); // TNS name
+ $isValid |= preg_match( '/^(?:\/\/)?[\w\-\.]+(?::[\d]+)?(?:\/(?:[\w\-\.]+(?::(pooled|dedicated|shared))?)?(?:\/[\w\-\.]+)?)?$/', $connect_string ); // EZConnect
+ // @@codingStandardsIgnoreEnd
+ return (bool)$isValid;
+ }
}
diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php
index e71c26fe..ec91e57b 100644
--- a/includes/installer/OracleUpdater.php
+++ b/includes/installer/OracleUpdater.php
@@ -48,27 +48,47 @@ class OracleUpdater extends DatabaseUpdater {
array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql' ),
//1.18
- array( 'addIndex', 'user', 'i02', 'patch-user_email_index.sql' ),
+ array( 'addIndex', 'user', 'i02', 'patch-user_email_index.sql' ),
array( 'modifyField', 'user_properties', 'up_property', 'patch-up_property.sql' ),
array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
array( 'doRecentchangesFK2Cascade' ),
//1.19
- array( 'addIndex', 'logging', 'i05', 'patch-logging_type_action_index.sql'),
+ array( 'addIndex', 'logging', 'i05', 'patch-logging_type_action_index.sql' ),
array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1_field.sql' ),
array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1_field.sql' ),
array( 'doRemoveNotNullEmptyDefaults2' ),
array( 'addIndex', 'page', 'i03', 'patch-page_redirect_namespace_len.sql' ),
- array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-us_chunk_inx_field.sql' ),
array( 'addField', 'job', 'job_timestamp', 'patch-job_timestamp_field.sql' ),
array( 'addIndex', 'job', 'i02', 'patch-job_timestamp_index.sql' ),
array( 'doPageRestrictionsPKUKFix' ),
- array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
//1.20
array( 'addIndex', 'ipblocks', 'i05', 'patch-ipblocks_i05_index.sql' ),
array( 'addIndex', 'revision', 'i05', 'patch-revision_i05_index.sql' ),
+ array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
+
+ //1.21
+ array( 'addField', 'revision', 'rev_content_format',
+ 'patch-revision-rev_content_format.sql' ),
+ array( 'addField', 'revision', 'rev_content_model',
+ 'patch-revision-rev_content_model.sql' ),
+ array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ),
+ array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ),
+ array( 'addField', 'archive', 'ar_id', 'patch-archive-ar_id.sql' ),
+ array( 'addField', 'externallinks', 'el_id', 'patch-externallinks-el_id.sql' ),
+ array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ),
+ array( 'dropField', 'site_stats', 'ss_admins', 'patch-ss_admins.sql' ),
+ array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
+ array( 'addTable', 'sites', 'patch-sites.sql' ),
+ array( 'addField', 'filearchive', 'fa_sha1', 'patch-fa_sha1.sql' ),
+ array( 'addField', 'job', 'job_token', 'patch-job_token.sql' ),
+ array( 'addField', 'job', 'job_attempts', 'patch-job_attempts.sql' ),
+ array( 'addField', 'uploadstash', 'us_props', 'patch-uploadstash-us_props.sql' ),
+ array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase-255.sql' ),
+ array( 'modifyField', 'user_former_groups', 'ufg_group',
+ 'patch-ufg_group-length-increase-255.sql' ),
// KEEP THIS AT THE BOTTOM!!
array( 'doRebuildDuplicateFunction' ),
@@ -87,14 +107,22 @@ class OracleUpdater extends DatabaseUpdater {
return;
}
- $this->applyPatch( 'patch_namespace_defaults.sql', false, "Altering namespace fields with default value" );
+ $this->applyPatch(
+ 'patch_namespace_defaults.sql',
+ false,
+ 'Altering namespace fields with default value'
+ );
}
/**
* Uniform FK names + deferrable state
*/
protected function doFKRenameDeferr() {
- $meta = $this->db->query( 'SELECT COUNT(*) cnt FROM user_constraints WHERE constraint_type = \'R\' AND deferrable = \'DEFERRABLE\'' );
+ $meta = $this->db->query( '
+ SELECT COUNT(*) cnt
+ FROM user_constraints
+ WHERE constraint_type = \'R\' AND deferrable = \'DEFERRABLE\''
+ );
$row = $meta->fetchRow();
if ( $row && $row['cnt'] > 0 ) {
return;
@@ -148,19 +176,27 @@ class OracleUpdater extends DatabaseUpdater {
* converted to NULL in Oracle
*/
protected function doRemoveNotNullEmptyDefaults() {
- $meta = $this->db->fieldInfo( 'categorylinks' , 'cl_sortkey_prefix' );
+ $meta = $this->db->fieldInfo( 'categorylinks', 'cl_sortkey_prefix' );
if ( $meta->isNullable() ) {
return;
}
- $this->applyPatch( 'patch_remove_not_null_empty_defs.sql', false, "Removing not null empty constraints" );
+ $this->applyPatch(
+ 'patch_remove_not_null_empty_defs.sql',
+ false,
+ 'Removing not null empty constraints'
+ );
}
protected function doRemoveNotNullEmptyDefaults2() {
- $meta = $this->db->fieldInfo( 'ipblocks' , 'ipb_by_text' );
+ $meta = $this->db->fieldInfo( 'ipblocks', 'ipb_by_text' );
if ( $meta->isNullable() ) {
return;
}
- $this->applyPatch( 'patch_remove_not_null_empty_defs2.sql', false, "Removing not null empty constraints" );
+ $this->applyPatch(
+ 'patch_remove_not_null_empty_defs2.sql',
+ false,
+ 'Removing not null empty constraints'
+ );
}
/**
@@ -168,7 +204,12 @@ class OracleUpdater extends DatabaseUpdater {
* cascading taken in account in the deleting function
*/
protected function doRecentchangesFK2Cascade() {
- $meta = $this->db->query( 'SELECT 1 FROM all_constraints WHERE owner = \''.strtoupper($this->db->getDBname()).'\' AND constraint_name = \''.$this->db->tablePrefix().'RECENTCHANGES_FK2\' AND delete_rule = \'CASCADE\'' );
+ $meta = $this->db->query( 'SELECT 1 FROM all_constraints WHERE owner = \'' .
+ strtoupper( $this->db->getDBname() ) .
+ '\' AND constraint_name = \'' .
+ $this->db->tablePrefix() .
+ 'RECENTCHANGES_FK2\' AND delete_rule = \'CASCADE\''
+ );
$row = $meta->fetchRow();
if ( $row ) {
return;
@@ -183,10 +224,16 @@ class OracleUpdater extends DatabaseUpdater {
protected function doPageRestrictionsPKUKFix() {
$this->output( "Altering PAGE_RESTRICTIONS keys ... " );
- $meta = $this->db->query( 'SELECT column_name FROM all_cons_columns WHERE owner = \''.strtoupper($this->db->getDBname()).'\' AND constraint_name = \'MW_PAGE_RESTRICTIONS_PK\' AND rownum = 1' );
+ $meta = $this->db->query( 'SELECT column_name FROM all_cons_columns WHERE owner = \'' .
+ strtoupper( $this->db->getDBname() ) .
+ '\' AND constraint_name = \'' .
+ $this->db->tablePrefix() .
+ 'PAGE_RESTRICTIONS_PK\' AND rownum = 1'
+ );
$row = $meta->fetchRow();
if ( $row['column_name'] == 'PR_ID' ) {
$this->output( "seems to be up to date.\n" );
+
return;
}
@@ -215,12 +262,11 @@ class OracleUpdater extends DatabaseUpdater {
/**
* Overload: because of the DDL_MODE tablename escaping is a bit dodgy
*/
- protected function purgeCache() {
+ public function purgeCache() {
# We can't guarantee that the user will be able to use TRUNCATE,
# but we know that DELETE is available to us
$this->output( "Purging caches..." );
- $this->db->delete( '/*Q*/'.$this->db->tableName( 'objectcache' ), '*', __METHOD__ );
+ $this->db->delete( '/*Q*/' . $this->db->tableName( 'objectcache' ), '*', __METHOD__ );
$this->output( "done.\n" );
}
-
}
diff --git a/includes/installer/PhpBugTests.php b/includes/installer/PhpBugTests.php
index 773debe0..54712644 100644
--- a/includes/installer/PhpBugTests.php
+++ b/includes/installer/PhpBugTests.php
@@ -30,6 +30,7 @@
class PhpXmlBugTester {
private $parsedData = '';
public $ok = false;
+
public function __construct() {
$charData = '<b>c</b>';
$xml = '<a>' . htmlspecialchars( $charData ) . '</a>';
@@ -39,6 +40,7 @@ class PhpXmlBugTester {
$parsedOk = xml_parse( $parser, $xml, true );
$this->ok = $parsedOk && ( $this->parsedData == $charData );
}
+
public function chardata( $parser, $data ) {
$this->parsedData .= $data;
}
diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php
index 3ac2b3a8..2cf41564 100644
--- a/includes/installer/PostgresInstaller.php
+++ b/includes/installer/PostgresInstaller.php
@@ -42,8 +42,8 @@ class PostgresInstaller extends DatabaseInstaller {
'_InstallUser' => 'postgres',
);
- var $minimumVersion = '8.3';
- var $maxRoleSearchDepth = 5;
+ public $minimumVersion = '8.3';
+ public $maxRoleSearchDepth = 5;
protected $pgConns = array();
@@ -56,13 +56,27 @@ class PostgresInstaller extends DatabaseInstaller {
}
function getConnectForm() {
- return
- $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
+ return $this->getTextBox(
+ 'wgDBserver',
+ 'config-db-host',
+ array(),
+ $this->parent->getHelpBox( 'config-db-host-help' )
+ ) .
$this->getTextBox( 'wgDBport', 'config-db-port' ) .
Html::openElement( 'fieldset' ) .
Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
- $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-db-name-help' ) ) .
- $this->getTextBox( 'wgDBmwschema', 'config-db-schema', array(), $this->parent->getHelpBox( 'config-db-schema-help' ) ) .
+ $this->getTextBox(
+ 'wgDBname',
+ 'config-db-name',
+ array(),
+ $this->parent->getHelpBox( 'config-db-name-help' )
+ ) .
+ $this->getTextBox(
+ 'wgDBmwschema',
+ 'config-db-schema',
+ array(),
+ $this->parent->getHelpBox( 'config-db-schema-help' )
+ ) .
Html::closeElement( 'fieldset' ) .
$this->getInstallUserBox();
}
@@ -108,6 +122,7 @@ class PostgresInstaller extends DatabaseInstaller {
$this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
$this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
+
return Status::newGood();
}
@@ -116,6 +131,7 @@ class PostgresInstaller extends DatabaseInstaller {
if ( $status->isOK() ) {
$this->db = $status->value;
}
+
return $status;
}
@@ -125,9 +141,9 @@ class PostgresInstaller extends DatabaseInstaller {
/**
* Open a PG connection with given parameters
- * @param $user string User name
- * @param $password string Password
- * @param $dbName string Database name
+ * @param string $user User name
+ * @param string $password Password
+ * @param string $dbName Database name
* @return Status
*/
protected function openConnectionWithParams( $user, $password, $dbName ) {
@@ -137,17 +153,19 @@ class PostgresInstaller extends DatabaseInstaller {
$this->getVar( 'wgDBserver' ),
$user,
$password,
- $dbName);
+ $dbName
+ );
$status->value = $db;
} catch ( DBConnectionError $e ) {
$status->fatal( 'config-connection-error', $e->getMessage() );
}
+
return $status;
}
/**
* Get a special type of connection
- * @param $type string See openPgConnection() for details.
+ * @param string $type See openPgConnection() for details.
* @return Status
*/
protected function getPgConnection( $type ) {
@@ -165,6 +183,7 @@ class PostgresInstaller extends DatabaseInstaller {
$conn->commit( __METHOD__ );
$this->pgConns[$type] = $conn;
}
+
return $status;
}
@@ -183,13 +202,14 @@ class PostgresInstaller extends DatabaseInstaller {
* separate connection for this allows us to avoid accidental cross-module
* dependencies.
*
- * @param $type string The type of connection to get:
+ * @param string $type The type of connection to get:
* - create-db: A connection for creating DBs, suitable for pre-
* installation.
* - create-schema: A connection to the new DB, for creating schemas and
* other similar objects in the new DB.
* - create-tables: A connection with a role suitable for creating tables.
*
+ * @throws MWException
* @return Status object. On success, a connection object will be in the
* value member.
*/
@@ -214,6 +234,7 @@ class PostgresInstaller extends DatabaseInstaller {
$safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
$conn->query( "SET ROLE $safeRole" );
}
+
return $status;
default:
throw new MWException( "Invalid special connection type: \"$type\"" );
@@ -266,6 +287,7 @@ class PostgresInstaller extends DatabaseInstaller {
$row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
array( 'rolname' => $superuser ), __METHOD__ );
+
return $row;
}
@@ -274,6 +296,7 @@ class PostgresInstaller extends DatabaseInstaller {
if ( !$perms ) {
return false;
}
+
return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
}
@@ -282,6 +305,7 @@ class PostgresInstaller extends DatabaseInstaller {
if ( !$perms ) {
return false;
}
+
return $perms->rolsuper === 't';
}
@@ -330,6 +354,7 @@ class PostgresInstaller extends DatabaseInstaller {
} else {
$msg = 'config-install-user-missing';
}
+
return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
}
@@ -382,9 +407,9 @@ class PostgresInstaller extends DatabaseInstaller {
/**
* Recursive helper for canCreateObjectsForWebUser().
* @param $conn DatabaseBase object
- * @param $targetMember int Role ID of the member to look for
- * @param $group int Role ID of the group to look for
- * @param $maxDepth int Maximum recursive search depth
+ * @param int $targetMember Role ID of the member to look for
+ * @param int $group Role ID of the group to look for
+ * @param int $maxDepth Maximum recursive search depth
* @return bool
*/
protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
@@ -409,6 +434,7 @@ class PostgresInstaller extends DatabaseInstaller {
}
}
}
+
return false;
}
@@ -430,7 +456,7 @@ class PostgresInstaller extends DatabaseInstaller {
'callback' => array( $this, 'setupSchema' )
);
- if( $this->getVar( '_CreateDBAccount' ) ) {
+ if ( $this->getVar( '_CreateDBAccount' ) ) {
$this->parent->addInstallStep( $createDbAccount, 'database' );
}
$this->parent->addInstallStep( $commitCB, 'interwiki' );
@@ -453,6 +479,7 @@ class PostgresInstaller extends DatabaseInstaller {
$safedb = $conn->addIdentifierQuotes( $dbName );
$conn->query( "CREATE DATABASE $safedb", __METHOD__ );
}
+
return Status::newGood();
}
@@ -468,7 +495,7 @@ class PostgresInstaller extends DatabaseInstaller {
$schema = $this->getVar( 'wgDBmwschema' );
$safeschema = $conn->addIdentifierQuotes( $schema );
$safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
- if( !$conn->schemaExists( $schema ) ) {
+ if ( !$conn->schemaExists( $schema ) ) {
try {
$conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
} catch ( DBQueryError $e ) {
@@ -479,11 +506,13 @@ class PostgresInstaller extends DatabaseInstaller {
// Select the new schema in the current connection
$conn->determineCoreSchema( $schema );
+
return Status::newGood();
}
function commitChanges() {
$this->db->commit( __METHOD__ );
+
return Status::newGood();
}
@@ -528,10 +557,10 @@ class PostgresInstaller extends DatabaseInstaller {
function getLocalSettings() {
$port = $this->getVar( 'wgDBport' );
$schema = $this->getVar( 'wgDBmwschema' );
- return
-"# Postgres specific settings
-\$wgDBport = \"{$port}\";
-\$wgDBmwschema = \"{$schema}\";";
+
+ return "# Postgres specific settings
+\$wgDBport = \"{$port}\";
+\$wgDBmwschema = \"{$schema}\";";
}
public function preUpgrade() {
@@ -556,20 +585,22 @@ class PostgresInstaller extends DatabaseInstaller {
*/
$conn = $status->value;
- if( $conn->tableExists( 'archive' ) ) {
+ if ( $conn->tableExists( 'archive' ) ) {
$status->warning( 'config-install-tables-exist' );
$this->enableLB();
+
return $status;
}
$conn->begin( __METHOD__ );
- if( !$conn->schemaExists( $schema ) ) {
+ if ( !$conn->schemaExists( $schema ) ) {
$status->fatal( 'config-install-pg-schema-not-exist' );
+
return $status;
}
$error = $conn->sourceFile( $conn->getSchemaPath() );
- if( $error !== true ) {
+ if ( $error !== true ) {
$conn->reportQueryError( $error, 0, '', __METHOD__ );
$conn->rollback( __METHOD__ );
$status->fatal( 'config-install-tables-failed', $error );
@@ -577,9 +608,10 @@ class PostgresInstaller extends DatabaseInstaller {
$conn->commit( __METHOD__ );
}
// Resume normal operations
- if( $status->isOk() ) {
+ if ( $status->isOk() ) {
$this->enableLB();
}
+
return $status;
}
@@ -622,6 +654,7 @@ class PostgresInstaller extends DatabaseInstaller {
} else {
return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
}
+
return Status::newGood();
}
}
diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php
index 6cffe84a..599b523b 100644
--- a/includes/installer/PostgresUpdater.php
+++ b/includes/installer/PostgresUpdater.php
@@ -46,285 +46,352 @@ class PostgresUpdater extends DatabaseUpdater {
# r15791 Change reserved word table names "user" and "text"
array( 'renameTable', 'user', 'mwuser' ),
array( 'renameTable', 'text', 'pagecontent' ),
- array( 'renameIndex', 'mwuser', 'user_pkey', 'mwuser_pkey'),
+ array( 'renameIndex', 'mwuser', 'user_pkey', 'mwuser_pkey' ),
array( 'renameIndex', 'mwuser', 'user_user_name_key', 'mwuser_user_name_key' ),
- array( 'renameIndex', 'pagecontent','text_pkey', 'pagecontent_pkey' ),
+ array( 'renameIndex', 'pagecontent', 'text_pkey', 'pagecontent_pkey' ),
# renamed sequences
- array( 'renameSequence', 'ipblocks_ipb_id_val', 'ipblocks_ipb_id_seq' ),
- array( 'renameSequence', 'rev_rev_id_val', 'revision_rev_id_seq' ),
- array( 'renameSequence', 'text_old_id_val', 'text_old_id_seq' ),
- array( 'renameSequence', 'rc_rc_id_seq', 'recentchanges_rc_id_seq' ),
- array( 'renameSequence', 'log_log_id_seq', 'logging_log_id_seq' ),
- array( 'renameSequence', 'pr_id_val', 'page_restrictions_pr_id_seq' ),
- array( 'renameSequence', 'us_id_seq', 'uploadstash_us_id_seq' ),
+ array( 'renameSequence', 'ipblocks_ipb_id_val', 'ipblocks_ipb_id_seq' ),
+ array( 'renameSequence', 'rev_rev_id_val', 'revision_rev_id_seq' ),
+ array( 'renameSequence', 'text_old_id_val', 'text_old_id_seq' ),
+ array( 'renameSequence', 'rc_rc_id_seq', 'recentchanges_rc_id_seq' ),
+ array( 'renameSequence', 'log_log_id_seq', 'logging_log_id_seq' ),
+ array( 'renameSequence', 'pr_id_val', 'page_restrictions_pr_id_seq' ),
+ array( 'renameSequence', 'us_id_seq', 'uploadstash_us_id_seq' ),
# since r58263
- array( 'renameSequence', 'category_id_seq', 'category_cat_id_seq'),
+ array( 'renameSequence', 'category_id_seq', 'category_cat_id_seq' ),
# new sequences if not renamed above
array( 'addSequence', 'logging', false, 'logging_log_id_seq' ),
array( 'addSequence', 'page_restrictions', false, 'page_restrictions_pr_id_seq' ),
array( 'addSequence', 'filearchive', 'fa_id', 'filearchive_fa_id_seq' ),
+ array( 'addSequence', 'archive', false, 'archive_ar_id_seq' ),
+ array( 'addSequence', 'externallinks', false, 'externallinks_el_id_seq' ),
# new tables
- array( 'addTable', 'category', 'patch-category.sql' ),
- array( 'addTable', 'page', 'patch-page.sql' ),
- array( 'addTable', 'querycachetwo', 'patch-querycachetwo.sql' ),
- array( 'addTable', 'page_props', 'patch-page_props.sql' ),
+ array( 'addTable', 'category', 'patch-category.sql' ),
+ array( 'addTable', 'page', 'patch-page.sql' ),
+ array( 'addTable', 'querycachetwo', 'patch-querycachetwo.sql' ),
+ array( 'addTable', 'page_props', 'patch-page_props.sql' ),
array( 'addTable', 'page_restrictions', 'patch-page_restrictions.sql' ),
- array( 'addTable', 'profiling', 'patch-profiling.sql' ),
- array( 'addTable', 'protected_titles', 'patch-protected_titles.sql' ),
- array( 'addTable', 'redirect', 'patch-redirect.sql' ),
- array( 'addTable', 'updatelog', 'patch-updatelog.sql' ),
- array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
- array( 'addTable', 'tag_summary', 'patch-tag_summary.sql' ),
- array( 'addTable', 'valid_tag', 'patch-valid_tag.sql' ),
- array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
- array( 'addTable', 'log_search', 'patch-log_search.sql' ),
- array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
- array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
- array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
- array( 'addTable', 'msg_resource_links','patch-msg_resource_links.sql' ),
- array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
- array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
- array( 'addTable', 'user_former_groups','patch-user_former_groups.sql' ),
- array( 'addTable', 'external_user', 'patch-external_user.sql' ),
+ array( 'addTable', 'profiling', 'patch-profiling.sql' ),
+ array( 'addTable', 'protected_titles', 'patch-protected_titles.sql' ),
+ array( 'addTable', 'redirect', 'patch-redirect.sql' ),
+ array( 'addTable', 'updatelog', 'patch-updatelog.sql' ),
+ array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
+ array( 'addTable', 'tag_summary', 'patch-tag_summary.sql' ),
+ array( 'addTable', 'valid_tag', 'patch-valid_tag.sql' ),
+ array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
+ array( 'addTable', 'log_search', 'patch-log_search.sql' ),
+ array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
+ array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
+ array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
+ array( 'addTable', 'msg_resource_links', 'patch-msg_resource_links.sql' ),
+ array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
+ array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
+ array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql' ),
+ array( 'addTable', 'sites', 'patch-sites.sql' ),
# Needed before new field
array( 'convertArchive2' ),
# new fields
- array( 'addPgField', 'updatelog', 'ul_value', 'TEXT' ),
- array( 'addPgField', 'archive', 'ar_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'archive', 'ar_len', 'INTEGER' ),
- array( 'addPgField', 'archive', 'ar_page_id', 'INTEGER' ),
- array( 'addPgField', 'archive', 'ar_parent_id', 'INTEGER' ),
- array( 'addPgField', 'categorylinks', 'cl_sortkey_prefix', "TEXT NOT NULL DEFAULT ''"),
- array( 'addPgField', 'categorylinks', 'cl_collation', "TEXT NOT NULL DEFAULT 0"),
- array( 'addPgField', 'categorylinks', 'cl_type', "TEXT NOT NULL DEFAULT 'page'"),
- array( 'addPgField', 'image', 'img_sha1', "TEXT NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'ipblocks', 'ipb_allow_usertalk', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'ipblocks', 'ipb_anon_only', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'ipblocks', 'ipb_by_text', "TEXT NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'ipblocks', 'ipb_block_email', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'ipblocks', 'ipb_create_account', 'SMALLINT NOT NULL DEFAULT 1' ),
- array( 'addPgField', 'ipblocks', 'ipb_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'ipblocks', 'ipb_enable_autoblock', 'SMALLINT NOT NULL DEFAULT 1' ),
- array( 'addPgField', 'ipblocks', 'ipb_parent_block_id', 'INTEGER DEFAULT NULL REFERENCES ipblocks(ipb_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED' ),
- array( 'addPgField', 'filearchive', 'fa_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'logging', 'log_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'logging', 'log_id', "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('logging_log_id_seq')" ),
- array( 'addPgField', 'logging', 'log_params', 'TEXT' ),
- array( 'addPgField', 'mwuser', 'user_editcount', 'INTEGER' ),
- array( 'addPgField', 'mwuser', 'user_newpass_time', 'TIMESTAMPTZ' ),
- array( 'addPgField', 'oldimage', 'oi_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'oldimage', 'oi_major_mime', "TEXT NOT NULL DEFAULT 'unknown'" ),
- array( 'addPgField', 'oldimage', 'oi_media_type', 'TEXT' ),
- array( 'addPgField', 'oldimage', 'oi_metadata', "BYTEA NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'oldimage', 'oi_minor_mime', "TEXT NOT NULL DEFAULT 'unknown'" ),
- array( 'addPgField', 'oldimage', 'oi_sha1', "TEXT NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'page_restrictions', 'pr_id', "INTEGER NOT NULL UNIQUE DEFAULT nextval('page_restrictions_pr_id_seq')" ),
- array( 'addPgField', 'profiling', 'pf_memory', 'NUMERIC(18,10) NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'recentchanges', 'rc_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'recentchanges', 'rc_log_action', 'TEXT' ),
- array( 'addPgField', 'recentchanges', 'rc_log_type', 'TEXT' ),
- array( 'addPgField', 'recentchanges', 'rc_logid', 'INTEGER NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'recentchanges', 'rc_new_len', 'INTEGER' ),
- array( 'addPgField', 'recentchanges', 'rc_old_len', 'INTEGER' ),
- array( 'addPgField', 'recentchanges', 'rc_params', 'TEXT' ),
- array( 'addPgField', 'redirect', 'rd_interwiki', 'TEXT NULL' ),
- array( 'addPgField', 'redirect', 'rd_fragment', 'TEXT NULL' ),
- array( 'addPgField', 'revision', 'rev_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
- array( 'addPgField', 'revision', 'rev_len', 'INTEGER' ),
- array( 'addPgField', 'revision', 'rev_parent_id', 'INTEGER DEFAULT NULL' ),
- array( 'addPgField', 'site_stats', 'ss_active_users', "INTEGER DEFAULT '-1'" ),
- array( 'addPgField', 'user_newtalk', 'user_last_timestamp', 'TIMESTAMPTZ' ),
- array( 'addPgField', 'logging', 'log_user_text', "TEXT NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'logging', 'log_page', 'INTEGER' ),
- array( 'addPgField', 'interwiki', 'iw_api', "TEXT NOT NULL DEFAULT ''"),
- array( 'addPgField', 'interwiki', 'iw_wikiid', "TEXT NOT NULL DEFAULT ''"),
- array( 'addPgField', 'revision', 'rev_sha1', "TEXT NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'archive', 'ar_sha1', "TEXT NOT NULL DEFAULT ''" ),
- array( 'addPgField', 'uploadstash', 'us_chunk_inx', "INTEGER NULL" ),
- array( 'addPgField', 'job', 'job_timestamp', "TIMESTAMPTZ" ),
+ array( 'addPgField', 'updatelog', 'ul_value', 'TEXT' ),
+ array( 'addPgField', 'archive', 'ar_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'archive', 'ar_len', 'INTEGER' ),
+ array( 'addPgField', 'archive', 'ar_page_id', 'INTEGER' ),
+ array( 'addPgField', 'archive', 'ar_parent_id', 'INTEGER' ),
+ array( 'addPgField', 'archive', 'ar_content_model', 'TEXT' ),
+ array( 'addPgField', 'archive', 'ar_content_format', 'TEXT' ),
+ array( 'addPgField', 'categorylinks', 'cl_sortkey_prefix', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'categorylinks', 'cl_collation', "TEXT NOT NULL DEFAULT 0" ),
+ array( 'addPgField', 'categorylinks', 'cl_type', "TEXT NOT NULL DEFAULT 'page'" ),
+ array( 'addPgField', 'image', 'img_sha1', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'ipblocks', 'ipb_allow_usertalk', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'ipblocks', 'ipb_anon_only', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'ipblocks', 'ipb_by_text', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'ipblocks', 'ipb_block_email', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'ipblocks', 'ipb_create_account', 'SMALLINT NOT NULL DEFAULT 1' ),
+ array( 'addPgField', 'ipblocks', 'ipb_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'ipblocks', 'ipb_enable_autoblock', 'SMALLINT NOT NULL DEFAULT 1' ),
+ array( 'addPgField', 'ipblocks', 'ipb_parent_block_id',
+ 'INTEGER DEFAULT NULL REFERENCES ipblocks(ipb_id) ' .
+ 'ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED' ),
+ array( 'addPgField', 'filearchive', 'fa_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'filearchive', 'fa_sha1', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'logging', 'log_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'logging', 'log_id',
+ "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('logging_log_id_seq')" ),
+ array( 'addPgField', 'logging', 'log_params', 'TEXT' ),
+ array( 'addPgField', 'mwuser', 'user_editcount', 'INTEGER' ),
+ array( 'addPgField', 'mwuser', 'user_newpass_time', 'TIMESTAMPTZ' ),
+ array( 'addPgField', 'oldimage', 'oi_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'oldimage', 'oi_major_mime', "TEXT NOT NULL DEFAULT 'unknown'" ),
+ array( 'addPgField', 'oldimage', 'oi_media_type', 'TEXT' ),
+ array( 'addPgField', 'oldimage', 'oi_metadata', "BYTEA NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'oldimage', 'oi_minor_mime', "TEXT NOT NULL DEFAULT 'unknown'" ),
+ array( 'addPgField', 'oldimage', 'oi_sha1', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'page', 'page_content_model', 'TEXT' ),
+ array( 'addPgField', 'page_restrictions', 'pr_id',
+ "INTEGER NOT NULL UNIQUE DEFAULT nextval('page_restrictions_pr_id_seq')" ),
+ array( 'addPgField', 'profiling', 'pf_memory', 'NUMERIC(18,10) NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'recentchanges', 'rc_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'recentchanges', 'rc_log_action', 'TEXT' ),
+ array( 'addPgField', 'recentchanges', 'rc_log_type', 'TEXT' ),
+ array( 'addPgField', 'recentchanges', 'rc_logid', 'INTEGER NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'recentchanges', 'rc_new_len', 'INTEGER' ),
+ array( 'addPgField', 'recentchanges', 'rc_old_len', 'INTEGER' ),
+ array( 'addPgField', 'recentchanges', 'rc_params', 'TEXT' ),
+ array( 'addPgField', 'redirect', 'rd_interwiki', 'TEXT NULL' ),
+ array( 'addPgField', 'redirect', 'rd_fragment', 'TEXT NULL' ),
+ array( 'addPgField', 'revision', 'rev_deleted', 'SMALLINT NOT NULL DEFAULT 0' ),
+ array( 'addPgField', 'revision', 'rev_len', 'INTEGER' ),
+ array( 'addPgField', 'revision', 'rev_parent_id', 'INTEGER DEFAULT NULL' ),
+ array( 'addPgField', 'revision', 'rev_content_model', 'TEXT' ),
+ array( 'addPgField', 'revision', 'rev_content_format', 'TEXT' ),
+ array( 'addPgField', 'site_stats', 'ss_active_users', "INTEGER DEFAULT '-1'" ),
+ array( 'addPgField', 'user_newtalk', 'user_last_timestamp', 'TIMESTAMPTZ' ),
+ array( 'addPgField', 'logging', 'log_user_text', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'logging', 'log_page', 'INTEGER' ),
+ array( 'addPgField', 'interwiki', 'iw_api', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'interwiki', 'iw_wikiid', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'revision', 'rev_sha1', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'archive', 'ar_sha1', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'uploadstash', 'us_chunk_inx', "INTEGER NULL" ),
+ array( 'addPgField', 'job', 'job_timestamp', "TIMESTAMPTZ" ),
+ array( 'addPgField', 'job', 'job_random', "INTEGER NOT NULL DEFAULT 0" ),
+ array( 'addPgField', 'job', 'job_attempts', "INTEGER NOT NULL DEFAULT 0" ),
+ array( 'addPgField', 'job', 'job_token', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'job', 'job_token_timestamp', "TIMESTAMPTZ" ),
+ array( 'addPgField', 'job', 'job_sha1', "TEXT NOT NULL DEFAULT ''" ),
+ array( 'addPgField', 'archive', 'ar_id',
+ "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('archive_ar_id_seq')" ),
+ array( 'addPgField', 'externallinks', 'el_id',
+ "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('externallinks_el_id_seq')" ),
# type changes
- array( 'changeField', 'archive', 'ar_deleted', 'smallint', '' ),
- array( 'changeField', 'archive', 'ar_minor_edit', 'smallint', 'ar_minor_edit::smallint DEFAULT 0' ),
- array( 'changeField', 'filearchive', 'fa_deleted', 'smallint', '' ),
- array( 'changeField', 'filearchive', 'fa_height', 'integer', '' ),
- array( 'changeField', 'filearchive', 'fa_metadata', 'bytea', "decode(fa_metadata,'escape')" ),
- array( 'changeField', 'filearchive', 'fa_size', 'integer', '' ),
- array( 'changeField', 'filearchive', 'fa_width', 'integer', '' ),
- array( 'changeField', 'filearchive', 'fa_storage_group', 'text', '' ),
- array( 'changeField', 'filearchive', 'fa_storage_key', 'text', '' ),
- array( 'changeField', 'image', 'img_metadata', 'bytea', "decode(img_metadata,'escape')" ),
- array( 'changeField', 'image', 'img_size', 'integer', '' ),
- array( 'changeField', 'image', 'img_width', 'integer', '' ),
- array( 'changeField', 'image', 'img_height', 'integer', '' ),
- array( 'changeField', 'interwiki', 'iw_local', 'smallint', 'iw_local::smallint' ),
- array( 'changeField', 'interwiki', 'iw_trans', 'smallint', 'iw_trans::smallint DEFAULT 0' ),
- array( 'changeField', 'ipblocks', 'ipb_auto', 'smallint', 'ipb_auto::smallint DEFAULT 0' ),
- array( 'changeField', 'ipblocks', 'ipb_anon_only', 'smallint', "CASE WHEN ipb_anon_only=' ' THEN 0 ELSE ipb_anon_only::smallint END DEFAULT 0" ),
- array( 'changeField', 'ipblocks', 'ipb_create_account', 'smallint', "CASE WHEN ipb_create_account=' ' THEN 0 ELSE ipb_create_account::smallint END DEFAULT 1" ),
- array( 'changeField', 'ipblocks', 'ipb_enable_autoblock', 'smallint', "CASE WHEN ipb_enable_autoblock=' ' THEN 0 ELSE ipb_enable_autoblock::smallint END DEFAULT 1" ),
- array( 'changeField', 'ipblocks', 'ipb_block_email', 'smallint', "CASE WHEN ipb_block_email=' ' THEN 0 ELSE ipb_block_email::smallint END DEFAULT 0" ),
- array( 'changeField', 'ipblocks', 'ipb_address', 'text', 'ipb_address::text' ),
- array( 'changeField', 'ipblocks', 'ipb_deleted', 'smallint', 'ipb_deleted::smallint DEFAULT 0' ),
- array( 'changeField', 'mwuser', 'user_token', 'text', '' ),
- array( 'changeField', 'mwuser', 'user_email_token', 'text', '' ),
- array( 'changeField', 'objectcache', 'keyname', 'text', '' ),
- array( 'changeField', 'oldimage', 'oi_height', 'integer', '' ),
- array( 'changeField', 'oldimage', 'oi_metadata', 'bytea', "decode(img_metadata,'escape')" ),
- array( 'changeField', 'oldimage', 'oi_size', 'integer', '' ),
- array( 'changeField', 'oldimage', 'oi_width', 'integer', '' ),
- array( 'changeField', 'page', 'page_is_redirect', 'smallint', 'page_is_redirect::smallint DEFAULT 0' ),
- array( 'changeField', 'page', 'page_is_new', 'smallint', 'page_is_new::smallint DEFAULT 0' ),
- array( 'changeField', 'querycache', 'qc_value', 'integer', '' ),
- array( 'changeField', 'querycachetwo', 'qcc_value', 'integer', '' ),
- array( 'changeField', 'recentchanges', 'rc_bot', 'smallint', 'rc_bot::smallint DEFAULT 0' ),
- array( 'changeField', 'recentchanges', 'rc_deleted', 'smallint', '' ),
- array( 'changeField', 'recentchanges', 'rc_minor', 'smallint', 'rc_minor::smallint DEFAULT 0' ),
- array( 'changeField', 'recentchanges', 'rc_new', 'smallint', 'rc_new::smallint DEFAULT 0' ),
- array( 'changeField', 'recentchanges', 'rc_type', 'smallint', 'rc_type::smallint DEFAULT 0' ),
- array( 'changeField', 'recentchanges', 'rc_patrolled', 'smallint', 'rc_patrolled::smallint DEFAULT 0' ),
- array( 'changeField', 'revision', 'rev_deleted', 'smallint', 'rev_deleted::smallint DEFAULT 0' ),
- array( 'changeField', 'revision', 'rev_minor_edit', 'smallint', 'rev_minor_edit::smallint DEFAULT 0' ),
- array( 'changeField', 'templatelinks', 'tl_namespace', 'smallint', 'tl_namespace::smallint' ),
- array( 'changeField', 'user_newtalk', 'user_ip', 'text', 'host(user_ip)' ),
- array( 'changeField', 'uploadstash', 'us_image_bits', 'smallint', '' ),
+ array( 'changeField', 'archive', 'ar_deleted', 'smallint', '' ),
+ array( 'changeField', 'archive', 'ar_minor_edit', 'smallint',
+ 'ar_minor_edit::smallint DEFAULT 0' ),
+ array( 'changeField', 'filearchive', 'fa_deleted', 'smallint', '' ),
+ array( 'changeField', 'filearchive', 'fa_height', 'integer', '' ),
+ array( 'changeField', 'filearchive', 'fa_metadata', 'bytea', "decode(fa_metadata,'escape')" ),
+ array( 'changeField', 'filearchive', 'fa_size', 'integer', '' ),
+ array( 'changeField', 'filearchive', 'fa_width', 'integer', '' ),
+ array( 'changeField', 'filearchive', 'fa_storage_group', 'text', '' ),
+ array( 'changeField', 'filearchive', 'fa_storage_key', 'text', '' ),
+ array( 'changeField', 'image', 'img_metadata', 'bytea', "decode(img_metadata,'escape')" ),
+ array( 'changeField', 'image', 'img_size', 'integer', '' ),
+ array( 'changeField', 'image', 'img_width', 'integer', '' ),
+ array( 'changeField', 'image', 'img_height', 'integer', '' ),
+ array( 'changeField', 'interwiki', 'iw_local', 'smallint', 'iw_local::smallint' ),
+ array( 'changeField', 'interwiki', 'iw_trans', 'smallint', 'iw_trans::smallint DEFAULT 0' ),
+ array( 'changeField', 'ipblocks', 'ipb_auto', 'smallint', 'ipb_auto::smallint DEFAULT 0' ),
+ array( 'changeField', 'ipblocks', 'ipb_anon_only', 'smallint',
+ "CASE WHEN ipb_anon_only=' ' THEN 0 ELSE ipb_anon_only::smallint END DEFAULT 0" ),
+ array( 'changeField', 'ipblocks', 'ipb_create_account', 'smallint',
+ "CASE WHEN ipb_create_account=' ' THEN 0 ELSE ipb_create_account::smallint END DEFAULT 1" ),
+ array( 'changeField', 'ipblocks', 'ipb_enable_autoblock', 'smallint',
+ "CASE WHEN ipb_enable_autoblock=' ' THEN 0 ELSE ipb_enable_autoblock::smallint END DEFAULT 1" ),
+ array( 'changeField', 'ipblocks', 'ipb_block_email', 'smallint',
+ "CASE WHEN ipb_block_email=' ' THEN 0 ELSE ipb_block_email::smallint END DEFAULT 0" ),
+ array( 'changeField', 'ipblocks', 'ipb_address', 'text', 'ipb_address::text' ),
+ array( 'changeField', 'ipblocks', 'ipb_deleted', 'smallint', 'ipb_deleted::smallint DEFAULT 0' ),
+ array( 'changeField', 'mwuser', 'user_token', 'text', '' ),
+ array( 'changeField', 'mwuser', 'user_email_token', 'text', '' ),
+ array( 'changeField', 'objectcache', 'keyname', 'text', '' ),
+ array( 'changeField', 'oldimage', 'oi_height', 'integer', '' ),
+ array( 'changeField', 'oldimage', 'oi_metadata', 'bytea', "decode(img_metadata,'escape')" ),
+ array( 'changeField', 'oldimage', 'oi_size', 'integer', '' ),
+ array( 'changeField', 'oldimage', 'oi_width', 'integer', '' ),
+ array( 'changeField', 'page', 'page_is_redirect', 'smallint',
+ 'page_is_redirect::smallint DEFAULT 0' ),
+ array( 'changeField', 'page', 'page_is_new', 'smallint', 'page_is_new::smallint DEFAULT 0' ),
+ array( 'changeField', 'querycache', 'qc_value', 'integer', '' ),
+ array( 'changeField', 'querycachetwo', 'qcc_value', 'integer', '' ),
+ array( 'changeField', 'recentchanges', 'rc_bot', 'smallint', 'rc_bot::smallint DEFAULT 0' ),
+ array( 'changeField', 'recentchanges', 'rc_deleted', 'smallint', '' ),
+ array( 'changeField', 'recentchanges', 'rc_minor', 'smallint', 'rc_minor::smallint DEFAULT 0' ),
+ array( 'changeField', 'recentchanges', 'rc_new', 'smallint', 'rc_new::smallint DEFAULT 0' ),
+ array( 'changeField', 'recentchanges', 'rc_type', 'smallint', 'rc_type::smallint DEFAULT 0' ),
+ array( 'changeField', 'recentchanges', 'rc_patrolled', 'smallint',
+ 'rc_patrolled::smallint DEFAULT 0' ),
+ array( 'changeField', 'revision', 'rev_deleted', 'smallint', 'rev_deleted::smallint DEFAULT 0' ),
+ array( 'changeField', 'revision', 'rev_minor_edit', 'smallint',
+ 'rev_minor_edit::smallint DEFAULT 0' ),
+ array( 'changeField', 'templatelinks', 'tl_namespace', 'smallint', 'tl_namespace::smallint' ),
+ array( 'changeField', 'user_newtalk', 'user_ip', 'text', 'host(user_ip)' ),
+ array( 'changeField', 'uploadstash', 'us_image_bits', 'smallint', '' ),
+ array( 'changeField', 'profiling', 'pf_time', 'float', '' ),
+ array( 'changeField', 'profiling', 'pf_memory', 'float', '' ),
# null changes
- array( 'changeNullableField', 'oldimage', 'oi_bits', 'NULL' ),
- array( 'changeNullableField', 'oldimage', 'oi_timestamp', 'NULL' ),
+ array( 'changeNullableField', 'oldimage', 'oi_bits', 'NULL' ),
+ array( 'changeNullableField', 'oldimage', 'oi_timestamp', 'NULL' ),
array( 'changeNullableField', 'oldimage', 'oi_major_mime', 'NULL' ),
array( 'changeNullableField', 'oldimage', 'oi_minor_mime', 'NULL' ),
- array( 'changeNullableField', 'image', 'img_metadata', 'NOT NULL'),
- array( 'changeNullableField', 'filearchive', 'fa_metadata', 'NOT NULL'),
+ array( 'changeNullableField', 'image', 'img_metadata', 'NOT NULL' ),
+ array( 'changeNullableField', 'filearchive', 'fa_metadata', 'NOT NULL' ),
array( 'changeNullableField', 'recentchanges', 'rc_cur_id', 'NULL' ),
array( 'checkOiDeleted' ),
# New indexes
- array( 'addPgIndex', 'archive', 'archive_user_text', '(ar_user_text)' ),
- array( 'addPgIndex', 'image', 'img_sha1', '(img_sha1)' ),
- array( 'addPgIndex', 'ipblocks', 'ipb_parent_block_id', '(ipb_parent_block_id)' ),
- array( 'addPgIndex', 'oldimage', 'oi_sha1', '(oi_sha1)' ),
- array( 'addPgIndex', 'page', 'page_mediawiki_title', '(page_title) WHERE page_namespace = 8' ),
- array( 'addPgIndex', 'pagelinks', 'pagelinks_title', '(pl_title)' ),
- array( 'addPgIndex', 'revision', 'rev_text_id_idx', '(rev_text_id)' ),
- array( 'addPgIndex', 'recentchanges', 'rc_timestamp_bot', '(rc_timestamp) WHERE rc_bot = 0' ),
- array( 'addPgIndex', 'templatelinks', 'templatelinks_from', '(tl_from)' ),
- array( 'addPgIndex', 'watchlist', 'wl_user', '(wl_user)' ),
- array( 'addPgIndex', 'logging', 'logging_user_type_time', '(log_user, log_type, log_timestamp)' ),
- array( 'addPgIndex', 'logging', 'logging_page_id_time', '(log_page,log_timestamp)' ),
- array( 'addPgIndex', 'iwlinks', 'iwl_prefix_title_from', '(iwl_prefix, iwl_title, iwl_from)' ),
- array( 'addPgIndex', 'job', 'job_timestamp_idx', '(job_timestamp)' ),
+ array( 'addPgIndex', 'archive', 'archive_user_text', '(ar_user_text)' ),
+ array( 'addPgIndex', 'image', 'img_sha1', '(img_sha1)' ),
+ array( 'addPgIndex', 'ipblocks', 'ipb_parent_block_id', '(ipb_parent_block_id)' ),
+ array( 'addPgIndex', 'oldimage', 'oi_sha1', '(oi_sha1)' ),
+ array( 'addPgIndex', 'page', 'page_mediawiki_title', '(page_title) WHERE page_namespace = 8' ),
+ array( 'addPgIndex', 'pagelinks', 'pagelinks_title', '(pl_title)' ),
+ array( 'addPgIndex', 'page_props', 'pp_propname_page', '(pp_propname, pp_page)' ),
+ array( 'addPgIndex', 'revision', 'rev_text_id_idx', '(rev_text_id)' ),
+ array( 'addPgIndex', 'recentchanges', 'rc_timestamp_bot', '(rc_timestamp) WHERE rc_bot = 0' ),
+ array( 'addPgIndex', 'templatelinks', 'templatelinks_from', '(tl_from)' ),
+ array( 'addPgIndex', 'watchlist', 'wl_user', '(wl_user)' ),
+ array( 'addPgIndex', 'logging', 'logging_user_type_time',
+ '(log_user, log_type, log_timestamp)' ),
+ array( 'addPgIndex', 'logging', 'logging_page_id_time', '(log_page,log_timestamp)' ),
+ array( 'addPgIndex', 'iwlinks', 'iwl_prefix_from_title', '(iwl_prefix, iwl_from, iwl_title)' ),
+ array( 'addPgIndex', 'iwlinks', 'iwl_prefix_title_from', '(iwl_prefix, iwl_title, iwl_from)' ),
+ array( 'addPgIndex', 'job', 'job_timestamp_idx', '(job_timestamp)' ),
+ array( 'addPgIndex', 'job', 'job_sha1', '(job_sha1)' ),
+ array( 'addPgIndex', 'job', 'job_cmd_token', '(job_cmd, job_token, job_random)' ),
+ array( 'addPgIndex', 'job', 'job_cmd_token_id', '(job_cmd, job_token, job_id)' ),
+ array( 'addPgIndex', 'filearchive', 'fa_sha1', '(fa_sha1)' ),
array( 'checkIndex', 'pagelink_unique', array(
- array('pl_from', 'int4_ops', 'btree', 0),
- array('pl_namespace', 'int2_ops', 'btree', 0),
- array('pl_title', 'text_ops', 'btree', 0),
+ array( 'pl_from', 'int4_ops', 'btree', 0 ),
+ array( 'pl_namespace', 'int2_ops', 'btree', 0 ),
+ array( 'pl_title', 'text_ops', 'btree', 0 ),
),
- 'CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title)' ),
+ 'CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title)' ),
array( 'checkIndex', 'cl_sortkey', array(
- array('cl_to', 'text_ops', 'btree', 0),
- array('cl_sortkey', 'text_ops', 'btree', 0),
- array('cl_from', 'int4_ops', 'btree', 0),
+ array( 'cl_to', 'text_ops', 'btree', 0 ),
+ array( 'cl_sortkey', 'text_ops', 'btree', 0 ),
+ array( 'cl_from', 'int4_ops', 'btree', 0 ),
),
- 'CREATE INDEX cl_sortkey ON "categorylinks" USING "btree" ("cl_to", "cl_sortkey", "cl_from")' ),
+ 'CREATE INDEX cl_sortkey ON "categorylinks" ' .
+ 'USING "btree" ("cl_to", "cl_sortkey", "cl_from")' ),
+ array( 'checkIndex', 'iwl_prefix_title_from', array(
+ array( 'iwl_prefix', 'text_ops', 'btree', 0 ),
+ array( 'iwl_title', 'text_ops', 'btree', 0 ),
+ array( 'iwl_from', 'int4_ops', 'btree', 0 ),
+ ),
+ 'CREATE INDEX iwl_prefix_title_from ON "iwlinks" ' .
+ 'USING "btree" ("iwl_prefix", "iwl_title", "iwl_from")' ),
array( 'checkIndex', 'logging_times', array(
- array('log_timestamp', 'timestamptz_ops', 'btree', 0),
+ array( 'log_timestamp', 'timestamptz_ops', 'btree', 0 ),
),
'CREATE INDEX "logging_times" ON "logging" USING "btree" ("log_timestamp")' ),
array( 'dropIndex', 'oldimage', 'oi_name' ),
array( 'checkIndex', 'oi_name_archive_name', array(
- array('oi_name', 'text_ops', 'btree', 0),
- array('oi_archive_name', 'text_ops', 'btree', 0),
+ array( 'oi_name', 'text_ops', 'btree', 0 ),
+ array( 'oi_archive_name', 'text_ops', 'btree', 0 ),
),
- 'CREATE INDEX "oi_name_archive_name" ON "oldimage" USING "btree" ("oi_name", "oi_archive_name")' ),
+ 'CREATE INDEX "oi_name_archive_name" ON "oldimage" ' .
+ 'USING "btree" ("oi_name", "oi_archive_name")' ),
array( 'checkIndex', 'oi_name_timestamp', array(
- array('oi_name', 'text_ops', 'btree', 0),
- array('oi_timestamp', 'timestamptz_ops', 'btree', 0),
+ array( 'oi_name', 'text_ops', 'btree', 0 ),
+ array( 'oi_timestamp', 'timestamptz_ops', 'btree', 0 ),
),
- 'CREATE INDEX "oi_name_timestamp" ON "oldimage" USING "btree" ("oi_name", "oi_timestamp")' ),
+ 'CREATE INDEX "oi_name_timestamp" ON "oldimage" ' .
+ 'USING "btree" ("oi_name", "oi_timestamp")' ),
array( 'checkIndex', 'page_main_title', array(
- array('page_title', 'text_pattern_ops', 'btree', 0),
+ array( 'page_title', 'text_pattern_ops', 'btree', 0 ),
),
- 'CREATE INDEX "page_main_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 0)' ),
+ 'CREATE INDEX "page_main_title" ON "page" ' .
+ 'USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 0)' ),
array( 'checkIndex', 'page_mediawiki_title', array(
- array('page_title', 'text_pattern_ops', 'btree', 0),
+ array( 'page_title', 'text_pattern_ops', 'btree', 0 ),
),
- 'CREATE INDEX "page_mediawiki_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 8)' ),
+ 'CREATE INDEX "page_mediawiki_title" ON "page" ' .
+ 'USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 8)' ),
array( 'checkIndex', 'page_project_title', array(
- array('page_title', 'text_pattern_ops', 'btree', 0),
+ array( 'page_title', 'text_pattern_ops', 'btree', 0 ),
),
- 'CREATE INDEX "page_project_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 4)' ),
+ 'CREATE INDEX "page_project_title" ON "page" ' .
+ 'USING "btree" ("page_title" "text_pattern_ops") ' .
+ 'WHERE ("page_namespace" = 4)' ),
array( 'checkIndex', 'page_talk_title', array(
- array('page_title', 'text_pattern_ops', 'btree', 0),
+ array( 'page_title', 'text_pattern_ops', 'btree', 0 ),
),
- 'CREATE INDEX "page_talk_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 1)' ),
+ 'CREATE INDEX "page_talk_title" ON "page" ' .
+ 'USING "btree" ("page_title" "text_pattern_ops") ' .
+ 'WHERE ("page_namespace" = 1)' ),
array( 'checkIndex', 'page_user_title', array(
- array('page_title', 'text_pattern_ops', 'btree', 0),
+ array( 'page_title', 'text_pattern_ops', 'btree', 0 ),
),
- 'CREATE INDEX "page_user_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 2)' ),
+ 'CREATE INDEX "page_user_title" ON "page" ' .
+ 'USING "btree" ("page_title" "text_pattern_ops") WHERE ' .
+ '("page_namespace" = 2)' ),
array( 'checkIndex', 'page_utalk_title', array(
- array('page_title', 'text_pattern_ops', 'btree', 0),
+ array( 'page_title', 'text_pattern_ops', 'btree', 0 ),
),
- 'CREATE INDEX "page_utalk_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 3)' ),
+ 'CREATE INDEX "page_utalk_title" ON "page" ' .
+ 'USING "btree" ("page_title" "text_pattern_ops") ' .
+ 'WHERE ("page_namespace" = 3)' ),
array( 'checkIndex', 'ts2_page_text', array(
- array('textvector', 'tsvector_ops', 'gist', 0),
+ array( 'textvector', 'tsvector_ops', 'gist', 0 ),
),
'CREATE INDEX "ts2_page_text" ON "pagecontent" USING "gist" ("textvector")' ),
array( 'checkIndex', 'ts2_page_title', array(
- array('titlevector', 'tsvector_ops', 'gist', 0),
+ array( 'titlevector', 'tsvector_ops', 'gist', 0 ),
),
'CREATE INDEX "ts2_page_title" ON "page" USING "gist" ("titlevector")' ),
array( 'checkOiNameConstraint' ),
array( 'checkPageDeletedTrigger' ),
array( 'checkRevUserFkey' ),
- array( 'dropIndex', 'ipblocks', 'ipb_address'),
+ array( 'dropIndex', 'ipblocks', 'ipb_address' ),
array( 'checkIndex', 'ipb_address_unique', array(
- array('ipb_address', 'text_ops', 'btree', 0),
- array('ipb_user', 'int4_ops', 'btree', 0),
- array('ipb_auto', 'int2_ops', 'btree', 0),
- array('ipb_anon_only', 'int2_ops', 'btree', 0),
+ array( 'ipb_address', 'text_ops', 'btree', 0 ),
+ array( 'ipb_user', 'int4_ops', 'btree', 0 ),
+ array( 'ipb_auto', 'int2_ops', 'btree', 0 ),
+ array( 'ipb_anon_only', 'int2_ops', 'btree', 0 ),
),
- 'CREATE UNIQUE INDEX ipb_address_unique ON ipblocks (ipb_address,ipb_user,ipb_auto,ipb_anon_only)' ),
+ 'CREATE UNIQUE INDEX ipb_address_unique ' .
+ 'ON ipblocks (ipb_address,ipb_user,ipb_auto,ipb_anon_only)' ),
array( 'checkIwlPrefix' ),
# All FK columns should be deferred
- array( 'changeFkeyDeferrable', 'archive', 'ar_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'categorylinks', 'cl_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'externallinks', 'el_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'filearchive', 'fa_deleted_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'filearchive', 'fa_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'image', 'img_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'imagelinks', 'il_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_by', 'mwuser(user_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_parent_block_id', 'ipblocks(ipb_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'langlinks', 'll_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'logging', 'log_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'oldimage', 'oi_name', 'image(img_name) ON DELETE CASCADE ON UPDATE CASCADE' ),
- array( 'changeFkeyDeferrable', 'oldimage', 'oi_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'pagelinks', 'pl_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'page_props', 'pp_page', 'page (page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'page_restrictions', 'pr_page', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'protected_titles', 'pt_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'recentchanges', 'rc_cur_id', 'page(page_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'recentchanges', 'rc_user', 'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'redirect', 'rd_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'revision', 'rev_page', 'page (page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'revision', 'rev_user', 'mwuser(user_id) ON DELETE RESTRICT' ),
- array( 'changeFkeyDeferrable', 'templatelinks', 'tl_from', 'page(page_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'user_groups', 'ug_user', 'mwuser(user_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'user_newtalk', 'user_id', 'mwuser(user_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'user_properties', 'up_user', 'mwuser(user_id) ON DELETE CASCADE' ),
- array( 'changeFkeyDeferrable', 'watchlist', 'wl_user', 'mwuser(user_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'archive', 'ar_user', 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'categorylinks', 'cl_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'externallinks', 'el_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'filearchive', 'fa_deleted_user',
+ 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'filearchive', 'fa_user', 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'image', 'img_user', 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'imagelinks', 'il_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_by', 'mwuser(user_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_user', 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_parent_block_id',
+ 'ipblocks(ipb_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'langlinks', 'll_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'logging', 'log_user', 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'oldimage', 'oi_name',
+ 'image(img_name) ON DELETE CASCADE ON UPDATE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'oldimage', 'oi_user', 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'pagelinks', 'pl_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'page_props', 'pp_page', 'page (page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'page_restrictions', 'pr_page',
+ 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'protected_titles', 'pt_user',
+ 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'recentchanges', 'rc_cur_id',
+ 'page(page_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'recentchanges', 'rc_user',
+ 'mwuser(user_id) ON DELETE SET NULL' ),
+ array( 'changeFkeyDeferrable', 'redirect', 'rd_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'revision', 'rev_page', 'page (page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'revision', 'rev_user', 'mwuser(user_id) ON DELETE RESTRICT' ),
+ array( 'changeFkeyDeferrable', 'templatelinks', 'tl_from', 'page(page_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'user_groups', 'ug_user', 'mwuser(user_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'user_newtalk', 'user_id', 'mwuser(user_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'user_properties', 'up_user',
+ 'mwuser(user_id) ON DELETE CASCADE' ),
+ array( 'changeFkeyDeferrable', 'watchlist', 'wl_user', 'mwuser(user_id) ON DELETE CASCADE' ),
# r81574
array( 'addInterwikiType' ),
@@ -348,25 +415,25 @@ class PostgresUpdater extends DatabaseUpdater {
# Add missing extension fields
foreach ( $wgExtPGNewFields as $fieldRecord ) {
$updates[] = array(
- 'addPgField', $fieldRecord[0], $fieldRecord[1],
- $fieldRecord[2]
- );
+ 'addPgField', $fieldRecord[0], $fieldRecord[1],
+ $fieldRecord[2]
+ );
}
# Change altered columns
foreach ( $wgExtPGAlteredFields as $fieldRecord ) {
$updates[] = array(
- 'changeField', $fieldRecord[0], $fieldRecord[1],
- $fieldRecord[2]
- );
+ 'changeField', $fieldRecord[0], $fieldRecord[1],
+ $fieldRecord[2]
+ );
}
# Add missing extension indexes
foreach ( $wgExtNewIndexes as $fieldRecord ) {
$updates[] = array(
- 'addPgExtIndex', $fieldRecord[0], $fieldRecord[1],
- $fieldRecord[2]
- );
+ 'addPgExtIndex', $fieldRecord[0], $fieldRecord[1],
+ $fieldRecord[2]
+ );
}
return $updates;
@@ -380,8 +447,8 @@ SELECT attname, attnum FROM pg_namespace, pg_class, pg_attribute
AND relname=%s AND nspname=%s
END;
$res = $this->db->query( sprintf( $q,
- $this->db->addQuotes( $table ),
- $this->db->addQuotes( $this->db->getCoreSchema() ) ) );
+ $this->db->addQuotes( $table ),
+ $this->db->addQuotes( $this->db->getCoreSchema() ) ) );
if ( !$res ) {
return null;
}
@@ -389,10 +456,11 @@ END;
$cols = array();
foreach ( $res as $r ) {
$cols[] = array(
- "name" => $r[0],
- "ord" => $r[1],
- );
+ "name" => $r[0],
+ "ord" => $r[1],
+ );
}
+
return $cols;
}
@@ -462,11 +530,12 @@ END;
if ( !( $row = $this->db->fetchRow( $r ) ) ) {
return null;
}
+
return $row[0];
}
function ruleDef( $table, $rule ) {
- $q = <<<END
+ $q = <<<END
SELECT definition FROM pg_rules
WHERE schemaname = %s
AND tablename = %s
@@ -485,6 +554,7 @@ END;
return null;
}
$d = $row[0];
+
return $d;
}
@@ -492,8 +562,8 @@ END;
if ( !$this->db->sequenceExists( $ns ) ) {
$this->output( "Creating sequence $ns\n" );
$this->db->query( "CREATE SEQUENCE $ns" );
- if( $pkey !== false ) {
- $this->setDefault( $table, $pkey, '"nextval"(\'"' . $ns . '"\'::"regclass")' );
+ if ( $pkey !== false ) {
+ $this->setDefault( $table, $pkey, '"nextval"(\'"' . $ns . '"\'::"regclass")' );
}
}
}
@@ -501,6 +571,7 @@ END;
protected function renameSequence( $old, $new ) {
if ( $this->db->sequenceExists( $new ) ) {
$this->output( "...sequence $new already exists.\n" );
+
return;
}
if ( $this->db->sequenceExists( $old ) ) {
@@ -515,23 +586,51 @@ END;
$old = $this->db->realTableName( $old, "quoted" );
$new = $this->db->realTableName( $new, "quoted" );
$this->db->query( "ALTER TABLE $old RENAME TO $new" );
- if( $patch !== false ) {
+ if ( $patch !== false ) {
$this->applyPatch( $patch );
}
}
}
- protected function renameIndex( $table, $old, $new ) {
- if ( $this->db->indexExists( $table, $old ) ) {
- $this->output( "Renaming index $old to $new\n" );
- $this->db->query( "ALTER INDEX $old RENAME TO $new" );
+ protected function renameIndex(
+ $table, $old, $new, $skipBothIndexExistWarning = false, $a = false, $b = false
+ ) {
+ // First requirement: the table must exist
+ if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+ $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
+
+ return;
+ }
+
+ // Second requirement: the new index must be missing
+ if ( $this->db->indexExists( $table, $new, __METHOD__ ) ) {
+ $this->output( "...index $new already set on $table table.\n" );
+ if ( !$skipBothIndexExistWarning
+ && $this->db->indexExists( $table, $old, __METHOD__ )
+ ) {
+ $this->output( "...WARNING: $old still exists, despite it has been " .
+ "renamed into $new (which also exists).\n" .
+ " $old should be manually removed if not needed anymore.\n" );
+ }
+
+ return;
+ }
+
+ // Third requirement: the old index must exist
+ if ( !$this->db->indexExists( $table, $old, __METHOD__ ) ) {
+ $this->output( "...skipping: index $old doesn't exist.\n" );
+
+ return;
}
+
+ $this->db->query( "ALTER INDEX $old RENAME TO $new" );
}
protected function addPgField( $table, $field, $type ) {
$fi = $this->db->fieldInfo( $table, $field );
if ( !is_null( $fi ) ) {
$this->output( "...column '$table.$field' already exists\n" );
+
return;
} else {
$this->output( "Adding column '$table.$field'\n" );
@@ -546,9 +645,9 @@ END;
exit( 1 );
}
- if ( $fi->type() === $newtype )
+ if ( $fi->type() === $newtype ) {
$this->output( "...column '$table.$field' is already of type '$newtype'\n" );
- else {
+ } else {
$this->output( "Changing column type of '$table.$field' from '{$fi->type()}' to '$newtype'\n" );
$sql = "ALTER TABLE $table ALTER $field TYPE $newtype";
if ( strlen( $default ) ) {
@@ -573,7 +672,7 @@ END;
}
}
- protected function changeNullableField( $table, $field, $null) {
+ protected function changeNullableField( $table, $field, $null ) {
$fi = $this->db->fieldInfo( $table, $field );
if ( is_null( $fi ) ) {
$this->output( "...ERROR: expected column $table.$field to exist\n" );
@@ -592,8 +691,7 @@ END;
if ( 'NULL' === $null ) {
$this->output( "Changing '$table.$field' to allow NULLs\n" );
$this->db->query( "ALTER TABLE $table ALTER $field DROP NOT NULL" );
- }
- else {
+ } else {
$this->output( "...column '$table.$field' is already set as NOT NULL\n" );
}
}
@@ -624,7 +722,9 @@ END;
protected function changeFkeyDeferrable( $table, $field, $clause ) {
$fi = $this->db->fieldInfo( $table, $field );
if ( is_null( $fi ) ) {
- $this->output( "WARNING! Column '$table.$field' does not exist but it should! Please report this.\n" );
+ $this->output( "WARNING! Column '$table.$field' does not exist but it should! " .
+ "Please report this.\n" );
+
return;
}
if ( $fi->is_deferred() && $fi->is_deferrable() ) {
@@ -637,10 +737,13 @@ END;
$command = "ALTER TABLE $table DROP CONSTRAINT $conname";
$this->db->query( $command );
} else {
- $this->output( "Column '$table.$field' does not have a foreign key constraint, will be added\n" );
+ $this->output( "Column '$table.$field' does not have a foreign key " .
+ "constraint, will be added\n" );
$conclause = "";
}
- $command = "ALTER TABLE $table ADD $conclause FOREIGN KEY ($field) REFERENCES $clause DEFERRABLE INITIALLY DEFERRED";
+ $command =
+ "ALTER TABLE $table ADD $conclause " .
+ "FOREIGN KEY ($field) REFERENCES $clause DEFERRABLE INITIALLY DEFERRED";
$this->db->query( $command );
}
@@ -654,7 +757,11 @@ END;
$this->output( "Dropping rule 'archive_delete'\n" );
$this->db->query( 'DROP RULE archive_delete ON archive' );
}
- $this->applyPatch( 'patch-remove-archive2.sql', false, "Converting 'archive2' back to normal archive table" );
+ $this->applyPatch(
+ 'patch-remove-archive2.sql',
+ false,
+ "Converting 'archive2' back to normal archive table"
+ );
} else {
$this->output( "...obsolete table 'archive2' does not exist\n" );
}
@@ -664,7 +771,8 @@ END;
if ( $this->db->fieldInfo( 'oldimage', 'oi_deleted' )->type() !== 'smallint' ) {
$this->output( "Changing 'oldimage.oi_deleted' to type 'smallint'\n" );
$this->db->query( "ALTER TABLE oldimage ALTER oi_deleted DROP DEFAULT" );
- $this->db->query( "ALTER TABLE oldimage ALTER oi_deleted TYPE SMALLINT USING (oi_deleted::smallint)" );
+ $this->db->query(
+ "ALTER TABLE oldimage ALTER oi_deleted TYPE SMALLINT USING (oi_deleted::smallint)" );
$this->db->query( "ALTER TABLE oldimage ALTER oi_deleted SET DEFAULT 0" );
} else {
$this->output( "...column 'oldimage.oi_deleted' is already of type 'smallint'\n" );
@@ -673,23 +781,32 @@ END;
protected function checkOiNameConstraint() {
if ( $this->db->hasConstraint( "oldimage_oi_name_fkey_cascaded" ) ) {
- $this->output( "...table 'oldimage' has correct cascading delete/update foreign key to image\n" );
+ $this->output( "...table 'oldimage' has correct cascading delete/update " .
+ "foreign key to image\n" );
} else {
if ( $this->db->hasConstraint( "oldimage_oi_name_fkey" ) ) {
- $this->db->query( "ALTER TABLE oldimage DROP CONSTRAINT oldimage_oi_name_fkey" );
+ $this->db->query(
+ "ALTER TABLE oldimage DROP CONSTRAINT oldimage_oi_name_fkey" );
}
if ( $this->db->hasConstraint( "oldimage_oi_name_fkey_cascade" ) ) {
- $this->db->query( "ALTER TABLE oldimage DROP CONSTRAINT oldimage_oi_name_fkey_cascade" );
+ $this->db->query(
+ "ALTER TABLE oldimage DROP CONSTRAINT oldimage_oi_name_fkey_cascade" );
}
$this->output( "Making foreign key on table 'oldimage' (to image) a cascade delete/update\n" );
- $this->db->query( "ALTER TABLE oldimage ADD CONSTRAINT oldimage_oi_name_fkey_cascaded " .
- "FOREIGN KEY (oi_name) REFERENCES image(img_name) ON DELETE CASCADE ON UPDATE CASCADE" );
+ $this->db->query(
+ "ALTER TABLE oldimage ADD CONSTRAINT oldimage_oi_name_fkey_cascaded " .
+ "FOREIGN KEY (oi_name) REFERENCES image(img_name) " .
+ "ON DELETE CASCADE ON UPDATE CASCADE" );
}
}
protected function checkPageDeletedTrigger() {
if ( !$this->db->triggerExists( 'page', 'page_deleted' ) ) {
- $this->applyPatch( 'patch-page_deleted.sql', false, "Adding function and trigger 'page_deleted' to table 'page'" );
+ $this->applyPatch(
+ 'patch-page_deleted.sql',
+ false,
+ "Adding function and trigger 'page_deleted' to table 'page'"
+ );
} else {
$this->output( "...table 'page' has 'page_deleted' trigger\n" );
}
@@ -698,7 +815,7 @@ END;
protected function dropIndex( $table, $index, $patch = '', $fullpath = false ) {
if ( $this->db->indexExists( $table, $index ) ) {
$this->output( "Dropping obsolete index '$index'\n" );
- $this->db->query( "DROP INDEX \"". $index ."\"" );
+ $this->db->query( "DROP INDEX \"" . $index . "\"" );
}
}
@@ -706,7 +823,7 @@ END;
$pu = $this->db->indexAttributes( $index );
if ( !empty( $pu ) && $pu != $should_be ) {
$this->output( "Dropping obsolete version of index '$index'\n" );
- $this->db->query( "DROP INDEX \"". $index ."\"" );
+ $this->db->query( "DROP INDEX \"" . $index . "\"" );
$pu = array();
} else {
$this->output( "...no need to drop index '$index'\n" );
@@ -724,13 +841,21 @@ END;
if ( $this->fkeyDeltype( 'revision_rev_user_fkey' ) == 'r' ) {
$this->output( "...constraint 'revision_rev_user_fkey' is ON DELETE RESTRICT\n" );
} else {
- $this->applyPatch( 'patch-revision_rev_user_fkey.sql', false, "Changing constraint 'revision_rev_user_fkey' to ON DELETE RESTRICT" );
+ $this->applyPatch(
+ 'patch-revision_rev_user_fkey.sql',
+ false,
+ "Changing constraint 'revision_rev_user_fkey' to ON DELETE RESTRICT"
+ );
}
}
protected function checkIwlPrefix() {
if ( $this->db->indexExists( 'iwlinks', 'iwl_prefix' ) ) {
- $this->applyPatch( 'patch-rename-iwl_prefix.sql', false, "Replacing index 'iwl_prefix' with 'iwl_prefix_from_title'" );
+ $this->applyPatch(
+ 'patch-rename-iwl_prefix.sql',
+ false,
+ "Replacing index 'iwl_prefix' with 'iwl_prefix_title_from'"
+ );
}
}
diff --git a/includes/installer/SqliteInstaller.php b/includes/installer/SqliteInstaller.php
index 6e1a74f6..19218c60 100644
--- a/includes/installer/SqliteInstaller.php
+++ b/includes/installer/SqliteInstaller.php
@@ -60,9 +60,10 @@ class SqliteInstaller extends DatabaseInstaller {
$result->fatal( 'config-outdated-sqlite', $db->getServerVersion(), self::MINIMUM_VERSION );
}
// Check for FTS3 full-text search module
- if( DatabaseSqlite::getFulltextSearchModule() != 'FTS3' ) {
+ if ( DatabaseSqlite::getFulltextSearchModule() != 'FTS3' ) {
$result->warning( 'config-no-fts3' );
}
+
return $result;
}
@@ -73,6 +74,7 @@ class SqliteInstaller extends DatabaseInstaller {
DIRECTORY_SEPARATOR,
dirname( $_SERVER['DOCUMENT_ROOT'] ) . '/data'
);
+
return array( 'wgSQLiteDataDir' => $path );
} else {
return array();
@@ -80,8 +82,17 @@ class SqliteInstaller extends DatabaseInstaller {
}
public function getConnectForm() {
- return $this->getTextBox( 'wgSQLiteDataDir', 'config-sqlite-dir', array(), $this->parent->getHelpBox( 'config-sqlite-dir-help' ) ) .
- $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-sqlite-name-help' ) );
+ return $this->getTextBox(
+ 'wgSQLiteDataDir',
+ 'config-sqlite-dir', array(),
+ $this->parent->getHelpBox( 'config-sqlite-dir-help' )
+ ) .
+ $this->getTextBox(
+ 'wgDBname',
+ 'config-db-name',
+ array(),
+ $this->parent->getHelpBox( 'config-sqlite-name-help' )
+ );
}
/**
@@ -96,6 +107,7 @@ class SqliteInstaller extends DatabaseInstaller {
if ( !$result ) {
return $path;
}
+
return $result;
}
@@ -113,6 +125,9 @@ class SqliteInstaller extends DatabaseInstaller {
$dir = self::realpath( $dir );
$this->setVar( 'wgSQLiteDataDir', $dir );
}
+ # Table prefix is not used on SQLite, keep it empty
+ $this->setVar( 'wgDBprefix', '' );
+
return $result;
}
@@ -126,9 +141,16 @@ class SqliteInstaller extends DatabaseInstaller {
if ( !is_writable( dirname( $dir ) ) ) {
$webserverGroup = Installer::maybeGetWebserverPrimaryGroup();
if ( $webserverGroup !== null ) {
- return Status::newFatal( 'config-sqlite-parent-unwritable-group', $dir, dirname( $dir ), basename( $dir ), $webserverGroup );
+ return Status::newFatal(
+ 'config-sqlite-parent-unwritable-group',
+ $dir, dirname( $dir ), basename( $dir ),
+ $webserverGroup
+ );
} else {
- return Status::newFatal( 'config-sqlite-parent-unwritable-nogroup', $dir, dirname( $dir ), basename( $dir ) );
+ return Status::newFatal(
+ 'config-sqlite-parent-unwritable-nogroup',
+ $dir, dirname( $dir ), basename( $dir )
+ );
}
}
@@ -171,6 +193,7 @@ class SqliteInstaller extends DatabaseInstaller {
} catch ( DBConnectionError $e ) {
$status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
}
+
return $status;
}
@@ -218,6 +241,7 @@ class SqliteInstaller extends DatabaseInstaller {
$this->setVar( 'wgDBuser', '' );
$this->setVar( 'wgDBpassword', '' );
$this->setupSchemaVars();
+
return $this->getConnection();
}
@@ -226,6 +250,7 @@ class SqliteInstaller extends DatabaseInstaller {
*/
public function createTables() {
$status = parent::createTables();
+
return $this->setupSearchIndex( $status );
}
@@ -238,12 +263,13 @@ class SqliteInstaller extends DatabaseInstaller {
$module = DatabaseSqlite::getFulltextSearchModule();
$fts3tTable = $this->db->checkForEnabledSearch();
- if ( $fts3tTable && !$module ) {
+ if ( $fts3tTable && !$module ) {
$status->warning( 'config-sqlite-fts3-downgrade' );
$this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-no-fts.sql" );
} elseif ( !$fts3tTable && $module == 'FTS3' ) {
$this->db->sourceFile( "$IP/maintenance/sqlite/archives/searchindex-fts3.sql" );
}
+
return $status;
}
@@ -252,8 +278,8 @@ class SqliteInstaller extends DatabaseInstaller {
*/
public function getLocalSettings() {
$dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) );
- return
-"# SQLite-specific settings
-\$wgSQLiteDataDir = \"{$dir}\";";
+
+ return "# SQLite-specific settings
+\$wgSQLiteDataDir = \"{$dir}\";";
}
}
diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php
index 12a310af..78ca5110 100644
--- a/includes/installer/SqliteUpdater.php
+++ b/includes/installer/SqliteUpdater.php
@@ -32,73 +32,99 @@ class SqliteUpdater extends DatabaseUpdater {
protected function getCoreUpdateList() {
return array(
// 1.14
- array( 'addField', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ),
+ array( 'addField', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ),
array( 'doActiveUsersInit' ),
- array( 'addField', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ),
+ array( 'addField', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ),
array( 'sqliteInitialIndexes' ),
// 1.15
- array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
- array( 'addTable', 'tag_summary', 'patch-change_tag.sql' ),
- array( 'addTable', 'valid_tag', 'patch-change_tag.sql' ),
+ array( 'addTable', 'change_tag', 'patch-change_tag.sql' ),
+ array( 'addTable', 'tag_summary', 'patch-tag_summary.sql' ),
+ array( 'addTable', 'valid_tag', 'patch-valid_tag.sql' ),
// 1.16
- array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
- array( 'addTable', 'log_search', 'patch-log_search.sql' ),
- array( 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ),
- array( 'doLogUsertextPopulation' ), # listed separately from the previous update because 1.16 was released without this update
+ array( 'addTable', 'user_properties', 'patch-user_properties.sql' ),
+ array( 'addTable', 'log_search', 'patch-log_search.sql' ),
+ array( 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ),
+ # listed separately from the previous update because 1.16 was released without this update
+ array( 'doLogUsertextPopulation' ),
array( 'doLogSearchPopulation' ),
- array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
- array( 'addTable', 'external_user', 'patch-external_user.sql' ),
- array( 'addIndex', 'log_search', 'ls_field_val', 'patch-log_search-rename-index.sql' ),
- array( 'addIndex', 'change_tag', 'change_tag_rc_tag', 'patch-change_tag-indexes.sql' ),
- array( 'addField', 'redirect', 'rd_interwiki', 'patch-rd_interwiki.sql' ),
+ array( 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ),
+ array( 'addIndex', 'log_search', 'ls_field_val', 'patch-log_search-rename-index.sql' ),
+ array( 'addIndex', 'change_tag', 'change_tag_rc_tag', 'patch-change_tag-indexes.sql' ),
+ array( 'addField', 'redirect', 'rd_interwiki', 'patch-rd_interwiki.sql' ),
array( 'doUpdateTranscacheField' ),
array( 'sqliteSetupSearchindex' ),
// 1.17
- array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
- array( 'addIndex', 'iwlinks', 'iwl_prefix_title_from', 'patch-rename-iwl_prefix.sql' ),
- array( 'addField', 'updatelog', 'ul_value', 'patch-ul_value.sql' ),
- array( 'addField', 'interwiki', 'iw_api', 'patch-iw_api_and_wikiid.sql' ),
- array( 'dropIndex', 'iwlinks', 'iwl_prefix', 'patch-kill-iwl_prefix.sql' ),
- array( 'dropIndex', 'iwlinks', 'iwl_prefix_from_title', 'patch-kill-iwl_pft.sql' ),
- array( 'addField', 'categorylinks', 'cl_collation', 'patch-categorylinks-better-collation.sql' ),
+ array( 'addTable', 'iwlinks', 'patch-iwlinks.sql' ),
+ array( 'addIndex', 'iwlinks', 'iwl_prefix_title_from', 'patch-rename-iwl_prefix.sql' ),
+ array( 'addField', 'updatelog', 'ul_value', 'patch-ul_value.sql' ),
+ array( 'addField', 'interwiki', 'iw_api', 'patch-iw_api_and_wikiid.sql' ),
+ array( 'dropIndex', 'iwlinks', 'iwl_prefix', 'patch-kill-iwl_prefix.sql' ),
+ array( 'addField', 'categorylinks', 'cl_collation', 'patch-categorylinks-better-collation.sql' ),
array( 'doCollationUpdate' ),
- array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
- array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
- array( 'dropIndex', 'archive', 'ar_page_revid', 'patch-archive_kill_ar_page_revid.sql' ),
- array( 'addIndex', 'archive', 'ar_revid', 'patch-archive_ar_revid.sql' ),
+ array( 'addTable', 'msg_resource', 'patch-msg_resource.sql' ),
+ array( 'addTable', 'module_deps', 'patch-module_deps.sql' ),
+ array( 'dropIndex', 'archive', 'ar_page_revid', 'patch-archive_kill_ar_page_revid.sql' ),
+ array( 'addIndex', 'archive', 'ar_revid', 'patch-archive_ar_revid.sql' ),
// 1.18
- array( 'addIndex', 'user', 'user_email', 'patch-user_email_index.sql' ),
- array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
- array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql'),
+ array( 'addIndex', 'user', 'user_email', 'patch-user_email_index.sql' ),
+ array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
+ array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql' ),
// 1.19
- array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql'),
+ array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql' ),
array( 'doMigrateUserOptions' ),
- array( 'dropField', 'user', 'user_options', 'patch-drop-user_options.sql' ),
- array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ),
- array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ),
- array( 'addIndex', 'page', 'page_redirect_namespace_len', 'patch-page_redirect_namespace_len.sql' ),
- array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
- array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ),
- array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ),
- array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ),
+ array( 'dropField', 'user', 'user_options', 'patch-drop-user_options.sql' ),
+ array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ),
+ array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ),
+ array( 'addIndex', 'page', 'page_redirect_namespace_len',
+ 'patch-page_redirect_namespace_len.sql' ),
+ array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ),
+ array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ),
// 1.20
array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
- array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
+ array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
+
+ // 1.21
+ array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ),
+ array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ),
+ array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ),
+ array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ),
+ array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ),
+ array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ),
+ array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
+ array( 'addTable', 'sites', 'patch-sites.sql' ),
+ array( 'addField', 'filearchive', 'fa_sha1', 'patch-fa_sha1.sql' ),
+ array( 'addField', 'job', 'job_token', 'patch-job_token.sql' ),
+ array( 'addField', 'job', 'job_attempts', 'patch-job_attempts.sql' ),
+ array( 'doEnableProfiling' ),
+ array( 'addField', 'uploadstash', 'us_props', 'patch-uploadstash-us_props.sql' ),
+ array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase-255.sql' ),
+ array( 'modifyField', 'user_former_groups', 'ufg_group',
+ 'patch-ufg_group-length-increase-255.sql' ),
+ array( 'addIndex', 'page_props', 'pp_propname_page',
+ 'patch-page_props-propname-page-index.sql' ),
+ array( 'addIndex', 'image', 'img_media_mime', 'patch-img_media_mime-index.sql' ),
+ array( 'addIndex', 'iwlinks', 'iwl_prefix_from_title', 'patch-iwlinks-from-title-index.sql' ),
+ array( 'addField', 'archive', 'ar_id', 'patch-archive-ar_id.sql' ),
+ array( 'addField', 'externallinks', 'el_id', 'patch-externallinks-el_id.sql' ),
);
}
protected function sqliteInitialIndexes() {
- // initial-indexes.sql fails if the indexes are already present, so we perform a quick check if our database is newer.
- if ( $this->updateRowExists( 'initial_indexes' ) || $this->db->indexExists( 'user', 'user_name', __METHOD__ ) ) {
+ // initial-indexes.sql fails if the indexes are already present,
+ // so we perform a quick check if our database is newer.
+ if ( $this->updateRowExists( 'initial_indexes' ) ||
+ $this->db->indexExists( 'user', 'user_name', __METHOD__ )
+ ) {
$this->output( "...have initial indexes\n" );
+
return;
}
$this->applyPatch( 'initial-indexes.sql', false, "Adding initial indexes" );
@@ -107,12 +133,23 @@ class SqliteUpdater extends DatabaseUpdater {
protected function sqliteSetupSearchindex() {
$module = DatabaseSqlite::getFulltextSearchModule();
$fts3tTable = $this->updateRowExists( 'fts3' );
- if ( $fts3tTable && !$module ) {
- $this->applyPatch( 'searchindex-no-fts.sql', false, 'PHP is missing FTS3 support, downgrading tables' );
+ if ( $fts3tTable && !$module ) {
+ $this->applyPatch(
+ 'searchindex-no-fts.sql',
+ false,
+ 'PHP is missing FTS3 support, downgrading tables'
+ );
} elseif ( !$fts3tTable && $module == 'FTS3' ) {
$this->applyPatch( 'searchindex-fts3.sql', false, "Adding FTS3 search capabilities" );
} else {
$this->output( "...fulltext search table appears to be in order.\n" );
}
}
+
+ protected function doEnableProfiling() {
+ global $wgProfileToDatabase;
+ if ( $wgProfileToDatabase === true && !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
+ $this->applyPatch( 'patch-profiling.sql', false, 'Add profiling table' );
+ }
+ }
}
diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php
index 2f46ff0b..1f8ee00a 100644
--- a/includes/installer/WebInstaller.php
+++ b/includes/installer/WebInstaller.php
@@ -56,10 +56,11 @@ class WebInstaller extends Installer {
/**
* The main sequence of page names. These will be displayed in turn.
- * To add one:
- * * Add it here
- * * Add a config-page-<name> message
- * * Add a WebInstaller_<name> class
+ *
+ * To add a new installer page:
+ * * Add it to this WebInstaller::$pageSequence property
+ * * Add a "config-page-<name>" message
+ * * Add a "WebInstaller_<name>" class
* @var array
*/
public $pageSequence = array(
@@ -139,7 +140,7 @@ class WebInstaller extends Installer {
/**
* Main entry point.
*
- * @param $session Array: initial session array
+ * @param array $session initial session array
*
* @return Array: new session array
*/
@@ -153,9 +154,9 @@ class WebInstaller extends Installer {
$this->exportVars();
$this->setupLanguage();
- if( ( $this->getVar( '_InstallDone' ) || $this->getVar( '_UpgradeDone' ) )
- && $this->request->getVal( 'localsettings' ) )
- {
+ if ( ( $this->getVar( '_InstallDone' ) || $this->getVar( '_UpgradeDone' ) )
+ && $this->request->getVal( 'localsettings' )
+ ) {
$this->request->response()->header( 'Content-type: application/x-httpd-php' );
$this->request->response()->header(
'Content-Disposition: attachment; filename="LocalSettings.php"'
@@ -163,18 +164,20 @@ class WebInstaller extends Installer {
$ls = InstallerOverrides::getLocalSettingsGenerator( $this );
$rightsProfile = $this->rightsProfiles[$this->getVar( '_RightsProfile' )];
- foreach( $rightsProfile as $group => $rightsArr ) {
+ foreach ( $rightsProfile as $group => $rightsArr ) {
$ls->setGroupRights( $group, $rightsArr );
}
echo $ls->getText();
+
return $this->session;
}
$cssDir = $this->request->getVal( 'css' );
- if( $cssDir ) {
+ if ( $cssDir ) {
$cssDir = ( $cssDir == 'rtl' ? 'rtl' : 'ltr' );
$this->request->response()->header( 'Content-type: text/css' );
echo $this->output->getCSS( $cssDir );
+
return $this->session;
}
@@ -198,6 +201,7 @@ class WebInstaller extends Installer {
$this->output->useShortHeader();
$this->output->allowFrames();
$page->submitCC();
+
return $this->finish();
}
@@ -206,6 +210,7 @@ class WebInstaller extends Installer {
$this->output->useShortHeader();
$this->output->allowFrames();
$this->output->addHTML( $page->getCCDoneBox() );
+
return $this->finish();
}
@@ -249,12 +254,13 @@ class WebInstaller extends Installer {
do {
$nextPageId--;
$nextPage = $this->pageSequence[$nextPageId];
- } while( isset( $this->skippedPages[$nextPage] ) );
+ } while ( isset( $this->skippedPages[$nextPage] ) );
} else {
$nextPage = $this->pageSequence[$lowestUnhappy];
}
$this->output->redirect( $this->getUrl( array( 'page' => $nextPage ) ) );
+
return $this->finish();
}
@@ -262,7 +268,7 @@ class WebInstaller extends Installer {
$this->currentPageName = $page->getName();
$this->startPageWrapper( $pageName );
- if( $page->isSlow() ) {
+ if ( $page->isSlow() ) {
$this->disableTimeLimit();
}
@@ -323,7 +329,7 @@ class WebInstaller extends Installer {
* @return bool
*/
public function startSession() {
- if( wfIniGetBool( 'session.auto_start' ) || session_id() ) {
+ if ( wfIniGetBool( 'session.auto_start' ) || session_id() ) {
// Done already
return true;
}
@@ -335,6 +341,7 @@ class WebInstaller extends Installer {
if ( $this->phpErrors ) {
$this->showError( 'config-session-error', $this->phpErrors[0] );
+
return false;
}
@@ -352,7 +359,7 @@ class WebInstaller extends Installer {
public function getFingerprint() {
// Get the base URL of the installation
$url = $this->request->getFullRequestURL();
- if ( preg_match( '!^(.*\?)!', $url, $m) ) {
+ if ( preg_match( '!^(.*\?)!', $url, $m ) ) {
// Trim query string
$url = $m[1];
}
@@ -361,6 +368,7 @@ class WebInstaller extends Installer {
// the /mw-config/index.php. Kinda scary though?
$url = $m[1];
}
+
return md5( serialize( array(
'local path' => dirname( __DIR__ ),
'url' => $url,
@@ -425,7 +433,7 @@ class WebInstaller extends Installer {
$url = preg_replace( '/\?.*$/', '', $url );
if ( $query ) {
- $url .= '?' . wfArrayToCGI( $query );
+ $url .= '?' . wfArrayToCgi( $query );
}
return $url;
@@ -460,7 +468,7 @@ class WebInstaller extends Installer {
/**
* Set a session variable.
- * @param $name String key for the variable
+ * @param string $name key for the variable
* @param $value Mixed
*/
public function setSession( $name, $value ) {
@@ -515,7 +523,7 @@ class WebInstaller extends Installer {
/**
* Called by execute() before page output starts, to show a page list.
*
- * @param $currentPageName String
+ * @param $currentPageName string
*/
private function startPageWrapper( $currentPageName ) {
$s = "<div class=\"config-page-wrapper\">\n";
@@ -538,7 +546,14 @@ class WebInstaller extends Installer {
$s .= "</ul><br/><ul>\n";
$s .= $this->getPageListItem( 'Restart', true, $currentPageName );
- $s .= "</ul></div>\n"; // end list pane
+ // End list pane
+ $s .= "</ul></div>\n";
+
+ // Messages:
+ // config-page-language, config-page-welcome, config-page-dbconnect, config-page-upgrade,
+ // config-page-dbsettings, config-page-name, config-page-options, config-page-install,
+ // config-page-complete, config-page-restart, config-page-readme, config-page-releasenotes,
+ // config-page-copying, config-page-upgradedoc, config-page-existingwiki
$s .= Html::element( 'h2', array(),
wfMessage( 'config-page-' . strtolower( $currentPageName ) )->text() );
@@ -548,14 +563,20 @@ class WebInstaller extends Installer {
/**
* Get a list item for the page list.
*
- * @param $pageName String
- * @param $enabled Boolean
- * @param $currentPageName String
+ * @param $pageName string
+ * @param $enabled boolean
+ * @param $currentPageName string
*
* @return string
*/
private function getPageListItem( $pageName, $enabled, $currentPageName ) {
$s = "<li class=\"config-page-list-item\">";
+
+ // Messages:
+ // config-page-language, config-page-welcome, config-page-dbconnect, config-page-upgrade,
+ // config-page-dbsettings, config-page-name, config-page-options, config-page-install,
+ // config-page-complete, config-page-restart, config-page-readme, config-page-releasenotes,
+ // config-page-copying, config-page-upgradedoc, config-page-existingwiki
$name = wfMessage( 'config-page-' . strtolower( $pageName ) )->text();
if ( $enabled ) {
@@ -600,16 +621,16 @@ class WebInstaller extends Installer {
*/
private function endPageWrapper() {
$this->output->addHTMLNoFlush(
- "<div class=\"visualClear\"></div>\n" .
- "</div>\n" .
- "<div class=\"visualClear\"></div>\n" .
+ "<div class=\"visualClear\"></div>\n" .
+ "</div>\n" .
+ "<div class=\"visualClear\"></div>\n" .
"</div>" );
}
/**
* Get HTML for an error box with an icon.
*
- * @param $text String: wikitext, get this with wfMessage()->plain()
+ * @param string $text wikitext, get this with wfMessage()->plain()
*
* @return string
*/
@@ -620,7 +641,7 @@ class WebInstaller extends Installer {
/**
* Get HTML for a warning box with an icon.
*
- * @param $text String: wikitext, get this with wfMessage()->plain()
+ * @param string $text wikitext, get this with wfMessage()->plain()
*
* @return string
*/
@@ -631,16 +652,19 @@ class WebInstaller extends Installer {
/**
* Get HTML for an info box with an icon.
*
- * @param $text String: wikitext, get this with wfMessage()->plain()
- * @param $icon String: icon name, file in skins/common/images
- * @param $class String: additional class name to add to the wrapper div
+ * @param string $text wikitext, get this with wfMessage()->plain()
+ * @param string $icon icon name, file in skins/common/images
+ * @param string $class additional class name to add to the wrapper div
*
* @return string
*/
public function getInfoBox( $text, $icon = false, $class = false ) {
$text = $this->parse( $text, true );
- $icon = ( $icon == false ) ? '../skins/common/images/info-32.png' : '../skins/common/images/'.$icon;
+ $icon = ( $icon == false ) ?
+ '../skins/common/images/info-32.png' :
+ '../skins/common/images/' . $icon;
$alt = wfMessage( 'config-information' )->text();
+
return Html::infoBox( $text, $icon, $alt, $class, false );
}
@@ -667,7 +691,7 @@ class WebInstaller extends Installer {
/**
* Output a help box.
- * @param $msg String key for wfMessage()
+ * @param string $msg key for wfMessage()
*/
public function showHelpBox( $msg /*, ... */ ) {
$args = func_get_args();
@@ -685,7 +709,7 @@ class WebInstaller extends Installer {
$args = func_get_args();
array_shift( $args );
$html = '<div class="config-message">' .
- $this->parse( wfMessage( $msg, $args )->useDatabase( false )->plain() ) .
+ $this->parse( wfMessage( $msg, $args )->useDatabase( false )->plain() ) .
"</div>\n";
$this->output->addHTML( $html );
}
@@ -723,16 +747,16 @@ class WebInstaller extends Installer {
$attributes['for'] = $forId;
}
- return
- "<div class=\"config-block\">\n" .
+ return "<div class=\"config-block\">\n" .
" <div class=\"config-block-label\">\n" .
Xml::tags( 'label',
$attributes,
- $labelText ) . "\n" .
- $helpData .
+ $labelText
+ ) . "\n" .
+ $helpData .
" </div>\n" .
" <div class=\"config-block-elements\">\n" .
- $contents .
+ $contents .
" </div>\n" .
"</div>\n";
}
@@ -742,12 +766,12 @@ class WebInstaller extends Installer {
*
* @param $params Array
* Parameters are:
- * var: The variable to be configured (required)
- * label: The message name for the label (required)
- * attribs: Additional attributes for the input element (optional)
+ * var: The variable to be configured (required)
+ * label: The message name for the label (required)
+ * attribs: Additional attributes for the input element (optional)
* controlName: The name for the input element (optional)
- * value: The current value of the variable (optional)
- * help: The html for the help text (optional)
+ * value: The current value of the variable (optional)
+ * help: The html for the help text (optional)
*
* @return string
*/
@@ -766,22 +790,22 @@ class WebInstaller extends Installer {
if ( !isset( $params['help'] ) ) {
$params['help'] = "";
}
- return
- $this->label(
- $params['label'],
+
+ return $this->label(
+ $params['label'],
+ $params['controlName'],
+ Xml::input(
$params['controlName'],
- Xml::input(
- $params['controlName'],
- 30, // intended to be overridden by CSS
- $params['value'],
- $params['attribs'] + array(
- 'id' => $params['controlName'],
- 'class' => 'config-input-text',
- 'tabindex' => $this->nextTabIndex()
- )
- ),
- $params['help']
- );
+ 30, // intended to be overridden by CSS
+ $params['value'],
+ $params['attribs'] + array(
+ 'id' => $params['controlName'],
+ 'class' => 'config-input-text',
+ 'tabindex' => $this->nextTabIndex()
+ )
+ ),
+ $params['help']
+ );
}
/**
@@ -789,12 +813,12 @@ class WebInstaller extends Installer {
*
* @param $params Array
* Parameters are:
- * var: The variable to be configured (required)
- * label: The message name for the label (required)
- * attribs: Additional attributes for the input element (optional)
+ * var: The variable to be configured (required)
+ * label: The message name for the label (required)
+ * attribs: Additional attributes for the input element (optional)
* controlName: The name for the input element (optional)
- * value: The current value of the variable (optional)
- * help: The html for the help text (optional)
+ * value: The current value of the variable (optional)
+ * help: The html for the help text (optional)
*
* @return string
*/
@@ -813,23 +837,23 @@ class WebInstaller extends Installer {
if ( !isset( $params['help'] ) ) {
$params['help'] = "";
}
- return
- $this->label(
- $params['label'],
+
+ return $this->label(
+ $params['label'],
+ $params['controlName'],
+ Xml::textarea(
$params['controlName'],
- Xml::textarea(
- $params['controlName'],
- $params['value'],
- 30,
- 5,
- $params['attribs'] + array(
- 'id' => $params['controlName'],
- 'class' => 'config-input-text',
- 'tabindex' => $this->nextTabIndex()
- )
- ),
- $params['help']
- );
+ $params['value'],
+ 30,
+ 5,
+ $params['attribs'] + array(
+ 'id' => $params['controlName'],
+ 'class' => 'config-input-text',
+ 'tabindex' => $this->nextTabIndex()
+ )
+ ),
+ $params['help']
+ );
}
/**
@@ -838,12 +862,12 @@ class WebInstaller extends Installer {
* Implements password hiding
* @param $params Array
* Parameters are:
- * var: The variable to be configured (required)
- * label: The message name for the label (required)
- * attribs: Additional attributes for the input element (optional)
+ * var: The variable to be configured (required)
+ * label: The message name for the label (required)
+ * attribs: Additional attributes for the input element (optional)
* controlName: The name for the input element (optional)
- * value: The current value of the variable (optional)
- * help: The html for the help text (optional)
+ * value: The current value of the variable (optional)
+ * help: The html for the help text (optional)
*
* @return string
*/
@@ -867,12 +891,12 @@ class WebInstaller extends Installer {
*
* @param $params Array
* Parameters are:
- * var: The variable to be configured (required)
- * label: The message name for the label (required)
- * attribs: Additional attributes for the input element (optional)
+ * var: The variable to be configured (required)
+ * label: The message name for the label (required)
+ * attribs: Additional attributes for the input element (optional)
* controlName: The name for the input element (optional)
- * value: The current value of the variable (optional)
- * help: The html for the help text (optional)
+ * value: The current value of the variable (optional)
+ * help: The html for the help text (optional)
*
* @return string
*/
@@ -891,14 +915,15 @@ class WebInstaller extends Installer {
if ( !isset( $params['help'] ) ) {
$params['help'] = "";
}
- if( isset( $params['rawtext'] ) ) {
+ if ( isset( $params['rawtext'] ) ) {
$labelText = $params['rawtext'];
- } else {
+ } elseif ( isset( $params['label'] ) ) {
$labelText = $this->parse( wfMessage( $params['label'] )->text() );
+ } else {
+ $labelText = "";
}
- return
- "<div class=\"config-input-check\">\n" .
+ return "<div class=\"config-input-check\">\n" .
$params['help'] .
"<label>\n" .
Xml::check(
@@ -919,20 +944,20 @@ class WebInstaller extends Installer {
*
* @param $params Array
* Parameters are:
- * var: The variable to be configured (required)
- * label: The message name for the label (required)
+ * var: The variable to be configured (required)
+ * label: The message name for the label (required)
* itemLabelPrefix: The message name prefix for the item labels (required)
- * values: List of allowed values (required)
- * itemAttribs Array of attribute arrays, outer key is the value name (optional)
- * commonAttribs Attribute array applied to all items
- * controlName: The name for the input element (optional)
- * value: The current value of the variable (optional)
- * help: The html for the help text (optional)
+ * values: List of allowed values (required)
+ * itemAttribs: Array of attribute arrays, outer key is the value name (optional)
+ * commonAttribs: Attribute array applied to all items
+ * controlName: The name for the input element (optional)
+ * value: The current value of the variable (optional)
+ * help: The html for the help text (optional)
*
* @return string
*/
public function getRadioSet( $params ) {
- if ( !isset( $params['controlName'] ) ) {
+ if ( !isset( $params['controlName'] ) ) {
$params['controlName'] = 'config_' . $params['var'];
}
@@ -986,10 +1011,10 @@ class WebInstaller extends Installer {
* @param $status Status
*/
public function showStatusBox( $status ) {
- if( !$status->isGood() ) {
+ if ( !$status->isGood() ) {
$text = $status->getWikiText();
- if( $status->isOk() ) {
+ if ( $status->isOk() ) {
$box = $this->getWarningBox( $text );
} else {
$box = $this->getErrorBox( $text );
@@ -1005,7 +1030,7 @@ class WebInstaller extends Installer {
* fake) passwords.
*
* @param $varNames Array
- * @param $prefix String: the prefix added to variables to obtain form names
+ * @param string $prefix the prefix added to variables to obtain form names
*
* @return array
*/
@@ -1057,6 +1082,7 @@ class WebInstaller extends Installer {
*/
public function docLink( $linkText, $attribs, $parser ) {
$url = $this->getDocUrl( $attribs['href'] );
+
return '<a href="' . htmlspecialchars( $url ) . '">' .
htmlspecialchars( $linkText ) .
'</a>';
@@ -1070,7 +1096,7 @@ class WebInstaller extends Installer {
* @param $parser
* @return String Html for download link
*/
- public function downloadLinkHook( $text, $attribs, $parser ) {
+ public function downloadLinkHook( $text, $attribs, $parser ) {
$img = Html::element( 'img', array(
'src' => '../skins/common/images/download-32.png',
'width' => '32',
@@ -1078,14 +1104,15 @@ class WebInstaller extends Installer {
) );
$anchor = Html::rawElement( 'a',
array( 'href' => $this->getURL( array( 'localsettings' => 1 ) ) ),
- $img . ' ' . wfMessage( 'config-download-localsettings' )->escaped() );
+ $img . ' ' . wfMessage( 'config-download-localsettings' )->parse() );
+
return Html::rawElement( 'div', array( 'class' => 'config-download-link' ), $anchor );
}
/**
* @return bool
*/
- public function envCheckPath( ) {
+ public function envCheckPath() {
// 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
@@ -1095,13 +1122,15 @@ class WebInstaller extends Installer {
} elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
$path = $_SERVER['SCRIPT_NAME'];
}
- if ($path !== false) {
+ if ( $path !== false ) {
$uri = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
$this->setVar( 'wgScriptPath', $uri );
} else {
$this->showError( 'config-no-uri' );
+
return false;
}
+
return parent::envCheckPath();
}
diff --git a/includes/installer/WebInstallerOutput.php b/includes/installer/WebInstallerOutput.php
index f3166c25..f2dc37fe 100644
--- a/includes/installer/WebInstallerOutput.php
+++ b/includes/installer/WebInstallerOutput.php
@@ -104,49 +104,85 @@ class WebInstallerOutput {
/**
* Get the raw vector CSS, flipping if needed
- * @param $dir String 'ltr' or 'rtl'
+ *
+ * @todo Possibly get rid of this function and use ResourceLoader in the manner it was
+ * designed to be used in, rather than just grabbing a list of filenames from it,
+ * and not properly handling such details as media types in module definitions.
+ *
+ * @param string $dir 'ltr' or 'rtl'
* @return String
*/
public function getCSS( $dir ) {
- $skinDir = dirname( dirname( __DIR__ ) ) . '/skins';
-
- // All these files will be concatenated in sequence and loaded
- // as one file.
- // The string 'images/' in the files' contents will be replaced
- // by '../skins/$skinName/images/', where $skinName is what appears
- // before the last '/' in each of the strings.
- $cssFileNames = array(
-
- // Basically the "skins.vector" ResourceLoader module styles
- 'common/shared.css',
- 'common/commonElements.css',
- 'common/commonContent.css',
- 'common/commonInterface.css',
- 'vector/screen.css',
-
- // mw-config specific
- 'common/config.css',
+ // All CSS files these modules reference will be concatenated in sequence
+ // and loaded as one file.
+ $moduleNames = array(
+ 'mediawiki.legacy.shared',
+ 'skins.vector',
+ 'mediawiki.legacy.config',
);
+ $prepend = '';
$css = '';
- wfSuppressWarnings();
- foreach ( $cssFileNames as $cssFileName ) {
- $fullCssFileName = "$skinDir/$cssFileName";
- $cssFileContents = file_get_contents( $fullCssFileName );
- if ( $cssFileContents ) {
- preg_match( "/^(\w+)\//", $cssFileName, $match );
- $skinName = $match[1];
- $css .= str_replace( 'images/', "../skins/$skinName/images/", $cssFileContents );
- } else {
- $css .= "/** Your webserver cannot read $fullCssFileName. Please check file permissions. */";
+ $cssFileNames = array();
+ $resourceLoader = new ResourceLoader();
+ foreach ( $moduleNames as $moduleName ) {
+ $module = $resourceLoader->getModule( $moduleName );
+ $cssFileNames = $module->getAllStyleFiles();
+
+ wfSuppressWarnings();
+ foreach ( $cssFileNames as $cssFileName ) {
+ if ( !file_exists( $cssFileName ) ) {
+ $prepend .= ResourceLoader::makeComment( "Unable to find $cssFileName." );
+ continue;
+ }
+
+ if ( !is_readable( $cssFileName ) ) {
+ $prepend .= ResourceLoader::makeComment( "Unable to read $cssFileName. Please check file permissions." );
+ continue;
+ }
+
+ try {
+
+ if ( preg_match( '/\.less$/', $cssFileName ) ) {
+ // Run the LESS compiler for *.less files (bug 55589)
+ $compiler = ResourceLoader::getLessCompiler();
+ $cssFileContents = $compiler->compileFile( $cssFileName );
+ } else {
+ // Regular CSS file
+ $cssFileContents = file_get_contents( $cssFileName );
+ }
+
+ if ( $cssFileContents ) {
+ // Rewrite URLs, though don't bother embedding images. While static image
+ // files may be cached, CSS returned by this function is definitely not.
+ $cssDirName = dirname( $cssFileName );
+ $css .= CSSMin::remap(
+ /* source */ $cssFileContents,
+ /* local */ $cssDirName,
+ /* remote */ '..' . str_replace(
+ array( $GLOBALS['IP'], DIRECTORY_SEPARATOR ),
+ array( '', '/' ),
+ $cssDirName
+ ),
+ /* embedData */ false
+ );
+ } else {
+ $prepend .= ResourceLoader::makeComment( "Unable to read $cssFileName." );
+ }
+
+ } catch ( Exception $e ) {
+ $prepend .= ResourceLoader::formatException( $e );
+ }
+
+ $css .= "\n";
}
-
- $css .= "\n";
+ wfRestoreWarnings();
}
- wfRestoreWarnings();
- if( $dir == 'rtl' ) {
+ $css = $prepend . $css;
+
+ if ( $dir == 'rtl' ) {
$css = CSSJanus::transform( $css, true );
}
@@ -157,7 +193,7 @@ class WebInstallerOutput {
* "<link>" to index.php?css=foobar for the "<head>"
* @return String
*/
- private function getCssUrl( ) {
+ private function getCssUrl() {
return Html::linkedStyle( $_SERVER['PHP_SELF'] . '?css=' . $this->getDir() );
}
@@ -185,6 +221,7 @@ class WebInstallerOutput {
*/
public function getDir() {
global $wgLang;
+
return is_object( $wgLang ) ? $wgLang->getDir() : 'ltr';
}
@@ -193,6 +230,7 @@ class WebInstallerOutput {
*/
public function getLanguageCode() {
global $wgLang;
+
return is_object( $wgLang ) ? $wgLang->getCode() : 'en';
}
@@ -216,22 +254,23 @@ class WebInstallerOutput {
public function outputHeader() {
$this->headerDone = true;
- $dbTypes = $this->parent->getDBTypes();
-
$this->parent->request->response()->header( 'Content-Type: text/html; charset=utf-8' );
- if (!$this->allowFrames) {
+
+ if ( !$this->allowFrames ) {
$this->parent->request->response()->header( 'X-Frame-Options: DENY' );
}
+
if ( $this->redirectTarget ) {
- $this->parent->request->response()->header( 'Location: '.$this->redirectTarget );
+ $this->parent->request->response()->header( 'Location: ' . $this->redirectTarget );
+
return;
}
if ( $this->useShortHeader ) {
$this->outputShortHeader();
+
return;
}
-
?>
<?php echo Html::htmlHeader( $this->getHeadAttribs() ); ?>
<head>
@@ -239,7 +278,6 @@ class WebInstallerOutput {
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title><?php $this->outputTitle(); ?></title>
<?php echo $this->getCssUrl() . "\n"; ?>
- <?php echo Html::inlineScript( "var dbTypes = " . Xml::encodeJsVar( $dbTypes ) ) . "\n"; ?>
<?php echo $this->getJQuery() . "\n"; ?>
<?php echo Html::linkedScript( '../skins/common/config.js' ) . "\n"; ?>
</head>
diff --git a/includes/installer/WebInstallerPage.php b/includes/installer/WebInstallerPage.php
index a193afb7..ad399133 100644
--- a/includes/installer/WebInstallerPage.php
+++ b/includes/installer/WebInstallerPage.php
@@ -36,7 +36,7 @@ abstract class WebInstallerPage {
*/
public $parent;
- public abstract function execute();
+ abstract public function execute();
/**
* Constructor.
@@ -74,6 +74,10 @@ abstract class WebInstallerPage {
);
}
+ /**
+ * @param string|bool $continue
+ * @param string|bool $back
+ */
public function endForm( $continue = 'continue', $back = 'back' ) {
$s = "<div class=\"config-submit\">\n";
$id = $this->getId();
@@ -84,25 +88,36 @@ abstract class WebInstallerPage {
if ( $continue ) {
// Fake submit button for enter keypress (bug 26267)
- $s .= Xml::submitButton( wfMessage( "config-$continue" )->text(),
- array( 'name' => "enter-$continue", 'style' =>
- 'visibility:hidden;overflow:hidden;width:1px;margin:0' ) ) . "\n";
+ // Messages: config-continue, config-restart, config-regenerate
+ $s .= Xml::submitButton(
+ wfMessage( "config-$continue" )->text(),
+ array(
+ 'name' => "enter-$continue",
+ 'style' => 'visibility:hidden;overflow:hidden;width:1px;margin:0'
+ )
+ ) . "\n";
}
if ( $back ) {
- $s .= Xml::submitButton( wfMessage( "config-$back" )->text(),
+ // Message: config-back
+ $s .= Xml::submitButton(
+ wfMessage( "config-$back" )->text(),
array(
'name' => "submit-$back",
'tabindex' => $this->parent->nextTabIndex()
- ) ) . "\n";
+ )
+ ) . "\n";
}
if ( $continue ) {
- $s .= Xml::submitButton( wfMessage( "config-$continue" )->text(),
+ // Messages: config-continue, config-restart, config-regenerate
+ $s .= Xml::submitButton(
+ wfMessage( "config-$continue" )->text(),
array(
'name' => "submit-$continue",
'tabindex' => $this->parent->nextTabIndex(),
- ) ) . "\n";
+ )
+ ) . "\n";
}
$s .= "</div></form></div>\n";
@@ -128,7 +143,7 @@ abstract class WebInstallerPage {
/**
* Get the starting tags of a fieldset.
*
- * @param $legend String: message name
+ * @param string $legend message name
*
* @return string
*/
@@ -204,6 +219,7 @@ class WebInstaller_Language extends WebInstallerPage {
if ( isset( $languages[$contLang] ) ) {
$this->setVar( 'wgLanguageCode', $contLang );
}
+
return 'continue';
}
} elseif ( $this->parent->showSessionWarning ) {
@@ -251,13 +267,15 @@ class WebInstaller_Language extends WebInstallerPage {
$languages = Language::fetchLanguageNames();
ksort( $languages );
foreach ( $languages as $code => $lang ) {
- if ( isset( $wgDummyLanguageCodes[$code] ) ) continue;
+ if ( isset( $wgDummyLanguageCodes[$code] ) ) {
+ continue;
+ }
$s .= "\n" . Xml::option( "$code - $lang", $code, $code == $selectedCode );
}
$s .= "\n</select>\n";
+
return $this->parent->label( $label, $name, $s );
}
-
}
class WebInstaller_ExistingWiki extends WebInstallerPage {
@@ -271,8 +289,8 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
// Check if the upgrade key supplied to the user has appeared in LocalSettings.php
if ( $vars['wgUpgradeKey'] !== false
&& $this->getVar( '_UpgradeKeySupplied' )
- && $this->getVar( 'wgUpgradeKey' ) === $vars['wgUpgradeKey'] )
- {
+ && $this->getVar( 'wgUpgradeKey' ) === $vars['wgUpgradeKey']
+ ) {
// It's there, so the user is authorized
$status = $this->handleExistingUpgrade( $vars );
if ( $status->isOK() ) {
@@ -281,6 +299,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
$this->startForm();
$this->parent->showStatusBox( $status );
$this->endForm( 'continue' );
+
return 'output';
}
}
@@ -299,6 +318,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
$this->getVar( 'wgUpgradeKey' ) . "';</pre>" )->plain()
) );
$this->endForm( 'continue' );
+
return 'output';
}
@@ -307,9 +327,10 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
$r = $this->parent->request;
if ( $r->wasPosted() ) {
$key = $r->getText( 'config_wgUpgradeKey' );
- if( !$key || $key !== $vars['wgUpgradeKey'] ) {
+ if ( !$key || $key !== $vars['wgUpgradeKey'] ) {
$this->parent->showError( 'config-localsettings-badkey' );
$this->showKeyForm();
+
return 'output';
}
// Key was OK
@@ -319,10 +340,12 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
} else {
$this->parent->showStatusBox( $status );
$this->showKeyForm();
+
return 'output';
}
} else {
$this->showKeyForm();
+
return 'output';
}
}
@@ -333,7 +356,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
protected function showKeyForm() {
$this->startForm();
$this->addHTML(
- $this->parent->getInfoBox( wfMessage( 'config-localsettings-upgrade' )->plain() ).
+ $this->parent->getInfoBox( wfMessage( 'config-localsettings-upgrade' )->plain() ) .
'<br />' .
$this->parent->getTextBox( array(
'var' => 'wgUpgradeKey',
@@ -352,24 +375,26 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
}
$this->setVar( $name, $vars[$name] );
}
+
return $status;
}
/**
* Initiate an upgrade of the existing database
- * @param $vars array Variables from LocalSettings.php and AdminSettings.php
+ * @param array $vars Variables from LocalSettings.php and AdminSettings.php
* @return Status
*/
protected function handleExistingUpgrade( $vars ) {
// Check $wgDBtype
if ( !isset( $vars['wgDBtype'] ) ||
- !in_array( $vars['wgDBtype'], Installer::getDBTypes() ) ) {
+ !in_array( $vars['wgDBtype'], Installer::getDBTypes() )
+ ) {
return Status::newFatal( 'config-localsettings-connection-error', '' );
}
// Set the relevant variables from LocalSettings.php
$requiredVars = array( 'wgDBtype' );
- $status = $this->importVariables( $requiredVars , $vars );
+ $status = $this->importVariables( $requiredVars, $vars );
$installer = $this->parent->getDBInstaller();
$status->merge( $this->importVariables( $installer->getGlobalNames(), $vars ) );
if ( !$status->isOK() ) {
@@ -393,11 +418,13 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
// Adjust the error message to explain things correctly
$status->replaceMessage( 'config-connection-error',
'config-localsettings-connection-error' );
+
return $status;
}
// All good
$this->setVar( '_ExistingDBSettings', true );
+
return $status;
}
}
@@ -422,8 +449,9 @@ class WebInstaller_Welcome extends WebInstallerPage {
} else {
$this->parent->showStatusMessage( $status );
}
- }
+ return '';
+ }
}
class WebInstaller_DBConnect extends WebInstallerPage {
@@ -439,6 +467,7 @@ class WebInstaller_DBConnect extends WebInstallerPage {
if ( $status->isGood() ) {
$this->setVar( '_UpgradeDone', false );
+
return 'continue';
} else {
$this->parent->showStatusBox( $status );
@@ -451,15 +480,23 @@ class WebInstaller_DBConnect extends WebInstallerPage {
$settings = '';
$defaultType = $this->getVar( 'wgDBtype' );
+ // Messages: config-dbsupport-mysql, config-dbsupport-postgres, config-dbsupport-oracle,
+ // config-dbsupport-sqlite
$dbSupport = '';
- foreach( $this->parent->getDBTypes() as $type ) {
- $link = DatabaseBase::factory( $type )->getSoftwareLink();
- $dbSupport .= wfMessage( "config-support-$type", $link )->plain() . "\n";
+ foreach ( $this->parent->getDBTypes() as $type ) {
+ $dbSupport .= wfMessage( "config-dbsupport-$type" )->plain() . "\n";
}
$this->addHTML( $this->parent->getInfoBox(
wfMessage( 'config-support-info', trim( $dbSupport ) )->text() ) );
- foreach ( $this->parent->getVar( '_CompiledDBs' ) as $type ) {
+ // It's possible that the library for the default DB type is not compiled in.
+ // In that case, instead select the first supported DB type in the list.
+ $compiledDBs = $this->parent->getCompiledDBs();
+ if ( !in_array( $defaultType, $compiledDBs ) ) {
+ $defaultType = $compiledDBs[0];
+ }
+
+ foreach ( $compiledDBs as $type ) {
$installer = $this->parent->getDBInstaller( $type );
$types .=
'<li>' .
@@ -473,34 +510,40 @@ class WebInstaller_DBConnect extends WebInstallerPage {
) .
"</li>\n";
- $settings .=
- Html::openElement( 'div', array( 'id' => 'DB_wrapper_' . $type,
- 'class' => 'dbWrapper' ) ) .
+ // Messages: config-header-mysql, config-header-postgres, config-header-oracle,
+ // config-header-sqlite
+ $settings .= Html::openElement(
+ 'div',
+ array(
+ 'id' => 'DB_wrapper_' . $type,
+ 'class' => 'dbWrapper'
+ )
+ ) .
Html::element( 'h3', array(), wfMessage( 'config-header-' . $type )->text() ) .
$installer->getConnectForm() .
"</div>\n";
}
- $types .= "</ul><br style=\"clear: left\"/>\n";
- $this->addHTML(
- $this->parent->label( 'config-db-type', false, $types ) .
- $settings
- );
+ $types .= "</ul><br style=\"clear: left\"/>\n";
+ $this->addHTML( $this->parent->label( 'config-db-type', false, $types ) . $settings );
$this->endForm();
}
public function submit() {
$r = $this->parent->request;
$type = $r->getVal( 'DBType' );
+ if ( !$type ) {
+ return Status::newFatal( 'config-invalid-db-type' );
+ }
$this->setVar( 'wgDBtype', $type );
$installer = $this->parent->getDBInstaller( $type );
if ( !$installer ) {
return Status::newFatal( 'config-invalid-db-type' );
}
+
return $installer->submitConnectForm();
}
-
}
class WebInstaller_Upgrade extends WebInstallerPage {
@@ -521,6 +564,7 @@ class WebInstaller_Upgrade extends WebInstallerPage {
// Show the done message again
// Make them click back again if they want to do the upgrade again
$this->showDoneMessage();
+
return 'output';
}
}
@@ -544,11 +588,12 @@ class WebInstaller_Upgrade extends WebInstallerPage {
if ( $result ) {
// If they're going to possibly regenerate LocalSettings, we
// need to create the upgrade/secret keys. Bug 26481
- if( !$this->getVar( '_ExistingDBSettings' ) ) {
+ if ( !$this->getVar( '_ExistingDBSettings' ) ) {
$this->parent->generateKeys();
}
$this->setVar( '_UpgradeDone', true );
$this->showDoneMessage();
+
return 'output';
}
}
@@ -572,15 +617,14 @@ class WebInstaller_Upgrade extends WebInstallerPage {
$this->parent->getInfoBox(
wfMessage( $msg,
$this->getVar( 'wgServer' ) .
- $this->getVar( 'wgScriptPath' ) . '/index' .
- $this->getVar( 'wgScriptExtension' )
+ $this->getVar( 'wgScriptPath' ) . '/index' .
+ $this->getVar( 'wgScriptExtension' )
)->plain(), 'tick-32.png'
)
);
$this->parent->restoreLinkPopups();
$this->endForm( $regenerate ? 'regenerate' : false, false );
}
-
}
class WebInstaller_DBSettings extends WebInstallerPage {
@@ -609,7 +653,6 @@ class WebInstaller_DBSettings extends WebInstallerPage {
$this->addHTML( $form );
$this->endForm();
}
-
}
class WebInstaller_Name extends WebInstallerPage {
@@ -644,8 +687,11 @@ class WebInstaller_Name extends WebInstallerPage {
$this->parent->getTextBox( array(
'var' => 'wgSitename',
'label' => 'config-site-name',
- 'help' => $this->parent->getHelpBox( 'config-site-name-help' )
+ 'help' => $this->parent->getHelpBox( 'config-site-name-help' )
) ) .
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-ns-site-name, config-ns-generic, config-ns-other
$this->parent->getRadioSet( array(
'var' => '_NamespaceType',
'label' => 'config-project-namespace',
@@ -657,9 +703,8 @@ class WebInstaller_Name extends WebInstallerPage {
) ) .
$this->parent->getTextBox( array(
'var' => 'wgMetaNamespace',
- 'label' => '', //TODO: Needs a label?
- 'attribs' => array( 'readonly' => 'readonly', 'class' => 'enabledByOther' ),
-
+ 'label' => '', // @todo Needs a label?
+ 'attribs' => array( 'readonly' => 'readonly', 'class' => 'enabledByOther' )
) ) .
$this->getFieldSetStart( 'config-admin-box' ) .
$this->parent->getTextBox( array(
@@ -687,6 +732,9 @@ class WebInstaller_Name extends WebInstallerPage {
) ) .
$this->getFieldSetEnd() .
$this->parent->getInfoBox( wfMessage( 'config-almost-done' )->text() ) .
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-optional-continue, config-optional-skip
$this->parent->getRadioSet( array(
'var' => '_SkipOptional',
'itemLabelPrefix' => 'config-optional-',
@@ -698,6 +746,7 @@ class WebInstaller_Name extends WebInstallerPage {
$this->setVar( 'wgMetaNamespace', $metaNS );
$this->endForm();
+
return 'output';
}
@@ -750,7 +799,7 @@ class WebInstaller_Name extends WebInstallerPage {
// Make sure it won't conflict with any existing namespaces
global $wgContLang;
$nsIndex = $wgContLang->getNsIndex( $name );
- if( $nsIndex !== false && $nsIndex !== NS_PROJECT ) {
+ if ( $nsIndex !== false && $nsIndex !== NS_PROJECT ) {
$this->parent->showError( 'config-ns-conflict', $name );
$retVal = false;
}
@@ -798,24 +847,22 @@ class WebInstaller_Name extends WebInstallerPage {
// Validate e-mail if provided
$email = $this->getVar( '_AdminEmail' );
- if( $email && !Sanitizer::validateEmail( $email ) ) {
+ if ( $email && !Sanitizer::validateEmail( $email ) ) {
$this->parent->showError( 'config-admin-error-bademail' );
$retVal = false;
}
// If they asked to subscribe to mediawiki-announce but didn't give
// an e-mail, show an error. Bug 29332
- if( !$email && $this->getVar( '_Subscribe' ) ) {
+ if ( !$email && $this->getVar( '_Subscribe' ) ) {
$this->parent->showError( 'config-subscribe-noemail' );
$retVal = false;
}
return $retVal;
}
-
}
class WebInstaller_Options extends WebInstallerPage {
-
public function execute() {
if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
return 'skip';
@@ -830,6 +877,9 @@ class WebInstaller_Options extends WebInstallerPage {
$this->startForm();
$this->addHTML(
# User Rights
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-profile-wiki, config-profile-no-anon, config-profile-fishbowl, config-profile-private
$this->parent->getRadioSet( array(
'var' => '_RightsProfile',
'label' => 'config-profile',
@@ -839,6 +889,11 @@ class WebInstaller_Options extends WebInstallerPage {
$this->parent->getInfoBox( wfMessage( 'config-profile-help' )->plain() ) .
# Licensing
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
+ // config-license-cc-0, config-license-pd, config-license-gfdl,
+ // config-license-none, config-license-cc-choose
$this->parent->getRadioSet( array(
'var' => '_LicenseCode',
'label' => 'config-license',
@@ -889,18 +944,27 @@ class WebInstaller_Options extends WebInstallerPage {
$extensions = $this->parent->findExtensions();
- if( $extensions ) {
+ if ( $extensions ) {
$extHtml = $this->getFieldSetStart( 'config-extensions' );
- foreach( $extensions as $ext ) {
+ /* Force a recache, so we load extensions descriptions */
+ global $wgLang;
+ $lc = Language::getLocalisationCache();
+ $lc->setInitialisedLanguages( array() );
+ $lc->getItem( $wgLang->mCode, '' );
+ LinkCache::singleton()->useDatabase( false );
+
+ foreach ( $extensions as $ext ) {
$extHtml .= $this->parent->getCheckBox( array(
- 'var' => "ext-$ext",
- 'rawtext' => $ext,
- ) );
+ 'var' => "ext-{$ext['name']}",
+ 'rawtext' => "<b>{$ext['name']}</b>: " .
+ wfMessage( $ext['descriptionmsg'] )->useDatabase( false )->parse(),
+ ) );
+
}
$extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
- $this->getFieldSetEnd();
+ $this->getFieldSetEnd();
$this->addHTML( $extHtml );
}
@@ -911,6 +975,10 @@ class WebInstaller_Options extends WebInstallerPage {
$this->getVar( 'wgDeletedDirectory' )
)
);
+ // If we're using the default, let the user set it relative to $wgScriptPath
+ $curLogo = $this->getVar( 'wgLogo' );
+ $logoString = ( $curLogo == "/wiki/skins/common/images/wiki.png" ) ?
+ '$wgStylePath/common/images/wiki.png' : $curLogo;
$uploadwrapperStyle = $this->getVar( 'wgEnableUploads' ) ? '' : 'display: none';
$this->addHTML(
@@ -932,6 +1000,7 @@ class WebInstaller_Options extends WebInstallerPage {
'</div>' .
$this->parent->getTextBox( array(
'var' => 'wgLogo',
+ 'value' => $logoString,
'label' => 'config-logo',
'attribs' => array( 'dir' => 'ltr' ),
'help' => $this->parent->getHelpBox( 'config-logo-help' )
@@ -947,24 +1016,27 @@ class WebInstaller_Options extends WebInstallerPage {
);
$caches = array( 'none' );
- if( count( $this->getVar( '_Caches' ) ) ) {
+ if ( count( $this->getVar( '_Caches' ) ) ) {
$caches[] = 'accel';
}
$caches[] = 'memcached';
// We'll hide/show this on demand when the value changes, see config.js.
$cacheval = $this->getVar( 'wgMainCacheType' );
- if (!$cacheval) {
+ if ( !$cacheval ) {
// We need to set a default here; but don't hardcode it
// or we lose it every time we reload the page for validation
// or going back!
$cacheval = 'none';
}
- $hidden = ($cacheval == 'memcached') ? '' : 'display: none';
+ $hidden = ( $cacheval == 'memcached' ) ? '' : 'display: none';
$this->addHTML(
# Advanced settings
$this->getFieldSetStart( 'config-advanced-settings' ) .
# Object cache settings
+ // getRadioSet() builds a set of labeled radio buttons.
+ // For grep: The following messages are used as the item labels:
+ // config-cache-none, config-cache-accel, config-cache-memcached
$this->parent->getRadioSet( array(
'var' => 'wgMainCacheType',
'label' => 'config-cache-options',
@@ -1001,12 +1073,13 @@ class WebInstaller_Options extends WebInstallerPage {
$styleUrl = $server . dirname( dirname( $this->parent->getUrl() ) ) .
'/skins/common/config-cc.css';
$iframeUrl = 'http://creativecommons.org/license/?' .
- wfArrayToCGI( array(
+ wfArrayToCgi( array(
'partner' => 'MediaWiki',
'exit_url' => $exitUrl,
'lang' => $this->getVar( '_UserLang' ),
'stylesheet' => $styleUrl,
) );
+
return $iframeUrl;
}
@@ -1024,10 +1097,9 @@ class WebInstaller_Options extends WebInstallerPage {
} else {
$iframeAttribs['src'] = $this->getCCPartnerUrl();
}
- $wrapperStyle = ($this->getVar('_LicenseCode') == 'cc-choose') ? '' : 'display: none';
+ $wrapperStyle = ( $this->getVar( '_LicenseCode' ) == 'cc-choose' ) ? '' : 'display: none';
- return
- "<div class=\"config-cc-wrapper\" id=\"config-cc-wrapper\" style=\"$wrapperStyle\">\n" .
+ return "<div class=\"config-cc-wrapper\" id=\"config-cc-wrapper\" style=\"$wrapperStyle\">\n" .
Html::element( 'iframe', $iframeAttribs, '', false /* not short */ ) .
"</div>\n";
}
@@ -1037,13 +1109,13 @@ class WebInstaller_Options extends WebInstallerPage {
// If you change this height, also change it in config.css
$expandJs = str_replace( '$1', '54em', $js );
$reduceJs = str_replace( '$1', '70px', $js );
- return
- '<p>'.
+
+ return '<p>' .
Html::element( 'img', array( 'src' => $this->getVar( 'wgRightsIcon' ) ) ) .
'&#160;&#160;' .
htmlspecialchars( $this->getVar( 'wgRightsText' ) ) .
"</p>\n" .
- "<p style=\"text-align: center\">" .
+ "<p style=\"text-align: center;\">" .
Html::element( 'a',
array(
'href' => $this->getCCPartnerUrl(),
@@ -1052,7 +1124,7 @@ class WebInstaller_Options extends WebInstallerPage {
wfMessage( 'config-cc-again' )->text()
) .
"</p>\n" .
- "<script type=\"text/javascript\">\n" .
+ "<script>\n" .
# Reduce the wrapper div height
htmlspecialchars( $reduceJs ) .
"\n" .
@@ -1064,6 +1136,7 @@ class WebInstaller_Options extends WebInstallerPage {
array( 'wgRightsUrl', 'wgRightsText', 'wgRightsIcon' ) );
if ( count( $newValues ) != 3 ) {
$this->parent->showError( 'config-cc-error' );
+
return;
}
$this->setVar( '_CCDone', true );
@@ -1078,8 +1151,8 @@ class WebInstaller_Options extends WebInstallerPage {
'wgUseInstantCommons' ) );
if ( !in_array( $this->getVar( '_RightsProfile' ),
- array_keys( $this->parent->rightsProfiles ) ) )
- {
+ array_keys( $this->parent->rightsProfiles ) )
+ ) {
reset( $this->parent->rightsProfiles );
$this->setVar( '_RightsProfile', key( $this->parent->rightsProfiles ) );
}
@@ -1088,9 +1161,14 @@ class WebInstaller_Options extends WebInstallerPage {
if ( $code == 'cc-choose' ) {
if ( !$this->getVar( '_CCDone' ) ) {
$this->parent->showError( 'config-cc-not-chosen' );
+
return false;
}
} elseif ( in_array( $code, array_keys( $this->parent->licenses ) ) ) {
+ // Messages:
+ // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
+ // config-license-cc-0, config-license-pd, config-license-gfdl, config-license-none,
+ // config-license-cc-choose
$entry = $this->parent->licenses[$code];
if ( isset( $entry['text'] ) ) {
$this->setVar( 'wgRightsText', $entry['text'] );
@@ -1105,41 +1183,51 @@ class WebInstaller_Options extends WebInstallerPage {
$this->setVar( 'wgRightsIcon', '' );
}
- $extsAvailable = $this->parent->findExtensions();
+ $extsAvailable = array_map(
+ function( $e ) {
+ if( isset( $e['name'] ) ) {
+ return $e['name'];
+ }
+ }, $this->parent->findExtensions() );
$extsToInstall = array();
- foreach( $extsAvailable as $ext ) {
- if( $this->parent->request->getCheck( 'config_ext-' . $ext ) ) {
- $extsToInstall[] = $ext;
+ foreach ( $extsAvailable as $key => $ext ) {
+ if ( $this->parent->request->getCheck( 'config_ext-' . $ext ) ) {
+ $extsToInstall[] = $extsAvailable[ $key ];
}
}
$this->parent->setVar( '_Extensions', $extsToInstall );
- if( $this->getVar( 'wgMainCacheType' ) == 'memcached' ) {
+ if ( $this->getVar( 'wgMainCacheType' ) == 'memcached' ) {
$memcServers = explode( "\n", $this->getVar( '_MemCachedServers' ) );
- if( !$memcServers ) {
+ if ( !$memcServers ) {
$this->parent->showError( 'config-memcache-needservers' );
+
return false;
}
- foreach( $memcServers as $server ) {
+ foreach ( $memcServers as $server ) {
$memcParts = explode( ":", $server, 2 );
if ( !isset( $memcParts[0] )
- || ( !IP::isValid( $memcParts[0] )
- && ( gethostbyname( $memcParts[0] ) == $memcParts[0] ) ) ) {
+ || ( !IP::isValid( $memcParts[0] )
+ && ( gethostbyname( $memcParts[0] ) == $memcParts[0] ) )
+ ) {
$this->parent->showError( 'config-memcache-badip', $memcParts[0] );
+
return false;
- } elseif( !isset( $memcParts[1] ) ) {
+ } elseif ( !isset( $memcParts[1] ) ) {
$this->parent->showError( 'config-memcache-noport', $memcParts[0] );
+
return false;
- } elseif( $memcParts[1] < 1 || $memcParts[1] > 65535 ) {
+ } elseif ( $memcParts[1] < 1 || $memcParts[1] > 65535 ) {
$this->parent->showError( 'config-memcache-badport', 1, 65535 );
+
return false;
}
}
}
+
return true;
}
-
}
class WebInstaller_Install extends WebInstallerPage {
@@ -1148,18 +1236,18 @@ class WebInstaller_Install extends WebInstallerPage {
}
public function execute() {
- if( $this->getVar( '_UpgradeDone' ) ) {
+ if ( $this->getVar( '_UpgradeDone' ) ) {
return 'skip';
- } elseif( $this->getVar( '_InstallDone' ) ) {
+ } elseif ( $this->getVar( '_InstallDone' ) ) {
return 'continue';
- } elseif( $this->parent->request->wasPosted() ) {
+ } elseif ( $this->parent->request->wasPosted() ) {
$this->startForm();
- $this->addHTML("<ul>");
+ $this->addHTML( "<ul>" );
$results = $this->parent->performInstallation(
- array( $this, 'startStage'),
+ array( $this, 'startStage' ),
array( $this, 'endStage' )
);
- $this->addHTML("</ul>");
+ $this->addHTML( "</ul>" );
// PerformInstallation bails on a fatal, so make sure the last item
// completed before giving 'next.' Likewise, only provide back on failure
$lastStep = end( $results );
@@ -1171,11 +1259,16 @@ class WebInstaller_Install extends WebInstallerPage {
$this->addHTML( $this->parent->getInfoBox( wfMessage( 'config-install-begin' )->plain() ) );
$this->endForm();
}
+
return true;
}
public function startStage( $step ) {
- $this->addHTML( "<li>" . wfMessage( "config-install-$step" )->escaped() . wfMessage( 'ellipsis')->escaped() );
+ // Messages: config-install-database, config-install-tables, config-install-interwiki,
+ // config-install-stats, config-install-keys, config-install-sysop, config-install-mainpage
+ $this->addHTML( "<li>" . wfMessage( "config-install-$step" )->escaped() .
+ wfMessage( 'ellipsis' )->escaped() );
+
if ( $step == 'extension-tables' ) {
$this->startLiveBox();
}
@@ -1195,25 +1288,23 @@ class WebInstaller_Install extends WebInstallerPage {
$html = "<span class=\"error\">$html</span>";
}
$this->addHTML( $html . "</li>\n" );
- if( !$status->isGood() ) {
+ if ( !$status->isGood() ) {
$this->parent->showStatusBox( $status );
}
}
-
}
class WebInstaller_Complete extends WebInstallerPage {
-
public function execute() {
// Pop up a dialog box, to make it difficult for the user to forget
// to download the file
$lsUrl = $this->getVar( 'wgServer' ) . $this->parent->getURL( array( 'localsettings' => 1 ) );
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) &&
- strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE' ) !== false ) {
- // JS appears the only method that works consistently with IE7+
- $this->addHtml( "\n<script type=\"" . $GLOBALS['wgJsMimeType'] .
- '">jQuery( document ).ready( function() { document.location=' .
- Xml::encodeJsVar( $lsUrl) . "; } );</script>\n" );
+ strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE' ) !== false
+ ) {
+ // JS appears to be the only method that works consistently with IE7+
+ $this->addHtml( "\n<script>jQuery( function () { document.location = " .
+ Xml::encodeJsVar( $lsUrl ) . "; } );</script>\n" );
} else {
$this->parent->request->response()->header( "Refresh: 0;url=$lsUrl" );
}
@@ -1225,12 +1316,15 @@ class WebInstaller_Complete extends WebInstallerPage {
wfMessage( 'config-install-done',
$lsUrl,
$this->getVar( 'wgServer' ) .
- $this->getVar( 'wgScriptPath' ) . '/index' .
- $this->getVar( 'wgScriptExtension' ),
+ $this->getVar( 'wgScriptPath' ) . '/index' .
+ $this->getVar( 'wgScriptExtension' ),
'<downloadlink/>'
)->plain(), 'tick-32.png'
)
);
+ $this->addHTML( $this->parent->getInfoBox(
+ wfMessage( 'config-extension-link' )->text() ) );
+
$this->parent->restoreLinkPopups();
$this->endForm( false, false );
}
@@ -1245,6 +1339,7 @@ class WebInstaller_Restart extends WebInstallerPage {
if ( $really ) {
$this->parent->reset();
}
+
return 'continue';
}
@@ -1253,14 +1348,13 @@ class WebInstaller_Restart extends WebInstallerPage {
$this->addHTML( $s );
$this->endForm( 'restart' );
}
-
}
abstract class WebInstaller_Document extends WebInstallerPage {
- protected abstract function getFileName();
+ abstract protected function getFileName();
- public function execute() {
+ public function execute() {
$text = $this->getFileContents();
$text = InstallDocFormatter::format( $text );
$this->parent->output->addWikiText( $text );
@@ -1270,24 +1364,26 @@ abstract class WebInstaller_Document extends WebInstallerPage {
public function getFileContents() {
$file = __DIR__ . '/../../' . $this->getFileName();
- if( ! file_exists( $file ) ) {
+ if ( !file_exists( $file ) ) {
return wfMessage( 'config-nofile', $file )->plain();
}
+
return file_get_contents( $file );
}
-
}
class WebInstaller_Readme extends WebInstaller_Document {
- protected function getFileName() { return 'README'; }
+ protected function getFileName() {
+ return 'README';
+ }
}
class WebInstaller_ReleaseNotes extends WebInstaller_Document {
protected function getFileName() {
global $wgVersion;
- if(! preg_match( '/^(\d+)\.(\d+).*/i', $wgVersion, $result ) ) {
- throw new MWException('Variable $wgVersion has an invalid value.');
+ if ( !preg_match( '/^(\d+)\.(\d+).*/i', $wgVersion, $result ) ) {
+ throw new MWException( 'Variable $wgVersion has an invalid value.' );
}
return 'RELEASE-NOTES-' . $result[1] . '.' . $result[2];
@@ -1295,10 +1391,13 @@ class WebInstaller_ReleaseNotes extends WebInstaller_Document {
}
class WebInstaller_UpgradeDoc extends WebInstaller_Document {
- protected function getFileName() { return 'UPGRADE'; }
+ protected function getFileName() {
+ return 'UPGRADE';
+ }
}
class WebInstaller_Copying extends WebInstaller_Document {
- protected function getFileName() { return 'COPYING'; }
+ protected function getFileName() {
+ return 'COPYING';
+ }
}
-
diff --git a/includes/interwiki/Interwiki.php b/includes/interwiki/Interwiki.php
index eacf9a87..4003fa88 100644
--- a/includes/interwiki/Interwiki.php
+++ b/includes/interwiki/Interwiki.php
@@ -27,14 +27,15 @@
* schema updates etc, which aren't wiki-related)
*/
class Interwiki {
-
// Cache - removes oldest entry when it hits limit
protected static $smCache = array();
const CACHE_LIMIT = 100; // 0 means unlimited, any other value is max number of entries.
protected $mPrefix, $mURL, $mAPI, $mWikiID, $mLocal, $mTrans;
- public function __construct( $prefix = null, $url = '', $api = '', $wikiId = '', $local = 0, $trans = 0 ) {
+ public function __construct( $prefix = null, $url = '', $api = '', $wikiId = '', $local = 0,
+ $trans = 0
+ ) {
$this->mPrefix = $prefix;
$this->mURL = $url;
$this->mAPI = $api;
@@ -46,10 +47,10 @@ class Interwiki {
/**
* Check whether an interwiki prefix exists
*
- * @param $prefix String: interwiki prefix to use
- * @return Boolean: whether it exists
+ * @param string $prefix Interwiki prefix to use
+ * @return bool Whether it exists
*/
- static public function isValidInterwiki( $prefix ) {
+ public static function isValidInterwiki( $prefix ) {
$result = self::fetch( $prefix );
return (bool)$result;
}
@@ -57,28 +58,28 @@ class Interwiki {
/**
* Fetch an Interwiki object
*
- * @param $prefix String: interwiki prefix to use
+ * @param string $prefix Interwiki prefix to use
* @return Interwiki|null|bool
*/
- static public function fetch( $prefix ) {
+ public static function fetch( $prefix ) {
global $wgContLang;
- if( $prefix == '' ) {
+ if ( $prefix == '' ) {
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 )] );
}
@@ -91,7 +92,7 @@ class Interwiki {
*
* @note More logic is explained in DefaultSettings.
*
- * @param $prefix String: interwiki prefix
+ * @param string $prefix Interwiki prefix
* @return Interwiki object
*/
protected static function getInterwikiCached( $prefix ) {
@@ -114,19 +115,19 @@ class Interwiki {
*
* @note More logic is explained in DefaultSettings.
*
- * @param $prefix String: database key
- * @return String: the entry
+ * @param string $prefix Database key
+ * @return string The interwiki entry
*/
protected static function getInterwikiCacheEntry( $prefix ) {
global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
static $db, $site;
wfDebug( __METHOD__ . "( $prefix )\n" );
- if( !$db ) {
+ if ( !$db ) {
$db = CdbReader::open( $wgInterwikiCache );
}
/* Resolve site name */
- if( $wgInterwikiScopes >= 3 && !$site ) {
+ if ( $wgInterwikiScopes >= 3 && !$site ) {
$site = $db->get( '__sites:' . wfWikiID() );
if ( $site == '' ) {
$site = $wgInterwikiFallbackSite;
@@ -146,15 +147,14 @@ class Interwiki {
$value = '';
}
-
return $value;
}
/**
* Load the interwiki, trying first memcached then the DB
*
- * @param $prefix string The interwiki prefix
- * @return Boolean: the prefix is valid
+ * @param string $prefix The interwiki prefix
+ * @return bool If $prefix is valid
*/
protected static function load( $prefix ) {
global $wgMemc, $wgInterwikiExpiry;
@@ -172,9 +172,9 @@ class Interwiki {
}
}
- if( $iwData && is_array( $iwData ) ) { // is_array is hack for old keys
+ if ( $iwData && is_array( $iwData ) ) { // is_array is hack for old keys
$iw = Interwiki::loadFromArray( $iwData );
- if( $iw ) {
+ if ( $iw ) {
return $iw;
}
}
@@ -203,11 +203,11 @@ class Interwiki {
/**
* Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc)
*
- * @param $mc array Associative array: row from the interwiki table
- * @return Boolean|Interwiki whether everything was there
+ * @param array $mc Associative array: row from the interwiki table
+ * @return Interwiki|bool Interwiki object or false if $mc['iw_url'] is not set
*/
protected static function loadFromArray( $mc ) {
- if( isset( $mc['iw_url'] ) ) {
+ if ( isset( $mc['iw_url'] ) ) {
$iw = new Interwiki();
$iw->mURL = $mc['iw_url'];
$iw->mLocal = isset( $mc['iw_local'] ) ? $mc['iw_local'] : 0;
@@ -223,8 +223,8 @@ class Interwiki {
/**
* Fetch all interwiki prefixes from interwiki cache
*
- * @param $local null|string If not null, limits output to local/non-local interwikis
- * @return Array List of prefixes
+ * @param null|string $local If not null, limits output to local/non-local interwikis
+ * @return array List of prefixes
* @since 1.19
*/
protected static function getAllPrefixesCached( $local ) {
@@ -232,11 +232,11 @@ class Interwiki {
static $db, $site;
wfDebug( __METHOD__ . "()\n" );
- if( !$db ) {
+ if ( !$db ) {
$db = CdbReader::open( $wgInterwikiCache );
}
/* Resolve site name */
- if( $wgInterwikiScopes >= 3 && !$site ) {
+ if ( $wgInterwikiScopes >= 3 && !$site ) {
$site = $db->get( '__sites:' . wfWikiID() );
if ( $site == '' ) {
$site = $wgInterwikiFallbackSite;
@@ -257,11 +257,11 @@ class Interwiki {
$data = array();
- foreach( $sources as $source ) {
+ foreach ( $sources as $source ) {
$list = $db->get( "__list:{$source}" );
foreach ( explode( ' ', $list ) as $iw_prefix ) {
$row = $db->get( "{$source}:{$iw_prefix}" );
- if( !$row ) {
+ if ( !$row ) {
continue;
}
@@ -273,8 +273,8 @@ class Interwiki {
$data[$iw_prefix] = array(
'iw_prefix' => $iw_prefix,
- 'iw_url' => $iw_url,
- 'iw_local' => $iw_local,
+ 'iw_url' => $iw_url,
+ 'iw_local' => $iw_local,
);
}
}
@@ -287,8 +287,8 @@ class Interwiki {
/**
* Fetch all interwiki prefixes from DB
*
- * @param $local string|null If not null, limits output to local/non-local interwikis
- * @return Array List of prefixes
+ * @param string|null $local If not null, limits output to local/non-local interwikis
+ * @return array List of prefixes
* @since 1.19
*/
protected static function getAllPrefixesDB( $local ) {
@@ -318,8 +318,8 @@ class Interwiki {
/**
* Returns all interwiki prefixes
*
- * @param $local string|null If set, limits output to local/non-local interwikis
- * @return Array List of prefixes
+ * @param string|null $local If set, limits output to local/non-local interwikis
+ * @return array List of prefixes
* @since 1.19
*/
public static function getAllPrefixes( $local = null ) {
@@ -335,15 +335,15 @@ class Interwiki {
/**
* 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 string $title What text to put for the article name
+ * @return string The URL
* @note Prior to 1.19 The getURL with an argument was broken.
* If you if you use this arg in an extension that supports MW earlier
* than 1.19 please wfUrlencode and substitute $1 on your own.
*/
public function getURL( $title = null ) {
$url = $this->mURL;
- if( $title !== null ) {
+ if ( $title !== null ) {
$url = str_replace( "$1", wfUrlencode( $title ), $url );
}
return $url;
@@ -352,7 +352,7 @@ class Interwiki {
/**
* Get the API URL for this wiki
*
- * @return String: the URL
+ * @return string The URL
*/
public function getAPI() {
return $this->mAPI;
@@ -361,7 +361,7 @@ class Interwiki {
/**
* Get the DB name for this wiki
*
- * @return String: the DB name
+ * @return string The DB name
*/
public function getWikiID() {
return $this->mWikiID;
@@ -371,7 +371,7 @@ class Interwiki {
* Is this a local link from a sister project, or is
* it something outside, like Google
*
- * @return Boolean
+ * @return bool
*/
public function isLocal() {
return $this->mLocal;
@@ -381,7 +381,7 @@ class Interwiki {
* Can pages from this wiki be transcluded?
* Still requires $wgEnableScaryTransclusion
*
- * @return Boolean
+ * @return bool
*/
public function isTranscludable() {
return $this->mTrans;
@@ -390,7 +390,7 @@ class Interwiki {
/**
* Get the name for the interwiki site
*
- * @return String
+ * @return string
*/
public function getName() {
$msg = wfMessage( 'interwiki-name-' . $this->mPrefix )->inContentLanguage();
@@ -400,7 +400,7 @@ class Interwiki {
/**
* Get a description for this interwiki
*
- * @return String
+ * @return string
*/
public function getDescription() {
$msg = wfMessage( 'interwiki-desc-' . $this->mPrefix )->inContentLanguage();
@@ -409,8 +409,8 @@ class Interwiki {
/**
* Return the list of interwiki fields that should be selected to create
- * a new interwiki object.
- * @return array
+ * a new Interwiki object.
+ * @return string[]
*/
public static function selectFields() {
return array(
diff --git a/includes/job/Job.php b/includes/job/Job.php
index d777a5d4..ab7df5d2 100644
--- a/includes/job/Job.php
+++ b/includes/job/Job.php
@@ -23,11 +23,11 @@
/**
* Class to both describe a background job and handle jobs.
+ * The queue aspects of this class are now deprecated.
*
* @ingroup JobQueue
*/
abstract class Job {
-
/**
* @var Title
*/
@@ -39,6 +39,9 @@ abstract class Job {
$removeDuplicates,
$error;
+ /** @var Array Additional queue metadata */
+ public $metadata = array();
+
/*-------------------------------------------------------------------------
* Abstract functions
*------------------------------------------------------------------------*/
@@ -47,178 +50,25 @@ abstract class Job {
* Run the job
* @return boolean success
*/
- abstract function run();
+ abstract public function run();
/*-------------------------------------------------------------------------
* Static functions
*------------------------------------------------------------------------*/
/**
- * Pop a job of a certain type. This tries less hard than pop() to
- * actually find a job; it may be adversely affected by concurrent job
- * runners.
- *
- * @param $type string
- *
- * @return Job
- */
- static function pop_type( $type ) {
- wfProfilein( __METHOD__ );
-
- $dbw = wfGetDB( DB_MASTER );
-
- $dbw->begin( __METHOD__ );
-
- $row = $dbw->selectRow(
- 'job',
- '*',
- array( 'job_cmd' => $type ),
- __METHOD__,
- array( 'LIMIT' => 1, 'FOR UPDATE' )
- );
-
- if ( $row === false ) {
- $dbw->commit( __METHOD__ );
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- /* Ensure we "own" this row */
- $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
- $affected = $dbw->affectedRows();
- $dbw->commit( __METHOD__ );
-
- if ( $affected == 0 ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- wfIncrStats( 'job-pop' );
- $namespace = $row->job_namespace;
- $dbkey = $row->job_title;
- $title = Title::makeTitleSafe( $namespace, $dbkey );
- $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ),
- $row->job_id );
-
- $job->removeDuplicates();
-
- wfProfileOut( __METHOD__ );
- return $job;
- }
-
- /**
- * Pop a job off the front of the queue
- *
- * @param $offset Integer: Number of jobs to skip
- * @return Job or false if there's no jobs
- */
- static function pop( $offset = 0 ) {
- wfProfileIn( __METHOD__ );
-
- $dbr = wfGetDB( DB_SLAVE );
-
- /* Get a job from the slave, start with an offset,
- scan full set afterwards, avoid hitting purged rows
-
- NB: If random fetch previously was used, offset
- will always be ahead of few entries
- */
-
- $conditions = self::defaultQueueConditions();
-
- $offset = intval( $offset );
- $options = array( 'ORDER BY' => 'job_id', 'USE INDEX' => 'PRIMARY' );
-
- $row = $dbr->selectRow( 'job', '*',
- array_merge( $conditions, array( "job_id >= $offset" ) ),
- __METHOD__,
- $options
- );
-
- // 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 ) {
- $row = $dbr->selectRow( 'job', '*', $conditions, __METHOD__, $options );
- }
-
- if ( $row === false ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
- }
-
- // Try to delete it from the master
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
- $affected = $dbw->affectedRows();
-
- if ( !$affected ) {
- // Failed, someone else beat us to it
- // Try getting a random row
- $row = $dbw->selectRow( 'job', array( 'minjob' => 'MIN(job_id)',
- 'maxjob' => 'MAX(job_id)' ), '1=1', __METHOD__ );
- if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) {
- // No jobs to get
- wfProfileOut( __METHOD__ );
- return false;
- }
- // Get the random row
- $row = $dbw->selectRow( 'job', '*',
- '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
- wfProfileOut( __METHOD__ );
- return false;
- }
- // Delete the random row
- $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
- $affected = $dbw->affectedRows();
-
- if ( !$affected ) {
- // Random job gone before we exclusively deleted it
- // Give up
- wfProfileOut( __METHOD__ );
- return false;
- }
- }
-
- // If execution got to here, there's a row in $row that has been deleted from the database
- // by this thread. Hence the concurrent pop was successful.
- wfIncrStats( 'job-pop' );
- $namespace = $row->job_namespace;
- $dbkey = $row->job_title;
- $title = Title::makeTitleSafe( $namespace, $dbkey );
-
- if ( is_null( $title ) ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
-
- // Remove any duplicates it may have later in the queue
- $job->removeDuplicates();
-
- wfProfileOut( __METHOD__ );
- return $job;
- }
-
- /**
* Create the appropriate object to handle a specific job
*
- * @param $command String: Job command
+ * @param string $command Job command
* @param $title Title: Associated title
- * @param $params Array|bool: Job parameters
- * @param $id Int: Job identifier
+ * @param array|bool $params Job parameters
+ * @param int $id Job identifier
* @throws MWException
* @return Job
*/
- static function factory( $command, Title $title, $params = false, $id = 0 ) {
+ public static function factory( $command, Title $title, $params = false, $id = 0 ) {
global $wgJobClasses;
- if( isset( $wgJobClasses[$command] ) ) {
+ if ( isset( $wgJobClasses[$command] ) ) {
$class = $wgJobClasses[$command];
return new $class( $title, $params, $id );
}
@@ -226,64 +76,18 @@ abstract class Job {
}
/**
- * @param $params
- * @return string
- */
- static function makeBlob( $params ) {
- if ( $params !== false ) {
- return serialize( $params );
- } else {
- return '';
- }
- }
-
- /**
- * @param $blob
- * @return bool|mixed
- */
- static function extractBlob( $blob ) {
- if ( (string)$blob !== '' ) {
- return unserialize( $blob );
- } else {
- return false;
- }
- }
-
- /**
* Batch-insert a group of jobs into the queue.
* This will be wrapped in a transaction with a forced commit.
*
* This may add duplicate at insert time, but they will be
* removed later on, when the first one is popped.
*
- * @param $jobs array of Job objects
+ * @param array $jobs of Job objects
+ * @return bool
+ * @deprecated since 1.21
*/
- static function batchInsert( $jobs ) {
- if ( !count( $jobs ) ) {
- return;
- }
- $dbw = wfGetDB( DB_MASTER );
- $rows = array();
-
- /**
- * @var $job Job
- */
- foreach ( $jobs as $job ) {
- $rows[] = $job->insertFields();
- if ( count( $rows ) >= 50 ) {
- # Do a small transaction to avoid slave lag
- $dbw->begin( __METHOD__ );
- $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
- $dbw->commit( __METHOD__ );
- $rows = array();
- }
- }
- if ( $rows ) { // last chunk
- $dbw->begin( __METHOD__ );
- $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
- $dbw->commit( __METHOD__ );
- }
- wfIncrStats( 'job-insert', count( $jobs ) );
+ public static function batchInsert( $jobs ) {
+ return JobQueueGroup::singleton()->push( $jobs );
}
/**
@@ -293,46 +97,36 @@ abstract class Job {
* be rolled-back as part of a larger transaction. However,
* large batches of jobs can cause slave lag.
*
- * @param $jobs array of Job objects
+ * @param array $jobs of Job objects
+ * @return bool
+ * @deprecated since 1.21
*/
- static function safeBatchInsert( $jobs ) {
- if ( !count( $jobs ) ) {
- return;
- }
- $dbw = wfGetDB( DB_MASTER );
- $rows = array();
- foreach ( $jobs as $job ) {
- $rows[] = $job->insertFields();
- if ( count( $rows ) >= 500 ) {
- $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
- $rows = array();
- }
- }
- if ( $rows ) { // last chunk
- $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
- }
- wfIncrStats( 'job-insert', count( $jobs ) );
+ public static function safeBatchInsert( $jobs ) {
+ return JobQueueGroup::singleton()->push( $jobs, JobQueue::QOS_ATOMIC );
}
-
/**
- * SQL conditions to apply on most JobQueue queries
+ * Pop a job of a certain type. This tries less hard than pop() to
+ * actually find a job; it may be adversely affected by concurrent job
+ * runners.
*
- * Whenever we exclude jobs types from the default queue, we want to make
- * sure that queries to the job queue actually ignore them.
+ * @param $type string
+ * @return Job|bool Returns false if there are no jobs
+ * @deprecated since 1.21
+ */
+ public static function pop_type( $type ) {
+ return JobQueueGroup::singleton()->get( $type )->pop();
+ }
+
+ /**
+ * Pop a job off the front of the queue.
+ * This is subject to $wgJobTypesExcludedFromDefaultQueue.
*
- * @return array SQL conditions suitable for Database:: methods
+ * @return Job or false if there's no jobs
+ * @deprecated since 1.21
*/
- static function defaultQueueConditions( ) {
- global $wgJobTypesExcludedFromDefaultQueue;
- $conditions = array();
- if ( count( $wgJobTypesExcludedFromDefaultQueue ) > 0 ) {
- $dbr = wfGetDB( DB_SLAVE );
- foreach ( $wgJobTypesExcludedFromDefaultQueue as $cmdType ) {
- $conditions[] = "job_cmd != " . $dbr->addQuotes( $cmdType );
- }
- }
- return $conditions;
+ public static function pop() {
+ return JobQueueGroup::singleton()->pop();
}
/*-------------------------------------------------------------------------
@@ -345,83 +139,163 @@ abstract class Job {
* @param $params array|bool
* @param $id int
*/
- function __construct( $command, $title, $params = false, $id = 0 ) {
+ public function __construct( $command, $title, $params = false, $id = 0 ) {
$this->command = $command;
$this->title = $title;
$this->params = $params;
$this->id = $id;
- // A bit of premature generalisation
- // Oh well, the whole class is premature generalisation really
- $this->removeDuplicates = true;
+ $this->removeDuplicates = false; // expensive jobs may set this to true
}
/**
- * Insert a single job into the queue.
- * @return bool true on success
+ * @return integer May be 0 for jobs stored outside the DB
+ * @deprecated since 1.22
*/
- function insert() {
- $fields = $this->insertFields();
+ public function getId() {
+ return $this->id;
+ }
- $dbw = wfGetDB( DB_MASTER );
+ /**
+ * @return string
+ */
+ public function getType() {
+ return $this->command;
+ }
- if ( $this->removeDuplicates ) {
- $res = $dbw->select( 'job', array( '1' ), $fields, __METHOD__ );
- if ( $dbw->numRows( $res ) ) {
- return true;
- }
- }
- wfIncrStats( 'job-insert' );
- return $dbw->insert( 'job', $fields, __METHOD__ );
+ /**
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
}
/**
* @return array
*/
- protected function insertFields() {
- $dbw = wfGetDB( DB_MASTER );
+ public function getParams() {
+ return $this->params;
+ }
+
+ /**
+ * @return integer|null UNIX timestamp to delay running this job until, otherwise null
+ * @since 1.22
+ */
+ public function getReleaseTimestamp() {
+ return isset( $this->params['jobReleaseTimestamp'] )
+ ? wfTimestampOrNull( TS_UNIX, $this->params['jobReleaseTimestamp'] )
+ : null;
+ }
+
+ /**
+ * @return bool Whether only one of each identical set of jobs should be run
+ */
+ public function ignoreDuplicates() {
+ return $this->removeDuplicates;
+ }
+
+ /**
+ * @return bool Whether this job can be retried on failure by job runners
+ * @since 1.21
+ */
+ public function allowRetries() {
+ return true;
+ }
+
+ /**
+ * Subclasses may need to override this to make duplication detection work.
+ * The resulting map conveys everything that makes the job unique. This is
+ * only checked if ignoreDuplicates() returns true, meaning that duplicate
+ * jobs are supposed to be ignored.
+ *
+ * @return Array Map of key/values
+ * @since 1.21
+ */
+ public function getDeduplicationInfo() {
+ $info = array(
+ 'type' => $this->getType(),
+ 'namespace' => $this->getTitle()->getNamespace(),
+ 'title' => $this->getTitle()->getDBkey(),
+ 'params' => $this->getParams()
+ );
+ if ( is_array( $info['params'] ) ) {
+ // Identical jobs with different "root" jobs should count as duplicates
+ unset( $info['params']['rootJobSignature'] );
+ unset( $info['params']['rootJobTimestamp'] );
+ // Likewise for jobs with different delay times
+ unset( $info['params']['jobReleaseTimestamp'] );
+ }
+ return $info;
+ }
+
+ /**
+ * @see JobQueue::deduplicateRootJob()
+ * @param string $key A key that identifies the task
+ * @return Array
+ * @since 1.21
+ */
+ public static function newRootJobParams( $key ) {
return array(
- 'job_id' => $dbw->nextSequenceValue( 'job_job_id_seq' ),
- 'job_cmd' => $this->command,
- 'job_namespace' => $this->title->getNamespace(),
- 'job_title' => $this->title->getDBkey(),
- 'job_timestamp' => $dbw->timestamp(),
- 'job_params' => Job::makeBlob( $this->params )
+ 'rootJobSignature' => sha1( $key ),
+ 'rootJobTimestamp' => wfTimestampNow()
);
}
/**
- * Remove jobs in the job queue which are duplicates of this job.
- * This is deadlock-prone and so starts its own transaction.
+ * @see JobQueue::deduplicateRootJob()
+ * @return Array
+ * @since 1.21
*/
- function removeDuplicates() {
- if ( !$this->removeDuplicates ) {
- return;
- }
+ public function getRootJobParams() {
+ return array(
+ 'rootJobSignature' => isset( $this->params['rootJobSignature'] )
+ ? $this->params['rootJobSignature']
+ : null,
+ 'rootJobTimestamp' => isset( $this->params['rootJobTimestamp'] )
+ ? $this->params['rootJobTimestamp']
+ : null
+ );
+ }
- $fields = $this->insertFields();
- unset( $fields['job_id'] );
- unset( $fields['job_timestamp'] );
- $dbw = wfGetDB( DB_MASTER );
- $dbw->begin( __METHOD__ );
- $dbw->delete( 'job', $fields, __METHOD__ );
- $affected = $dbw->affectedRows();
- $dbw->commit( __METHOD__ );
- if ( $affected ) {
- wfIncrStats( 'job-dup-delete', $affected );
- }
+ /**
+ * @see JobQueue::deduplicateRootJob()
+ * @return bool
+ * @since 1.22
+ */
+ public function hasRootJobParams() {
+ return isset( $this->params['rootJobSignature'] )
+ && isset( $this->params['rootJobTimestamp'] );
+ }
+
+ /**
+ * Insert a single job into the queue.
+ * @return bool true on success
+ * @deprecated since 1.21
+ */
+ public function insert() {
+ return JobQueueGroup::singleton()->push( $this );
}
/**
* @return string
*/
- function toString() {
+ public function toString() {
$paramString = '';
if ( $this->params ) {
foreach ( $this->params as $key => $value ) {
if ( $paramString != '' ) {
$paramString .= ' ';
}
+ if ( is_array( $value ) ) {
+ $value = "array(" . count( $value ) . ")";
+ } elseif ( is_object( $value ) && !method_exists( $value, '__toString' ) ) {
+ $value = "object(" . get_class( $value ) . ")";
+ }
+ $value = (string)$value;
+ if ( mb_strlen( $value ) > 1024 ) {
+ $value = "string(" . mb_strlen( $value ) . ")";
+ }
+
$paramString .= "$key=$value";
}
}
@@ -441,7 +315,7 @@ abstract class Job {
$this->error = $error;
}
- function getLastError() {
+ public function getLastError() {
return $this->error;
}
}
diff --git a/includes/job/JobQueue.php b/includes/job/JobQueue.php
new file mode 100644
index 00000000..6556ee85
--- /dev/null
+++ b/includes/job/JobQueue.php
@@ -0,0 +1,706 @@
+<?php
+/**
+ * Job queue base code.
+ *
+ * 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
+ * @defgroup JobQueue JobQueue
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle enqueueing and running of background jobs
+ *
+ * @ingroup JobQueue
+ * @since 1.21
+ */
+abstract class JobQueue {
+ protected $wiki; // string; wiki ID
+ protected $type; // string; job type
+ protected $order; // string; job priority for pop()
+ protected $claimTTL; // integer; seconds
+ protected $maxTries; // integer; maximum number of times to try a job
+ protected $checkDelay; // boolean; allow delayed jobs
+
+ /** @var BagOStuff */
+ protected $dupCache;
+
+ const QOS_ATOMIC = 1; // integer; "all-or-nothing" job insertions
+
+ const ROOTJOB_TTL = 2419200; // integer; seconds to remember root jobs (28 days)
+
+ /**
+ * @param $params array
+ */
+ protected function __construct( array $params ) {
+ $this->wiki = $params['wiki'];
+ $this->type = $params['type'];
+ $this->claimTTL = isset( $params['claimTTL'] ) ? $params['claimTTL'] : 0;
+ $this->maxTries = isset( $params['maxTries'] ) ? $params['maxTries'] : 3;
+ if ( isset( $params['order'] ) && $params['order'] !== 'any' ) {
+ $this->order = $params['order'];
+ } else {
+ $this->order = $this->optimalOrder();
+ }
+ if ( !in_array( $this->order, $this->supportedOrders() ) ) {
+ throw new MWException( __CLASS__ . " does not support '{$this->order}' order." );
+ }
+ $this->checkDelay = !empty( $params['checkDelay'] );
+ if ( $this->checkDelay && !$this->supportsDelayedJobs() ) {
+ throw new MWException( __CLASS__ . " does not support delayed jobs." );
+ }
+ $this->dupCache = wfGetCache( CACHE_ANYTHING );
+ }
+
+ /**
+ * Get a job queue object of the specified type.
+ * $params includes:
+ * - class : What job class to use (determines job type)
+ * - wiki : wiki ID of the wiki the jobs are for (defaults to current wiki)
+ * - type : The name of the job types this queue handles
+ * - order : Order that pop() selects jobs, one of "fifo", "timestamp" or "random".
+ * If "fifo" is used, the queue will effectively be FIFO. Note that job
+ * completion will not appear to be exactly FIFO if there are multiple
+ * job runners since jobs can take different times to finish once popped.
+ * If "timestamp" is used, the queue will at least be loosely ordered
+ * by timestamp, allowing for some jobs to be popped off out of order.
+ * If "random" is used, pop() will pick jobs in random order.
+ * Note that it may only be weakly random (e.g. a lottery of the oldest X).
+ * If "any" is choosen, the queue will use whatever order is the fastest.
+ * This might be useful for improving concurrency for job acquisition.
+ * - claimTTL : If supported, the queue will recycle jobs that have been popped
+ * but not acknowledged as completed after this many seconds. Recycling
+ * of jobs simple means re-inserting them into the queue. Jobs can be
+ * attempted up to three times before being discarded.
+ * - checkDelay : If supported, respect Job::getReleaseTimestamp() in the push functions.
+ * This lets delayed jobs wait in a staging area until a given timestamp is
+ * reached, at which point they will enter the queue. If this is not enabled
+ * or not supported, an exception will be thrown on delayed job insertion.
+ *
+ * Queue classes should throw an exception if they do not support the options given.
+ *
+ * @param $params array
+ * @return JobQueue
+ * @throws MWException
+ */
+ final public static function factory( array $params ) {
+ $class = $params['class'];
+ if ( !class_exists( $class ) ) {
+ throw new MWException( "Invalid job queue class '$class'." );
+ }
+ $obj = new $class( $params );
+ if ( !( $obj instanceof self ) ) {
+ throw new MWException( "Class '$class' is not a " . __CLASS__ . " class." );
+ }
+ return $obj;
+ }
+
+ /**
+ * @return string Wiki ID
+ */
+ final public function getWiki() {
+ return $this->wiki;
+ }
+
+ /**
+ * @return string Job type that this queue handles
+ */
+ final public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * @return string One of (random, timestamp, fifo, undefined)
+ */
+ final public function getOrder() {
+ return $this->order;
+ }
+
+ /**
+ * @return bool Whether delayed jobs are enabled
+ * @since 1.22
+ */
+ final public function delayedJobsEnabled() {
+ return $this->checkDelay;
+ }
+
+ /**
+ * Get the allowed queue orders for configuration validation
+ *
+ * @return Array Subset of (random, timestamp, fifo, undefined)
+ */
+ abstract protected function supportedOrders();
+
+ /**
+ * Get the default queue order to use if configuration does not specify one
+ *
+ * @return string One of (random, timestamp, fifo, undefined)
+ */
+ abstract protected function optimalOrder();
+
+ /**
+ * Find out if delayed jobs are supported for configuration validation
+ *
+ * @return boolean Whether delayed jobs are supported
+ */
+ protected function supportsDelayedJobs() {
+ return false; // not implemented
+ }
+
+ /**
+ * Quickly check if the queue has no available (unacquired, non-delayed) jobs.
+ * Queue classes should use caching if they are any slower without memcached.
+ *
+ * If caching is used, this might return false when there are actually no jobs.
+ * If pop() is called and returns false then it should correct the cache. Also,
+ * calling flushCaches() first prevents this. However, this affect is typically
+ * not distinguishable from the race condition between isEmpty() and pop().
+ *
+ * @return bool
+ * @throws JobQueueError
+ */
+ final public function isEmpty() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doIsEmpty();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueue::isEmpty()
+ * @return bool
+ */
+ abstract protected function doIsEmpty();
+
+ /**
+ * Get the number of available (unacquired, non-delayed) jobs in the queue.
+ * Queue classes should use caching if they are any slower without memcached.
+ *
+ * If caching is used, this number might be out of date for a minute.
+ *
+ * @return integer
+ * @throws JobQueueError
+ */
+ final public function getSize() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doGetSize();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueue::getSize()
+ * @return integer
+ */
+ abstract protected function doGetSize();
+
+ /**
+ * Get the number of acquired jobs (these are temporarily out of the queue).
+ * Queue classes should use caching if they are any slower without memcached.
+ *
+ * If caching is used, this number might be out of date for a minute.
+ *
+ * @return integer
+ * @throws JobQueueError
+ */
+ final public function getAcquiredCount() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doGetAcquiredCount();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueue::getAcquiredCount()
+ * @return integer
+ */
+ abstract protected function doGetAcquiredCount();
+
+ /**
+ * Get the number of delayed jobs (these are temporarily out of the queue).
+ * Queue classes should use caching if they are any slower without memcached.
+ *
+ * If caching is used, this number might be out of date for a minute.
+ *
+ * @return integer
+ * @throws JobQueueError
+ * @since 1.22
+ */
+ final public function getDelayedCount() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doGetDelayedCount();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueue::getDelayedCount()
+ * @return integer
+ */
+ protected function doGetDelayedCount() {
+ return 0; // not implemented
+ }
+
+ /**
+ * Get the number of acquired jobs that can no longer be attempted.
+ * Queue classes should use caching if they are any slower without memcached.
+ *
+ * If caching is used, this number might be out of date for a minute.
+ *
+ * @return integer
+ * @throws JobQueueError
+ */
+ final public function getAbandonedCount() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doGetAbandonedCount();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueue::getAbandonedCount()
+ * @return integer
+ */
+ protected function doGetAbandonedCount() {
+ return 0; // not implemented
+ }
+
+ /**
+ * Push a single jobs into the queue.
+ * This does not require $wgJobClasses to be set for the given job type.
+ * Outside callers should use JobQueueGroup::push() instead of this function.
+ *
+ * @param $jobs Job|Array
+ * @param $flags integer Bitfield (supports JobQueue::QOS_ATOMIC)
+ * @return bool Returns false on failure
+ * @throws JobQueueError
+ */
+ final public function push( $jobs, $flags = 0 ) {
+ return $this->batchPush( is_array( $jobs ) ? $jobs : array( $jobs ), $flags );
+ }
+
+ /**
+ * Push a batch of jobs into the queue.
+ * This does not require $wgJobClasses to be set for the given job type.
+ * Outside callers should use JobQueueGroup::push() instead of this function.
+ *
+ * @param array $jobs List of Jobs
+ * @param $flags integer Bitfield (supports JobQueue::QOS_ATOMIC)
+ * @return bool Returns false on failure
+ * @throws JobQueueError
+ */
+ final public function batchPush( array $jobs, $flags = 0 ) {
+ if ( !count( $jobs ) ) {
+ return true; // nothing to do
+ }
+
+ foreach ( $jobs as $job ) {
+ if ( $job->getType() !== $this->type ) {
+ throw new MWException(
+ "Got '{$job->getType()}' job; expected a '{$this->type}' job." );
+ } elseif ( $job->getReleaseTimestamp() && !$this->checkDelay ) {
+ throw new MWException(
+ "Got delayed '{$job->getType()}' job; delays are not supported." );
+ }
+ }
+
+ wfProfileIn( __METHOD__ );
+ $ok = $this->doBatchPush( $jobs, $flags );
+ wfProfileOut( __METHOD__ );
+ return $ok;
+ }
+
+ /**
+ * @see JobQueue::batchPush()
+ * @return bool
+ */
+ abstract protected function doBatchPush( array $jobs, $flags );
+
+ /**
+ * Pop a job off of the queue.
+ * This requires $wgJobClasses to be set for the given job type.
+ * Outside callers should use JobQueueGroup::pop() instead of this function.
+ *
+ * @return Job|bool Returns false if there are no jobs
+ * @throws JobQueueError
+ */
+ final public function pop() {
+ global $wgJobClasses;
+
+ if ( $this->wiki !== wfWikiID() ) {
+ throw new MWException( "Cannot pop '{$this->type}' job off foreign wiki queue." );
+ } elseif ( !isset( $wgJobClasses[$this->type] ) ) {
+ // Do not pop jobs if there is no class for the queue type
+ throw new MWException( "Unrecognized job type '{$this->type}'." );
+ }
+
+ wfProfileIn( __METHOD__ );
+ $job = $this->doPop();
+ wfProfileOut( __METHOD__ );
+
+ // Flag this job as an old duplicate based on its "root" job...
+ try {
+ if ( $job && $this->isRootJobOldDuplicate( $job ) ) {
+ JobQueue::incrStats( 'job-pop-duplicate', $this->type );
+ $job = DuplicateJob::newFromJob( $job ); // convert to a no-op
+ }
+ } catch ( MWException $e ) {} // don't lose jobs over this
+
+ return $job;
+ }
+
+ /**
+ * @see JobQueue::pop()
+ * @return Job
+ */
+ abstract protected function doPop();
+
+ /**
+ * Acknowledge that a job was completed.
+ *
+ * This does nothing for certain queue classes or if "claimTTL" is not set.
+ * Outside callers should use JobQueueGroup::ack() instead of this function.
+ *
+ * @param $job Job
+ * @return bool
+ * @throws JobQueueError
+ */
+ final public function ack( Job $job ) {
+ if ( $job->getType() !== $this->type ) {
+ throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." );
+ }
+ wfProfileIn( __METHOD__ );
+ $ok = $this->doAck( $job );
+ wfProfileOut( __METHOD__ );
+ return $ok;
+ }
+
+ /**
+ * @see JobQueue::ack()
+ * @return bool
+ */
+ abstract protected function doAck( Job $job );
+
+ /**
+ * Register the "root job" of a given job into the queue for de-duplication.
+ * This should only be called right *after* all the new jobs have been inserted.
+ * This is used to turn older, duplicate, job entries into no-ops. The root job
+ * information will remain in the registry until it simply falls out of cache.
+ *
+ * This requires that $job has two special fields in the "params" array:
+ * - rootJobSignature : hash (e.g. SHA1) that identifies the task
+ * - rootJobTimestamp : TS_MW timestamp of this instance of the task
+ *
+ * A "root job" is a conceptual job that consist of potentially many smaller jobs
+ * that are actually inserted into the queue. For example, "refreshLinks" jobs are
+ * spawned when a template is edited. One can think of the task as "update links
+ * of pages that use template X" and an instance of that task as a "root job".
+ * However, what actually goes into the queue are potentially many refreshLinks2 jobs.
+ * Since these jobs include things like page ID ranges and DB master positions, and morph
+ * into smaller refreshLinks2 jobs recursively, simple duplicate detection (like job_sha1)
+ * for individual jobs being identical is not useful.
+ *
+ * In the case of "refreshLinks", if these jobs are still in the queue when the template
+ * is edited again, we want all of these old refreshLinks jobs for that template to become
+ * no-ops. This can greatly reduce server load, since refreshLinks jobs involves parsing.
+ * Essentially, the new batch of jobs belong to a new "root job" and the older ones to a
+ * previous "root job" for the same task of "update links of pages that use template X".
+ *
+ * This does nothing for certain queue classes.
+ *
+ * @param $job Job
+ * @return bool
+ * @throws JobQueueError
+ */
+ final public function deduplicateRootJob( Job $job ) {
+ if ( $job->getType() !== $this->type ) {
+ throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." );
+ }
+ wfProfileIn( __METHOD__ );
+ $ok = $this->doDeduplicateRootJob( $job );
+ wfProfileOut( __METHOD__ );
+ return $ok;
+ }
+
+ /**
+ * @see JobQueue::deduplicateRootJob()
+ * @param $job Job
+ * @return bool
+ */
+ protected function doDeduplicateRootJob( Job $job ) {
+ if ( !$job->hasRootJobParams() ) {
+ throw new MWException( "Cannot register root job; missing parameters." );
+ }
+ $params = $job->getRootJobParams();
+
+ $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
+ // Callers should call batchInsert() and then this function so that if the insert
+ // fails, the de-duplication registration will be aborted. Since the insert is
+ // deferred till "transaction idle", do the same here, so that the ordering is
+ // maintained. Having only the de-duplication registration succeed would cause
+ // jobs to become no-ops without any actual jobs that made them redundant.
+ $timestamp = $this->dupCache->get( $key ); // current last timestamp of this job
+ if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+ return true; // a newer version of this root job was enqueued
+ }
+
+ // Update the timestamp of the last root job started at the location...
+ return $this->dupCache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+ }
+
+ /**
+ * Check if the "root" job of a given job has been superseded by a newer one
+ *
+ * @param $job Job
+ * @return bool
+ * @throws JobQueueError
+ */
+ final protected function isRootJobOldDuplicate( Job $job ) {
+ if ( $job->getType() !== $this->type ) {
+ throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." );
+ }
+ wfProfileIn( __METHOD__ );
+ $isDuplicate = $this->doIsRootJobOldDuplicate( $job );
+ wfProfileOut( __METHOD__ );
+ return $isDuplicate;
+ }
+
+ /**
+ * @see JobQueue::isRootJobOldDuplicate()
+ * @param Job $job
+ * @return bool
+ */
+ protected function doIsRootJobOldDuplicate( Job $job ) {
+ if ( !$job->hasRootJobParams() ) {
+ return false; // job has no de-deplication info
+ }
+ $params = $job->getRootJobParams();
+
+ $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
+ // Get the last time this root job was enqueued
+ $timestamp = $this->dupCache->get( $key );
+
+ // Check if a new root job was started at the location after this one's...
+ return ( $timestamp && $timestamp > $params['rootJobTimestamp'] );
+ }
+
+ /**
+ * @param string $signature Hash identifier of the root job
+ * @return string
+ */
+ protected function getRootJobCacheKey( $signature ) {
+ list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
+ return wfForeignMemcKey( $db, $prefix, 'jobqueue', $this->type, 'rootjob', $signature );
+ }
+
+ /**
+ * Deleted all unclaimed and delayed jobs from the queue
+ *
+ * @return bool Success
+ * @throws JobQueueError
+ * @since 1.22
+ */
+ final public function delete() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doDelete();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueue::delete()
+ * @return bool Success
+ */
+ protected function doDelete() {
+ throw new MWException( "This method is not implemented." );
+ }
+
+ /**
+ * Wait for any slaves or backup servers to catch up.
+ *
+ * This does nothing for certain queue classes.
+ *
+ * @return void
+ * @throws JobQueueError
+ */
+ final public function waitForBackups() {
+ wfProfileIn( __METHOD__ );
+ $this->doWaitForBackups();
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * @see JobQueue::waitForBackups()
+ * @return void
+ */
+ protected function doWaitForBackups() {}
+
+ /**
+ * Return a map of task names to task definition maps.
+ * A "task" is a fast periodic queue maintenance action.
+ * Mutually exclusive tasks must implement their own locking in the callback.
+ *
+ * Each task value is an associative array with:
+ * - name : the name of the task
+ * - callback : a PHP callable that performs the task
+ * - period : the period in seconds corresponding to the task frequency
+ *
+ * @return Array
+ */
+ final public function getPeriodicTasks() {
+ $tasks = $this->doGetPeriodicTasks();
+ foreach ( $tasks as $name => &$def ) {
+ $def['name'] = $name;
+ }
+ return $tasks;
+ }
+
+ /**
+ * @see JobQueue::getPeriodicTasks()
+ * @return Array
+ */
+ protected function doGetPeriodicTasks() {
+ return array();
+ }
+
+ /**
+ * Clear any process and persistent caches
+ *
+ * @return void
+ */
+ final public function flushCaches() {
+ wfProfileIn( __METHOD__ );
+ $this->doFlushCaches();
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * @see JobQueue::flushCaches()
+ * @return void
+ */
+ protected function doFlushCaches() {}
+
+ /**
+ * Get an iterator to traverse over all available jobs in this queue.
+ * This does not include jobs that are currently acquired or delayed.
+ * Note: results may be stale if the queue is concurrently modified.
+ *
+ * @return Iterator
+ * @throws JobQueueError
+ */
+ abstract public function getAllQueuedJobs();
+
+ /**
+ * Get an iterator to traverse over all delayed jobs in this queue.
+ * Note: results may be stale if the queue is concurrently modified.
+ *
+ * @return Iterator
+ * @throws JobQueueError
+ * @since 1.22
+ */
+ public function getAllDelayedJobs() {
+ return new ArrayIterator( array() ); // not implemented
+ }
+
+ /**
+ * Do not use this function outside of JobQueue/JobQueueGroup
+ *
+ * @return string
+ * @since 1.22
+ */
+ public function getCoalesceLocationInternal() {
+ return null;
+ }
+
+ /**
+ * Check whether each of the given queues are empty.
+ * This is used for batching checks for queues stored at the same place.
+ *
+ * @param array $types List of queues types
+ * @return array|null (list of non-empty queue types) or null if unsupported
+ * @throws MWException
+ * @since 1.22
+ */
+ final public function getSiblingQueuesWithJobs( array $types ) {
+ $section = new ProfileSection( __METHOD__ );
+ return $this->doGetSiblingQueuesWithJobs( $types );
+ }
+
+ /**
+ * @see JobQueue::getSiblingQueuesWithJobs()
+ * @param array $types List of queues types
+ * @return array|null (list of queue types) or null if unsupported
+ */
+ protected function doGetSiblingQueuesWithJobs( array $types ) {
+ return null; // not supported
+ }
+
+ /**
+ * Check the size of each of the given queues.
+ * For queues not served by the same store as this one, 0 is returned.
+ * This is used for batching checks for queues stored at the same place.
+ *
+ * @param array $types List of queues types
+ * @return array|null (job type => whether queue is empty) or null if unsupported
+ * @throws MWException
+ * @since 1.22
+ */
+ final public function getSiblingQueueSizes( array $types ) {
+ $section = new ProfileSection( __METHOD__ );
+ return $this->doGetSiblingQueueSizes( $types );
+ }
+
+ /**
+ * @see JobQueue::getSiblingQueuesSize()
+ * @param array $types List of queues types
+ * @return array|null (list of queue types) or null if unsupported
+ */
+ protected function doGetSiblingQueueSizes( array $types ) {
+ return null; // not supported
+ }
+
+ /**
+ * Call wfIncrStats() for the queue overall and for the queue type
+ *
+ * @param string $key Event type
+ * @param string $type Job type
+ * @param integer $delta
+ * @since 1.22
+ */
+ public static function incrStats( $key, $type, $delta = 1 ) {
+ wfIncrStats( $key, $delta );
+ wfIncrStats( "{$key}-{$type}", $delta );
+ }
+
+ /**
+ * Namespace the queue with a key to isolate it for testing
+ *
+ * @param $key string
+ * @return void
+ * @throws MWException
+ */
+ public function setTestingPrefix( $key ) {
+ throw new MWException( "Queue namespacing not supported for this queue type." );
+ }
+}
+
+/**
+ * @ingroup JobQueue
+ * @since 1.22
+ */
+class JobQueueError extends MWException {}
+class JobQueueConnectionError extends JobQueueError {}
diff --git a/includes/job/JobQueueDB.php b/includes/job/JobQueueDB.php
new file mode 100644
index 00000000..c39083df
--- /dev/null
+++ b/includes/job/JobQueueDB.php
@@ -0,0 +1,816 @@
+<?php
+/**
+ * Database-backed job queue code.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle job queues stored in the DB
+ *
+ * @ingroup JobQueue
+ * @since 1.21
+ */
+class JobQueueDB extends JobQueue {
+ const CACHE_TTL_SHORT = 30; // integer; seconds to cache info without re-validating
+ const CACHE_TTL_LONG = 300; // integer; seconds to cache info that is kept up to date
+ const MAX_AGE_PRUNE = 604800; // integer; seconds a job can live once claimed
+ const MAX_JOB_RANDOM = 2147483647; // integer; 2^31 - 1, used for job_random
+ const MAX_OFFSET = 255; // integer; maximum number of rows to skip
+
+ /** @var BagOStuff */
+ protected $cache;
+
+ protected $cluster = false; // string; name of an external DB cluster
+
+ /**
+ * Additional parameters include:
+ * - cluster : The name of an external cluster registered via LBFactory.
+ * If not specified, the primary DB cluster for the wiki will be used.
+ * This can be overridden with a custom cluster so that DB handles will
+ * be retrieved via LBFactory::getExternalLB() and getConnection().
+ * @param $params array
+ */
+ protected function __construct( array $params ) {
+ global $wgMemc;
+
+ parent::__construct( $params );
+
+ $this->cluster = isset( $params['cluster'] ) ? $params['cluster'] : false;
+ // Make sure that we don't use the SQL cache, which would be harmful
+ $this->cache = ( $wgMemc instanceof SqlBagOStuff ) ? new EmptyBagOStuff() : $wgMemc;
+ }
+
+ protected function supportedOrders() {
+ return array( 'random', 'timestamp', 'fifo' );
+ }
+
+ protected function optimalOrder() {
+ return 'random';
+ }
+
+ /**
+ * @see JobQueue::doIsEmpty()
+ * @return bool
+ */
+ protected function doIsEmpty() {
+ $key = $this->getCacheKey( 'empty' );
+
+ $isEmpty = $this->cache->get( $key );
+ if ( $isEmpty === 'true' ) {
+ return true;
+ } elseif ( $isEmpty === 'false' ) {
+ return false;
+ }
+
+ $dbr = $this->getSlaveDB();
+ try {
+ $found = $dbr->selectField( // unclaimed job
+ 'job', '1', array( 'job_cmd' => $this->type, 'job_token' => '' ), __METHOD__
+ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+ $this->cache->add( $key, $found ? 'false' : 'true', self::CACHE_TTL_LONG );
+
+ return !$found;
+ }
+
+ /**
+ * @see JobQueue::doGetSize()
+ * @return integer
+ */
+ protected function doGetSize() {
+ $key = $this->getCacheKey( 'size' );
+
+ $size = $this->cache->get( $key );
+ if ( is_int( $size ) ) {
+ return $size;
+ }
+
+ try {
+ $dbr = $this->getSlaveDB();
+ $size = (int)$dbr->selectField( 'job', 'COUNT(*)',
+ array( 'job_cmd' => $this->type, 'job_token' => '' ),
+ __METHOD__
+ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+ $this->cache->set( $key, $size, self::CACHE_TTL_SHORT );
+
+ return $size;
+ }
+
+ /**
+ * @see JobQueue::doGetAcquiredCount()
+ * @return integer
+ */
+ protected function doGetAcquiredCount() {
+ if ( $this->claimTTL <= 0 ) {
+ return 0; // no acknowledgements
+ }
+
+ $key = $this->getCacheKey( 'acquiredcount' );
+
+ $count = $this->cache->get( $key );
+ if ( is_int( $count ) ) {
+ return $count;
+ }
+
+ $dbr = $this->getSlaveDB();
+ try {
+ $count = (int)$dbr->selectField( 'job', 'COUNT(*)',
+ array( 'job_cmd' => $this->type, "job_token != {$dbr->addQuotes( '' )}" ),
+ __METHOD__
+ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+ $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+
+ return $count;
+ }
+
+ /**
+ * @see JobQueue::doGetAbandonedCount()
+ * @return integer
+ * @throws MWException
+ */
+ protected function doGetAbandonedCount() {
+ global $wgMemc;
+
+ if ( $this->claimTTL <= 0 ) {
+ return 0; // no acknowledgements
+ }
+
+ $key = $this->getCacheKey( 'abandonedcount' );
+
+ $count = $wgMemc->get( $key );
+ if ( is_int( $count ) ) {
+ return $count;
+ }
+
+ $dbr = $this->getSlaveDB();
+ try {
+ $count = (int)$dbr->selectField( 'job', 'COUNT(*)',
+ array(
+ 'job_cmd' => $this->type,
+ "job_token != {$dbr->addQuotes( '' )}",
+ "job_attempts >= " . $dbr->addQuotes( $this->maxTries )
+ ),
+ __METHOD__
+ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+ $wgMemc->set( $key, $count, self::CACHE_TTL_SHORT );
+
+ return $count;
+ }
+
+ /**
+ * @see JobQueue::doBatchPush()
+ * @param array $jobs
+ * @param $flags
+ * @throws DBError|Exception
+ * @return bool
+ */
+ protected function doBatchPush( array $jobs, $flags ) {
+ $dbw = $this->getMasterDB();
+
+ $that = $this;
+ $method = __METHOD__;
+ $dbw->onTransactionIdle(
+ function() use ( $dbw, $that, $jobs, $flags, $method ) {
+ $that->doBatchPushInternal( $dbw, $jobs, $flags, $method );
+ }
+ );
+
+ return true;
+ }
+
+ /**
+ * This function should *not* be called outside of JobQueueDB
+ *
+ * @param DatabaseBase $dbw
+ * @param array $jobs
+ * @param int $flags
+ * @param string $method
+ * @return boolean
+ * @throws type
+ */
+ public function doBatchPushInternal( IDatabase $dbw, array $jobs, $flags, $method ) {
+ if ( !count( $jobs ) ) {
+ return true;
+ }
+
+ $rowSet = array(); // (sha1 => job) map for jobs that are de-duplicated
+ $rowList = array(); // list of jobs for jobs that are are not de-duplicated
+ foreach ( $jobs as $job ) {
+ $row = $this->insertFields( $job );
+ if ( $job->ignoreDuplicates() ) {
+ $rowSet[$row['job_sha1']] = $row;
+ } else {
+ $rowList[] = $row;
+ }
+ }
+
+ if ( $flags & self::QOS_ATOMIC ) {
+ $dbw->begin( $method ); // wrap all the job additions in one transaction
+ }
+ try {
+ // Strip out any duplicate jobs that are already in the queue...
+ if ( count( $rowSet ) ) {
+ $res = $dbw->select( 'job', 'job_sha1',
+ array(
+ // No job_type condition since it's part of the job_sha1 hash
+ 'job_sha1' => array_keys( $rowSet ),
+ 'job_token' => '' // unclaimed
+ ),
+ $method
+ );
+ foreach ( $res as $row ) {
+ wfDebug( "Job with hash '{$row->job_sha1}' is a duplicate.\n" );
+ unset( $rowSet[$row->job_sha1] ); // already enqueued
+ }
+ }
+ // Build the full list of job rows to insert
+ $rows = array_merge( $rowList, array_values( $rowSet ) );
+ // Insert the job rows in chunks to avoid slave lag...
+ foreach ( array_chunk( $rows, 50 ) as $rowBatch ) {
+ $dbw->insert( 'job', $rowBatch, $method );
+ }
+ JobQueue::incrStats( 'job-insert', $this->type, count( $rows ) );
+ JobQueue::incrStats( 'job-insert-duplicate', $this->type,
+ count( $rowSet ) + count( $rowList ) - count( $rows ) );
+ } catch ( DBError $e ) {
+ if ( $flags & self::QOS_ATOMIC ) {
+ $dbw->rollback( $method );
+ }
+ throw $e;
+ }
+ if ( $flags & self::QOS_ATOMIC ) {
+ $dbw->commit( $method );
+ }
+
+ $this->cache->set( $this->getCacheKey( 'empty' ), 'false', JobQueueDB::CACHE_TTL_LONG );
+
+ return true;
+ }
+
+ /**
+ * @see JobQueue::doPop()
+ * @return Job|bool
+ */
+ protected function doPop() {
+ if ( $this->cache->get( $this->getCacheKey( 'empty' ) ) === 'true' ) {
+ return false; // queue is empty
+ }
+
+ $dbw = $this->getMasterDB();
+ try {
+ $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
+ $autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
+ $dbw->clearFlag( DBO_TRX ); // make each query its own transaction
+ $scopedReset = new ScopedCallback( function() use ( $dbw, $autoTrx ) {
+ $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore old setting
+ } );
+
+ $uuid = wfRandomString( 32 ); // pop attempt
+ $job = false; // job popped off
+ do { // retry when our row is invalid or deleted as a duplicate
+ // Try to reserve a row in the DB...
+ if ( in_array( $this->order, array( 'fifo', 'timestamp' ) ) ) {
+ $row = $this->claimOldest( $uuid );
+ } else { // random first
+ $rand = mt_rand( 0, self::MAX_JOB_RANDOM ); // encourage concurrent UPDATEs
+ $gte = (bool)mt_rand( 0, 1 ); // find rows with rand before/after $rand
+ $row = $this->claimRandom( $uuid, $rand, $gte );
+ }
+ // Check if we found a row to reserve...
+ if ( !$row ) {
+ $this->cache->set( $this->getCacheKey( 'empty' ), 'true', self::CACHE_TTL_LONG );
+ break; // nothing to do
+ }
+ JobQueue::incrStats( 'job-pop', $this->type );
+ // Get the job object from the row...
+ $title = Title::makeTitleSafe( $row->job_namespace, $row->job_title );
+ if ( !$title ) {
+ $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
+ wfDebug( "Row has invalid title '{$row->job_title}'." );
+ continue; // try again
+ }
+ $job = Job::factory( $row->job_cmd, $title,
+ self::extractBlob( $row->job_params ), $row->job_id );
+ $job->metadata['id'] = $row->job_id;
+ $job->id = $row->job_id; // XXX: work around broken subclasses
+ break; // done
+ } while ( true );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+
+ return $job;
+ }
+
+ /**
+ * Reserve a row with a single UPDATE without holding row locks over RTTs...
+ *
+ * @param string $uuid 32 char hex string
+ * @param $rand integer Random unsigned integer (31 bits)
+ * @param bool $gte Search for job_random >= $random (otherwise job_random <= $random)
+ * @return Row|false
+ */
+ protected function claimRandom( $uuid, $rand, $gte ) {
+ $dbw = $this->getMasterDB();
+ // Check cache to see if the queue has <= OFFSET items
+ $tinyQueue = $this->cache->get( $this->getCacheKey( 'small' ) );
+
+ $row = false; // the row acquired
+ $invertedDirection = false; // whether one job_random direction was already scanned
+ // This uses a replication safe method for acquiring jobs. One could use UPDATE+LIMIT
+ // instead, but that either uses ORDER BY (in which case it deadlocks in MySQL) or is
+ // not replication safe. Due to http://bugs.mysql.com/bug.php?id=6980, subqueries cannot
+ // be used here with MySQL.
+ do {
+ if ( $tinyQueue ) { // queue has <= MAX_OFFSET rows
+ // For small queues, using OFFSET will overshoot and return no rows more often.
+ // Instead, this uses job_random to pick a row (possibly checking both directions).
+ $ineq = $gte ? '>=' : '<=';
+ $dir = $gte ? 'ASC' : 'DESC';
+ $row = $dbw->selectRow( 'job', '*', // find a random job
+ array(
+ 'job_cmd' => $this->type,
+ 'job_token' => '', // unclaimed
+ "job_random {$ineq} {$dbw->addQuotes( $rand )}" ),
+ __METHOD__,
+ array( 'ORDER BY' => "job_random {$dir}" )
+ );
+ if ( !$row && !$invertedDirection ) {
+ $gte = !$gte;
+ $invertedDirection = true;
+ continue; // try the other direction
+ }
+ } else { // table *may* have >= MAX_OFFSET rows
+ // Bug 42614: "ORDER BY job_random" with a job_random inequality causes high CPU
+ // in MySQL if there are many rows for some reason. This uses a small OFFSET
+ // instead of job_random for reducing excess claim retries.
+ $row = $dbw->selectRow( 'job', '*', // find a random job
+ array(
+ 'job_cmd' => $this->type,
+ 'job_token' => '', // unclaimed
+ ),
+ __METHOD__,
+ array( 'OFFSET' => mt_rand( 0, self::MAX_OFFSET ) )
+ );
+ if ( !$row ) {
+ $tinyQueue = true; // we know the queue must have <= MAX_OFFSET rows
+ $this->cache->set( $this->getCacheKey( 'small' ), 1, 30 );
+ continue; // use job_random
+ }
+ }
+ if ( $row ) { // claim the job
+ $dbw->update( 'job', // update by PK
+ array(
+ 'job_token' => $uuid,
+ 'job_token_timestamp' => $dbw->timestamp(),
+ 'job_attempts = job_attempts+1' ),
+ array( 'job_cmd' => $this->type, 'job_id' => $row->job_id, 'job_token' => '' ),
+ __METHOD__
+ );
+ // This might get raced out by another runner when claiming the previously
+ // selected row. The use of job_random should minimize this problem, however.
+ if ( !$dbw->affectedRows() ) {
+ $row = false; // raced out
+ }
+ } else {
+ break; // nothing to do
+ }
+ } while ( !$row );
+
+ return $row;
+ }
+
+ /**
+ * Reserve a row with a single UPDATE without holding row locks over RTTs...
+ *
+ * @param string $uuid 32 char hex string
+ * @return Row|false
+ */
+ protected function claimOldest( $uuid ) {
+ $dbw = $this->getMasterDB();
+
+ $row = false; // the row acquired
+ do {
+ if ( $dbw->getType() === 'mysql' ) {
+ // Per http://bugs.mysql.com/bug.php?id=6980, we can't use subqueries on the
+ // same table being changed in an UPDATE query in MySQL (gives Error: 1093).
+ // Oracle and Postgre have no such limitation. However, MySQL offers an
+ // alternative here by supporting ORDER BY + LIMIT for UPDATE queries.
+ $dbw->query( "UPDATE {$dbw->tableName( 'job' )} " .
+ "SET " .
+ "job_token = {$dbw->addQuotes( $uuid ) }, " .
+ "job_token_timestamp = {$dbw->addQuotes( $dbw->timestamp() )}, " .
+ "job_attempts = job_attempts+1 " .
+ "WHERE ( " .
+ "job_cmd = {$dbw->addQuotes( $this->type )} " .
+ "AND job_token = {$dbw->addQuotes( '' )} " .
+ ") ORDER BY job_id ASC LIMIT 1",
+ __METHOD__
+ );
+ } else {
+ // Use a subquery to find the job, within an UPDATE to claim it.
+ // This uses as much of the DB wrapper functions as possible.
+ $dbw->update( 'job',
+ array(
+ 'job_token' => $uuid,
+ 'job_token_timestamp' => $dbw->timestamp(),
+ 'job_attempts = job_attempts+1' ),
+ array( 'job_id = (' .
+ $dbw->selectSQLText( 'job', 'job_id',
+ array( 'job_cmd' => $this->type, 'job_token' => '' ),
+ __METHOD__,
+ array( 'ORDER BY' => 'job_id ASC', 'LIMIT' => 1 ) ) .
+ ')'
+ ),
+ __METHOD__
+ );
+ }
+ // Fetch any row that we just reserved...
+ if ( $dbw->affectedRows() ) {
+ $row = $dbw->selectRow( 'job', '*',
+ array( 'job_cmd' => $this->type, 'job_token' => $uuid ), __METHOD__
+ );
+ if ( !$row ) { // raced out by duplicate job removal
+ wfDebug( "Row deleted as duplicate by another process." );
+ }
+ } else {
+ break; // nothing to do
+ }
+ } while ( !$row );
+
+ return $row;
+ }
+
+ /**
+ * @see JobQueue::doAck()
+ * @param Job $job
+ * @throws MWException
+ * @return Job|bool
+ */
+ protected function doAck( Job $job ) {
+ if ( !isset( $job->metadata['id'] ) ) {
+ throw new MWException( "Job of type '{$job->getType()}' has no ID." );
+ }
+
+ $dbw = $this->getMasterDB();
+ try {
+ $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
+ $autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
+ $dbw->clearFlag( DBO_TRX ); // make each query its own transaction
+ $scopedReset = new ScopedCallback( function() use ( $dbw, $autoTrx ) {
+ $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore old setting
+ } );
+
+ // Delete a row with a single DELETE without holding row locks over RTTs...
+ $dbw->delete( 'job',
+ array( 'job_cmd' => $this->type, 'job_id' => $job->metadata['id'] ), __METHOD__ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+
+ return true;
+ }
+
+ /**
+ * @see JobQueue::doDeduplicateRootJob()
+ * @param Job $job
+ * @throws MWException
+ * @return bool
+ */
+ protected function doDeduplicateRootJob( Job $job ) {
+ $params = $job->getParams();
+ if ( !isset( $params['rootJobSignature'] ) ) {
+ throw new MWException( "Cannot register root job; missing 'rootJobSignature'." );
+ } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
+ throw new MWException( "Cannot register root job; missing 'rootJobTimestamp'." );
+ }
+ $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
+ // Callers should call batchInsert() and then this function so that if the insert
+ // fails, the de-duplication registration will be aborted. Since the insert is
+ // deferred till "transaction idle", do the same here, so that the ordering is
+ // maintained. Having only the de-duplication registration succeed would cause
+ // jobs to become no-ops without any actual jobs that made them redundant.
+ $dbw = $this->getMasterDB();
+ $cache = $this->dupCache;
+ $dbw->onTransactionIdle( function() use ( $cache, $params, $key, $dbw ) {
+ $timestamp = $cache->get( $key ); // current last timestamp of this job
+ if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+ return true; // a newer version of this root job was enqueued
+ }
+
+ // Update the timestamp of the last root job started at the location...
+ return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+ } );
+
+ return true;
+ }
+
+ /**
+ * @see JobQueue::doDelete()
+ * @return bool
+ */
+ protected function doDelete() {
+ $dbw = $this->getMasterDB();
+ try {
+ $dbw->delete( 'job', array( 'job_cmd' => $this->type ) );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+ return true;
+ }
+
+ /**
+ * @see JobQueue::doWaitForBackups()
+ * @return void
+ */
+ protected function doWaitForBackups() {
+ wfWaitForSlaves();
+ }
+
+ /**
+ * @return Array
+ */
+ protected function doGetPeriodicTasks() {
+ return array(
+ 'recycleAndDeleteStaleJobs' => array(
+ 'callback' => array( $this, 'recycleAndDeleteStaleJobs' ),
+ 'period' => ceil( $this->claimTTL / 2 )
+ )
+ );
+ }
+
+ /**
+ * @return void
+ */
+ protected function doFlushCaches() {
+ foreach ( array( 'empty', 'size', 'acquiredcount' ) as $type ) {
+ $this->cache->delete( $this->getCacheKey( $type ) );
+ }
+ }
+
+ /**
+ * @see JobQueue::getAllQueuedJobs()
+ * @return Iterator
+ */
+ public function getAllQueuedJobs() {
+ $dbr = $this->getSlaveDB();
+ try {
+ return new MappedIterator(
+ $dbr->select( 'job', '*',
+ array( 'job_cmd' => $this->getType(), 'job_token' => '' ) ),
+ function( $row ) use ( $dbr ) {
+ $job = Job::factory(
+ $row->job_cmd,
+ Title::makeTitle( $row->job_namespace, $row->job_title ),
+ strlen( $row->job_params ) ? unserialize( $row->job_params ) : false,
+ $row->job_id
+ );
+ $job->metadata['id'] = $row->job_id;
+ $job->id = $row->job_id; // XXX: work around broken subclasses
+ return $job;
+ }
+ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+ }
+
+ public function getCoalesceLocationInternal() {
+ return $this->cluster
+ ? "DBCluster:{$this->cluster}:{$this->wiki}"
+ : "LBFactory:{$this->wiki}";
+ }
+
+ protected function doGetSiblingQueuesWithJobs( array $types ) {
+ $dbr = $this->getSlaveDB();
+ $res = $dbr->select( 'job', 'DISTINCT job_cmd',
+ array( 'job_cmd' => $types ), __METHOD__ );
+
+ $types = array();
+ foreach ( $res as $row ) {
+ $types[] = $row->job_cmd;
+ }
+ return $types;
+ }
+
+ protected function doGetSiblingQueueSizes( array $types ) {
+ $dbr = $this->getSlaveDB();
+ $res = $dbr->select( 'job', array( 'job_cmd', 'COUNT(*) AS count' ),
+ array( 'job_cmd' => $types ), __METHOD__, array( 'GROUP BY' => 'job_cmd' ) );
+
+ $sizes = array();
+ foreach ( $res as $row ) {
+ $sizes[$row->job_cmd] = (int)$row->count;
+ }
+ return $sizes;
+ }
+
+ /**
+ * Recycle or destroy any jobs that have been claimed for too long
+ *
+ * @return integer Number of jobs recycled/deleted
+ */
+ public function recycleAndDeleteStaleJobs() {
+ $now = time();
+ $count = 0; // affected rows
+ $dbw = $this->getMasterDB();
+
+ try {
+ if ( !$dbw->lock( "jobqueue-recycle-{$this->type}", __METHOD__, 1 ) ) {
+ return $count; // already in progress
+ }
+
+ // Remove claims on jobs acquired for too long if enabled...
+ if ( $this->claimTTL > 0 ) {
+ $claimCutoff = $dbw->timestamp( $now - $this->claimTTL );
+ // Get the IDs of jobs that have be claimed but not finished after too long.
+ // These jobs can be recycled into the queue by expiring the claim. Selecting
+ // the IDs first means that the UPDATE can be done by primary key (less deadlocks).
+ $res = $dbw->select( 'job', 'job_id',
+ array(
+ 'job_cmd' => $this->type,
+ "job_token != {$dbw->addQuotes( '' )}", // was acquired
+ "job_token_timestamp < {$dbw->addQuotes( $claimCutoff )}", // stale
+ "job_attempts < {$dbw->addQuotes( $this->maxTries )}" ), // retries left
+ __METHOD__
+ );
+ $ids = array_map(
+ function( $o ) {
+ return $o->job_id;
+ }, iterator_to_array( $res )
+ );
+ if ( count( $ids ) ) {
+ // Reset job_token for these jobs so that other runners will pick them up.
+ // Set the timestamp to the current time, as it is useful to now that the job
+ // was already tried before (the timestamp becomes the "released" time).
+ $dbw->update( 'job',
+ array(
+ 'job_token' => '',
+ 'job_token_timestamp' => $dbw->timestamp( $now ) ), // time of release
+ array(
+ 'job_id' => $ids ),
+ __METHOD__
+ );
+ $count += $dbw->affectedRows();
+ JobQueue::incrStats( 'job-recycle', $this->type, $dbw->affectedRows() );
+ $this->cache->set( $this->getCacheKey( 'empty' ), 'false', self::CACHE_TTL_LONG );
+ }
+ }
+
+ // Just destroy any stale jobs...
+ $pruneCutoff = $dbw->timestamp( $now - self::MAX_AGE_PRUNE );
+ $conds = array(
+ 'job_cmd' => $this->type,
+ "job_token != {$dbw->addQuotes( '' )}", // was acquired
+ "job_token_timestamp < {$dbw->addQuotes( $pruneCutoff )}" // stale
+ );
+ if ( $this->claimTTL > 0 ) { // only prune jobs attempted too many times...
+ $conds[] = "job_attempts >= {$dbw->addQuotes( $this->maxTries )}";
+ }
+ // Get the IDs of jobs that are considered stale and should be removed. Selecting
+ // the IDs first means that the UPDATE can be done by primary key (less deadlocks).
+ $res = $dbw->select( 'job', 'job_id', $conds, __METHOD__ );
+ $ids = array_map(
+ function( $o ) {
+ return $o->job_id;
+ }, iterator_to_array( $res )
+ );
+ if ( count( $ids ) ) {
+ $dbw->delete( 'job', array( 'job_id' => $ids ), __METHOD__ );
+ $count += $dbw->affectedRows();
+ JobQueue::incrStats( 'job-abandon', $this->type, $dbw->affectedRows() );
+ }
+
+ $dbw->unlock( "jobqueue-recycle-{$this->type}", __METHOD__ );
+ } catch ( DBError $e ) {
+ $this->throwDBException( $e );
+ }
+
+ return $count;
+ }
+
+ /**
+ * @param $job Job
+ * @return array
+ */
+ protected function insertFields( Job $job ) {
+ $dbw = $this->getMasterDB();
+ return array(
+ // Fields that describe the nature of the job
+ 'job_cmd' => $job->getType(),
+ 'job_namespace' => $job->getTitle()->getNamespace(),
+ 'job_title' => $job->getTitle()->getDBkey(),
+ 'job_params' => self::makeBlob( $job->getParams() ),
+ // Additional job metadata
+ 'job_id' => $dbw->nextSequenceValue( 'job_job_id_seq' ),
+ 'job_timestamp' => $dbw->timestamp(),
+ 'job_sha1' => wfBaseConvert(
+ sha1( serialize( $job->getDeduplicationInfo() ) ),
+ 16, 36, 31
+ ),
+ 'job_random' => mt_rand( 0, self::MAX_JOB_RANDOM )
+ );
+ }
+
+ /**
+ * @return DBConnRef
+ */
+ protected function getSlaveDB() {
+ try {
+ return $this->getDB( DB_SLAVE );
+ } catch ( DBConnectionError $e ) {
+ throw new JobQueueConnectionError( "DBConnectionError:" . $e->getMessage() );
+ }
+ }
+
+ /**
+ * @return DBConnRef
+ */
+ protected function getMasterDB() {
+ try {
+ return $this->getDB( DB_MASTER );
+ } catch ( DBConnectionError $e ) {
+ throw new JobQueueConnectionError( "DBConnectionError:" . $e->getMessage() );
+ }
+ }
+
+ /**
+ * @param $index integer (DB_SLAVE/DB_MASTER)
+ * @return DBConnRef
+ */
+ protected function getDB( $index ) {
+ $lb = ( $this->cluster !== false )
+ ? wfGetLBFactory()->getExternalLB( $this->cluster, $this->wiki )
+ : wfGetLB( $this->wiki );
+ return $lb->getConnectionRef( $index, array(), $this->wiki );
+ }
+
+ /**
+ * @return string
+ */
+ private function getCacheKey( $property ) {
+ list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
+ $cluster = is_string( $this->cluster ) ? $this->cluster : 'main';
+ return wfForeignMemcKey( $db, $prefix, 'jobqueue', $cluster, $this->type, $property );
+ }
+
+ /**
+ * @param $params
+ * @return string
+ */
+ protected static function makeBlob( $params ) {
+ if ( $params !== false ) {
+ return serialize( $params );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * @param $blob
+ * @return bool|mixed
+ */
+ protected static function extractBlob( $blob ) {
+ if ( (string)$blob !== '' ) {
+ return unserialize( $blob );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param DBError $e
+ * @throws JobQueueError
+ */
+ protected function throwDBException( DBError $e ) {
+ throw new JobQueueError( get_class( $e ) . ": " . $e->getMessage() );
+ }
+}
diff --git a/includes/job/JobQueueFederated.php b/includes/job/JobQueueFederated.php
new file mode 100644
index 00000000..d3ce164a
--- /dev/null
+++ b/includes/job/JobQueueFederated.php
@@ -0,0 +1,473 @@
+<?php
+/**
+ * Job queue code for federated queues.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle enqueueing and running of background jobs for federated queues
+ *
+ * This class allows for queues to be partitioned into smaller queues.
+ * A partition is defined by the configuration for a JobQueue instance.
+ * For example, one can set $wgJobTypeConf['refreshLinks'] to point to a
+ * JobQueueFederated instance, which itself would consist of three JobQueueRedis
+ * instances, each using their own redis server. This would allow for the jobs
+ * to be split (evenly or based on weights) accross multiple servers if a single
+ * server becomes impractical or expensive. Different JobQueue classes can be mixed.
+ *
+ * The basic queue configuration (e.g. "order", "claimTTL") of a federated queue
+ * is inherited by the partition queues. Additional configuration defines what
+ * section each wiki is in, what partition queues each section uses (and their weight),
+ * and the JobQueue configuration for each partition. Some sections might only need a
+ * single queue partition, like the sections for groups of small wikis.
+ *
+ * If used for performance, then $wgMainCacheType should be set to memcached/redis.
+ * Note that "fifo" cannot be used for the ordering, since the data is distributed.
+ * One can still use "timestamp" instead, as in "roughly timestamp ordered". Also,
+ * queue classes used by this should ignore down servers (with TTL) to avoid slowness.
+ *
+ * @ingroup JobQueue
+ * @since 1.22
+ */
+class JobQueueFederated extends JobQueue {
+ /** @var Array (partition name => weight) reverse sorted by weight */
+ protected $partitionMap = array();
+ /** @var Array (partition name => JobQueue) reverse sorted by weight */
+ protected $partitionQueues = array();
+ /** @var HashRing */
+ protected $partitionPushRing;
+ /** @var BagOStuff */
+ protected $cache;
+
+ const CACHE_TTL_SHORT = 30; // integer; seconds to cache info without re-validating
+ const CACHE_TTL_LONG = 300; // integer; seconds to cache info that is kept up to date
+
+ /**
+ * @params include:
+ * - sectionsByWiki : A map of wiki IDs to section names.
+ * Wikis will default to using the section "default".
+ * - partitionsBySection : Map of section names to maps of (partition name => weight).
+ * A section called 'default' must be defined if not all wikis
+ * have explicitly defined sections.
+ * - configByPartition : Map of queue partition names to configuration arrays.
+ * These configuration arrays are passed to JobQueue::factory().
+ * The options set here are overriden by those passed to this
+ * the federated queue itself (e.g. 'order' and 'claimTTL').
+ * - partitionsNoPush : List of partition names that can handle pop() but not push().
+ * This can be used to migrate away from a certain partition.
+ * @param array $params
+ */
+ protected function __construct( array $params ) {
+ parent::__construct( $params );
+ $section = isset( $params['sectionsByWiki'][$this->wiki] )
+ ? $params['sectionsByWiki'][$this->wiki]
+ : 'default';
+ if ( !isset( $params['partitionsBySection'][$section] ) ) {
+ throw new MWException( "No configuration for section '$section'." );
+ }
+ // Get the full partition map
+ $this->partitionMap = $params['partitionsBySection'][$section];
+ arsort( $this->partitionMap, SORT_NUMERIC );
+ // Get the partitions jobs can actually be pushed to
+ $partitionPushMap = $this->partitionMap;
+ if ( isset( $params['partitionsNoPush'] ) ) {
+ foreach ( $params['partitionsNoPush'] as $partition ) {
+ unset( $partitionPushMap[$partition] );
+ }
+ }
+ // Get the config to pass to merge into each partition queue config
+ $baseConfig = $params;
+ foreach ( array( 'class', 'sectionsByWiki',
+ 'partitionsBySection', 'configByPartition', 'partitionsNoPush' ) as $o )
+ {
+ unset( $baseConfig[$o] );
+ }
+ // Get the partition queue objects
+ foreach ( $this->partitionMap as $partition => $w ) {
+ if ( !isset( $params['configByPartition'][$partition] ) ) {
+ throw new MWException( "No configuration for partition '$partition'." );
+ }
+ $this->partitionQueues[$partition] = JobQueue::factory(
+ $baseConfig + $params['configByPartition'][$partition] );
+ }
+ // Get the ring of partitions to push jobs into
+ $this->partitionPushRing = new HashRing( $partitionPushMap );
+ // Aggregate cache some per-queue values if there are multiple partition queues
+ $this->cache = count( $this->partitionMap ) > 1 ? wfGetMainCache() : new EmptyBagOStuff();
+ }
+
+ protected function supportedOrders() {
+ // No FIFO due to partitioning, though "rough timestamp order" is supported
+ return array( 'undefined', 'random', 'timestamp' );
+ }
+
+ protected function optimalOrder() {
+ return 'undefined'; // defer to the partitions
+ }
+
+ protected function supportsDelayedJobs() {
+ return true; // defer checks to the partitions
+ }
+
+ protected function doIsEmpty() {
+ $key = $this->getCacheKey( 'empty' );
+
+ $isEmpty = $this->cache->get( $key );
+ if ( $isEmpty === 'true' ) {
+ return true;
+ } elseif ( $isEmpty === 'false' ) {
+ return false;
+ }
+
+ foreach ( $this->partitionQueues as $queue ) {
+ try {
+ if ( !$queue->doIsEmpty() ) {
+ $this->cache->add( $key, 'false', self::CACHE_TTL_LONG );
+ return false;
+ }
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+
+ $this->cache->add( $key, 'true', self::CACHE_TTL_LONG );
+ return true;
+ }
+
+ protected function doGetSize() {
+ return $this->getCrossPartitionSum( 'size', 'doGetSize' );
+ }
+
+ protected function doGetAcquiredCount() {
+ return $this->getCrossPartitionSum( 'acquiredcount', 'doGetAcquiredCount' );
+ }
+
+ protected function doGetDelayedCount() {
+ return $this->getCrossPartitionSum( 'delayedcount', 'doGetDelayedCount' );
+ }
+
+ protected function doGetAbandonedCount() {
+ return $this->getCrossPartitionSum( 'abandonedcount', 'doGetAbandonedCount' );
+ }
+
+ /**
+ * @param string $type
+ * @param string $method
+ * @return integer
+ */
+ protected function getCrossPartitionSum( $type, $method ) {
+ $key = $this->getCacheKey( $type );
+
+ $count = $this->cache->get( $key );
+ if ( is_int( $count ) ) {
+ return $count;
+ }
+
+ $count = 0;
+ foreach ( $this->partitionQueues as $queue ) {
+ try {
+ $count += $queue->$method();
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+
+ $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+ return $count;
+ }
+
+ protected function doBatchPush( array $jobs, $flags ) {
+ if ( !count( $jobs ) ) {
+ return true; // nothing to do
+ }
+ // Local ring variable that may be changed to point to a new ring on failure
+ $partitionRing = $this->partitionPushRing;
+ // Try to insert the jobs and update $partitionsTry on any failures
+ $jobsLeft = $this->tryJobInsertions( $jobs, $partitionRing, $flags );
+ if ( count( $jobsLeft ) ) { // some jobs failed to insert?
+ // Try to insert the remaning jobs once more, ignoring the bad partitions
+ return !count( $this->tryJobInsertions( $jobsLeft, $partitionRing, $flags ) );
+ }
+ return true;
+ }
+
+ /**
+ * @param array $jobs
+ * @param HashRing $partitionRing
+ * @param integer $flags
+ * @return array List of Job object that could not be inserted
+ */
+ protected function tryJobInsertions( array $jobs, HashRing &$partitionRing, $flags ) {
+ $jobsLeft = array();
+
+ // Because jobs are spread across partitions, per-job de-duplication needs
+ // to use a consistent hash to avoid allowing duplicate jobs per partition.
+ // When inserting a batch of de-duplicated jobs, QOS_ATOMIC is disregarded.
+ $uJobsByPartition = array(); // (partition name => job list)
+ foreach ( $jobs as $key => $job ) {
+ if ( $job->ignoreDuplicates() ) {
+ $sha1 = sha1( serialize( $job->getDeduplicationInfo() ) );
+ $uJobsByPartition[$partitionRing->getLocation( $sha1 )][] = $job;
+ unset( $jobs[$key] );
+ }
+ }
+ // Get the batches of jobs that are not de-duplicated
+ if ( $flags & self::QOS_ATOMIC ) {
+ $nuJobBatches = array( $jobs ); // all or nothing
+ } else {
+ // Split the jobs into batches and spread them out over servers if there
+ // are many jobs. This helps keep the partitions even. Otherwise, send all
+ // the jobs to a single partition queue to avoids the extra connections.
+ $nuJobBatches = array_chunk( $jobs, 300 );
+ }
+
+ // Insert the de-duplicated jobs into the queues...
+ foreach ( $uJobsByPartition as $partition => $jobBatch ) {
+ $queue = $this->partitionQueues[$partition];
+ try {
+ $ok = $queue->doBatchPush( $jobBatch, $flags | self::QOS_ATOMIC );
+ } catch ( JobQueueError $e ) {
+ $ok = false;
+ MWExceptionHandler::logException( $e );
+ }
+ if ( $ok ) {
+ $key = $this->getCacheKey( 'empty' );
+ $this->cache->set( $key, 'false', JobQueueDB::CACHE_TTL_LONG );
+ } else {
+ $partitionRing = $partitionRing->newWithoutLocation( $partition ); // blacklist
+ if ( !$partitionRing ) {
+ throw new JobQueueError( "Could not insert job(s), all partitions are down." );
+ }
+ $jobsLeft = array_merge( $jobsLeft, $jobBatch ); // not inserted
+ }
+ }
+
+ // Insert the jobs that are not de-duplicated into the queues...
+ foreach ( $nuJobBatches as $jobBatch ) {
+ $partition = ArrayUtils::pickRandom( $partitionRing->getLocationWeights() );
+ $queue = $this->partitionQueues[$partition];
+ try {
+ $ok = $queue->doBatchPush( $jobBatch, $flags | self::QOS_ATOMIC );
+ } catch ( JobQueueError $e ) {
+ $ok = false;
+ MWExceptionHandler::logException( $e );
+ }
+ if ( $ok ) {
+ $key = $this->getCacheKey( 'empty' );
+ $this->cache->set( $key, 'false', JobQueueDB::CACHE_TTL_LONG );
+ } else {
+ $partitionRing = $partitionRing->newWithoutLocation( $partition ); // blacklist
+ if ( !$partitionRing ) {
+ throw new JobQueueError( "Could not insert job(s), all partitions are down." );
+ }
+ $jobsLeft = array_merge( $jobsLeft, $jobBatch ); // not inserted
+ }
+ }
+
+ return $jobsLeft;
+ }
+
+ protected function doPop() {
+ $key = $this->getCacheKey( 'empty' );
+
+ $isEmpty = $this->cache->get( $key );
+ if ( $isEmpty === 'true' ) {
+ return false;
+ }
+
+ $partitionsTry = $this->partitionMap; // (partition => weight)
+
+ while ( count( $partitionsTry ) ) {
+ $partition = ArrayUtils::pickRandom( $partitionsTry );
+ if ( $partition === false ) {
+ break; // all partitions at 0 weight
+ }
+ $queue = $this->partitionQueues[$partition];
+ try {
+ $job = $queue->pop();
+ } catch ( JobQueueError $e ) {
+ $job = false;
+ MWExceptionHandler::logException( $e );
+ }
+ if ( $job ) {
+ $job->metadata['QueuePartition'] = $partition;
+ return $job;
+ } else {
+ unset( $partitionsTry[$partition] ); // blacklist partition
+ }
+ }
+
+ $this->cache->set( $key, 'true', JobQueueDB::CACHE_TTL_LONG );
+ return false;
+ }
+
+ protected function doAck( Job $job ) {
+ if ( !isset( $job->metadata['QueuePartition'] ) ) {
+ throw new MWException( "The given job has no defined partition name." );
+ }
+ return $this->partitionQueues[$job->metadata['QueuePartition']]->ack( $job );
+ }
+
+ protected function doIsRootJobOldDuplicate( Job $job ) {
+ $params = $job->getRootJobParams();
+ $partitions = $this->partitionPushRing->getLocations( $params['rootJobSignature'], 2 );
+ try {
+ return $this->partitionQueues[$partitions[0]]->doIsRootJobOldDuplicate( $job );
+ } catch ( JobQueueError $e ) {
+ if ( isset( $partitions[1] ) ) { // check fallback partition
+ return $this->partitionQueues[$partitions[1]]->doIsRootJobOldDuplicate( $job );
+ }
+ }
+ return false;
+ }
+
+ protected function doDeduplicateRootJob( Job $job ) {
+ $params = $job->getRootJobParams();
+ $partitions = $this->partitionPushRing->getLocations( $params['rootJobSignature'], 2 );
+ try {
+ return $this->partitionQueues[$partitions[0]]->doDeduplicateRootJob( $job );
+ } catch ( JobQueueError $e ) {
+ if ( isset( $partitions[1] ) ) { // check fallback partition
+ return $this->partitionQueues[$partitions[1]]->doDeduplicateRootJob( $job );
+ }
+ }
+ return false;
+ }
+
+ protected function doDelete() {
+ foreach ( $this->partitionQueues as $queue ) {
+ try {
+ $queue->doDelete();
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+ }
+
+ protected function doWaitForBackups() {
+ foreach ( $this->partitionQueues as $queue ) {
+ try {
+ $queue->waitForBackups();
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+ }
+
+ protected function doGetPeriodicTasks() {
+ $tasks = array();
+ foreach ( $this->partitionQueues as $partition => $queue ) {
+ foreach ( $queue->getPeriodicTasks() as $task => $def ) {
+ $tasks["{$partition}:{$task}"] = $def;
+ }
+ }
+ return $tasks;
+ }
+
+ protected function doFlushCaches() {
+ static $types = array(
+ 'empty',
+ 'size',
+ 'acquiredcount',
+ 'delayedcount',
+ 'abandonedcount'
+ );
+ foreach ( $types as $type ) {
+ $this->cache->delete( $this->getCacheKey( $type ) );
+ }
+ foreach ( $this->partitionQueues as $queue ) {
+ $queue->doFlushCaches();
+ }
+ }
+
+ public function getAllQueuedJobs() {
+ $iterator = new AppendIterator();
+ foreach ( $this->partitionQueues as $queue ) {
+ $iterator->append( $queue->getAllQueuedJobs() );
+ }
+ return $iterator;
+ }
+
+ public function getAllDelayedJobs() {
+ $iterator = new AppendIterator();
+ foreach ( $this->partitionQueues as $queue ) {
+ $iterator->append( $queue->getAllDelayedJobs() );
+ }
+ return $iterator;
+ }
+
+ public function getCoalesceLocationInternal() {
+ return "JobQueueFederated:wiki:{$this->wiki}" .
+ sha1( serialize( array_keys( $this->partitionMap ) ) );
+ }
+
+ protected function doGetSiblingQueuesWithJobs( array $types ) {
+ $result = array();
+ foreach ( $this->partitionQueues as $queue ) {
+ try {
+ $nonEmpty = $queue->doGetSiblingQueuesWithJobs( $types );
+ if ( is_array( $nonEmpty ) ) {
+ $result = array_unique( array_merge( $result, $nonEmpty ) );
+ } else {
+ return null; // not supported on all partitions; bail
+ }
+ if ( count( $result ) == count( $types ) ) {
+ break; // short-circuit
+ }
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+ return array_values( $result );
+ }
+
+ protected function doGetSiblingQueueSizes( array $types ) {
+ $result = array();
+ foreach ( $this->partitionQueues as $queue ) {
+ try {
+ $sizes = $queue->doGetSiblingQueueSizes( $types );
+ if ( is_array( $sizes ) ) {
+ foreach ( $sizes as $type => $size ) {
+ $result[$type] = isset( $result[$type] ) ? $result[$type] + $size : $size;
+ }
+ } else {
+ return null; // not supported on all partitions; bail
+ }
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+ return $result;
+ }
+
+ public function setTestingPrefix( $key ) {
+ foreach ( $this->partitionQueues as $queue ) {
+ $queue->setTestingPrefix( $key );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ private function getCacheKey( $property ) {
+ list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
+ return wfForeignMemcKey( $db, $prefix, 'jobqueue', $this->type, $property );
+ }
+}
diff --git a/includes/job/JobQueueGroup.php b/includes/job/JobQueueGroup.php
new file mode 100644
index 00000000..fa7fee5f
--- /dev/null
+++ b/includes/job/JobQueueGroup.php
@@ -0,0 +1,427 @@
+<?php
+/**
+ * Job queue base code.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle enqueueing of background jobs
+ *
+ * @ingroup JobQueue
+ * @since 1.21
+ */
+class JobQueueGroup {
+ /** @var Array */
+ protected static $instances = array();
+
+ /** @var ProcessCacheLRU */
+ protected $cache;
+
+ protected $wiki; // string; wiki ID
+
+ /** @var array Map of (bucket => (queue => JobQueue, types => list of types) */
+ protected $coalescedQueues;
+
+ const TYPE_DEFAULT = 1; // integer; jobs popped by default
+ const TYPE_ANY = 2; // integer; any job
+
+ const USE_CACHE = 1; // integer; use process or persistent cache
+ const USE_PRIORITY = 2; // integer; respect deprioritization
+
+ const PROC_CACHE_TTL = 15; // integer; seconds
+
+ const CACHE_VERSION = 1; // integer; cache version
+
+ /**
+ * @param string $wiki Wiki ID
+ */
+ protected function __construct( $wiki ) {
+ $this->wiki = $wiki;
+ $this->cache = new ProcessCacheLRU( 10 );
+ }
+
+ /**
+ * @param string $wiki Wiki ID
+ * @return JobQueueGroup
+ */
+ public static function singleton( $wiki = false ) {
+ $wiki = ( $wiki === false ) ? wfWikiID() : $wiki;
+ if ( !isset( self::$instances[$wiki] ) ) {
+ self::$instances[$wiki] = new self( $wiki );
+ }
+ return self::$instances[$wiki];
+ }
+
+ /**
+ * Destroy the singleton instances
+ *
+ * @return void
+ */
+ public static function destroySingletons() {
+ self::$instances = array();
+ }
+
+ /**
+ * Get the job queue object for a given queue type
+ *
+ * @param $type string
+ * @return JobQueue
+ */
+ public function get( $type ) {
+ global $wgJobTypeConf;
+
+ $conf = array( 'wiki' => $this->wiki, 'type' => $type );
+ if ( isset( $wgJobTypeConf[$type] ) ) {
+ $conf = $conf + $wgJobTypeConf[$type];
+ } else {
+ $conf = $conf + $wgJobTypeConf['default'];
+ }
+
+ return JobQueue::factory( $conf );
+ }
+
+ /**
+ * Insert jobs into the respective queues of with the belong.
+ *
+ * This inserts the jobs into the queue specified by $wgJobTypeConf
+ * and updates the aggregate job queue information cache as needed.
+ *
+ * @param $jobs Job|array A single Job or a list of Jobs
+ * @throws MWException
+ * @return bool
+ */
+ public function push( $jobs ) {
+ $jobs = is_array( $jobs ) ? $jobs : array( $jobs );
+
+ $jobsByType = array(); // (job type => list of jobs)
+ foreach ( $jobs as $job ) {
+ if ( $job instanceof Job ) {
+ $jobsByType[$job->getType()][] = $job;
+ } else {
+ throw new MWException( "Attempted to push a non-Job object into a queue." );
+ }
+ }
+
+ $ok = true;
+ foreach ( $jobsByType as $type => $jobs ) {
+ if ( $this->get( $type )->push( $jobs ) ) {
+ JobQueueAggregator::singleton()->notifyQueueNonEmpty( $this->wiki, $type );
+ } else {
+ $ok = false;
+ }
+ }
+
+ if ( $this->cache->has( 'queues-ready', 'list' ) ) {
+ $list = $this->cache->get( 'queues-ready', 'list' );
+ if ( count( array_diff( array_keys( $jobsByType ), $list ) ) ) {
+ $this->cache->clear( 'queues-ready' );
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Pop a job off one of the job queues
+ *
+ * This pops a job off a queue as specified by $wgJobTypeConf and
+ * updates the aggregate job queue information cache as needed.
+ *
+ * @param $qtype integer|string JobQueueGroup::TYPE_DEFAULT or type string
+ * @param $flags integer Bitfield of JobQueueGroup::USE_* constants
+ * @return Job|bool Returns false on failure
+ */
+ public function pop( $qtype = self::TYPE_DEFAULT, $flags = 0 ) {
+ if ( is_string( $qtype ) ) { // specific job type
+ if ( ( $flags & self::USE_PRIORITY ) && $this->isQueueDeprioritized( $qtype ) ) {
+ return false; // back off
+ }
+ $job = $this->get( $qtype )->pop();
+ if ( !$job ) {
+ JobQueueAggregator::singleton()->notifyQueueEmpty( $this->wiki, $qtype );
+ }
+ return $job;
+ } else { // any job in the "default" jobs types
+ if ( $flags & self::USE_CACHE ) {
+ if ( !$this->cache->has( 'queues-ready', 'list', self::PROC_CACHE_TTL ) ) {
+ $this->cache->set( 'queues-ready', 'list', $this->getQueuesWithJobs() );
+ }
+ $types = $this->cache->get( 'queues-ready', 'list' );
+ } else {
+ $types = $this->getQueuesWithJobs();
+ }
+
+ if ( $qtype == self::TYPE_DEFAULT ) {
+ $types = array_intersect( $types, $this->getDefaultQueueTypes() );
+ }
+ shuffle( $types ); // avoid starvation
+
+ foreach ( $types as $type ) { // for each queue...
+ if ( ( $flags & self::USE_PRIORITY ) && $this->isQueueDeprioritized( $type ) ) {
+ continue; // back off
+ }
+ $job = $this->get( $type )->pop();
+ if ( $job ) { // found
+ return $job;
+ } else { // not found
+ JobQueueAggregator::singleton()->notifyQueueEmpty( $this->wiki, $type );
+ $this->cache->clear( 'queues-ready' );
+ }
+ }
+
+ return false; // no jobs found
+ }
+ }
+
+ /**
+ * Acknowledge that a job was completed
+ *
+ * @param $job Job
+ * @return bool
+ */
+ public function ack( Job $job ) {
+ return $this->get( $job->getType() )->ack( $job );
+ }
+
+ /**
+ * Register the "root job" of a given job into the queue for de-duplication.
+ * This should only be called right *after* all the new jobs have been inserted.
+ *
+ * @param $job Job
+ * @return bool
+ */
+ public function deduplicateRootJob( Job $job ) {
+ return $this->get( $job->getType() )->deduplicateRootJob( $job );
+ }
+
+ /**
+ * Wait for any slaves or backup queue servers to catch up.
+ *
+ * This does nothing for certain queue classes.
+ *
+ * @return void
+ * @throws MWException
+ */
+ public function waitForBackups() {
+ global $wgJobTypeConf;
+
+ wfProfileIn( __METHOD__ );
+ // Try to avoid doing this more than once per queue storage medium
+ foreach ( $wgJobTypeConf as $type => $conf ) {
+ $this->get( $type )->waitForBackups();
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Get the list of queue types
+ *
+ * @return array List of strings
+ */
+ public function getQueueTypes() {
+ return array_keys( $this->getCachedConfigVar( 'wgJobClasses' ) );
+ }
+
+ /**
+ * Get the list of default queue types
+ *
+ * @return array List of strings
+ */
+ public function getDefaultQueueTypes() {
+ global $wgJobTypesExcludedFromDefaultQueue;
+
+ return array_diff( $this->getQueueTypes(), $wgJobTypesExcludedFromDefaultQueue );
+ }
+
+ /**
+ * Get the list of job types that have non-empty queues
+ *
+ * @return Array List of job types that have non-empty queues
+ */
+ public function getQueuesWithJobs() {
+ $types = array();
+ foreach ( $this->getCoalescedQueues() as $info ) {
+ $nonEmpty = $info['queue']->getSiblingQueuesWithJobs( $this->getQueueTypes() );
+ if ( is_array( $nonEmpty ) ) { // batching features supported
+ $types = array_merge( $types, $nonEmpty );
+ } else { // we have to go through the queues in the bucket one-by-one
+ foreach ( $info['types'] as $type ) {
+ if ( !$this->get( $type )->isEmpty() ) {
+ $types[] = $type;
+ }
+ }
+ }
+ }
+ return $types;
+ }
+
+ /**
+ * Get the size of the queus for a list of job types
+ *
+ * @return Array Map of (job type => size)
+ */
+ public function getQueueSizes() {
+ $sizeMap = array();
+ foreach ( $this->getCoalescedQueues() as $info ) {
+ $sizes = $info['queue']->getSiblingQueueSizes( $this->getQueueTypes() );
+ if ( is_array( $sizes ) ) { // batching features supported
+ $sizeMap = $sizeMap + $sizes;
+ } else { // we have to go through the queues in the bucket one-by-one
+ foreach ( $info['types'] as $type ) {
+ $sizeMap[$type] = $this->get( $type )->getSize();
+ }
+ }
+ }
+ return $sizeMap;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getCoalescedQueues() {
+ global $wgJobTypeConf;
+
+ if ( $this->coalescedQueues === null ) {
+ $this->coalescedQueues = array();
+ foreach ( $wgJobTypeConf as $type => $conf ) {
+ $queue = JobQueue::factory(
+ array( 'wiki' => $this->wiki, 'type' => 'null' ) + $conf );
+ $loc = $queue->getCoalesceLocationInternal();
+ if ( !isset( $this->coalescedQueues[$loc] ) ) {
+ $this->coalescedQueues[$loc]['queue'] = $queue;
+ $this->coalescedQueues[$loc]['types'] = array();
+ }
+ if ( $type === 'default' ) {
+ $this->coalescedQueues[$loc]['types'] = array_merge(
+ $this->coalescedQueues[$loc]['types'],
+ array_diff( $this->getQueueTypes(), array_keys( $wgJobTypeConf ) )
+ );
+ } else {
+ $this->coalescedQueues[$loc]['types'][] = $type;
+ }
+ }
+ }
+
+ return $this->coalescedQueues;
+ }
+
+ /**
+ * Check if jobs should not be popped of a queue right now.
+ * This is only used for performance, such as to avoid spamming
+ * the queue with many sub-jobs before they actually get run.
+ *
+ * @param $type string
+ * @return bool
+ */
+ public function isQueueDeprioritized( $type ) {
+ if ( $this->cache->has( 'isDeprioritized', $type, 5 ) ) {
+ return $this->cache->get( 'isDeprioritized', $type );
+ }
+ if ( $type === 'refreshLinks2' ) {
+ // Don't keep converting refreshLinks2 => refreshLinks jobs if the
+ // later jobs have not been done yet. This helps throttle queue spam.
+ $deprioritized = !$this->get( 'refreshLinks' )->isEmpty();
+ $this->cache->set( 'isDeprioritized', $type, $deprioritized );
+ return $deprioritized;
+ }
+ return false;
+ }
+
+ /**
+ * Execute any due periodic queue maintenance tasks for all queues.
+ *
+ * A task is "due" if the time ellapsed since the last run is greater than
+ * the defined run period. Concurrent calls to this function will cause tasks
+ * to be attempted twice, so they may need their own methods of mutual exclusion.
+ *
+ * @return integer Number of tasks run
+ */
+ public function executeReadyPeriodicTasks() {
+ global $wgMemc;
+
+ list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
+ $key = wfForeignMemcKey( $db, $prefix, 'jobqueuegroup', 'taskruns', 'v1' );
+ $lastRuns = $wgMemc->get( $key ); // (queue => task => UNIX timestamp)
+
+ $count = 0;
+ $tasksRun = array(); // (queue => task => UNIX timestamp)
+ foreach ( $this->getQueueTypes() as $type ) {
+ $queue = $this->get( $type );
+ foreach ( $queue->getPeriodicTasks() as $task => $definition ) {
+ if ( $definition['period'] <= 0 ) {
+ continue; // disabled
+ } elseif ( !isset( $lastRuns[$type][$task] )
+ || $lastRuns[$type][$task] < ( time() - $definition['period'] ) )
+ {
+ try {
+ if ( call_user_func( $definition['callback'] ) !== null ) {
+ $tasksRun[$type][$task] = time();
+ ++$count;
+ }
+ } catch ( JobQueueError $e ) {
+ MWExceptionHandler::logException( $e );
+ }
+ }
+ }
+ }
+
+ $wgMemc->merge( $key, function( $cache, $key, $lastRuns ) use ( $tasksRun ) {
+ if ( is_array( $lastRuns ) ) {
+ foreach ( $tasksRun as $type => $tasks ) {
+ foreach ( $tasks as $task => $timestamp ) {
+ if ( !isset( $lastRuns[$type][$task] )
+ || $timestamp > $lastRuns[$type][$task] )
+ {
+ $lastRuns[$type][$task] = $timestamp;
+ }
+ }
+ }
+ } else {
+ $lastRuns = $tasksRun;
+ }
+ return $lastRuns;
+ } );
+
+ return $count;
+ }
+
+ /**
+ * @param $name string
+ * @return mixed
+ */
+ private function getCachedConfigVar( $name ) {
+ global $wgConf, $wgMemc;
+
+ if ( $this->wiki === wfWikiID() ) {
+ return $GLOBALS[$name]; // common case
+ } else {
+ list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
+ $key = wfForeignMemcKey( $db, $prefix, 'configvalue', $name );
+ $value = $wgMemc->get( $key ); // ('v' => ...) or false
+ if ( is_array( $value ) ) {
+ return $value['v'];
+ } else {
+ $value = $wgConf->getConfig( $this->wiki, $name );
+ $wgMemc->set( $key, array( 'v' => $value ), 86400 + mt_rand( 0, 86400 ) );
+ return $value;
+ }
+ }
+ }
+}
diff --git a/includes/job/JobQueueRedis.php b/includes/job/JobQueueRedis.php
new file mode 100644
index 00000000..378e1755
--- /dev/null
+++ b/includes/job/JobQueueRedis.php
@@ -0,0 +1,856 @@
+<?php
+/**
+ * Redis-backed job queue code.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle job queues stored in Redis
+ *
+ * This is faster, less resource intensive, queue that JobQueueDB.
+ * All data for a queue using this class is placed into one redis server.
+ *
+ * There are eight main redis keys used to track jobs:
+ * - l-unclaimed : A list of job IDs used for ready unclaimed jobs
+ * - z-claimed : A sorted set of (job ID, UNIX timestamp as score) used for job retries
+ * - z-abandoned : A sorted set of (job ID, UNIX timestamp as score) used for broken jobs
+ * - z-delayed : A sorted set of (job ID, UNIX timestamp as score) used for delayed jobs
+ * - h-idBySha1 : A hash of (SHA1 => job ID) for unclaimed jobs used for de-duplication
+ * - h-sha1ById : A hash of (job ID => SHA1) for unclaimed jobs used for de-duplication
+ * - h-attempts : A hash of (job ID => attempt count) used for job claiming/retries
+ * - h-data : A hash of (job ID => serialized blobs) for job storage
+ * A job ID can be in only one of z-delayed, l-unclaimed, z-claimed, and z-abandoned.
+ * If an ID appears in any of those lists, it should have a h-data entry for its ID.
+ * If a job has a SHA1 de-duplication value and its ID is in l-unclaimed or z-delayed, then
+ * there should be no other such jobs with that SHA1. Every h-idBySha1 entry has an h-sha1ById
+ * entry and every h-sha1ById must refer to an ID that is l-unclaimed. If a job has its
+ * ID in z-claimed or z-abandoned, then it must also have an h-attempts entry for its ID.
+ *
+ * Additionally, "rootjob:* keys track "root jobs" used for additional de-duplication.
+ * Aside from root job keys, all keys have no expiry, and are only removed when jobs are run.
+ * All the keys are prefixed with the relevant wiki ID information.
+ *
+ * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
+ * Additionally, it should be noted that redis has different persistence modes, such
+ * as rdb snapshots, journaling, and no persistent. Appropriate configuration should be
+ * made on the servers based on what queues are using it and what tolerance they have.
+ *
+ * @ingroup JobQueue
+ * @ingroup Redis
+ * @since 1.22
+ */
+class JobQueueRedis extends JobQueue {
+ /** @var RedisConnectionPool */
+ protected $redisPool;
+
+ protected $server; // string; server address
+ protected $compression; // string; compression method to use
+
+ const MAX_AGE_PRUNE = 604800; // integer; seconds a job can live once claimed (7 days)
+
+ protected $key; // string; key to prefix the queue keys with (used for testing)
+
+ /**
+ * @params include:
+ * - redisConfig : An array of parameters to RedisConnectionPool::__construct().
+ * Note that the serializer option is ignored "none" is always used.
+ * - redisServer : A hostname/port combination or the absolute path of a UNIX socket.
+ * If a hostname is specified but no port, the standard port number
+ * 6379 will be used. Required.
+ * - compression : The type of compression to use; one of (none,gzip).
+ * @param array $params
+ */
+ public function __construct( array $params ) {
+ parent::__construct( $params );
+ $params['redisConfig']['serializer'] = 'none'; // make it easy to use Lua
+ $this->server = $params['redisServer'];
+ $this->compression = isset( $params['compression'] ) ? $params['compression'] : 'none';
+ $this->redisPool = RedisConnectionPool::singleton( $params['redisConfig'] );
+ }
+
+ protected function supportedOrders() {
+ return array( 'timestamp', 'fifo' );
+ }
+
+ protected function optimalOrder() {
+ return 'fifo';
+ }
+
+ protected function supportsDelayedJobs() {
+ return true;
+ }
+
+ /**
+ * @see JobQueue::doIsEmpty()
+ * @return bool
+ * @throws MWException
+ */
+ protected function doIsEmpty() {
+ return $this->doGetSize() == 0;
+ }
+
+ /**
+ * @see JobQueue::doGetSize()
+ * @return integer
+ * @throws MWException
+ */
+ protected function doGetSize() {
+ $conn = $this->getConnection();
+ try {
+ return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::doGetAcquiredCount()
+ * @return integer
+ * @throws MWException
+ */
+ protected function doGetAcquiredCount() {
+ if ( $this->claimTTL <= 0 ) {
+ return 0; // no acknowledgements
+ }
+ $conn = $this->getConnection();
+ try {
+ $conn->multi( Redis::PIPELINE );
+ $conn->zSize( $this->getQueueKey( 'z-claimed' ) );
+ $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
+ return array_sum( $conn->exec() );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::doGetDelayedCount()
+ * @return integer
+ * @throws MWException
+ */
+ protected function doGetDelayedCount() {
+ if ( !$this->checkDelay ) {
+ return 0; // no delayed jobs
+ }
+ $conn = $this->getConnection();
+ try {
+ return $conn->zSize( $this->getQueueKey( 'z-delayed' ) );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::doGetAbandonedCount()
+ * @return integer
+ * @throws MWException
+ */
+ protected function doGetAbandonedCount() {
+ if ( $this->claimTTL <= 0 ) {
+ return 0; // no acknowledgements
+ }
+ $conn = $this->getConnection();
+ try {
+ return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::doBatchPush()
+ * @param array $jobs
+ * @param $flags
+ * @return bool
+ * @throws MWException
+ */
+ protected function doBatchPush( array $jobs, $flags ) {
+ // Convert the jobs into field maps (de-duplicated against each other)
+ $items = array(); // (job ID => job fields map)
+ foreach ( $jobs as $job ) {
+ $item = $this->getNewJobFields( $job );
+ if ( strlen( $item['sha1'] ) ) { // hash identifier => de-duplicate
+ $items[$item['sha1']] = $item;
+ } else {
+ $items[$item['uuid']] = $item;
+ }
+ }
+
+ if ( !count( $items ) ) {
+ return true; // nothing to do
+ }
+
+ $conn = $this->getConnection();
+ try {
+ // Actually push the non-duplicate jobs into the queue...
+ if ( $flags & self::QOS_ATOMIC ) {
+ $batches = array( $items ); // all or nothing
+ } else {
+ $batches = array_chunk( $items, 500 ); // avoid tying up the server
+ }
+ $failed = 0;
+ $pushed = 0;
+ foreach ( $batches as $itemBatch ) {
+ $added = $this->pushBlobs( $conn, $itemBatch );
+ if ( is_int( $added ) ) {
+ $pushed += $added;
+ } else {
+ $failed += count( $itemBatch );
+ }
+ }
+ if ( $failed > 0 ) {
+ wfDebugLog( 'JobQueueRedis', "Could not insert {$failed} {$this->type} job(s)." );
+ return false;
+ }
+ JobQueue::incrStats( 'job-insert', $this->type, count( $items ) );
+ JobQueue::incrStats( 'job-insert-duplicate', $this->type,
+ count( $items ) - $failed - $pushed );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+
+ return true;
+ }
+
+ /**
+ * @param RedisConnRef $conn
+ * @param array $items List of results from JobQueueRedis::getNewJobFields()
+ * @return integer Number of jobs inserted (duplicates are ignored)
+ * @throws RedisException
+ */
+ protected function pushBlobs( RedisConnRef $conn, array $items ) {
+ $args = array(); // ([id, sha1, rtime, blob [, id, sha1, rtime, blob ... ] ] )
+ foreach ( $items as $item ) {
+ $args[] = (string)$item['uuid'];
+ $args[] = (string)$item['sha1'];
+ $args[] = (string)$item['rtimestamp'];
+ $args[] = (string)$this->serialize( $item );
+ }
+ static $script =
+<<<LUA
+ if #ARGV % 4 ~= 0 then return redis.error_reply('Unmatched arguments') end
+ local pushed = 0
+ for i = 1,#ARGV,4 do
+ local id,sha1,rtimestamp,blob = ARGV[i],ARGV[i+1],ARGV[i+2],ARGV[i+3]
+ if sha1 == '' or redis.call('hExists',KEYS[3],sha1) == 0 then
+ if 1*rtimestamp > 0 then
+ -- Insert into delayed queue (release time as score)
+ redis.call('zAdd',KEYS[4],rtimestamp,id)
+ else
+ -- Insert into unclaimed queue
+ redis.call('lPush',KEYS[1],id)
+ end
+ if sha1 ~= '' then
+ redis.call('hSet',KEYS[2],id,sha1)
+ redis.call('hSet',KEYS[3],sha1,id)
+ end
+ redis.call('hSet',KEYS[5],id,blob)
+ pushed = pushed + 1
+ end
+ end
+ return pushed
+LUA;
+ return $conn->luaEval( $script,
+ array_merge(
+ array(
+ $this->getQueueKey( 'l-unclaimed' ), # KEYS[1]
+ $this->getQueueKey( 'h-sha1ById' ), # KEYS[2]
+ $this->getQueueKey( 'h-idBySha1' ), # KEYS[3]
+ $this->getQueueKey( 'z-delayed' ), # KEYS[4]
+ $this->getQueueKey( 'h-data' ), # KEYS[5]
+ ),
+ $args
+ ),
+ 5 # number of first argument(s) that are keys
+ );
+ }
+
+ /**
+ * @see JobQueue::doPop()
+ * @return Job|bool
+ * @throws MWException
+ */
+ protected function doPop() {
+ $job = false;
+
+ // Push ready delayed jobs into the queue every 10 jobs to spread the load.
+ // This is also done as a periodic task, but we don't want too much done at once.
+ if ( $this->checkDelay && mt_rand( 0, 9 ) == 0 ) {
+ $this->releaseReadyDelayedJobs();
+ }
+
+ $conn = $this->getConnection();
+ try {
+ do {
+ if ( $this->claimTTL > 0 ) {
+ // Keep the claimed job list down for high-traffic queues
+ if ( mt_rand( 0, 99 ) == 0 ) {
+ $this->recycleAndDeleteStaleJobs();
+ }
+ $blob = $this->popAndAcquireBlob( $conn );
+ } else {
+ $blob = $this->popAndDeleteBlob( $conn );
+ }
+ if ( $blob === false ) {
+ break; // no jobs; nothing to do
+ }
+
+ JobQueue::incrStats( 'job-pop', $this->type );
+ $item = $this->unserialize( $blob );
+ if ( $item === false ) {
+ wfDebugLog( 'JobQueueRedis', "Could not unserialize {$this->type} job." );
+ continue;
+ }
+
+ // If $item is invalid, recycleAndDeleteStaleJobs() will cleanup as needed
+ $job = $this->getJobFromFields( $item ); // may be false
+ } while ( !$job ); // job may be false if invalid
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+
+ return $job;
+ }
+
+ /**
+ * @param RedisConnRef $conn
+ * @return array serialized string or false
+ * @throws RedisException
+ */
+ protected function popAndDeleteBlob( RedisConnRef $conn ) {
+ static $script =
+<<<LUA
+ -- Pop an item off the queue
+ local id = redis.call('rpop',KEYS[1])
+ if not id then return false end
+ -- Get the job data and remove it
+ local item = redis.call('hGet',KEYS[4],id)
+ redis.call('hDel',KEYS[4],id)
+ -- Allow new duplicates of this job
+ local sha1 = redis.call('hGet',KEYS[2],id)
+ if sha1 then redis.call('hDel',KEYS[3],sha1) end
+ redis.call('hDel',KEYS[2],id)
+ -- Return the job data
+ return item
+LUA;
+ return $conn->luaEval( $script,
+ array(
+ $this->getQueueKey( 'l-unclaimed' ), # KEYS[1]
+ $this->getQueueKey( 'h-sha1ById' ), # KEYS[2]
+ $this->getQueueKey( 'h-idBySha1' ), # KEYS[3]
+ $this->getQueueKey( 'h-data' ), # KEYS[4]
+ ),
+ 4 # number of first argument(s) that are keys
+ );
+ }
+
+ /**
+ * @param RedisConnRef $conn
+ * @return array serialized string or false
+ * @throws RedisException
+ */
+ protected function popAndAcquireBlob( RedisConnRef $conn ) {
+ static $script =
+<<<LUA
+ -- Pop an item off the queue
+ local id = redis.call('rPop',KEYS[1])
+ if not id then return false end
+ -- Allow new duplicates of this job
+ local sha1 = redis.call('hGet',KEYS[2],id)
+ if sha1 then redis.call('hDel',KEYS[3],sha1) end
+ redis.call('hDel',KEYS[2],id)
+ -- Mark the jobs as claimed and return it
+ redis.call('zAdd',KEYS[4],ARGV[1],id)
+ redis.call('hIncrBy',KEYS[5],id,1)
+ return redis.call('hGet',KEYS[6],id)
+LUA;
+ return $conn->luaEval( $script,
+ array(
+ $this->getQueueKey( 'l-unclaimed' ), # KEYS[1]
+ $this->getQueueKey( 'h-sha1ById' ), # KEYS[2]
+ $this->getQueueKey( 'h-idBySha1' ), # KEYS[3]
+ $this->getQueueKey( 'z-claimed' ), # KEYS[4]
+ $this->getQueueKey( 'h-attempts' ), # KEYS[5]
+ $this->getQueueKey( 'h-data' ), # KEYS[6]
+ time(), # ARGV[1] (injected to be replication-safe)
+ ),
+ 6 # number of first argument(s) that are keys
+ );
+ }
+
+ /**
+ * @see JobQueue::doAck()
+ * @param Job $job
+ * @return Job|bool
+ * @throws MWException
+ */
+ protected function doAck( Job $job ) {
+ if ( !isset( $job->metadata['uuid'] ) ) {
+ throw new MWException( "Job of type '{$job->getType()}' has no UUID." );
+ }
+ if ( $this->claimTTL > 0 ) {
+ $conn = $this->getConnection();
+ try {
+ static $script =
+<<<LUA
+ -- Unmark the job as claimed
+ redis.call('zRem',KEYS[1],ARGV[1])
+ redis.call('hDel',KEYS[2],ARGV[1])
+ -- Delete the job data itself
+ return redis.call('hDel',KEYS[3],ARGV[1])
+LUA;
+ $res = $conn->luaEval( $script,
+ array(
+ $this->getQueueKey( 'z-claimed' ), # KEYS[1]
+ $this->getQueueKey( 'h-attempts' ), # KEYS[2]
+ $this->getQueueKey( 'h-data' ), # KEYS[3]
+ $job->metadata['uuid'] # ARGV[1]
+ ),
+ 3 # number of first argument(s) that are keys
+ );
+
+ if ( !$res ) {
+ wfDebugLog( 'JobQueueRedis', "Could not acknowledge {$this->type} job." );
+ return false;
+ }
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @see JobQueue::doDeduplicateRootJob()
+ * @param Job $job
+ * @return bool
+ * @throws MWException
+ */
+ protected function doDeduplicateRootJob( Job $job ) {
+ if ( !$job->hasRootJobParams() ) {
+ throw new MWException( "Cannot register root job; missing parameters." );
+ }
+ $params = $job->getRootJobParams();
+
+ $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
+
+ $conn = $this->getConnection();
+ try {
+ $timestamp = $conn->get( $key ); // current last timestamp of this job
+ if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+ return true; // a newer version of this root job was enqueued
+ }
+ // Update the timestamp of the last root job started at the location...
+ return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::doIsRootJobOldDuplicate()
+ * @param Job $job
+ * @return bool
+ */
+ protected function doIsRootJobOldDuplicate( Job $job ) {
+ if ( !$job->hasRootJobParams() ) {
+ return false; // job has no de-deplication info
+ }
+ $params = $job->getRootJobParams();
+
+ $conn = $this->getConnection();
+ try {
+ // Get the last time this root job was enqueued
+ $timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+
+ // Check if a new root job was started at the location after this one's...
+ return ( $timestamp && $timestamp > $params['rootJobTimestamp'] );
+ }
+
+ /**
+ * @see JobQueue::doDelete()
+ * @return bool
+ */
+ protected function doDelete() {
+ static $props = array( 'l-unclaimed', 'z-claimed', 'z-abandoned',
+ 'z-delayed', 'h-idBySha1', 'h-sha1ById', 'h-attempts', 'h-data' );
+
+ $conn = $this->getConnection();
+ try {
+ $keys = array();
+ foreach ( $props as $prop ) {
+ $keys[] = $this->getQueueKey( $prop );
+ }
+ return ( $conn->delete( $keys ) !== false );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::getAllQueuedJobs()
+ * @return Iterator
+ */
+ public function getAllQueuedJobs() {
+ $conn = $this->getConnection();
+ try {
+ $that = $this;
+ return new MappedIterator(
+ $conn->lRange( $this->getQueueKey( 'l-unclaimed' ), 0, -1 ),
+ function( $uid ) use ( $that, $conn ) {
+ return $that->getJobFromUidInternal( $uid, $conn );
+ },
+ array( 'accept' => function ( $job ) { return is_object( $job ); } )
+ );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * @see JobQueue::getAllQueuedJobs()
+ * @return Iterator
+ */
+ public function getAllDelayedJobs() {
+ $conn = $this->getConnection();
+ try {
+ $that = $this;
+ return new MappedIterator( // delayed jobs
+ $conn->zRange( $this->getQueueKey( 'z-delayed' ), 0, -1 ),
+ function( $uid ) use ( $that, $conn ) {
+ return $that->getJobFromUidInternal( $uid, $conn );
+ },
+ array( 'accept' => function ( $job ) { return is_object( $job ); } )
+ );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ public function getCoalesceLocationInternal() {
+ return "RedisServer:" . $this->server;
+ }
+
+ protected function doGetSiblingQueuesWithJobs( array $types ) {
+ return array_keys( array_filter( $this->doGetSiblingQueueSizes( $types ) ) );
+ }
+
+ protected function doGetSiblingQueueSizes( array $types ) {
+ $sizes = array(); // (type => size)
+ $types = array_values( $types ); // reindex
+ try {
+ $conn = $this->getConnection();
+ $conn->multi( Redis::PIPELINE );
+ foreach ( $types as $type ) {
+ $conn->lSize( $this->getQueueKey( 'l-unclaimed', $type ) );
+ }
+ $res = $conn->exec();
+ if ( is_array( $res ) ) {
+ foreach ( $res as $i => $size ) {
+ $sizes[$types[$i]] = $size;
+ }
+ }
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ return $sizes;
+ }
+
+ /**
+ * This function should not be called outside JobQueueRedis
+ *
+ * @param $uid string
+ * @param $conn RedisConnRef
+ * @return Job|bool Returns false if the job does not exist
+ * @throws MWException
+ */
+ public function getJobFromUidInternal( $uid, RedisConnRef $conn ) {
+ try {
+ $data = $conn->hGet( $this->getQueueKey( 'h-data' ), $uid );
+ if ( $data === false ) {
+ return false; // not found
+ }
+ $item = $this->unserialize( $conn->hGet( $this->getQueueKey( 'h-data' ), $uid ) );
+ if ( !is_array( $item ) ) { // this shouldn't happen
+ throw new MWException( "Could not find job with ID '$uid'." );
+ }
+ $title = Title::makeTitle( $item['namespace'], $item['title'] );
+ $job = Job::factory( $item['type'], $title, $item['params'] );
+ $job->metadata['uuid'] = $item['uuid'];
+ return $job;
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+ }
+
+ /**
+ * Release any ready delayed jobs into the queue
+ *
+ * @return integer Number of jobs released
+ * @throws MWException
+ */
+ public function releaseReadyDelayedJobs() {
+ $count = 0;
+
+ $conn = $this->getConnection();
+ try {
+ static $script =
+<<<LUA
+ -- Get the list of ready delayed jobs, sorted by readiness
+ local ids = redis.call('zRangeByScore',KEYS[1],0,ARGV[1])
+ -- Migrate the jobs from the "delayed" set to the "unclaimed" list
+ for k,id in ipairs(ids) do
+ redis.call('lPush',KEYS[2],id)
+ redis.call('zRem',KEYS[1],id)
+ end
+ return #ids
+LUA;
+ $count += (int)$conn->luaEval( $script,
+ array(
+ $this->getQueueKey( 'z-delayed' ), // KEYS[1]
+ $this->getQueueKey( 'l-unclaimed' ), // KEYS[2]
+ time() // ARGV[1]; max "delay until" UNIX timestamp
+ ),
+ 2 # first two arguments are keys
+ );
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+
+ return $count;
+ }
+
+ /**
+ * Recycle or destroy any jobs that have been claimed for too long
+ *
+ * @return integer Number of jobs recycled/deleted
+ * @throws MWException
+ */
+ public function recycleAndDeleteStaleJobs() {
+ if ( $this->claimTTL <= 0 ) { // sanity
+ throw new MWException( "Cannot recycle jobs since acknowledgements are disabled." );
+ }
+ $count = 0;
+ // For each job item that can be retried, we need to add it back to the
+ // main queue and remove it from the list of currenty claimed job items.
+ // For those that cannot, they are marked as dead and kept around for
+ // investigation and manual job restoration but are eventually deleted.
+ $conn = $this->getConnection();
+ try {
+ $now = time();
+ static $script =
+<<<LUA
+ local released,abandoned,pruned = 0,0,0
+ -- Get all non-dead jobs that have an expired claim on them.
+ -- The score for each item is the last claim timestamp (UNIX).
+ local staleClaims = redis.call('zRangeByScore',KEYS[1],0,ARGV[1])
+ for k,id in ipairs(staleClaims) do
+ local timestamp = redis.call('zScore',KEYS[1],id)
+ local attempts = redis.call('hGet',KEYS[2],id)
+ if attempts < ARGV[3] then
+ -- Claim expired and retries left: re-enqueue the job
+ redis.call('lPush',KEYS[3],id)
+ redis.call('hIncrBy',KEYS[2],id,1)
+ released = released + 1
+ else
+ -- Claim expired and no retries left: mark the job as dead
+ redis.call('zAdd',KEYS[5],timestamp,id)
+ abandoned = abandoned + 1
+ end
+ redis.call('zRem',KEYS[1],id)
+ end
+ -- Get all of the dead jobs that have been marked as dead for too long.
+ -- The score for each item is the last claim timestamp (UNIX).
+ local deadClaims = redis.call('zRangeByScore',KEYS[5],0,ARGV[2])
+ for k,id in ipairs(deadClaims) do
+ -- Stale and out of retries: remove any traces of the job
+ redis.call('zRem',KEYS[5],id)
+ redis.call('hDel',KEYS[2],id)
+ redis.call('hDel',KEYS[4],id)
+ pruned = pruned + 1
+ end
+ return {released,abandoned,pruned}
+LUA;
+ $res = $conn->luaEval( $script,
+ array(
+ $this->getQueueKey( 'z-claimed' ), # KEYS[1]
+ $this->getQueueKey( 'h-attempts' ), # KEYS[2]
+ $this->getQueueKey( 'l-unclaimed' ), # KEYS[3]
+ $this->getQueueKey( 'h-data' ), # KEYS[4]
+ $this->getQueueKey( 'z-abandoned' ), # KEYS[5]
+ $now - $this->claimTTL, # ARGV[1]
+ $now - self::MAX_AGE_PRUNE, # ARGV[2]
+ $this->maxTries # ARGV[3]
+ ),
+ 5 # number of first argument(s) that are keys
+ );
+ if ( $res ) {
+ list( $released, $abandoned, $pruned ) = $res;
+ $count += $released + $pruned;
+ JobQueue::incrStats( 'job-recycle', $this->type, $released );
+ JobQueue::incrStats( 'job-abandon', $this->type, $abandoned );
+ }
+ } catch ( RedisException $e ) {
+ $this->throwRedisException( $this->server, $conn, $e );
+ }
+
+ return $count;
+ }
+
+ /**
+ * @return Array
+ */
+ protected function doGetPeriodicTasks() {
+ $tasks = array();
+ if ( $this->claimTTL > 0 ) {
+ $tasks['recycleAndDeleteStaleJobs'] = array(
+ 'callback' => array( $this, 'recycleAndDeleteStaleJobs' ),
+ 'period' => ceil( $this->claimTTL / 2 )
+ );
+ }
+ if ( $this->checkDelay ) {
+ $tasks['releaseReadyDelayedJobs'] = array(
+ 'callback' => array( $this, 'releaseReadyDelayedJobs' ),
+ 'period' => 300 // 5 minutes
+ );
+ }
+ return $tasks;
+ }
+
+ /**
+ * @param $job Job
+ * @return array
+ */
+ protected function getNewJobFields( Job $job ) {
+ return array(
+ // Fields that describe the nature of the job
+ 'type' => $job->getType(),
+ 'namespace' => $job->getTitle()->getNamespace(),
+ 'title' => $job->getTitle()->getDBkey(),
+ 'params' => $job->getParams(),
+ // Some jobs cannot run until a "release timestamp"
+ 'rtimestamp' => $job->getReleaseTimestamp() ?: 0,
+ // Additional job metadata
+ 'uuid' => UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND ),
+ 'sha1' => $job->ignoreDuplicates()
+ ? wfBaseConvert( sha1( serialize( $job->getDeduplicationInfo() ) ), 16, 36, 31 )
+ : '',
+ 'timestamp' => time() // UNIX timestamp
+ );
+ }
+
+ /**
+ * @param $fields array
+ * @return Job|bool
+ */
+ protected function getJobFromFields( array $fields ) {
+ $title = Title::makeTitleSafe( $fields['namespace'], $fields['title'] );
+ if ( $title ) {
+ $job = Job::factory( $fields['type'], $title, $fields['params'] );
+ $job->metadata['uuid'] = $fields['uuid'];
+ return $job;
+ }
+ return false;
+ }
+
+ /**
+ * @param array $fields
+ * @return string Serialized and possibly compressed version of $fields
+ */
+ protected function serialize( array $fields ) {
+ $blob = serialize( $fields );
+ if ( $this->compression === 'gzip'
+ && strlen( $blob ) >= 1024 && function_exists( 'gzdeflate' ) )
+ {
+ $object = (object)array( 'blob' => gzdeflate( $blob ), 'enc' => 'gzip' );
+ $blobz = serialize( $object );
+ return ( strlen( $blobz ) < strlen( $blob ) ) ? $blobz : $blob;
+ } else {
+ return $blob;
+ }
+ }
+
+ /**
+ * @param string $blob
+ * @return array|bool Unserialized version of $blob or false
+ */
+ protected function unserialize( $blob ) {
+ $fields = unserialize( $blob );
+ if ( is_object( $fields ) ) {
+ if ( $fields->enc === 'gzip' && function_exists( 'gzinflate' ) ) {
+ $fields = unserialize( gzinflate( $fields->blob ) );
+ } else {
+ $fields = false;
+ }
+ }
+ return is_array( $fields ) ? $fields : false;
+ }
+
+ /**
+ * Get a connection to the server that handles all sub-queues for this queue
+ *
+ * @return Array (server name, Redis instance)
+ * @throws MWException
+ */
+ protected function getConnection() {
+ $conn = $this->redisPool->getConnection( $this->server );
+ if ( !$conn ) {
+ throw new JobQueueConnectionError( "Unable to connect to redis server." );
+ }
+ return $conn;
+ }
+
+ /**
+ * @param $server string
+ * @param $conn RedisConnRef
+ * @param $e RedisException
+ * @throws MWException
+ */
+ protected function throwRedisException( $server, RedisConnRef $conn, $e ) {
+ $this->redisPool->handleException( $server, $conn, $e );
+ throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
+ }
+
+ /**
+ * @param $prop string
+ * @param $type string|null
+ * @return string
+ */
+ private function getQueueKey( $prop, $type = null ) {
+ $type = is_string( $type ) ? $type : $this->type;
+ list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
+ if ( strlen( $this->key ) ) { // namespaced queue (for testing)
+ return wfForeignMemcKey( $db, $prefix, 'jobqueue', $type, $this->key, $prop );
+ } else {
+ return wfForeignMemcKey( $db, $prefix, 'jobqueue', $type, $prop );
+ }
+ }
+
+ /**
+ * @param $key string
+ * @return void
+ */
+ public function setTestingPrefix( $key ) {
+ $this->key = $key;
+ }
+}
diff --git a/includes/job/README b/includes/job/README
new file mode 100644
index 00000000..c11d5a78
--- /dev/null
+++ b/includes/job/README
@@ -0,0 +1,81 @@
+/*!
+\ingroup JobQueue
+\page jobqueue_design Job queue design
+
+Notes on the Job queuing system architecture.
+
+\section intro Introduction
+
+The data model consist of the following main components:
+* The Job object represents a particular deferred task that happens in the
+ background. All jobs subclass the Job object and put the main logic in the
+ function called run().
+* The JobQueue object represents a particular queue of jobs of a certain type.
+ For example there may be a queue for email jobs and a queue for squid purge
+ jobs.
+
+\section jobqueue Job queues
+
+Each job type has its own queue and is associated to a storage medium. One
+queue might save its jobs in redis while another one uses would use a database.
+
+Storage medium are defined in a queue class. Before using it, you must
+define in $wgJobTypeConf a mapping of the job type to a queue class.
+
+The factory class JobQueueGroup provides helper functions:
+- getting the queue for a given job
+- route new job insertions to the proper queue
+
+The following queue classes are available:
+* JobQueueDB (stores jobs in the `job` table in a database)
+* JobQueueRedis (stores jobs in a redis server)
+
+All queue classes support some basic operations (though some may be no-ops):
+* enqueueing a batch of jobs
+* dequeueing a single job
+* acknowledging a job is completed
+* checking if the queue is empty
+
+Some queue classes (like JobQueueDB) may dequeue jobs in random order while other
+queues might dequeue jobs in exact FIFO order. Callers should thus not assume jobs
+are executed in FIFO order.
+
+Also note that not all queue classes will have the same reliability guarantees.
+In-memory queues may lose data when restarted depending on snapshot and journal
+settings (including journal fsync() frequency). Some queue types may totally remove
+jobs when dequeued while leaving the ack() function as a no-op; if a job is
+dequeued by a job runner, which crashes before completion, the job will be
+lost. Some jobs, like purging squid caches after a template change, may not
+require durable queues, whereas other jobs might be more important.
+
+\section aggregator Job queue aggregator
+
+The aggregators are used by nextJobDB.php, which is a script that will return a
+random ready queue (on any wiki in the farm) that can be used with runJobs.php.
+This can be used in conjunction with any scripts that handle wiki farm job queues.
+Note that $wgLocalDatabases defines what wikis are in the wiki farm.
+
+Since each job type has its own queue, and wiki-farms may have many wikis,
+there might be a large number of queues to keep track of. To avoid wasting
+large amounts of time polling empty queues, aggregators exists to keep track
+of which queues are ready.
+
+The following queue aggregator classes are available:
+* JobQueueAggregatorMemc (uses $wgMemc to track ready queues)
+* JobQueueAggregatorRedis (uses a redis server to track ready queues)
+
+Some aggregators cache data for a few minutes while others may be always up to date.
+This can be an important factor for jobs that need a low pickup time (or latency).
+
+\section jobs Jobs
+
+Callers should also try to make jobs maintain correctness when executed twice.
+This is useful for queues that actually implement ack(), since they may recycle
+dequeued but un-acknowledged jobs back into the queue to be attempted again. If
+a runner dequeues a job, runs it, but then crashes before calling ack(), the
+job may be returned to the queue and run a second time. Jobs like cache purging can
+happen several times without any correctness problems. However, a pathological case
+would be if a bug causes the problem to systematically keep repeating. For example,
+a job may always throw a DB error at the end of run(). This problem is trickier to
+solve and more obnoxious for things like email jobs, for example. For such jobs,
+it might be useful to use a queue that does not retry jobs.
diff --git a/includes/job/RefreshLinksJob.php b/includes/job/RefreshLinksJob.php
deleted file mode 100644
index b23951c6..00000000
--- a/includes/job/RefreshLinksJob.php
+++ /dev/null
@@ -1,202 +0,0 @@
-<?php
-/**
- * Job to update links for a given title.
- *
- * 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 JobQueue
- */
-
-/**
- * Background job to update links for a given title.
- *
- * @ingroup JobQueue
- */
-class RefreshLinksJob extends Job {
-
- function __construct( $title, $params = '', $id = 0 ) {
- parent::__construct( 'refreshLinks', $title, $params, $id );
- }
-
- /**
- * Run a refreshLinks job
- * @return boolean success
- */
- function run() {
- wfProfileIn( __METHOD__ );
-
- $linkCache = LinkCache::singleton();
- $linkCache->clear();
-
- if ( is_null( $this->title ) ) {
- $this->error = "refreshLinks: Invalid title";
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- # Wait for the DB of the current/next slave DB handle to catch up to the master.
- # This way, we get the correct page_latest for templates or files that just changed
- # milliseconds ago, having triggered this job to begin with.
- if ( isset( $this->params['masterPos'] ) ) {
- wfGetLB()->waitFor( $this->params['masterPos'] );
- }
-
- $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
- if ( !$revision ) {
- $this->error = 'refreshLinks: Article not found "' .
- $this->title->getPrefixedDBkey() . '"';
- wfProfileOut( __METHOD__ );
- return false; // XXX: what if it was just deleted?
- }
-
- self::runForTitleInternal( $this->title, $revision, __METHOD__ );
-
- wfProfileOut( __METHOD__ );
- return true;
- }
-
- public static function runForTitleInternal( Title $title, Revision $revision, $fname ) {
- global $wgParser, $wgContLang;
-
- wfProfileIn( $fname . '-parse' );
- $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
- $parserOutput = $wgParser->parse(
- $revision->getText(), $title, $options, true, true, $revision->getId() );
- wfProfileOut( $fname . '-parse' );
-
- wfProfileIn( $fname . '-update' );
- $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
- DataUpdate::runUpdates( $updates );
- wfProfileOut( $fname . '-update' );
- }
-}
-
-/**
- * Background job to update links for a given title.
- * Newer version for high use templates.
- *
- * @ingroup JobQueue
- */
-class RefreshLinksJob2 extends Job {
- const MAX_TITLES_RUN = 10;
-
- function __construct( $title, $params, $id = 0 ) {
- parent::__construct( 'refreshLinks2', $title, $params, $id );
- }
-
- /**
- * Run a refreshLinks2 job
- * @return boolean success
- */
- function run() {
- wfProfileIn( __METHOD__ );
-
- $linkCache = LinkCache::singleton();
- $linkCache->clear();
-
- if ( is_null( $this->title ) ) {
- $this->error = "refreshLinks2: Invalid title";
- wfProfileOut( __METHOD__ );
- return false;
- } elseif ( !isset( $this->params['start'] ) || !isset( $this->params['end'] ) ) {
- $this->error = "refreshLinks2: Invalid params";
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- // Back compat for pre-r94435 jobs
- $table = isset( $this->params['table'] ) ? $this->params['table'] : 'templatelinks';
-
- // Avoid slave lag when fetching templates
- if ( isset( $this->params['masterPos'] ) ) {
- $masterPos = $this->params['masterPos'];
- } elseif ( wfGetLB()->getServerCount() > 1 ) {
- $masterPos = wfGetLB()->getMasterPos();
- } else {
- $masterPos = false;
- }
-
- $titles = $this->title->getBacklinkCache()->getLinks(
- $table, $this->params['start'], $this->params['end'] );
-
- if ( $titles->count() > self::MAX_TITLES_RUN ) {
- # We don't want to parse too many pages per job as it can starve other jobs.
- # If there are too many pages to parse, break this up into smaller jobs. By passing
- # in the master position here we can cut down on the time spent waiting for slaves to
- # catch up by the runners handling these jobs since time will have passed between now
- # and when they pop these jobs off the queue.
- $start = 0; // batch start
- $end = 0; // batch end
- $bsize = 0; // batch size
- $first = true; // first of batch
- $jobs = array();
- foreach ( $titles as $title ) {
- $start = $first ? $title->getArticleId() : $start;
- $end = $title->getArticleId();
- $first = false;
- if ( ++$bsize >= self::MAX_TITLES_RUN ) {
- $jobs[] = new RefreshLinksJob2( $this->title, array(
- 'table' => $table,
- 'start' => $start,
- 'end' => $end,
- 'masterPos' => $masterPos
- ) );
- $first = true;
- $start = $end = $bsize = 0;
- }
- }
- if ( $bsize > 0 ) { // group remaining pages into a job
- $jobs[] = new RefreshLinksJob2( $this->title, array(
- 'table' => $table,
- 'start' => $start,
- 'end' => $end,
- 'masterPos' => $masterPos
- ) );
- }
- Job::batchInsert( $jobs );
- } elseif ( php_sapi_name() != 'cli' ) {
- # Not suitable for page load triggered job running!
- # Gracefully switch to refreshLinks jobs if this happens.
- $jobs = array();
- foreach ( $titles as $title ) {
- $jobs[] = new RefreshLinksJob( $title, array( 'masterPos' => $masterPos ) );
- }
- Job::batchInsert( $jobs );
- } else {
- # Wait for the DB of the current/next slave DB handle to catch up to the master.
- # This way, we get the correct page_latest for templates or files that just changed
- # milliseconds ago, having triggered this job to begin with.
- if ( $masterPos ) {
- wfGetLB()->waitFor( $masterPos );
- }
- # Re-parse each page that transcludes this page and update their tracking links...
- foreach ( $titles as $title ) {
- $revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
- if ( !$revision ) {
- $this->error = 'refreshLinks: Article not found "' .
- $title->getPrefixedDBkey() . '"';
- continue; // skip this page
- }
- RefreshLinksJob::runForTitleInternal( $title, $revision, __METHOD__ );
- wfWaitForSlaves();
- }
- }
-
- wfProfileOut( __METHOD__ );
- return true;
- }
-}
diff --git a/includes/job/aggregator/JobQueueAggregator.php b/includes/job/aggregator/JobQueueAggregator.php
new file mode 100644
index 00000000..a8186abd
--- /dev/null
+++ b/includes/job/aggregator/JobQueueAggregator.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Job queue aggregator code.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle tracking information about all queues
+ *
+ * @ingroup JobQueue
+ * @since 1.21
+ */
+abstract class JobQueueAggregator {
+ /** @var JobQueueAggregator */
+ protected static $instance = null;
+
+ /**
+ * @param array $params
+ */
+ protected function __construct( array $params ) {}
+
+ /**
+ * @return JobQueueAggregator
+ */
+ final public static function singleton() {
+ global $wgJobQueueAggregator;
+
+ if ( !isset( self::$instance ) ) {
+ $class = $wgJobQueueAggregator['class'];
+ $obj = new $class( $wgJobQueueAggregator );
+ if ( !( $obj instanceof JobQueueAggregator ) ) {
+ throw new MWException( "Class '$class' is not a JobQueueAggregator class." );
+ }
+ self::$instance = $obj;
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Destroy the singleton instance
+ *
+ * @return void
+ */
+ final public static function destroySingleton() {
+ self::$instance = null;
+ }
+
+ /**
+ * Mark a queue as being empty
+ *
+ * @param string $wiki
+ * @param string $type
+ * @return bool Success
+ */
+ final public function notifyQueueEmpty( $wiki, $type ) {
+ wfProfileIn( __METHOD__ );
+ $ok = $this->doNotifyQueueEmpty( $wiki, $type );
+ wfProfileOut( __METHOD__ );
+ return $ok;
+ }
+
+ /**
+ * @see JobQueueAggregator::notifyQueueEmpty()
+ */
+ abstract protected function doNotifyQueueEmpty( $wiki, $type );
+
+ /**
+ * Mark a queue as being non-empty
+ *
+ * @param string $wiki
+ * @param string $type
+ * @return bool Success
+ */
+ final public function notifyQueueNonEmpty( $wiki, $type ) {
+ wfProfileIn( __METHOD__ );
+ $ok = $this->doNotifyQueueNonEmpty( $wiki, $type );
+ wfProfileOut( __METHOD__ );
+ return $ok;
+ }
+
+ /**
+ * @see JobQueueAggregator::notifyQueueNonEmpty()
+ */
+ abstract protected function doNotifyQueueNonEmpty( $wiki, $type );
+
+ /**
+ * Get the list of all of the queues with jobs
+ *
+ * @return Array (job type => (list of wiki IDs))
+ */
+ final public function getAllReadyWikiQueues() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doGetAllReadyWikiQueues();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueueAggregator::getAllReadyWikiQueues()
+ */
+ abstract protected function doGetAllReadyWikiQueues();
+
+ /**
+ * Purge all of the aggregator information
+ *
+ * @return bool Success
+ */
+ final public function purge() {
+ wfProfileIn( __METHOD__ );
+ $res = $this->doPurge();
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see JobQueueAggregator::purge()
+ */
+ abstract protected function doPurge();
+
+ /**
+ * Get all databases that have a pending job.
+ * This poll all the queues and is this expensive.
+ *
+ * @return Array (job type => (list of wiki IDs))
+ */
+ protected function findPendingWikiQueues() {
+ global $wgLocalDatabases;
+
+ $pendingDBs = array(); // (job type => (db list))
+ foreach ( $wgLocalDatabases as $db ) {
+ foreach ( JobQueueGroup::singleton( $db )->getQueuesWithJobs() as $type ) {
+ $pendingDBs[$type][] = $db;
+ }
+ }
+
+ return $pendingDBs;
+ }
+}
diff --git a/includes/job/aggregator/JobQueueAggregatorMemc.php b/includes/job/aggregator/JobQueueAggregatorMemc.php
new file mode 100644
index 00000000..9434da04
--- /dev/null
+++ b/includes/job/aggregator/JobQueueAggregatorMemc.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Job queue aggregator code that uses BagOStuff.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle tracking information about all queues using BagOStuff
+ *
+ * @ingroup JobQueue
+ * @since 1.21
+ */
+class JobQueueAggregatorMemc extends JobQueueAggregator {
+ /** @var BagOStuff */
+ protected $cache;
+
+ protected $cacheTTL; // integer; seconds
+
+ /**
+ * @params include:
+ * - objectCache : Name of an object cache registered in $wgObjectCaches.
+ * This defaults to the one specified by $wgMainCacheType.
+ * - cacheTTL : Seconds to cache the aggregate data before regenerating.
+ * @param array $params
+ */
+ protected function __construct( array $params ) {
+ parent::__construct( $params );
+ $this->cache = isset( $params['objectCache'] )
+ ? wfGetCache( $params['objectCache'] )
+ : wfGetMainCache();
+ $this->cacheTTL = isset( $params['cacheTTL'] ) ? $params['cacheTTL'] : 180; // 3 min
+ }
+
+ /**
+ * @see JobQueueAggregator::doNotifyQueueEmpty()
+ */
+ protected function doNotifyQueueEmpty( $wiki, $type ) {
+ $key = $this->getReadyQueueCacheKey();
+ // Delist the queue from the "ready queue" list
+ if ( $this->cache->add( "$key:lock", 1, 60 ) ) { // lock
+ $curInfo = $this->cache->get( $key );
+ if ( is_array( $curInfo ) && isset( $curInfo['pendingDBs'][$type] ) ) {
+ if ( in_array( $wiki, $curInfo['pendingDBs'][$type] ) ) {
+ $curInfo['pendingDBs'][$type] = array_diff(
+ $curInfo['pendingDBs'][$type], array( $wiki ) );
+ $this->cache->set( $key, $curInfo );
+ }
+ }
+ $this->cache->delete( "$key:lock" ); // unlock
+ }
+ return true;
+ }
+
+ /**
+ * @see JobQueueAggregator::doNotifyQueueNonEmpty()
+ */
+ protected function doNotifyQueueNonEmpty( $wiki, $type ) {
+ return true; // updated periodically
+ }
+
+ /**
+ * @see JobQueueAggregator::doAllGetReadyWikiQueues()
+ */
+ protected function doGetAllReadyWikiQueues() {
+ $key = $this->getReadyQueueCacheKey();
+ // If the cache entry wasn't present, is stale, or in .1% of cases otherwise,
+ // regenerate the cache. Use any available stale cache if another process is
+ // currently regenerating the pending DB information.
+ $pendingDbInfo = $this->cache->get( $key );
+ if ( !is_array( $pendingDbInfo )
+ || ( time() - $pendingDbInfo['timestamp'] ) > $this->cacheTTL
+ || mt_rand( 0, 999 ) == 0
+ ) {
+ if ( $this->cache->add( "$key:rebuild", 1, 1800 ) ) { // lock
+ $pendingDbInfo = array(
+ 'pendingDBs' => $this->findPendingWikiQueues(),
+ 'timestamp' => time()
+ );
+ for ( $attempts = 1; $attempts <= 25; ++$attempts ) {
+ if ( $this->cache->add( "$key:lock", 1, 60 ) ) { // lock
+ $this->cache->set( $key, $pendingDbInfo );
+ $this->cache->delete( "$key:lock" ); // unlock
+ break;
+ }
+ }
+ $this->cache->delete( "$key:rebuild" ); // unlock
+ }
+ }
+ return is_array( $pendingDbInfo )
+ ? $pendingDbInfo['pendingDBs']
+ : array(); // cache is both empty and locked
+ }
+
+ /**
+ * @see JobQueueAggregator::doPurge()
+ */
+ protected function doPurge() {
+ return $this->cache->delete( $this->getReadyQueueCacheKey() );
+ }
+
+ /**
+ * @return string
+ */
+ private function getReadyQueueCacheKey() {
+ return "jobqueue:aggregator:ready-queues:v1"; // global
+ }
+}
diff --git a/includes/job/aggregator/JobQueueAggregatorRedis.php b/includes/job/aggregator/JobQueueAggregatorRedis.php
new file mode 100644
index 00000000..c6a799df
--- /dev/null
+++ b/includes/job/aggregator/JobQueueAggregatorRedis.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * Job queue aggregator code that uses PhpRedis.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle tracking information about all queues using PhpRedis
+ *
+ * @ingroup JobQueue
+ * @ingroup Redis
+ * @since 1.21
+ */
+class JobQueueAggregatorRedis extends JobQueueAggregator {
+ /** @var RedisConnectionPool */
+ protected $redisPool;
+
+ /**
+ * @params include:
+ * - redisConfig : An array of parameters to RedisConnectionPool::__construct().
+ * - redisServer : A hostname/port combination or the absolute path of a UNIX socket.
+ * If a hostname is specified but no port, the standard port number
+ * 6379 will be used. Required.
+ * @param array $params
+ */
+ protected function __construct( array $params ) {
+ parent::__construct( $params );
+ $this->server = $params['redisServer'];
+ $this->redisPool = RedisConnectionPool::singleton( $params['redisConfig'] );
+ }
+
+ /**
+ * @see JobQueueAggregator::doNotifyQueueEmpty()
+ */
+ protected function doNotifyQueueEmpty( $wiki, $type ) {
+ $conn = $this->getConnection();
+ if ( !$conn ) {
+ return false;
+ }
+ try {
+ $conn->hDel( $this->getReadyQueueKey(), $this->encQueueName( $type, $wiki ) );
+ return true;
+ } catch ( RedisException $e ) {
+ $this->handleException( $conn, $e );
+ return false;
+ }
+ }
+
+ /**
+ * @see JobQueueAggregator::doNotifyQueueNonEmpty()
+ */
+ protected function doNotifyQueueNonEmpty( $wiki, $type ) {
+ $conn = $this->getConnection();
+ if ( !$conn ) {
+ return false;
+ }
+ try {
+ $conn->hSet( $this->getReadyQueueKey(), $this->encQueueName( $type, $wiki ), time() );
+ return true;
+ } catch ( RedisException $e ) {
+ $this->handleException( $conn, $e );
+ return false;
+ }
+ }
+
+ /**
+ * @see JobQueueAggregator::doAllGetReadyWikiQueues()
+ */
+ protected function doGetAllReadyWikiQueues() {
+ $conn = $this->getConnection();
+ if ( !$conn ) {
+ return array();
+ }
+ try {
+ $conn->multi( Redis::PIPELINE );
+ $conn->exists( $this->getReadyQueueKey() );
+ $conn->hGetAll( $this->getReadyQueueKey() );
+ list( $exists, $map ) = $conn->exec();
+
+ if ( $exists ) { // cache hit
+ $pendingDBs = array(); // (type => list of wikis)
+ foreach ( $map as $key => $time ) {
+ list( $type, $wiki ) = $this->dencQueueName( $key );
+ $pendingDBs[$type][] = $wiki;
+ }
+ } else { // cache miss
+ // Avoid duplicated effort
+ $conn->multi( Redis::MULTI );
+ $conn->setnx( $this->getReadyQueueKey() . ":lock", 1 );
+ $conn->expire( $this->getReadyQueueKey() . ":lock", 3600 );
+ if ( $conn->exec() !== array( true, true ) ) { // lock
+ return array(); // already in progress
+ }
+
+ $pendingDBs = $this->findPendingWikiQueues(); // (type => list of wikis)
+
+ $conn->delete( $this->getReadyQueueKey() . ":lock" ); // unlock
+
+ $now = time();
+ $map = array();
+ foreach ( $pendingDBs as $type => $wikis ) {
+ foreach ( $wikis as $wiki ) {
+ $map[$this->encQueueName( $type, $wiki )] = $now;
+ }
+ }
+ $conn->hMSet( $this->getReadyQueueKey(), $map );
+ }
+
+ return $pendingDBs;
+ } catch ( RedisException $e ) {
+ $this->handleException( $conn, $e );
+ return array();
+ }
+ }
+
+ /**
+ * @see JobQueueAggregator::doPurge()
+ */
+ protected function doPurge() {
+ $conn = $this->getConnection();
+ if ( !$conn ) {
+ return false;
+ }
+ try {
+ $conn->delete( $this->getReadyQueueKey() );
+ } catch ( RedisException $e ) {
+ $this->handleException( $conn, $e );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get a connection to the server that handles all sub-queues for this queue
+ *
+ * @return Array (server name, Redis instance)
+ * @throws MWException
+ */
+ protected function getConnection() {
+ return $this->redisPool->getConnection( $this->server );
+ }
+
+ /**
+ * @param RedisConnRef $conn
+ * @param RedisException $e
+ * @return void
+ */
+ protected function handleException( RedisConnRef $conn, $e ) {
+ $this->redisPool->handleException( $this->server, $conn, $e );
+ }
+
+ /**
+ * @return string
+ */
+ private function getReadyQueueKey() {
+ return "jobqueue:aggregator:h-ready-queues:v1"; // global
+ }
+
+ /**
+ * @param string $type
+ * @param string $wiki
+ * @return string
+ */
+ private function encQueueName( $type, $wiki ) {
+ return rawurlencode( $type ) . '/' . rawurlencode( $wiki );
+ }
+
+ /**
+ * @param string $name
+ * @return string
+ */
+ private function dencQueueName( $name ) {
+ list( $type, $wiki ) = explode( '/', $name, 2 );
+ return array( rawurldecode( $type ), rawurldecode( $wiki ) );
+ }
+}
diff --git a/includes/job/jobs/AssembleUploadChunksJob.php b/includes/job/jobs/AssembleUploadChunksJob.php
new file mode 100644
index 00000000..6237e568
--- /dev/null
+++ b/includes/job/jobs/AssembleUploadChunksJob.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ * Assemble the segments of a chunked upload.
+ *
+ * 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 Upload
+ */
+
+/**
+ * Assemble the segments of a chunked upload.
+ *
+ * @ingroup Upload
+ */
+class AssembleUploadChunksJob extends Job {
+ public function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'AssembleUploadChunks', $title, $params, $id );
+ $this->removeDuplicates = true;
+ }
+
+ public function run() {
+ $scope = RequestContext::importScopedSession( $this->params['session'] );
+ $context = RequestContext::getMain();
+ try {
+ $user = $context->getUser();
+ if ( !$user->isLoggedIn() ) {
+ $this->setLastError( "Could not load the author user from session." );
+ return false;
+ }
+
+ if ( count( $_SESSION ) === 0 ) {
+ // Empty session probably indicates that we didn't associate
+ // with the session correctly. Note that being able to load
+ // the user does not necessarily mean the session was loaded.
+ // Most likely cause by suhosin.session.encrypt = On.
+ $this->setLastError( "Error associating with user session. Try setting suhosin.session.encrypt = Off" );
+ return false;
+ }
+
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array( 'result' => 'Poll', 'stage' => 'assembling', 'status' => Status::newGood() )
+ );
+
+ $upload = new UploadFromChunks( $user );
+ $upload->continueChunks(
+ $this->params['filename'],
+ $this->params['filekey'],
+ $context->getRequest()
+ );
+
+ // Combine all of the chunks into a local file and upload that to a new stash file
+ $status = $upload->concatenateChunks();
+ if ( !$status->isGood() ) {
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array( 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status )
+ );
+ $this->setLastError( $status->getWikiText() );
+ return false;
+ }
+
+ // We have a new filekey for the fully concatenated file
+ $newFileKey = $upload->getLocalFile()->getFileKey();
+
+ // Remove the old stash file row and first chunk file
+ $upload->stash->removeFileNoAuth( $this->params['filekey'] );
+
+ // Build the image info array while we have the local reference handy
+ $apiMain = new ApiMain(); // dummy object (XXX)
+ $imageInfo = $upload->getImageInfo( $apiMain->getResult() );
+
+ // Cleanup any temporary local file
+ $upload->cleanupTempFile();
+
+ // Cache the info so the user doesn't have to wait forever to get the final info
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array(
+ 'result' => 'Success',
+ 'stage' => 'assembling',
+ 'filekey' => $newFileKey,
+ 'imageinfo' => $imageInfo,
+ 'status' => Status::newGood()
+ )
+ );
+ } catch ( MWException $e ) {
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array(
+ 'result' => 'Failure',
+ 'stage' => 'assembling',
+ 'status' => Status::newFatal( 'api-error-stashfailed' )
+ )
+ );
+ $this->setLastError( get_class( $e ) . ": " . $e->getText() );
+ return false;
+ }
+ return true;
+ }
+
+ public function getDeduplicationInfo() {
+ $info = parent::getDeduplicationInfo();
+ if ( is_array( $info['params'] ) ) {
+ $info['params'] = array( 'filekey' => $info['params']['filekey'] );
+ }
+ return $info;
+ }
+
+ public function allowRetries() {
+ return false;
+ }
+}
diff --git a/includes/job/DoubleRedirectJob.php b/includes/job/jobs/DoubleRedirectJob.php
index 08af9975..33e749b8 100644
--- a/includes/job/DoubleRedirectJob.php
+++ b/includes/job/jobs/DoubleRedirectJob.php
@@ -27,7 +27,7 @@
* @ingroup JobQueue
*/
class DoubleRedirectJob extends Job {
- var $reason, $redirTitle, $destTitleText;
+ var $reason, $redirTitle;
/**
* @var User
@@ -36,9 +36,9 @@ class DoubleRedirectJob extends Job {
/**
* Insert jobs into the job queue to fix redirects to the given title
- * @param $reason String: the reason for the fix, see message "double-redirect-fixed-<reason>"
+ * @param string $reason 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 bool Not used
+ * @param bool $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
@@ -66,18 +66,17 @@ class DoubleRedirectJob extends Job {
'redirTitle' => $redirTitle->getPrefixedDBkey() ) );
# Avoid excessive memory usage
if ( count( $jobs ) > 10000 ) {
- Job::batchInsert( $jobs );
+ JobQueueGroup::singleton()->push( $jobs );
$jobs = array();
}
}
- Job::batchInsert( $jobs );
+ JobQueueGroup::singleton()->push( $jobs );
}
function __construct( $title, $params = false, $id = 0 ) {
parent::__construct( 'fixDoubleRedirect', $title, $params, $id );
$this->reason = $params['reason'];
$this->redirTitle = Title::newFromText( $params['redirTitle'] );
- $this->destTitleText = !empty( $params['destTitle'] ) ? $params['destTitle'] : '';
}
/**
@@ -91,60 +90,64 @@ class DoubleRedirectJob extends Job {
$targetRev = Revision::newFromTitle( $this->title, false, Revision::READ_LATEST );
if ( !$targetRev ) {
- wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" );
+ wfDebug( __METHOD__ . ": target redirect already deleted, ignoring\n" );
return true;
}
- $text = $targetRev->getText();
- $currentDest = Title::newFromRedirect( $text );
+ $content = $targetRev->getContent();
+ $currentDest = $content ? $content->getRedirectTarget() : null;
if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
- wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" );
+ wfDebug( __METHOD__ . ": Redirect has changed since the job was queued\n" );
return true;
}
- # Check for a suppression tag (used e.g. in periodically archived discussions)
+ // Check for a suppression tag (used e.g. in periodically archived discussions)
$mw = MagicWord::get( 'staticredirect' );
- if ( $mw->match( $text ) ) {
- wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" );
+ if ( $content->matchMagicWord( $mw ) ) {
+ wfDebug( __METHOD__ . ": skipping: suppressed with __STATICREDIRECT__\n" );
return true;
}
- # Find the current final destination
+ // Find the current final destination
$newTitle = self::getFinalDestination( $this->redirTitle );
if ( !$newTitle ) {
- wfDebug( __METHOD__.": skipping: single redirect, circular redirect or invalid redirect destination\n" );
+ wfDebug( __METHOD__ . ": skipping: single redirect, circular redirect or invalid redirect destination\n" );
return true;
}
if ( $newTitle->equals( $this->redirTitle ) ) {
- # The redirect is already right, no need to change it
- # This can happen if the page was moved back (say after vandalism)
- wfDebug( __METHOD__.": skipping, already good\n" );
+ // The redirect is already right, no need to change it
+ // This can happen if the page was moved back (say after vandalism)
+ wfDebug( __METHOD__ . " : skipping, already good\n" );
}
- # Preserve fragment (bug 14904)
+ // Preserve fragment (bug 14904)
$newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
- $currentDest->getFragment() );
+ $currentDest->getFragment(), $newTitle->getInterwiki() );
- # Fix the text
- # Remember that redirect pages can have categories, templates, etc.,
- # so the regex has to be fairly general
- $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x',
- '[[' . $newTitle->getFullText() . ']]',
- $text, 1 );
+ // Fix the text
+ $newContent = $content->updateRedirect( $newTitle );
- if ( $newText === $text ) {
- $this->setLastError( 'Text unchanged???' );
+ if ( $newContent->equals( $content ) ) {
+ $this->setLastError( 'Content unchanged???' );
return false;
}
- # Save it
+ $user = $this->getUser();
+ if ( !$user ) {
+ $this->setLastError( 'Invalid user' );
+ return false;
+ }
+
+ // Save it
global $wgUser;
$oldUser = $wgUser;
- $wgUser = $this->getUser();
+ $wgUser = $user;
$article = WikiPage::factory( $this->title );
+
+ // Messages: double-redirect-fixed-move, double-redirect-fixed-maintenance
$reason = wfMessage( 'double-redirect-fixed-' . $this->reason,
$this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText()
)->inContentLanguage()->text();
- $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() );
+ $article->doEditContent( $newContent, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $user );
$wgUser = $oldUser;
return true;
@@ -160,7 +163,8 @@ class DoubleRedirectJob extends Job {
public static function getFinalDestination( $title ) {
$dbw = wfGetDB( DB_MASTER );
- $seenTitles = array(); # Circular redirect check
+ // Circular redirect check
+ $seenTitles = array();
$dest = false;
while ( true ) {
@@ -171,9 +175,17 @@ class DoubleRedirectJob extends Job {
}
$seenTitles[$titleText] = true;
+ if ( $title->getInterwiki() ) {
+ // If the target is interwiki, we have to break early (bug 40352).
+ // Otherwise it will look up a row in the local page table
+ // with the namespace/page of the interwiki target which can cause
+ // unexpected results (e.g. X -> foo:Bar -> Bar -> .. )
+ break;
+ }
+
$row = $dbw->selectRow(
array( 'redirect', 'page' ),
- array( 'rd_namespace', 'rd_title' ),
+ array( 'rd_namespace', 'rd_title', 'rd_interwiki' ),
array(
'rd_from=page_id',
'page_namespace' => $title->getNamespace(),
@@ -183,7 +195,7 @@ class DoubleRedirectJob extends Job {
# No redirect from here, chain terminates
break;
} else {
- $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title );
+ $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title, '', $row->rd_interwiki );
}
}
return $dest;
@@ -191,17 +203,19 @@ class DoubleRedirectJob extends Job {
/**
* Get a user object for doing edits, from a request-lifetime cache
- * @return User
+ * False will be returned if the user name specified in the
+ * 'double-redirect-fixer' message is invalid.
+ *
+ * @return User|bool
*/
function getUser() {
if ( !self::$user ) {
- self::$user = User::newFromName( wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text(), false );
- # FIXME: newFromName could return false on a badly configured wiki.
- if ( !self::$user->isLoggedIn() ) {
+ self::$user = User::newFromName( wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text() );
+ # User::newFromName() can return false on a badly configured wiki.
+ if ( self::$user && !self::$user->isLoggedIn() ) {
self::$user->addToDatabase();
}
}
return self::$user;
}
}
-
diff --git a/includes/job/jobs/DuplicateJob.php b/includes/job/jobs/DuplicateJob.php
new file mode 100644
index 00000000..be1bfe5c
--- /dev/null
+++ b/includes/job/jobs/DuplicateJob.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * No-op job that does nothing.
+ *
+ * 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 Cache
+ */
+
+/**
+ * No-op job that does nothing. Used to represent duplicates.
+ *
+ * @ingroup JobQueue
+ */
+final class DuplicateJob extends Job {
+ /**
+ * Callers should use DuplicateJob::newFromJob() instead
+ *
+ * @param $title Title
+ * @param array $params job parameters
+ * @param $id Integer: job id
+ */
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'duplicate', $title, $params, $id );
+ }
+
+ /**
+ * Get a duplicate no-op version of a job
+ *
+ * @param Job $job
+ * @return Job
+ */
+ public static function newFromJob( Job $job ) {
+ $djob = new self( $job->getTitle(), $job->getParams(), $job->id );
+ $djob->command = $job->getType();
+ $djob->params = is_array( $djob->params ) ? $djob->params : array();
+ $djob->params = array( 'isDuplicate' => true ) + $djob->params;
+ $djob->metadata = $job->metadata;
+ return $djob;
+ }
+
+ public function run() {
+ return true;
+ }
+}
diff --git a/includes/job/EmaillingJob.php b/includes/job/jobs/EmaillingJob.php
index d3599882..9fbf3124 100644
--- a/includes/job/EmaillingJob.php
+++ b/includes/job/jobs/EmaillingJob.php
@@ -33,14 +33,15 @@ class EmaillingJob extends Job {
}
function run() {
- UserMailer::send(
+ $status = UserMailer::send(
$this->params['to'],
$this->params['from'],
$this->params['subj'],
$this->params['body'],
$this->params['replyto']
);
- return true;
+
+ return $status->isOK();
}
}
diff --git a/includes/job/EnotifNotifyJob.php b/includes/job/jobs/EnotifNotifyJob.php
index b4c925e9..bbe988d0 100644
--- a/includes/job/EnotifNotifyJob.php
+++ b/includes/job/jobs/EnotifNotifyJob.php
@@ -35,7 +35,7 @@ class EnotifNotifyJob extends Job {
function run() {
$enotif = new EmailNotification();
// Get the user from ID (rename safe). Anons are 0, so defer to name.
- if( isset( $this->params['editorID'] ) && $this->params['editorID'] ) {
+ if ( isset( $this->params['editorID'] ) && $this->params['editorID'] ) {
$editor = User::newFromId( $this->params['editorID'] );
// B/C, only the name might be given.
} else {
@@ -49,7 +49,8 @@ class EnotifNotifyJob extends Job {
$this->params['summary'],
$this->params['minorEdit'],
$this->params['oldid'],
- $this->params['watchers']
+ $this->params['watchers'],
+ $this->params['pageStatus']
);
return true;
}
diff --git a/includes/job/jobs/HTMLCacheUpdateJob.php b/includes/job/jobs/HTMLCacheUpdateJob.php
new file mode 100644
index 00000000..44c240bb
--- /dev/null
+++ b/includes/job/jobs/HTMLCacheUpdateJob.php
@@ -0,0 +1,263 @@
+<?php
+/**
+ * HTML cache invalidation of all pages linking to a given title.
+ *
+ * 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 Cache
+ */
+
+/**
+ * Job wrapper for HTMLCacheUpdate. Gets run whenever a related
+ * job gets called from the queue.
+ *
+ * This class is designed to work efficiently with small numbers of links, and
+ * to work reasonably well with up to ~10^5 links. Above ~10^6 links, the memory
+ * and time requirements of loading all backlinked IDs in doUpdate() might become
+ * prohibitive. The requirements measured at Wikimedia are approximately:
+ *
+ * memory: 48 bytes per row
+ * time: 16us per row for the query plus processing
+ *
+ * The reason this query is done is to support partitioning of the job
+ * by backlinked ID. The memory issue could be allieviated by doing this query in
+ * batches, but of course LIMIT with an offset is inefficient on the DB side.
+ *
+ * The class is nevertheless a vast improvement on the previous method of using
+ * File::getLinksTo() and Title::touchArray(), which uses about 2KB of memory per
+ * link.
+ *
+ * @ingroup JobQueue
+ */
+class HTMLCacheUpdateJob extends Job {
+ /** @var BacklinkCache */
+ protected $blCache;
+
+ protected $rowsPerJob, $rowsPerQuery;
+
+ /**
+ * Construct a job
+ * @param $title Title: the title linked to
+ * @param array $params job parameters (table, start and end page_ids)
+ * @param $id Integer: job id
+ */
+ function __construct( $title, $params, $id = 0 ) {
+ global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
+
+ parent::__construct( 'htmlCacheUpdate', $title, $params, $id );
+
+ $this->rowsPerJob = $wgUpdateRowsPerJob;
+ $this->rowsPerQuery = $wgUpdateRowsPerQuery;
+ $this->blCache = $title->getBacklinkCache();
+ }
+
+ public function run() {
+ if ( isset( $this->params['start'] ) && isset( $this->params['end'] ) ) {
+ # This is hit when a job is actually performed
+ return $this->doPartialUpdate();
+ } else {
+ # This is hit when the jobs have to be inserted
+ return $this->doFullUpdate();
+ }
+ }
+
+ /**
+ * Update all of the backlinks
+ */
+ protected function doFullUpdate() {
+ global $wgMaxBacklinksInvalidate;
+
+ # Get an estimate of the number of rows from the BacklinkCache
+ $max = max( $this->rowsPerJob * 2, $wgMaxBacklinksInvalidate ) + 1;
+ $numRows = $this->blCache->getNumLinks( $this->params['table'], $max );
+ if ( $wgMaxBacklinksInvalidate !== false && $numRows > $wgMaxBacklinksInvalidate ) {
+ wfDebug( "Skipped HTML cache invalidation of {$this->title->getPrefixedText()}." );
+ return true;
+ }
+
+ if ( $numRows > $this->rowsPerJob * 2 ) {
+ # Do fast cached partition
+ $this->insertPartitionJobs();
+ } else {
+ # Get the links from the DB
+ $titleArray = $this->blCache->getLinks( $this->params['table'] );
+ # Check if the row count estimate was correct
+ if ( $titleArray->count() > $this->rowsPerJob * 2 ) {
+ # Not correct, do accurate partition
+ wfDebug( __METHOD__ . ": row count estimate was incorrect, repartitioning\n" );
+ $this->insertJobsFromTitles( $titleArray );
+ } else {
+ $this->invalidateTitles( $titleArray ); // just do the query
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Update some of the backlinks, defined by a page ID range
+ */
+ protected function doPartialUpdate() {
+ $titleArray = $this->blCache->getLinks(
+ $this->params['table'], $this->params['start'], $this->params['end'] );
+ if ( $titleArray->count() <= $this->rowsPerJob * 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 );
+ }
+ return true;
+ }
+
+ /**
+ * Partition the current range given by $this->params['start'] and $this->params['end'],
+ * using a pre-calculated title array which gives the links in that range.
+ * Queue the resulting jobs.
+ *
+ * @param $titleArray array
+ * @param $rootJobParams array
+ * @return void
+ */
+ protected function insertJobsFromTitles( $titleArray, $rootJobParams = array() ) {
+ // Carry over any "root job" information
+ $rootJobParams = $this->getRootJobParams();
+ # 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->params['start']; # 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->rowsPerJob ) {
+ # Add a job up to but not including the current ID
+ $jobs[] = new HTMLCacheUpdateJob( $this->title,
+ array(
+ 'table' => $this->params['table'],
+ 'start' => $start,
+ 'end' => $id - 1
+ ) + $rootJobParams // carry over information for de-duplication
+ );
+ $start = $id;
+ $numTitles = 0;
+ }
+ $numTitles++;
+ }
+ # Last job
+ $jobs[] = new HTMLCacheUpdateJob( $this->title,
+ array(
+ 'table' => $this->params['table'],
+ 'start' => $start,
+ 'end' => $this->params['end']
+ ) + $rootJobParams // carry over information for de-duplication
+ );
+ 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 );
+ } else {
+ JobQueueGroup::singleton()->push( $jobs );
+ }
+ }
+
+ /**
+ * @param $rootJobParams array
+ * @return void
+ */
+ protected function insertPartitionJobs( $rootJobParams = array() ) {
+ // Carry over any "root job" information
+ $rootJobParams = $this->getRootJobParams();
+
+ $batches = $this->blCache->partition( $this->params['table'], $this->rowsPerJob );
+ if ( !count( $batches ) ) {
+ return; // no jobs to insert
+ }
+
+ $jobs = array();
+ foreach ( $batches as $batch ) {
+ list( $start, $end ) = $batch;
+ $jobs[] = new HTMLCacheUpdateJob( $this->title,
+ array(
+ 'table' => $this->params['table'],
+ 'start' => $start,
+ 'end' => $end,
+ ) + $rootJobParams // carry over information for de-duplication
+ );
+ }
+
+ JobQueueGroup::singleton()->push( $jobs );
+ }
+
+ /**
+ * Invalidate an array (or iterator) of Title objects, right now
+ * @param $titleArray array
+ */
+ protected function invalidateTitles( $titleArray ) {
+ global $wgUseFileCache, $wgUseSquid;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $timestamp = $dbw->timestamp();
+
+ # Get all IDs in this query into an array
+ $ids = array();
+ foreach ( $titleArray as $title ) {
+ $ids[] = $title->getArticleID();
+ }
+
+ if ( !$ids ) {
+ return;
+ }
+
+ # Don't invalidated pages that were already invalidated
+ $touchedCond = isset( $this->params['rootJobTimestamp'] )
+ ? array( "page_touched < " .
+ $dbw->addQuotes( $dbw->timestamp( $this->params['rootJobTimestamp'] ) ) )
+ : array();
+
+ # Update page_touched
+ $batches = array_chunk( $ids, $this->rowsPerQuery );
+ foreach ( $batches as $batch ) {
+ $dbw->update( 'page',
+ array( 'page_touched' => $timestamp ),
+ array( 'page_id' => $batch ) + $touchedCond,
+ __METHOD__
+ );
+ }
+
+ # Update squid
+ if ( $wgUseSquid ) {
+ $u = SquidUpdate::newFromTitles( $titleArray );
+ $u->doUpdate();
+ }
+
+ # Update file cache
+ if ( $wgUseFileCache ) {
+ foreach ( $titleArray as $title ) {
+ HTMLFileCache::clearFileCache( $title );
+ }
+ }
+ }
+}
diff --git a/includes/job/jobs/NullJob.php b/includes/job/jobs/NullJob.php
new file mode 100644
index 00000000..b6164a5d
--- /dev/null
+++ b/includes/job/jobs/NullJob.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Degenerate job that does nothing.
+ *
+ * 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 Cache
+ */
+
+/**
+ * Degenerate job that does nothing, but can optionally replace itself
+ * in the queue and/or sleep for a brief time period. These can be used
+ * to represent "no-op" jobs or test lock contention and performance.
+ *
+ * @par Example:
+ * Inserting a null job in the configured job queue:
+ * @code
+ * $ php maintenance/eval.php
+ * > $queue = JobQueueGroup::singleton();
+ * > $job = new NullJob( Title::newMainPage(), array( 'lives' => 10 ) );
+ * > $queue->push( $job );
+ * @endcode
+ * You can then confirm the job has been enqueued by using the showJobs.php
+ * maintenance utility:
+ * @code
+ * $ php maintenance/showJobs.php --group
+ * null: 1 queue; 0 claimed (0 active, 0 abandoned)
+ * $
+ * @endcode
+ *
+ * @ingroup JobQueue
+ */
+class NullJob extends Job {
+ /**
+ * @param $title Title (can be anything)
+ * @param array $params job parameters (lives, usleep)
+ * @param $id Integer: job id
+ */
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'null', $title, $params, $id );
+ if ( !isset( $this->params['lives'] ) ) {
+ $this->params['lives'] = 1;
+ }
+ if ( !isset( $this->params['usleep'] ) ) {
+ $this->params['usleep'] = 0;
+ }
+ $this->removeDuplicates = !empty( $this->params['removeDuplicates'] );
+ }
+
+ public function run() {
+ if ( $this->params['usleep'] > 0 ) {
+ usleep( $this->params['usleep'] );
+ }
+ if ( $this->params['lives'] > 1 ) {
+ $params = $this->params;
+ $params['lives']--;
+ $job = new self( $this->title, $params );
+ JobQueueGroup::singleton()->push( $job );
+ }
+ return true;
+ }
+}
diff --git a/includes/job/jobs/PublishStashedFileJob.php b/includes/job/jobs/PublishStashedFileJob.php
new file mode 100644
index 00000000..5a24f93c
--- /dev/null
+++ b/includes/job/jobs/PublishStashedFileJob.php
@@ -0,0 +1,140 @@
+<?php
+/**
+ * Upload a file from the upload stash into the local file repo.
+ *
+ * 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 Upload
+ */
+
+/**
+ * Upload a file from the upload stash into the local file repo.
+ *
+ * @ingroup Upload
+ */
+class PublishStashedFileJob extends Job {
+ public function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'PublishStashedFile', $title, $params, $id );
+ $this->removeDuplicates = true;
+ }
+
+ public function run() {
+ $scope = RequestContext::importScopedSession( $this->params['session'] );
+ $context = RequestContext::getMain();
+ try {
+ $user = $context->getUser();
+ if ( !$user->isLoggedIn() ) {
+ $this->setLastError( "Could not load the author user from session." );
+ return false;
+ }
+
+ if ( count( $_SESSION ) === 0 ) {
+ // Empty session probably indicates that we didn't associate
+ // with the session correctly. Note that being able to load
+ // the user does not necessarily mean the session was loaded.
+ // Most likely cause by suhosin.session.encrypt = On.
+ $this->setLastError( "Error associating with user session. Try setting suhosin.session.encrypt = Off" );
+ return false;
+ }
+
+
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array( 'result' => 'Poll', 'stage' => 'publish', 'status' => Status::newGood() )
+ );
+
+ $upload = new UploadFromStash( $user );
+ // @todo initialize() causes a GET, ideally we could frontload the antivirus
+ // checks and anything else to the stash stage (which includes concatenation and
+ // the local file is thus already there). That way, instead of GET+PUT, there could
+ // just be a COPY operation from the stash to the public zone.
+ $upload->initialize( $this->params['filekey'], $this->params['filename'] );
+
+ // Check if the local file checks out (this is generally a no-op)
+ $verification = $upload->verifyUpload();
+ if ( $verification['status'] !== UploadBase::OK ) {
+ $status = Status::newFatal( 'verification-error' );
+ $status->value = array( 'verification' => $verification );
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array( 'result' => 'Failure', 'stage' => 'publish', 'status' => $status )
+ );
+ $this->setLastError( "Could not verify upload." );
+ return false;
+ }
+
+ // Upload the stashed file to a permanent location
+ $status = $upload->performUpload(
+ $this->params['comment'],
+ $this->params['text'],
+ $this->params['watch'],
+ $user
+ );
+ if ( !$status->isGood() ) {
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array( 'result' => 'Failure', 'stage' => 'publish', 'status' => $status )
+ );
+ $this->setLastError( $status->getWikiText() );
+ return false;
+ }
+
+ // Build the image info array while we have the local reference handy
+ $apiMain = new ApiMain(); // dummy object (XXX)
+ $imageInfo = $upload->getImageInfo( $apiMain->getResult() );
+
+ // Cleanup any temporary local file
+ $upload->cleanupTempFile();
+
+ // Cache the info so the user doesn't have to wait forever to get the final info
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array(
+ 'result' => 'Success',
+ 'stage' => 'publish',
+ 'filename' => $upload->getLocalFile()->getName(),
+ 'imageinfo' => $imageInfo,
+ 'status' => Status::newGood()
+ )
+ );
+ } catch ( MWException $e ) {
+ UploadBase::setSessionStatus(
+ $this->params['filekey'],
+ array(
+ 'result' => 'Failure',
+ 'stage' => 'publish',
+ 'status' => Status::newFatal( 'api-error-publishfailed' )
+ )
+ );
+ $this->setLastError( get_class( $e ) . ": " . $e->getText() );
+ return false;
+ }
+ return true;
+ }
+
+ public function getDeduplicationInfo() {
+ $info = parent::getDeduplicationInfo();
+ if ( is_array( $info['params'] ) ) {
+ $info['params'] = array( 'filekey' => $info['params']['filekey'] );
+ }
+ return $info;
+ }
+
+ public function allowRetries() {
+ return false;
+ }
+}
diff --git a/includes/job/jobs/RefreshLinksJob.php b/includes/job/jobs/RefreshLinksJob.php
new file mode 100644
index 00000000..4fc8bac6
--- /dev/null
+++ b/includes/job/jobs/RefreshLinksJob.php
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Job to update links for a given title.
+ *
+ * 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 JobQueue
+ */
+
+/**
+ * Background job to update links for a given title.
+ *
+ * @ingroup JobQueue
+ */
+class RefreshLinksJob extends Job {
+ function __construct( $title, $params = '', $id = 0 ) {
+ parent::__construct( 'refreshLinks', $title, $params, $id );
+ $this->removeDuplicates = true; // job is expensive
+ }
+
+ /**
+ * Run a refreshLinks job
+ * @return boolean success
+ */
+ function run() {
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ if ( is_null( $this->title ) ) {
+ $this->error = "refreshLinks: Invalid title";
+ return false;
+ }
+
+ # Wait for the DB of the current/next slave DB handle to catch up to the master.
+ # This way, we get the correct page_latest for templates or files that just changed
+ # milliseconds ago, having triggered this job to begin with.
+ if ( isset( $this->params['masterPos'] ) && $this->params['masterPos'] !== false ) {
+ wfGetLB()->waitFor( $this->params['masterPos'] );
+ }
+
+ $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
+ if ( !$revision ) {
+ $this->error = 'refreshLinks: Article not found "' .
+ $this->title->getPrefixedDBkey() . '"';
+ return false; // XXX: what if it was just deleted?
+ }
+
+ self::runForTitleInternal( $this->title, $revision, __METHOD__ );
+
+ return true;
+ }
+
+ /**
+ * @return Array
+ */
+ public function getDeduplicationInfo() {
+ $info = parent::getDeduplicationInfo();
+ // Don't let highly unique "masterPos" values ruin duplicate detection
+ if ( is_array( $info['params'] ) ) {
+ unset( $info['params']['masterPos'] );
+ }
+ return $info;
+ }
+
+ /**
+ * @param $title Title
+ * @param $revision Revision
+ * @param $fname string
+ * @return void
+ */
+ public static function runForTitleInternal( Title $title, Revision $revision, $fname ) {
+ wfProfileIn( $fname );
+ $content = $revision->getContent( Revision::RAW );
+
+ if ( !$content ) {
+ // if there is no content, pretend the content is empty
+ $content = $revision->getContentHandler()->makeEmptyContent();
+ }
+
+ // Revision ID must be passed to the parser output to get revision variables correct
+ $parserOutput = $content->getParserOutput( $title, $revision->getId(), null, false );
+
+ $updates = $content->getSecondaryDataUpdates( $title, null, false, $parserOutput );
+ DataUpdate::runUpdates( $updates );
+
+ InfoAction::invalidateCache( $title );
+
+ wfProfileOut( $fname );
+ }
+}
+
+/**
+ * Background job to update links for a given title.
+ * Newer version for high use templates.
+ *
+ * @ingroup JobQueue
+ */
+class RefreshLinksJob2 extends Job {
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'refreshLinks2', $title, $params, $id );
+ // Base jobs for large templates can easily be de-duplicated
+ $this->removeDuplicates = !isset( $params['start'] ) && !isset( $params['end'] );
+ }
+
+ /**
+ * Run a refreshLinks2 job
+ * @return boolean success
+ */
+ function run() {
+ global $wgUpdateRowsPerJob;
+
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ if ( is_null( $this->title ) ) {
+ $this->error = "refreshLinks2: Invalid title";
+ return false;
+ }
+
+ // Back compat for pre-r94435 jobs
+ $table = isset( $this->params['table'] ) ? $this->params['table'] : 'templatelinks';
+
+ // Avoid slave lag when fetching templates.
+ // When the outermost job is run, we know that the caller that enqueued it must have
+ // committed the relevant changes to the DB by now. At that point, record the master
+ // position and pass it along as the job recursively breaks into smaller range jobs.
+ // Hopefully, when leaf jobs are popped, the slaves will have reached that position.
+ if ( isset( $this->params['masterPos'] ) ) {
+ $masterPos = $this->params['masterPos'];
+ } elseif ( wfGetLB()->getServerCount() > 1 ) {
+ $masterPos = wfGetLB()->getMasterPos();
+ } else {
+ $masterPos = false;
+ }
+
+ $tbc = $this->title->getBacklinkCache();
+
+ $jobs = array(); // jobs to insert
+ if ( isset( $this->params['start'] ) && isset( $this->params['end'] ) ) {
+ # This is a partition job to trigger the insertion of leaf jobs...
+ $jobs = array_merge( $jobs, $this->getSingleTitleJobs( $table, $masterPos ) );
+ } else {
+ # This is a base job to trigger the insertion of partitioned jobs...
+ if ( $tbc->getNumLinks( $table, $wgUpdateRowsPerJob + 1 ) <= $wgUpdateRowsPerJob ) {
+ # Just directly insert the single per-title jobs
+ $jobs = array_merge( $jobs, $this->getSingleTitleJobs( $table, $masterPos ) );
+ } else {
+ # Insert the partition jobs to make per-title jobs
+ foreach ( $tbc->partition( $table, $wgUpdateRowsPerJob ) as $batch ) {
+ list( $start, $end ) = $batch;
+ $jobs[] = new RefreshLinksJob2( $this->title,
+ array(
+ 'table' => $table,
+ 'start' => $start,
+ 'end' => $end,
+ 'masterPos' => $masterPos,
+ ) + $this->getRootJobParams() // carry over information for de-duplication
+ );
+ }
+ }
+ }
+
+ if ( count( $jobs ) ) {
+ JobQueueGroup::singleton()->push( $jobs );
+ }
+
+ return true;
+ }
+
+ /**
+ * @param $table string
+ * @param $masterPos mixed
+ * @return Array
+ */
+ protected function getSingleTitleJobs( $table, $masterPos ) {
+ # The "start"/"end" fields are not set for the base jobs
+ $start = isset( $this->params['start'] ) ? $this->params['start'] : false;
+ $end = isset( $this->params['end'] ) ? $this->params['end'] : false;
+ $titles = $this->title->getBacklinkCache()->getLinks( $table, $start, $end );
+ # Convert into single page refresh links jobs.
+ # This handles well when in sapi mode and is useful in any case for job
+ # de-duplication. If many pages use template A, and that template itself
+ # uses template B, then an edit to both will create many duplicate jobs.
+ # Roughly speaking, for each page, one of the "RefreshLinksJob" jobs will
+ # get run first, and when it does, it will remove the duplicates. Of course,
+ # one page could have its job popped when the other page's job is still
+ # buried within the logic of a refreshLinks2 job.
+ $jobs = array();
+ foreach ( $titles as $title ) {
+ $jobs[] = new RefreshLinksJob( $title,
+ array( 'masterPos' => $masterPos ) + $this->getRootJobParams()
+ ); // carry over information for de-duplication
+ }
+ return $jobs;
+ }
+
+ /**
+ * @return Array
+ */
+ public function getDeduplicationInfo() {
+ $info = parent::getDeduplicationInfo();
+ // Don't let highly unique "masterPos" values ruin duplicate detection
+ if ( is_array( $info['params'] ) ) {
+ unset( $info['params']['masterPos'] );
+ }
+ return $info;
+ }
+}
diff --git a/includes/job/UploadFromUrlJob.php b/includes/job/jobs/UploadFromUrlJob.php
index e06f68e4..c993cfb4 100644
--- a/includes/job/UploadFromUrlJob.php
+++ b/includes/job/jobs/UploadFromUrlJob.php
@@ -48,6 +48,7 @@ class UploadFromUrlJob extends Job {
}
public function run() {
+ global $wgCopyUploadAsyncTimeout;
# Initialize this object and the upload object
$this->upload = new UploadFromUrl();
$this->upload->initialize(
@@ -58,7 +59,11 @@ class UploadFromUrlJob extends Job {
$this->user = User::newFromName( $this->params['userName'] );
# Fetch the file
- $status = $this->upload->fetchFile();
+ $opts = array();
+ if ( $wgCopyUploadAsyncTimeout ) {
+ $opts['timeout'] = $wgCopyUploadAsyncTimeout;
+ }
+ $status = $this->upload->fetchFile( $opts );
if ( !$status->isOk() ) {
$this->leaveMessage( $status );
return true;
@@ -148,8 +153,8 @@ class UploadFromUrlJob extends Job {
* Store a result in the session data. Note that the caller is responsible
* for appropriate session_start and session_write_close calls.
*
- * @param $result String: the result (Success|Warning|Failure)
- * @param $dataKey String: the key of the extra data
+ * @param string $result the result (Success|Warning|Failure)
+ * @param string $dataKey the key of the extra data
* @param $dataValue Mixed: the extra data itself
*/
protected function storeResultInSession( $result, $dataKey, $dataValue ) {
diff --git a/includes/json/FormatJson.php b/includes/json/FormatJson.php
index f67700c9..d6116512 100644
--- a/includes/json/FormatJson.php
+++ b/includes/json/FormatJson.php
@@ -1,6 +1,6 @@
<?php
/**
- * Simple wrapper for json_econde and json_decode that falls back on Services_JSON class.
+ * Wrapper for json_encode and json_decode.
*
* 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
@@ -20,55 +20,217 @@
* @file
*/
-require_once __DIR__ . '/Services_JSON.php';
-
/**
* JSON formatter wrapper class
*/
class FormatJson {
/**
+ * Skip escaping most characters above U+007F for readability and compactness.
+ * This encoding option saves 3 to 8 bytes (uncompressed) for each such character;
+ * however, it could break compatibility with systems that incorrectly handle UTF-8.
+ *
+ * @since 1.22
+ */
+ const UTF8_OK = 1;
+
+ /**
+ * Skip escaping the characters '<', '>', and '&', which have special meanings in
+ * HTML and XML.
+ *
+ * @warning Do not use this option for JSON that could end up in inline scripts.
+ * - HTML5, §4.3.1.2 Restrictions for contents of script elements
+ * - XML 1.0 (5th Ed.), §2.4 Character Data and Markup
+ *
+ * @since 1.22
+ */
+ const XMLMETA_OK = 2;
+
+ /**
+ * Skip escaping as many characters as reasonably possible.
+ *
+ * @warning When generating inline script blocks, use FormatJson::UTF8_OK instead.
+ *
+ * @since 1.22
+ */
+ const ALL_OK = 3;
+
+ /**
+ * Regex that matches whitespace inside empty arrays and objects.
+ *
+ * This doesn't affect regular strings inside the JSON because those can't
+ * have a real line break (\n) in them, at this point they are already escaped
+ * as the string "\n" which this doesn't match.
+ *
+ * @private
+ */
+ const WS_CLEANUP_REGEX = '/(?<=[\[{])\n\s*+(?=[\]}])/';
+
+ /**
+ * Characters problematic in JavaScript.
+ *
+ * @note These are listed in ECMA-262 (5.1 Ed.), §7.3 Line Terminators along with U+000A (LF)
+ * and U+000D (CR). However, PHP already escapes LF and CR according to RFC 4627.
+ */
+ private static $badChars = array(
+ "\xe2\x80\xa8", // U+2028 LINE SEPARATOR
+ "\xe2\x80\xa9", // U+2029 PARAGRAPH SEPARATOR
+ );
+
+ /**
+ * Escape sequences for characters listed in FormatJson::$badChars.
+ */
+ private static $badCharsEscaped = array(
+ '\u2028', // U+2028 LINE SEPARATOR
+ '\u2029', // U+2029 PARAGRAPH SEPARATOR
+ );
+
+ /**
* Returns the JSON representation of a value.
*
- * @param $value Mixed: the value being encoded. Can be any type except a resource.
- * @param $isHtml Boolean
+ * @note Empty arrays are encoded as numeric arrays, not as objects, so cast any associative
+ * array that might be empty to an object before encoding it.
*
- * @todo FIXME: "$isHtml" parameter's purpose is not documented. It appears to
- * map to a parameter labeled "pretty-print output with indents and
- * newlines" in Services_JSON::encode(), which has no string relation
- * to HTML output.
+ * @note In pre-1.22 versions of MediaWiki, using this function for generating inline script
+ * blocks may result in an XSS vulnerability, and quite likely will in XML documents
+ * (cf. FormatJson::XMLMETA_OK). Use Xml::encodeJsVar() instead in such cases.
*
- * @return string
+ * @param mixed $value The value to encode. Can be any type except a resource.
+ * @param bool $pretty If true, add non-significant whitespace to improve readability.
+ * @param int $escaping Bitfield consisting of _OK class constants
+ * @return string|bool: String if successful; false upon failure
*/
- public static function encode( $value, $isHtml = false ) {
- if ( !function_exists( 'json_encode' ) || ( $isHtml && version_compare( PHP_VERSION, '5.4.0', '<' ) ) ) {
- $json = new Services_JSON();
- return $json->encode( $value, $isHtml );
- } else {
- return json_encode( $value, $isHtml ? JSON_PRETTY_PRINT : 0 );
+ public static function encode( $value, $pretty = false, $escaping = 0 ) {
+ if ( defined( 'JSON_UNESCAPED_UNICODE' ) ) {
+ return self::encode54( $value, $pretty, $escaping );
}
+ return self::encode53( $value, $pretty, $escaping );
}
/**
* Decodes a JSON string.
*
- * @param $value String: the json string being decoded.
- * @param $assoc Boolean: when true, returned objects will be converted into associative arrays.
+ * @param string $value The JSON string being decoded
+ * @param bool $assoc When true, returned objects will be converted into associative arrays.
*
- * @return Mixed: the value encoded in json in appropriate PHP type.
- * Values true, false and null (case-insensitive) are returned as true, false
- * and "&null;" respectively. "&null;" is returned if the json cannot be
- * decoded or if the encoded data is deeper than the recursion limit.
+ * @return mixed: the value encoded in JSON in appropriate PHP type.
+ * `null` is returned if the JSON cannot be decoded or if the encoded data is deeper than
+ * the recursion limit.
*/
public static function decode( $value, $assoc = false ) {
- if ( !function_exists( 'json_decode' ) ) {
- $json = $assoc ? new Services_JSON( SERVICES_JSON_LOOSE_TYPE ) :
- new Services_JSON();
- $jsonDec = $json->decode( $value );
- return $jsonDec;
- } else {
- return json_decode( $value, $assoc );
+ return json_decode( $value, $assoc );
+ }
+
+ /**
+ * JSON encoder wrapper for PHP >= 5.4, which supports useful encoding options.
+ *
+ * @param mixed $value
+ * @param bool $pretty
+ * @param int $escaping
+ * @return string|bool
+ */
+ private static function encode54( $value, $pretty, $escaping ) {
+ // PHP escapes '/' to prevent breaking out of inline script blocks using '</script>',
+ // which is hardly useful when '<' and '>' are escaped (and inadequate), and such
+ // escaping negatively impacts the human readability of URLs and similar strings.
+ $options = JSON_UNESCAPED_SLASHES;
+ $options |= $pretty ? JSON_PRETTY_PRINT : 0;
+ $options |= ( $escaping & self::UTF8_OK ) ? JSON_UNESCAPED_UNICODE : 0;
+ $options |= ( $escaping & self::XMLMETA_OK ) ? 0 : ( JSON_HEX_TAG | JSON_HEX_AMP );
+ $json = json_encode( $value, $options );
+ if ( $json === false ) {
+ return false;
+ }
+
+ if ( $pretty ) {
+ // Remove whitespace inside empty arrays/objects; different JSON encoders
+ // vary on this, and we want our output to be consistent across implementations.
+ $json = preg_replace( self::WS_CLEANUP_REGEX, '', $json );
+ }
+ if ( $escaping & self::UTF8_OK ) {
+ $json = str_replace( self::$badChars, self::$badCharsEscaped, $json );
+ }
+ return $json;
+ }
+
+ /**
+ * JSON encoder wrapper for PHP 5.3, which lacks native support for some encoding options.
+ * Therefore, the missing options are implemented here purely in PHP code.
+ *
+ * @param mixed $value
+ * @param bool $pretty
+ * @param int $escaping
+ * @return string|bool
+ */
+ private static function encode53( $value, $pretty, $escaping ) {
+ $options = ( $escaping & self::XMLMETA_OK ) ? 0 : ( JSON_HEX_TAG | JSON_HEX_AMP );
+ $json = json_encode( $value, $options );
+ if ( $json === false ) {
+ return false;
+ }
+
+ // Emulate JSON_UNESCAPED_SLASHES. Because the JSON contains no unescaped slashes
+ // (only escaped slashes), a simple string replacement works fine.
+ $json = str_replace( '\/', '/', $json );
+
+ if ( $escaping & self::UTF8_OK ) {
+ // JSON hex escape sequences follow the format \uDDDD, where DDDD is four hex digits
+ // indicating the equivalent UTF-16 code unit's value. To most efficiently unescape
+ // them, we exploit the JSON extension's built-in decoder.
+ // * We escape the input a second time, so any such sequence becomes \\uDDDD.
+ // * To avoid interpreting escape sequences that were in the original input,
+ // each double-escaped backslash (\\\\) is replaced with \\\u005c.
+ // * We strip one of the backslashes from each of the escape sequences to unescape.
+ // * Then the JSON decoder can perform the actual unescaping.
+ $json = str_replace( "\\\\\\\\", "\\\\\\u005c", addcslashes( $json, '\"' ) );
+ $json = json_decode( preg_replace( "/\\\\\\\\u(?!00[0-7])/", "\\\\u", "\"$json\"" ) );
+ $json = str_replace( self::$badChars, self::$badCharsEscaped, $json );
+ }
+
+ if ( $pretty ) {
+ return self::prettyPrint( $json );
}
+ return $json;
}
+ /**
+ * Adds non-significant whitespace to an existing JSON representation of an object.
+ * Only needed for PHP < 5.4, which lacks the JSON_PRETTY_PRINT option.
+ *
+ * @param string $json
+ * @return string
+ */
+ private static function prettyPrint( $json ) {
+ $buf = '';
+ $indent = 0;
+ $json = strtr( $json, array( '\\\\' => '\\\\', '\"' => "\x01" ) );
+ for ( $i = 0, $n = strlen( $json ); $i < $n; $i += $skip ) {
+ $skip = 1;
+ switch ( $json[$i] ) {
+ case ':':
+ $buf .= ': ';
+ break;
+ case '[':
+ case '{':
+ ++$indent;
+ // falls through
+ case ',':
+ $buf .= $json[$i] . "\n" . str_repeat( ' ', $indent );
+ break;
+ case ']':
+ case '}':
+ $buf .= "\n" . str_repeat( ' ', --$indent ) . $json[$i];
+ break;
+ case '"':
+ $skip = strcspn( $json, '"', $i + 1 ) + 2;
+ $buf .= substr( $json, $i, $skip );
+ break;
+ default:
+ $skip = strcspn( $json, ',]}"', $i + 1 ) + 1;
+ $buf .= substr( $json, $i, $skip );
+ }
+ }
+ $buf = preg_replace( self::WS_CLEANUP_REGEX, '', $buf );
+ return str_replace( "\x01", '\"', $buf );
+ }
}
diff --git a/includes/json/Services_JSON.php b/includes/json/Services_JSON.php
deleted file mode 100644
index 398ed6a2..00000000
--- a/includes/json/Services_JSON.php
+++ /dev/null
@@ -1,882 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
-* Converts to and from JSON format.
-*
-* JSON (JavaScript Object Notation) is a lightweight data-interchange
-* format. It is easy for humans to read and write. It is easy for machines
-* to parse and generate. It is based on a subset of the JavaScript
-* Programming Language, Standard ECMA-262 3rd Edition - December 1999.
-* This feature can also be found in Python. JSON is a text format that is
-* completely language independent but uses conventions that are familiar
-* to programmers of the C-family of languages, including C, C++, C#, Java,
-* JavaScript, Perl, TCL, and many others. These properties make JSON an
-* ideal data-interchange language.
-*
-* This package provides a simple encoder and decoder for JSON notation. It
-* is intended for use with client-side Javascript applications that make
-* use of HTTPRequest to perform server communication functions - data can
-* be encoded into JSON notation for use in a client-side javascript, or
-* decoded from incoming Javascript requests. JSON format is native to
-* Javascript, and can be directly eval()'ed with no further parsing
-* overhead
-*
-* All strings should be in ASCII or UTF-8 format!
-*
-* LICENSE: Redistribution and use in source and binary forms, with or
-* without modification, are permitted provided that the following
-* conditions are met: Redistributions of source code must retain the
-* above copyright notice, this list of conditions and the following
-* disclaimer. Redistributions in binary form must reproduce the above
-* copyright notice, this list of conditions and the following disclaimer
-* in the documentation and/or other materials provided with the
-* distribution.
-*
-* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
-* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
-* NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
-* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-* DAMAGE.
-*
-* @file
-* @ingroup API
-* @author Michal Migurski <mike-json@teczno.com>
-* @author Matt Knapp <mdknapp[at]gmail[dot]com>
-* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
-* @copyright 2005 Michal Migurski
-* @version CVS: $Id$
-* @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:
- *
- * <code>
- * // 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);
- * </code>
- *
- * @ingroup API
- */
-class Services_JSON
-{
- /**
- * constructs a new JSON instance
- *
- * @param $use Integer: 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 __construct($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 $utf16 String: 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 $utf8 String: 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 $var Mixed: 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 $pretty Boolean: 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 $var Mixed: 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($this->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($this->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($this->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 $name String: name of key to use
- * @param $value Mixed: 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($this->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 $str String: 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()
- * @return bool
- */
- 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)
- {
- $this->message = $message;
- }
-
- function __toString()
- {
- return $this->message;
- }
- }
-}
diff --git a/includes/libs/CSSJanus.php b/includes/libs/CSSJanus.php
index 4ebbc497..5a52fc7c 100644
--- a/includes/libs/CSSJanus.php
+++ b/includes/libs/CSSJanus.php
@@ -57,6 +57,7 @@ class CSSJanus {
'lookahead_not_open_brace' => null,
'lookahead_not_closing_paren' => null,
'lookahead_for_closing_paren' => null,
+ 'lookahead_not_letter' => '(?![a-zA-Z])',
'lookbehind_not_letter' => '(?<![a-zA-Z])',
'chars_within_selector' => '[^\}]*?',
'noflip_annotation' => '\/\*\s*@noflip\s*\*\/',
@@ -75,6 +76,10 @@ class CSSJanus {
'cursor_west' => null,
'four_notation_quantity' => null,
'four_notation_color' => null,
+ 'border_radius' => null,
+ 'box_shadow' => null,
+ 'text_shadow1' => null,
+ 'text_shadow2' => null,
'bg_horizontal_percentage' => null,
'bg_horizontal_percentage_x' => null,
);
@@ -95,25 +100,29 @@ class CSSJanus {
$patterns['ident'] = "-?{$patterns['nmstart']}{$patterns['nmchar']}*";
$patterns['quantity'] = "{$patterns['num']}(?:\s*{$patterns['unit']}|{$patterns['ident']})?";
$patterns['possibly_negative_quantity'] = "((?:-?{$patterns['quantity']})|(?:inherit|auto))";
- $patterns['color'] = "(#?{$patterns['nmchar']}+)";
+ $patterns['color'] = "(#?{$patterns['nmchar']}+|(?:rgba?|hsla?)\([ \d.,%-]+\))";
$patterns['url_chars'] = "(?:{$patterns['url_special_chars']}|{$patterns['nonAscii']}|{$patterns['escape']})*";
- $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\r?\n|\s|#|\:|\.|\,|\+|>)*?{)";
+ $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\r?\n|\s|#|\:|\.|\,|\+|>|\(|\))*?{)";
$patterns['lookahead_not_closing_paren'] = "(?!{$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))";
$patterns['lookahead_for_closing_paren'] = "(?={$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))";
$patterns['noflip_single'] = "/({$patterns['noflip_annotation']}{$patterns['lookahead_not_open_brace']}[^;}]+;?)/i";
$patterns['noflip_class'] = "/({$patterns['noflip_annotation']}{$patterns['chars_within_selector']}})/i";
$patterns['direction_ltr'] = "/({$patterns['direction']})ltr/i";
$patterns['direction_rtl'] = "/({$patterns['direction']})rtl/i";
- $patterns['left'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i";
- $patterns['right'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i";
+ $patterns['left'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i";
+ $patterns['right'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i";
$patterns['left_in_url'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_for_closing_paren']}/i";
$patterns['right_in_url'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_for_closing_paren']}/i";
$patterns['ltr_in_url'] = "/{$patterns['lookbehind_not_letter']}(ltr){$patterns['lookahead_for_closing_paren']}/i";
$patterns['rtl_in_url'] = "/{$patterns['lookbehind_not_letter']}(rtl){$patterns['lookahead_for_closing_paren']}/i";
$patterns['cursor_east'] = "/{$patterns['lookbehind_not_letter']}([ns]?)e-resize/";
$patterns['cursor_west'] = "/{$patterns['lookbehind_not_letter']}([ns]?)w-resize/";
- $patterns['four_notation_quantity'] = "/{$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}/i";
- $patterns['four_notation_color'] = "/(-color\s*:\s*){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}/i";
+ $patterns['four_notation_quantity'] = "/(:\s*){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s*[;}])/i";
+ $patterns['four_notation_color'] = "/(-color\s*:\s*){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s*[;}])/i";
+ $patterns['border_radius'] = "/(border-radius\s*:\s*){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s*[;}])/i";
+ $patterns['box_shadow'] = "/(box-shadow\s*:\s*(?:inset\s*)?){$patterns['possibly_negative_quantity']}/i";
+ $patterns['text_shadow1'] = "/(text-shadow\s*:\s*){$patterns['color']}(\s*){$patterns['possibly_negative_quantity']}/i";
+ $patterns['text_shadow2'] = "/(text-shadow\s*:\s*){$patterns['possibly_negative_quantity']}/i";
// The two regexes below are parenthesized differently then in the original implementation to make the
// callback's job more straightforward
$patterns['bg_horizontal_percentage'] = "/(background(?:-position)?\s*:\s*[^%]*?)(-?{$patterns['num']})(%\s*(?:{$patterns['quantity']}|{$patterns['ident']}))/";
@@ -122,7 +131,7 @@ class CSSJanus {
/**
* Transform an LTR stylesheet to RTL
- * @param $css String: stylesheet to transform
+ * @param string $css stylesheet to transform
* @param $swapLtrRtlInURL Boolean: If true, swap 'ltr' and 'rtl' in URLs
* @param $swapLeftRightInURL Boolean: If true, swap 'left' and 'right' in URLs
* @return string Transformed stylesheet
@@ -159,7 +168,9 @@ class CSSJanus {
$css = self::fixLeftAndRight( $css );
$css = self::fixCursorProperties( $css );
$css = self::fixFourPartNotation( $css );
+ $css = self::fixBorderRadius( $css );
$css = self::fixBackgroundPosition( $css );
+ $css = self::fixShadows( $css );
// Detokenize stuff we tokenized before
$css = $comments->detokenize( $css );
@@ -256,8 +267,58 @@ class CSSJanus {
* @return string
*/
private static function fixFourPartNotation( $css ) {
- $css = preg_replace( self::$patterns['four_notation_quantity'], '$1$2$7$4$5$6$3', $css );
- $css = preg_replace( self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4', $css );
+ $css = preg_replace( self::$patterns['four_notation_quantity'], '$1$2$3$8$5$6$7$4$9', $css );
+ $css = preg_replace( self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4$9', $css );
+ return $css;
+ }
+
+ /**
+ * Swaps appropriate corners in four-part border-radius rules.
+ * Needs to undo the effect of fixFourPartNotation() on those rules, too.
+ *
+ * @param $css string
+ * @return string
+ */
+ private static function fixBorderRadius( $css ) {
+ // Undo four_notation_quantity
+ $css = preg_replace( self::$patterns['border_radius'], '$1$2$3$8$5$6$7$4$9', $css );
+ // Do the real thing
+ $css = preg_replace( self::$patterns['border_radius'], '$1$4$3$2$5$8$7$6$9', $css );
+
+ return $css;
+ }
+
+ /**
+ * Negates horizontal offset in box-shadow and text-shadow rules.
+ *
+ * @param $css string
+ * @return string
+ */
+ private static function fixShadows( $css ) {
+ // Flips the sign of a CSS value, possibly with a unit.
+ // (We can't just negate the value with unary minus due to the units.)
+ $flipSign = function ( $cssValue ) {
+ // Don't mangle zeroes
+ if ( intval( $cssValue ) === 0 ) {
+ return $cssValue;
+ } elseif ( $cssValue[0] === '-' ) {
+ return substr( $cssValue, 1 );
+ } else {
+ return "-" . $cssValue;
+ }
+ };
+
+ $css = preg_replace_callback( self::$patterns['box_shadow'], function ( $matches ) use ( $flipSign ) {
+ return $matches[1] . $flipSign( $matches[2] );
+ }, $css );
+
+ $css = preg_replace_callback( self::$patterns['text_shadow1'], function ( $matches ) use ( $flipSign ) {
+ return $matches[1] . $matches[2] . $matches[3] . $flipSign( $matches[4] );
+ }, $css );
+
+ $css = preg_replace_callback( self::$patterns['text_shadow2'], function ( $matches ) use ( $flipSign ) {
+ return $matches[1] . $flipSign( $matches[2] );
+ }, $css );
return $css;
}
@@ -304,8 +365,8 @@ class CSSJanus_Tokenizer {
/**
* Constructor
- * @param $regex string Regular expression whose matches to replace by a token.
- * @param $token string Token
+ * @param string $regex Regular expression whose matches to replace by a token.
+ * @param string $token Token
*/
public function __construct( $regex, $token ) {
$this->regex = $regex;
@@ -316,7 +377,7 @@ class CSSJanus_Tokenizer {
/**
* Replace all occurrences of $regex in $str with a token and remember
* the original strings.
- * @param $str String to tokenize
+ * @param string $str to tokenize
* @return string Tokenized string
*/
public function tokenize( $str ) {
@@ -335,7 +396,7 @@ class CSSJanus_Tokenizer {
/**
* Replace tokens with their originals. If multiple strings were tokenized, it's important they be
* detokenized in exactly the SAME ORDER.
- * @param $str String: previously run through tokenize()
+ * @param string $str previously run through tokenize()
* @return string Original string
*/
public function detokenize( $str ) {
diff --git a/includes/libs/CSSMin.php b/includes/libs/CSSMin.php
index fc75cdcc..4f142fc7 100644
--- a/includes/libs/CSSMin.php
+++ b/includes/libs/CSSMin.php
@@ -59,8 +59,8 @@ class CSSMin {
/**
* Gets a list of local file paths which are referenced in a CSS style sheet
*
- * @param $source string CSS data to remap
- * @param $path string File path where the source was read from (optional)
+ * @param string $source CSS data to remap
+ * @param string $path File path where the source was read from (optional)
* @return array List of local file references
*/
public static function getLocalFileReferences( $source, $path = null ) {
@@ -82,10 +82,38 @@ class CSSMin {
}
/**
+ * Encode an image file as a base64 data URI.
+ * If the image file has a suitable MIME type and size, encode it as a
+ * base64 data URI. Return false if the image type is unfamiliar or exceeds
+ * the size limit.
+ *
+ * @param string $file Image file to encode.
+ * @param string|null $type File's MIME type or null. If null, CSSMin will
+ * try to autodetect the type.
+ * @param int|bool $sizeLimit If the size of the target file is greater than
+ * this value, decline to encode the image file and return false
+ * instead. If $sizeLimit is false, no limit is enforced.
+ * @return string|bool: Image contents encoded as a data URI or false.
+ */
+ public static function encodeImageAsDataURI( $file, $type = null, $sizeLimit = self::EMBED_SIZE_LIMIT ) {
+ if ( $sizeLimit !== false && filesize( $file ) >= $sizeLimit ) {
+ return false;
+ }
+ if ( $type === null ) {
+ $type = self::getMimeType( $file );
+ }
+ if ( !$type ) {
+ return false;
+ }
+ $data = base64_encode( file_get_contents( $file ) );
+ return 'data:' . $type . ';base64,' . $data;
+ }
+
+ /**
* @param $file string
* @return bool|string
*/
- protected static function getMimeType( $file ) {
+ public static function getMimeType( $file ) {
$realpath = realpath( $file );
// Try a couple of different ways to get the mime-type of a file, in order of
// preference
@@ -115,10 +143,10 @@ class CSSMin {
* Remaps CSS URL paths and automatically embeds data URIs for URL rules
* preceded by an /* @embed * / comment
*
- * @param $source string CSS data to remap
- * @param $local string File path where the source was read from
- * @param $remote string URL path to the file
- * @param $embedData bool If false, never do any data URI embedding, even if / * @embed * / is found
+ * @param string $source CSS data to remap
+ * @param string $local File path where the source was read from
+ * @param string $remote URL path to the file
+ * @param bool $embedData If false, never do any data URI embedding, even if / * @embed * / is found
* @return string Remapped CSS data
*/
public static function remap( $source, $local, $remote, $embedData = true ) {
@@ -174,23 +202,14 @@ class CSSMin {
// using Z for the timezone, meaning GMT
$url .= '?' . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $file ), -2 ) );
// Embedding requires a bit of extra processing, so let's skip that if we can
- if ( $embedData && $embed ) {
- $type = self::getMimeType( $file );
- // Detect when URLs were preceeded with embed tags, and also verify file size is
- // below the limit
- if (
- $type
- && $match['embed'][1] > 0
- && filesize( $file ) < self::EMBED_SIZE_LIMIT
- ) {
- // Strip off any trailing = symbols (makes browsers freak out)
- $data = base64_encode( file_get_contents( $file ) );
+ if ( $embedData && $embed && $match['embed'][1] > 0 ) {
+ $data = self::encodeImageAsDataURI( $file );
+ if ( $data !== false ) {
// Build 2 CSS properties; one which uses a base64 encoded data URI in place
// of the @embed comment to try and retain line-number integrity, and the
// other with a remapped an versioned URL and an Internet Explorer hack
// making it ignored in all browsers that support data URIs
- $replacement = "{$pre}url(data:{$type};base64,{$data}){$post};";
- $replacement .= "{$pre}url({$url}){$post}!ie;";
+ $replacement = "{$pre}url({$data}){$post};{$pre}url({$url}){$post}!ie;";
}
}
if ( $replacement === false ) {
@@ -219,7 +238,7 @@ class CSSMin {
/**
* Removes whitespace from CSS data
*
- * @param $css string CSS data to minify
+ * @param string $css CSS data to minify
* @return string Minified CSS data
*/
public static function minify( $css ) {
diff --git a/includes/libs/GenericArrayObject.php b/includes/libs/GenericArrayObject.php
index b4b9d610..d77d8ad6 100644
--- a/includes/libs/GenericArrayObject.php
+++ b/includes/libs/GenericArrayObject.php
@@ -28,9 +28,8 @@
* @since 1.20
*
* @file
- * @ingroup Diff
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
abstract class GenericArrayObject extends ArrayObject {
@@ -42,7 +41,7 @@ abstract class GenericArrayObject extends ArrayObject {
*
* @return string
*/
- public abstract function getObjectType();
+ abstract public function getObjectType();
/**
* @see SiteList::getNewOffset()
@@ -61,13 +60,11 @@ abstract class GenericArrayObject extends ArrayObject {
* @return integer
*/
protected function getNewOffset() {
- while ( true ) {
- if ( !$this->offsetExists( $this->indexOffset ) ) {
- return $this->indexOffset;
- }
-
+ while ( $this->offsetExists( $this->indexOffset ) ) {
$this->indexOffset++;
}
+
+ return $this->indexOffset;
}
/**
@@ -194,7 +191,7 @@ abstract class GenericArrayObject extends ArrayObject {
/**
* Returns an array holding all the data that should go into serialization calls.
* This is intended to allow overloading without having to reimplement the
- * behaviour of this base class.
+ * behavior of this base class.
*
* @since 1.20
*
diff --git a/includes/libs/HttpStatus.php b/includes/libs/HttpStatus.php
index 78d81803..4f626b23 100644
--- a/includes/libs/HttpStatus.php
+++ b/includes/libs/HttpStatus.php
@@ -26,7 +26,7 @@
class HttpStatus {
/**
- * Get the message associed with the HTTP response code $code
+ * Get the message associated with HTTP response code $code
*
* Replace OutputPage::getStatusMessage( $code )
*
diff --git a/includes/libs/IEContentAnalyzer.php b/includes/libs/IEContentAnalyzer.php
index cfc7f536..7f461a03 100644
--- a/includes/libs/IEContentAnalyzer.php
+++ b/includes/libs/IEContentAnalyzer.php
@@ -329,9 +329,9 @@ class IEContentAnalyzer {
* Get the MIME types from getMimesFromData(), but convert the result from IE's
* idiosyncratic private types into something other apps will understand.
*
- * @param $fileName String: the file name (unused at present)
- * @param $chunk String: the first 256 bytes of the file
- * @param $proposed String: the MIME type proposed by the server
+ * @param string $fileName the file name (unused at present)
+ * @param string $chunk the first 256 bytes of the file
+ * @param string $proposed the MIME type proposed by the server
*
* @return Array: map of IE version to detected mime type
*/
@@ -367,9 +367,9 @@ class IEContentAnalyzer {
/**
* Get the untranslated MIME types for all known versions
*
- * @param $fileName String: the file name (unused at present)
- * @param $chunk String: the first 256 bytes of the file
- * @param $proposed String: the MIME type proposed by the server
+ * @param string $fileName the file name (unused at present)
+ * @param string $chunk the first 256 bytes of the file
+ * @param string $proposed the MIME type proposed by the server
*
* @return Array: map of IE version to detected mime type
*/
@@ -848,4 +848,3 @@ class IEContentAnalyzer {
return 'unknown';
}
}
-
diff --git a/includes/libs/IEUrlExtension.php b/includes/libs/IEUrlExtension.php
index e9cfa997..49d05d4b 100644
--- a/includes/libs/IEUrlExtension.php
+++ b/includes/libs/IEUrlExtension.php
@@ -55,8 +55,8 @@ class IEUrlExtension {
*
* If the a variable is unset in $_SERVER, it should be unset in $vars.
*
- * @param $vars array A subset of $_SERVER.
- * @param $extWhitelist array Extensions which are allowed, assumed harmless.
+ * @param array $vars A subset of $_SERVER.
+ * @param array $extWhitelist Extensions which are allowed, assumed harmless.
* @return bool
*/
public static function areServerVarsBad( $vars, $extWhitelist = array() ) {
@@ -92,8 +92,8 @@ class IEUrlExtension {
* Given a right-hand portion of a URL, determine whether IE would detect
* a potentially harmful file extension.
*
- * @param $urlPart string The right-hand portion of a URL
- * @param $extWhitelist array An array of file extensions which may occur in this
+ * @param string $urlPart The right-hand portion of a URL
+ * @param array $extWhitelist An array of file extensions which may occur in this
* URL, and which should be allowed.
* @return bool
*/
@@ -187,7 +187,7 @@ class IEUrlExtension {
* - if we find a possible extension followed by a dot or another illegal
* character, we ignore it and continue searching
*
- * @param $url string URL
+ * @param string $url URL
* @return mixed Detected extension (string), or false if none found
*/
public static function findIE6Extension( $url ) {
@@ -232,7 +232,7 @@ class IEUrlExtension {
}
// We found an illegal character or another dot
// Skip to that character and continue the loop
- $pos = $nextPos + 1;
+ $pos = $nextPos;
$remainingLength = $urlLength - $pos;
}
return false;
@@ -245,7 +245,7 @@ class IEUrlExtension {
* whether the script filename has been obscured.
*
* The function returns false if the server is not known to have this
- * behaviour. Microsoft IIS in particular is known to decode escaped script
+ * behavior. Microsoft IIS in particular is known to decode escaped script
* filenames.
*
* SERVER_SOFTWARE typically contains either a plain string such as "Zeus",
diff --git a/includes/libs/JavaScriptMinifier.php b/includes/libs/JavaScriptMinifier.php
index 0b4be9ae..998805ae 100644
--- a/includes/libs/JavaScriptMinifier.php
+++ b/includes/libs/JavaScriptMinifier.php
@@ -59,7 +59,7 @@ class JavaScriptMinifier {
const TYPE_DO = 15; // keywords: case, var, finally, else, do, try
const TYPE_FUNC = 16; // keywords: function
const TYPE_LITERAL = 17; // all literals, identifiers and unrecognised tokens
-
+
// Sanity limit to avoid excessive memory usage
const STACK_LIMIT = 1000;
@@ -72,9 +72,9 @@ class JavaScriptMinifier {
* literals (e.g. quoted strings) longer than $maxLineLength are encountered
* or when required to guard against semicolon insertion.
*
- * @param $s String JavaScript code to minify
- * @param $statementsOnOwnLine Bool Whether to put each statement on its own line
- * @param $maxLineLength Int Maximum length of a single line, or -1 for no maximum.
+ * @param string $s JavaScript code to minify
+ * @param bool $statementsOnOwnLine Whether to put each statement on its own line
+ * @param int $maxLineLength Maximum length of a single line, or -1 for no maximum.
* @return String Minified code
*/
public static function minify( $s, $statementsOnOwnLine = false, $maxLineLength = 1000 ) {
@@ -385,7 +385,7 @@ class JavaScriptMinifier {
self::TYPE_LITERAL => true
)
);
-
+
// Rules for when newlines should be inserted if
// $statementsOnOwnLine is enabled.
// $newlineBefore is checked before switching state,
@@ -514,7 +514,7 @@ class JavaScriptMinifier {
return self::parseError($s, $end, 'Number with several E' );
}
$end++;
-
+
// + sign is optional; - sign is required.
$end += strspn( $s, '-+', $end );
$len = strspn( $s, '0123456789', $end );
@@ -564,13 +564,13 @@ class JavaScriptMinifier {
$out .= ' ';
$lineLength++;
}
-
+
$out .= $token;
$lineLength += $end - $pos; // += strlen( $token )
$last = $s[$end - 1];
$pos = $end;
$newlineFound = false;
-
+
// Output a newline after the token if required
// This is checked before AND after switching state
$newlineAdded = false;
@@ -589,7 +589,7 @@ class JavaScriptMinifier {
} elseif( isset( $goto[$state][$type] ) ) {
$state = $goto[$state][$type];
}
-
+
// Check for newline insertion again
if ( $statementsOnOwnLine && !$newlineAdded && isset( $newlineAfter[$state][$type] ) ) {
$out .= "\n";
@@ -598,7 +598,7 @@ class JavaScriptMinifier {
}
return $out;
}
-
+
static function parseError($fullJavascript, $position, $errorMsg) {
// TODO: Handle the error: trigger_error, throw exception, return false...
return false;
diff --git a/includes/libs/jsminplus.php b/includes/libs/jsminplus.php
index 7c4e32bd..f250217f 100644
--- a/includes/libs/jsminplus.php
+++ b/includes/libs/jsminplus.php
@@ -256,7 +256,7 @@ class JSMinPlus
}
elseif ($type == KEYWORD_VAR && $type == $lastType)
{
- // mutiple var-statements can go into one
+ // multiple var-statements can go into one
$t = ',' . substr($t, 4);
}
else
@@ -298,7 +298,7 @@ class JSMinPlus
if ($elsePart)
{
- // be carefull and always make a block out of the thenPart; could be more optimized but is a lot of trouble
+ // be careful and always make a block out of the thenPart; could be more optimized but is a lot of trouble
if ($thenPart != ';' && $thenPart[0] != '{')
$thenPart = '{' . $thenPart . '}';
@@ -521,7 +521,7 @@ class JSMinPlus
break;
case TOKEN_STRING:
- //combine concatted strings with same quotestyle
+ //combine concatenated strings with same quote style
if ($n->type == OP_PLUS && substr($left, -1) == $right[0])
{
$s = substr($left, 0, -1) . substr($right, 1);
diff --git a/includes/libs/lessc.inc.php b/includes/libs/lessc.inc.php
new file mode 100644
index 00000000..3dce961e
--- /dev/null
+++ b/includes/libs/lessc.inc.php
@@ -0,0 +1,3742 @@
+<?php
+
+/**
+ * lessphp v0.4.0@261f1bd28f
+ * http://leafo.net/lessphp
+ *
+ * LESS CSS compiler, adapted from http://lesscss.org
+ *
+ * For ease of distribution, lessphp 0.4.0 is under a dual license.
+ * You are free to pick which one suits your needs.
+ *
+ * MIT LICENSE
+ *
+ * Copyright 2013, Leaf Corcoran <leafot@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * GPL VERSION 3
+ *
+ * Please refer to http://www.gnu.org/licenses/gpl-3.0.html for the full
+ * text of the GPL version 3
+ */
+
+
+/**
+ * The LESS compiler and parser.
+ *
+ * Converting LESS to CSS is a three stage process. The incoming file is parsed
+ * by `lessc_parser` into a syntax tree, then it is compiled into another tree
+ * representing the CSS structure by `lessc`. The CSS tree is fed into a
+ * formatter, like `lessc_formatter` which then outputs CSS as a string.
+ *
+ * During the first compile, all values are *reduced*, which means that their
+ * types are brought to the lowest form before being dump as strings. This
+ * handles math equations, variable dereferences, and the like.
+ *
+ * The `parse` function of `lessc` is the entry point.
+ *
+ * In summary:
+ *
+ * The `lessc` class creates an instance of the parser, feeds it LESS code,
+ * then transforms the resulting tree to a CSS tree. This class also holds the
+ * evaluation context, such as all available mixins and variables at any given
+ * time.
+ *
+ * The `lessc_parser` class is only concerned with parsing its input.
+ *
+ * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
+ * handling things like indentation.
+ */
+class lessc {
+ static public $VERSION = "v0.4.0";
+
+ static public $TRUE = array("keyword", "true");
+ static public $FALSE = array("keyword", "false");
+
+ protected $libFunctions = array();
+ protected $registeredVars = array();
+ protected $preserveComments = false;
+
+ public $vPrefix = '@'; // prefix of abstract properties
+ public $mPrefix = '$'; // prefix of abstract blocks
+ public $parentSelector = '&';
+
+ public $importDisabled = false;
+ public $importDir = '';
+
+ protected $numberPrecision = null;
+
+ protected $allParsedFiles = array();
+
+ // set to the parser that generated the current line when compiling
+ // so we know how to create error messages
+ protected $sourceParser = null;
+ protected $sourceLoc = null;
+
+ static protected $nextImportId = 0; // uniquely identify imports
+
+ // attempts to find the path of an import url, returns null for css files
+ protected function findImport($url) {
+ foreach ((array)$this->importDir as $dir) {
+ $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
+ if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
+ return $file;
+ }
+ }
+
+ return null;
+ }
+
+ protected function fileExists($name) {
+ return is_file($name);
+ }
+
+ static public function compressList($items, $delim) {
+ if (!isset($items[1]) && isset($items[0])) return $items[0];
+ else return array('list', $delim, $items);
+ }
+
+ static public function preg_quote($what) {
+ return preg_quote($what, '/');
+ }
+
+ protected function tryImport($importPath, $parentBlock, $out) {
+ if ($importPath[0] == "function" && $importPath[1] == "url") {
+ $importPath = $this->flattenList($importPath[2]);
+ }
+
+ $str = $this->coerceString($importPath);
+ if ($str === null) return false;
+
+ $url = $this->compileValue($this->lib_e($str));
+
+ // don't import if it ends in css
+ if (substr_compare($url, '.css', -4, 4) === 0) return false;
+
+ $realPath = $this->findImport($url);
+
+ if ($realPath === null) return false;
+
+ if ($this->importDisabled) {
+ return array(false, "/* import disabled */");
+ }
+
+ if (isset($this->allParsedFiles[realpath($realPath)])) {
+ return array(false, null);
+ }
+
+ $this->addParsedFile($realPath);
+ $parser = $this->makeParser($realPath);
+ $root = $parser->parse(file_get_contents($realPath));
+
+ // set the parents of all the block props
+ foreach ($root->props as $prop) {
+ if ($prop[0] == "block") {
+ $prop[1]->parent = $parentBlock;
+ }
+ }
+
+ // copy mixins into scope, set their parents
+ // bring blocks from import into current block
+ // TODO: need to mark the source parser these came from this file
+ foreach ($root->children as $childName => $child) {
+ if (isset($parentBlock->children[$childName])) {
+ $parentBlock->children[$childName] = array_merge(
+ $parentBlock->children[$childName],
+ $child);
+ } else {
+ $parentBlock->children[$childName] = $child;
+ }
+ }
+
+ $pi = pathinfo($realPath);
+ $dir = $pi["dirname"];
+
+ list($top, $bottom) = $this->sortProps($root->props, true);
+ $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
+
+ return array(true, $bottom, $parser, $dir);
+ }
+
+ protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
+ $oldSourceParser = $this->sourceParser;
+
+ $oldImport = $this->importDir;
+
+ // TODO: this is because the importDir api is stupid
+ $this->importDir = (array)$this->importDir;
+ array_unshift($this->importDir, $importDir);
+
+ foreach ($props as $prop) {
+ $this->compileProp($prop, $block, $out);
+ }
+
+ $this->importDir = $oldImport;
+ $this->sourceParser = $oldSourceParser;
+ }
+
+ /**
+ * Recursively compiles a block.
+ *
+ * A block is analogous to a CSS block in most cases. A single LESS document
+ * is encapsulated in a block when parsed, but it does not have parent tags
+ * so all of it's children appear on the root level when compiled.
+ *
+ * Blocks are made up of props and children.
+ *
+ * Props are property instructions, array tuples which describe an action
+ * to be taken, eg. write a property, set a variable, mixin a block.
+ *
+ * The children of a block are just all the blocks that are defined within.
+ * This is used to look up mixins when performing a mixin.
+ *
+ * Compiling the block involves pushing a fresh environment on the stack,
+ * and iterating through the props, compiling each one.
+ *
+ * See lessc::compileProp()
+ *
+ */
+ protected function compileBlock($block) {
+ switch ($block->type) {
+ case "root":
+ $this->compileRoot($block);
+ break;
+ case null:
+ $this->compileCSSBlock($block);
+ break;
+ case "media":
+ $this->compileMedia($block);
+ break;
+ case "directive":
+ $name = "@" . $block->name;
+ if (!empty($block->value)) {
+ $name .= " " . $this->compileValue($this->reduce($block->value));
+ }
+
+ $this->compileNestedBlock($block, array($name));
+ break;
+ default:
+ $this->throwError("unknown block type: $block->type\n");
+ }
+ }
+
+ protected function compileCSSBlock($block) {
+ $env = $this->pushEnv();
+
+ $selectors = $this->compileSelectors($block->tags);
+ $env->selectors = $this->multiplySelectors($selectors);
+ $out = $this->makeOutputBlock(null, $env->selectors);
+
+ $this->scope->children[] = $out;
+ $this->compileProps($block, $out);
+
+ $block->scope = $env; // mixins carry scope with them!
+ $this->popEnv();
+ }
+
+ protected function compileMedia($media) {
+ $env = $this->pushEnv($media);
+ $parentScope = $this->mediaParent($this->scope);
+
+ $query = $this->compileMediaQuery($this->multiplyMedia($env));
+
+ $this->scope = $this->makeOutputBlock($media->type, array($query));
+ $parentScope->children[] = $this->scope;
+
+ $this->compileProps($media, $this->scope);
+
+ if (count($this->scope->lines) > 0) {
+ $orphanSelelectors = $this->findClosestSelectors();
+ if (!is_null($orphanSelelectors)) {
+ $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
+ $orphan->lines = $this->scope->lines;
+ array_unshift($this->scope->children, $orphan);
+ $this->scope->lines = array();
+ }
+ }
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function mediaParent($scope) {
+ while (!empty($scope->parent)) {
+ if (!empty($scope->type) && $scope->type != "media") {
+ break;
+ }
+ $scope = $scope->parent;
+ }
+
+ return $scope;
+ }
+
+ protected function compileNestedBlock($block, $selectors) {
+ $this->pushEnv($block);
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
+ $this->scope->parent->children[] = $this->scope;
+
+ $this->compileProps($block, $this->scope);
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function compileRoot($root) {
+ $this->pushEnv();
+ $this->scope = $this->makeOutputBlock($root->type);
+ $this->compileProps($root, $this->scope);
+ $this->popEnv();
+ }
+
+ protected function compileProps($block, $out) {
+ foreach ($this->sortProps($block->props) as $prop) {
+ $this->compileProp($prop, $block, $out);
+ }
+ $out->lines = $this->deduplicate($out->lines);
+ }
+
+ /**
+ * Deduplicate lines in a block. Comments are not deduplicated. If a
+ * duplicate rule is detected, the comments immediately preceding each
+ * occurence are consolidated.
+ */
+ protected function deduplicate($lines) {
+ $unique = array();
+ $comments = array();
+
+ foreach($lines as $line) {
+ if (strpos($line, '/*') === 0) {
+ $comments[] = $line;
+ continue;
+ }
+ if (!in_array($line, $unique)) {
+ $unique[] = $line;
+ }
+ array_splice($unique, array_search($line, $unique), 0, $comments);
+ $comments = array();
+ }
+ return array_merge($unique, $comments);
+ }
+
+ protected function sortProps($props, $split = false) {
+ $vars = array();
+ $imports = array();
+ $other = array();
+ $stack = array();
+
+ foreach ($props as $prop) {
+ switch ($prop[0]) {
+ case "comment":
+ $stack[] = $prop;
+ break;
+ case "assign":
+ $stack[] = $prop;
+ if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
+ $vars = array_merge($vars, $stack);
+ } else {
+ $other = array_merge($other, $stack);
+ }
+ $stack = array();
+ break;
+ case "import":
+ $id = self::$nextImportId++;
+ $prop[] = $id;
+ $stack[] = $prop;
+ $imports = array_merge($imports, $stack);
+ $other[] = array("import_mixin", $id);
+ $stack = array();
+ break;
+ default:
+ $stack[] = $prop;
+ $other = array_merge($other, $stack);
+ $stack = array();
+ break;
+ }
+ }
+ $other = array_merge($other, $stack);
+
+ if ($split) {
+ return array(array_merge($vars, $imports), $other);
+ } else {
+ return array_merge($vars, $imports, $other);
+ }
+ }
+
+ protected function compileMediaQuery($queries) {
+ $compiledQueries = array();
+ foreach ($queries as $query) {
+ $parts = array();
+ foreach ($query as $q) {
+ switch ($q[0]) {
+ case "mediaType":
+ $parts[] = implode(" ", array_slice($q, 1));
+ break;
+ case "mediaExp":
+ if (isset($q[2])) {
+ $parts[] = "($q[1]: " .
+ $this->compileValue($this->reduce($q[2])) . ")";
+ } else {
+ $parts[] = "($q[1])";
+ }
+ break;
+ case "variable":
+ $parts[] = $this->compileValue($this->reduce($q));
+ break;
+ }
+ }
+
+ if (count($parts) > 0) {
+ $compiledQueries[] = implode(" and ", $parts);
+ }
+ }
+
+ $out = "@media";
+ if (!empty($parts)) {
+ $out .= " " .
+ implode($this->formatter->selectorSeparator, $compiledQueries);
+ }
+ return $out;
+ }
+
+ protected function multiplyMedia($env, $childQueries = null) {
+ if (is_null($env) ||
+ !empty($env->block->type) && $env->block->type != "media")
+ {
+ return $childQueries;
+ }
+
+ // plain old block, skip
+ if (empty($env->block->type)) {
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ $out = array();
+ $queries = $env->block->queries;
+ if (is_null($childQueries)) {
+ $out = $queries;
+ } else {
+ foreach ($queries as $parent) {
+ foreach ($childQueries as $child) {
+ $out[] = array_merge($parent, $child);
+ }
+ }
+ }
+
+ return $this->multiplyMedia($env->parent, $out);
+ }
+
+ protected function expandParentSelectors(&$tag, $replace) {
+ $parts = explode("$&$", $tag);
+ $count = 0;
+ foreach ($parts as &$part) {
+ $part = str_replace($this->parentSelector, $replace, $part, $c);
+ $count += $c;
+ }
+ $tag = implode($this->parentSelector, $parts);
+ return $count;
+ }
+
+ protected function findClosestSelectors() {
+ $env = $this->env;
+ $selectors = null;
+ while ($env !== null) {
+ if (isset($env->selectors)) {
+ $selectors = $env->selectors;
+ break;
+ }
+ $env = $env->parent;
+ }
+
+ return $selectors;
+ }
+
+
+ // multiply $selectors against the nearest selectors in env
+ protected function multiplySelectors($selectors) {
+ // find parent selectors
+
+ $parentSelectors = $this->findClosestSelectors();
+ if (is_null($parentSelectors)) {
+ // kill parent reference in top level selector
+ foreach ($selectors as &$s) {
+ $this->expandParentSelectors($s, "");
+ }
+
+ return $selectors;
+ }
+
+ $out = array();
+ foreach ($parentSelectors as $parent) {
+ foreach ($selectors as $child) {
+ $count = $this->expandParentSelectors($child, $parent);
+
+ // don't prepend the parent tag if & was used
+ if ($count > 0) {
+ $out[] = trim($child);
+ } else {
+ $out[] = trim($parent . ' ' . $child);
+ }
+ }
+ }
+
+ return $out;
+ }
+
+ // reduces selector expressions
+ protected function compileSelectors($selectors) {
+ $out = array();
+
+ foreach ($selectors as $s) {
+ if (is_array($s)) {
+ list(, $value) = $s;
+ $out[] = trim($this->compileValue($this->reduce($value)));
+ } else {
+ $out[] = $s;
+ }
+ }
+
+ return $out;
+ }
+
+ protected function eq($left, $right) {
+ return $left == $right;
+ }
+
+ protected function patternMatch($block, $orderedArgs, $keywordArgs) {
+ // match the guards if it has them
+ // any one of the groups must have all its guards pass for a match
+ if (!empty($block->guards)) {
+ $groupPassed = false;
+ foreach ($block->guards as $guardGroup) {
+ foreach ($guardGroup as $guard) {
+ $this->pushEnv();
+ $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
+
+ $negate = false;
+ if ($guard[0] == "negate") {
+ $guard = $guard[1];
+ $negate = true;
+ }
+
+ $passed = $this->reduce($guard) == self::$TRUE;
+ if ($negate) $passed = !$passed;
+
+ $this->popEnv();
+
+ if ($passed) {
+ $groupPassed = true;
+ } else {
+ $groupPassed = false;
+ break;
+ }
+ }
+
+ if ($groupPassed) break;
+ }
+
+ if (!$groupPassed) {
+ return false;
+ }
+ }
+
+ if (empty($block->args)) {
+ return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
+ }
+
+ $remainingArgs = $block->args;
+ if ($keywordArgs) {
+ $remainingArgs = array();
+ foreach ($block->args as $arg) {
+ if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
+ continue;
+ }
+
+ $remainingArgs[] = $arg;
+ }
+ }
+
+ $i = -1; // no args
+ // try to match by arity or by argument literal
+ foreach ($remainingArgs as $i => $arg) {
+ switch ($arg[0]) {
+ case "lit":
+ if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
+ return false;
+ }
+ break;
+ case "arg":
+ // no arg and no default value
+ if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
+ return false;
+ }
+ break;
+ case "rest":
+ $i--; // rest can be empty
+ break 2;
+ }
+ }
+
+ if ($block->isVararg) {
+ return true; // not having enough is handled above
+ } else {
+ $numMatched = $i + 1;
+ // greater than becuase default values always match
+ return $numMatched >= count($orderedArgs);
+ }
+ }
+
+ protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
+ $matches = null;
+ foreach ($blocks as $block) {
+ // skip seen blocks that don't have arguments
+ if (isset($skip[$block->id]) && !isset($block->args)) {
+ continue;
+ }
+
+ if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
+ $matches[] = $block;
+ }
+ }
+
+ return $matches;
+ }
+
+ // attempt to find blocks matched by path and args
+ protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
+ if ($searchIn == null) return null;
+ if (isset($seen[$searchIn->id])) return null;
+ $seen[$searchIn->id] = true;
+
+ $name = $path[0];
+
+ if (isset($searchIn->children[$name])) {
+ $blocks = $searchIn->children[$name];
+ if (count($path) == 1) {
+ $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
+ if (!empty($matches)) {
+ // This will return all blocks that match in the closest
+ // scope that has any matching block, like lessjs
+ return $matches;
+ }
+ } else {
+ $matches = array();
+ foreach ($blocks as $subBlock) {
+ $subMatches = $this->findBlocks($subBlock,
+ array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
+
+ if (!is_null($subMatches)) {
+ foreach ($subMatches as $sm) {
+ $matches[] = $sm;
+ }
+ }
+ }
+
+ return count($matches) > 0 ? $matches : null;
+ }
+ }
+ if ($searchIn->parent === $searchIn) return null;
+ return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
+ }
+
+ // sets all argument names in $args to either the default value
+ // or the one passed in through $values
+ protected function zipSetArgs($args, $orderedValues, $keywordValues) {
+ $assignedValues = array();
+
+ $i = 0;
+ foreach ($args as $a) {
+ if ($a[0] == "arg") {
+ if (isset($keywordValues[$a[1]])) {
+ // has keyword arg
+ $value = $keywordValues[$a[1]];
+ } elseif (isset($orderedValues[$i])) {
+ // has ordered arg
+ $value = $orderedValues[$i];
+ $i++;
+ } elseif (isset($a[2])) {
+ // has default value
+ $value = $a[2];
+ } else {
+ $this->throwError("Failed to assign arg " . $a[1]);
+ $value = null; // :(
+ }
+
+ $value = $this->reduce($value);
+ $this->set($a[1], $value);
+ $assignedValues[] = $value;
+ } else {
+ // a lit
+ $i++;
+ }
+ }
+
+ // check for a rest
+ $last = end($args);
+ if ($last[0] == "rest") {
+ $rest = array_slice($orderedValues, count($args) - 1);
+ $this->set($last[1], $this->reduce(array("list", " ", $rest)));
+ }
+
+ // wow is this the only true use of PHP's + operator for arrays?
+ $this->env->arguments = $assignedValues + $orderedValues;
+ }
+
+ // compile a prop and update $lines or $blocks appropriately
+ protected function compileProp($prop, $block, $out) {
+ // set error position context
+ $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
+
+ switch ($prop[0]) {
+ case 'assign':
+ list(, $name, $value) = $prop;
+ if ($name[0] == $this->vPrefix) {
+ $this->set($name, $value);
+ } else {
+ $out->lines[] = $this->formatter->property($name,
+ $this->compileValue($this->reduce($value)));
+ }
+ break;
+ case 'block':
+ list(, $child) = $prop;
+ $this->compileBlock($child);
+ break;
+ case 'mixin':
+ list(, $path, $args, $suffix) = $prop;
+
+ $orderedArgs = array();
+ $keywordArgs = array();
+ foreach ((array)$args as $arg) {
+ $argval = null;
+ switch ($arg[0]) {
+ case "arg":
+ if (!isset($arg[2])) {
+ $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
+ } else {
+ $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
+ }
+ break;
+
+ case "lit":
+ $orderedArgs[] = $this->reduce($arg[1]);
+ break;
+ default:
+ $this->throwError("Unknown arg type: " . $arg[0]);
+ }
+ }
+
+ $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
+
+ if ($mixins === null) {
+ $this->throwError("{$prop[1][0]} is undefined");
+ }
+
+ foreach ($mixins as $mixin) {
+ if ($mixin === $block && !$orderedArgs) {
+ continue;
+ }
+
+ $haveScope = false;
+ if (isset($mixin->parent->scope)) {
+ $haveScope = true;
+ $mixinParentEnv = $this->pushEnv();
+ $mixinParentEnv->storeParent = $mixin->parent->scope;
+ }
+
+ $haveArgs = false;
+ if (isset($mixin->args)) {
+ $haveArgs = true;
+ $this->pushEnv();
+ $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
+ }
+
+ $oldParent = $mixin->parent;
+ if ($mixin != $block) $mixin->parent = $block;
+
+ foreach ($this->sortProps($mixin->props) as $subProp) {
+ if ($suffix !== null &&
+ $subProp[0] == "assign" &&
+ is_string($subProp[1]) &&
+ $subProp[1]{0} != $this->vPrefix)
+ {
+ $subProp[2] = array(
+ 'list', ' ',
+ array($subProp[2], array('keyword', $suffix))
+ );
+ }
+
+ $this->compileProp($subProp, $mixin, $out);
+ }
+
+ $mixin->parent = $oldParent;
+
+ if ($haveArgs) $this->popEnv();
+ if ($haveScope) $this->popEnv();
+ }
+
+ break;
+ case 'raw':
+ $out->lines[] = $prop[1];
+ break;
+ case "directive":
+ list(, $name, $value) = $prop;
+ $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
+ break;
+ case "comment":
+ $out->lines[] = $prop[1];
+ break;
+ case "import";
+ list(, $importPath, $importId) = $prop;
+ $importPath = $this->reduce($importPath);
+
+ if (!isset($this->env->imports)) {
+ $this->env->imports = array();
+ }
+
+ $result = $this->tryImport($importPath, $block, $out);
+
+ $this->env->imports[$importId] = $result === false ?
+ array(false, "@import " . $this->compileValue($importPath).";") :
+ $result;
+
+ break;
+ case "import_mixin":
+ list(,$importId) = $prop;
+ $import = $this->env->imports[$importId];
+ if ($import[0] === false) {
+ if (isset($import[1])) {
+ $out->lines[] = $import[1];
+ }
+ } else {
+ list(, $bottom, $parser, $importDir) = $import;
+ $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
+ }
+
+ break;
+ default:
+ $this->throwError("unknown op: {$prop[0]}\n");
+ }
+ }
+
+
+ /**
+ * Compiles a primitive value into a CSS property value.
+ *
+ * Values in lessphp are typed by being wrapped in arrays, their format is
+ * typically:
+ *
+ * array(type, contents [, additional_contents]*)
+ *
+ * The input is expected to be reduced. This function will not work on
+ * things like expressions and variables.
+ */
+ protected function compileValue($value) {
+ switch ($value[0]) {
+ case 'list':
+ // [1] - delimiter
+ // [2] - array of values
+ return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
+ case 'raw_color':
+ if (!empty($this->formatter->compressColors)) {
+ return $this->compileValue($this->coerceColor($value));
+ }
+ return $value[1];
+ case 'keyword':
+ // [1] - the keyword
+ return $value[1];
+ case 'number':
+ list(, $num, $unit) = $value;
+ // [1] - the number
+ // [2] - the unit
+ if ($this->numberPrecision !== null) {
+ $num = round($num, $this->numberPrecision);
+ }
+ return $num . $unit;
+ case 'string':
+ // [1] - contents of string (includes quotes)
+ list(, $delim, $content) = $value;
+ foreach ($content as &$part) {
+ if (is_array($part)) {
+ $part = $this->compileValue($part);
+ }
+ }
+ return $delim . implode($content) . $delim;
+ case 'color':
+ // [1] - red component (either number or a %)
+ // [2] - green component
+ // [3] - blue component
+ // [4] - optional alpha component
+ list(, $r, $g, $b) = $value;
+ $r = round($r);
+ $g = round($g);
+ $b = round($b);
+
+ if (count($value) == 5 && $value[4] != 1) { // rgba
+ return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
+ }
+
+ $h = sprintf("#%02x%02x%02x", $r, $g, $b);
+
+ if (!empty($this->formatter->compressColors)) {
+ // Converting hex color to short notation (e.g. #003399 to #039)
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
+ $h = '#' . $h[1] . $h[3] . $h[5];
+ }
+ }
+
+ return $h;
+
+ case 'function':
+ list(, $name, $args) = $value;
+ return $name.'('.$this->compileValue($args).')';
+ default: // assumed to be unit
+ $this->throwError("unknown value type: $value[0]");
+ }
+ }
+
+ protected function lib_pow($args) {
+ list($base, $exp) = $this->assertArgs($args, 2, "pow");
+ return pow($this->assertNumber($base), $this->assertNumber($exp));
+ }
+
+ protected function lib_pi() {
+ return pi();
+ }
+
+ protected function lib_mod($args) {
+ list($a, $b) = $this->assertArgs($args, 2, "mod");
+ return $this->assertNumber($a) % $this->assertNumber($b);
+ }
+
+ protected function lib_tan($num) {
+ return tan($this->assertNumber($num));
+ }
+
+ protected function lib_sin($num) {
+ return sin($this->assertNumber($num));
+ }
+
+ protected function lib_cos($num) {
+ return cos($this->assertNumber($num));
+ }
+
+ protected function lib_atan($num) {
+ $num = atan($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_asin($num) {
+ $num = asin($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_acos($num) {
+ $num = acos($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_sqrt($num) {
+ return sqrt($this->assertNumber($num));
+ }
+
+ protected function lib_extract($value) {
+ list($list, $idx) = $this->assertArgs($value, 2, "extract");
+ $idx = $this->assertNumber($idx);
+ // 1 indexed
+ if ($list[0] == "list" && isset($list[2][$idx - 1])) {
+ return $list[2][$idx - 1];
+ }
+ }
+
+ protected function lib_isnumber($value) {
+ return $this->toBool($value[0] == "number");
+ }
+
+ protected function lib_isstring($value) {
+ return $this->toBool($value[0] == "string");
+ }
+
+ protected function lib_iscolor($value) {
+ return $this->toBool($this->coerceColor($value));
+ }
+
+ protected function lib_iskeyword($value) {
+ return $this->toBool($value[0] == "keyword");
+ }
+
+ protected function lib_ispixel($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "px");
+ }
+
+ protected function lib_ispercentage($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "%");
+ }
+
+ protected function lib_isem($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "em");
+ }
+
+ protected function lib_isrem($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "rem");
+ }
+
+ protected function lib_rgbahex($color) {
+ $color = $this->coerceColor($color);
+ if (is_null($color))
+ $this->throwError("color expected for rgbahex");
+
+ return sprintf("#%02x%02x%02x%02x",
+ isset($color[4]) ? $color[4]*255 : 255,
+ $color[1],$color[2], $color[3]);
+ }
+
+ protected function lib_argb($color){
+ return $this->lib_rgbahex($color);
+ }
+
+ // utility func to unquote a string
+ protected function lib_e($arg) {
+ switch ($arg[0]) {
+ case "list":
+ $items = $arg[2];
+ if (isset($items[0])) {
+ return $this->lib_e($items[0]);
+ }
+ $this->throwError("unrecognised input");
+ case "string":
+ $arg[1] = "";
+ return $arg;
+ case "keyword":
+ return $arg;
+ default:
+ return array("keyword", $this->compileValue($arg));
+ }
+ }
+
+ protected function lib__sprintf($args) {
+ if ($args[0] != "list") return $args;
+ $values = $args[2];
+ $string = array_shift($values);
+ $template = $this->compileValue($this->lib_e($string));
+
+ $i = 0;
+ if (preg_match_all('/%[dsa]/', $template, $m)) {
+ foreach ($m[0] as $match) {
+ $val = isset($values[$i]) ?
+ $this->reduce($values[$i]) : array('keyword', '');
+
+ // lessjs compat, renders fully expanded color, not raw color
+ if ($color = $this->coerceColor($val)) {
+ $val = $color;
+ }
+
+ $i++;
+ $rep = $this->compileValue($this->lib_e($val));
+ $template = preg_replace('/'.self::preg_quote($match).'/',
+ $rep, $template, 1);
+ }
+ }
+
+ $d = $string[0] == "string" ? $string[1] : '"';
+ return array("string", $d, array($template));
+ }
+
+ protected function lib_floor($arg) {
+ $value = $this->assertNumber($arg);
+ return array("number", floor($value), $arg[2]);
+ }
+
+ protected function lib_ceil($arg) {
+ $value = $this->assertNumber($arg);
+ return array("number", ceil($value), $arg[2]);
+ }
+
+ protected function lib_round($arg) {
+ if($arg[0] != "list") {
+ $value = $this->assertNumber($arg);
+ return array("number", round($value), $arg[2]);
+ } else {
+ $value = $this->assertNumber($arg[2][0]);
+ $precision = $this->assertNumber($arg[2][1]);
+ return array("number", round($value, $precision), $arg[2][0][2]);
+ }
+ }
+
+ protected function lib_unit($arg) {
+ if ($arg[0] == "list") {
+ list($number, $newUnit) = $arg[2];
+ return array("number", $this->assertNumber($number),
+ $this->compileValue($this->lib_e($newUnit)));
+ } else {
+ return array("number", $this->assertNumber($arg), "");
+ }
+ }
+
+ /**
+ * Helper function to get arguments for color manipulation functions.
+ * takes a list that contains a color like thing and a percentage
+ */
+ public function colorArgs($args) {
+ if ($args[0] != 'list' || count($args[2]) < 2) {
+ return array(array('color', 0, 0, 0), 0);
+ }
+ list($color, $delta) = $args[2];
+ $color = $this->assertColor($color);
+ $delta = floatval($delta[1]);
+
+ return array($color, $delta);
+ }
+
+ protected function lib_darken($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_lighten($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_saturate($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_desaturate($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_spin($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+
+ $hsl[1] = $hsl[1] + $delta % 360;
+ if ($hsl[1] < 0) $hsl[1] += 360;
+
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_fadeout($args) {
+ list($color, $delta) = $this->colorArgs($args);
+ $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
+ return $color;
+ }
+
+ protected function lib_fadein($args) {
+ list($color, $delta) = $this->colorArgs($args);
+ $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
+ return $color;
+ }
+
+ protected function lib_hue($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[1]);
+ }
+
+ protected function lib_saturation($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[2]);
+ }
+
+ protected function lib_lightness($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[3]);
+ }
+
+ // get the alpha of a color
+ // defaults to 1 for non-colors or colors without an alpha
+ protected function lib_alpha($value) {
+ if (!is_null($color = $this->coerceColor($value))) {
+ return isset($color[4]) ? $color[4] : 1;
+ }
+ }
+
+ // set the alpha of the color
+ protected function lib_fade($args) {
+ list($color, $alpha) = $this->colorArgs($args);
+ $color[4] = $this->clamp($alpha / 100.0);
+ return $color;
+ }
+
+ protected function lib_percentage($arg) {
+ $num = $this->assertNumber($arg);
+ return array("number", $num*100, "%");
+ }
+
+ // mixes two colors by weight
+ // mix(@color1, @color2, [@weight: 50%]);
+ // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
+ protected function lib_mix($args) {
+ if ($args[0] != "list" || count($args[2]) < 2)
+ $this->throwError("mix expects (color1, color2, weight)");
+
+ list($first, $second) = $args[2];
+ $first = $this->assertColor($first);
+ $second = $this->assertColor($second);
+
+ $first_a = $this->lib_alpha($first);
+ $second_a = $this->lib_alpha($second);
+
+ if (isset($args[2][2])) {
+ $weight = $args[2][2][1] / 100.0;
+ } else {
+ $weight = 0.5;
+ }
+
+ $w = $weight * 2 - 1;
+ $a = $first_a - $second_a;
+
+ $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
+ $w2 = 1.0 - $w1;
+
+ $new = array('color',
+ $w1 * $first[1] + $w2 * $second[1],
+ $w1 * $first[2] + $w2 * $second[2],
+ $w1 * $first[3] + $w2 * $second[3],
+ );
+
+ if ($first_a != 1.0 || $second_a != 1.0) {
+ $new[] = $first_a * $weight + $second_a * ($weight - 1);
+ }
+
+ return $this->fixColor($new);
+ }
+
+ protected function lib_contrast($args) {
+ if ($args[0] != 'list' || count($args[2]) < 3) {
+ return array(array('color', 0, 0, 0), 0);
+ }
+
+ list($inputColor, $darkColor, $lightColor) = $args[2];
+
+ $inputColor = $this->assertColor($inputColor);
+ $darkColor = $this->assertColor($darkColor);
+ $lightColor = $this->assertColor($lightColor);
+ $hsl = $this->toHSL($inputColor);
+
+ if ($hsl[3] > 50) {
+ return $darkColor;
+ }
+
+ return $lightColor;
+ }
+
+ public function assertColor($value, $error = "expected color value") {
+ $color = $this->coerceColor($value);
+ if (is_null($color)) $this->throwError($error);
+ return $color;
+ }
+
+ public function assertNumber($value, $error = "expecting number") {
+ if ($value[0] == "number") return $value[1];
+ $this->throwError($error);
+ }
+
+ public function assertArgs($value, $expectedArgs, $name="") {
+ if ($expectedArgs == 1) {
+ return $value;
+ } else {
+ if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
+ $values = $value[2];
+ $numValues = count($values);
+ if ($expectedArgs != $numValues) {
+ if ($name) {
+ $name = $name . ": ";
+ }
+
+ $this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
+ }
+
+ return $values;
+ }
+ }
+
+ protected function toHSL($color) {
+ if ($color[0] == 'hsl') return $color;
+
+ $r = $color[1] / 255;
+ $g = $color[2] / 255;
+ $b = $color[3] / 255;
+
+ $min = min($r, $g, $b);
+ $max = max($r, $g, $b);
+
+ $L = ($min + $max) / 2;
+ if ($min == $max) {
+ $S = $H = 0;
+ } else {
+ if ($L < 0.5)
+ $S = ($max - $min)/($max + $min);
+ else
+ $S = ($max - $min)/(2.0 - $max - $min);
+
+ if ($r == $max) $H = ($g - $b)/($max - $min);
+ elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
+ elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
+
+ }
+
+ $out = array('hsl',
+ ($H < 0 ? $H + 6 : $H)*60,
+ $S*100,
+ $L*100,
+ );
+
+ if (count($color) > 4) $out[] = $color[4]; // copy alpha
+ return $out;
+ }
+
+ protected function toRGB_helper($comp, $temp1, $temp2) {
+ if ($comp < 0) $comp += 1.0;
+ elseif ($comp > 1) $comp -= 1.0;
+
+ if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
+ if (2 * $comp < 1) return $temp2;
+ if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
+
+ return $temp1;
+ }
+
+ /**
+ * Converts a hsl array into a color value in rgb.
+ * Expects H to be in range of 0 to 360, S and L in 0 to 100
+ */
+ protected function toRGB($color) {
+ if ($color[0] == 'color') return $color;
+
+ $H = $color[1] / 360;
+ $S = $color[2] / 100;
+ $L = $color[3] / 100;
+
+ if ($S == 0) {
+ $r = $g = $b = $L;
+ } else {
+ $temp2 = $L < 0.5 ?
+ $L*(1.0 + $S) :
+ $L + $S - $L * $S;
+
+ $temp1 = 2.0 * $L - $temp2;
+
+ $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
+ $g = $this->toRGB_helper($H, $temp1, $temp2);
+ $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
+ }
+
+ // $out = array('color', round($r*255), round($g*255), round($b*255));
+ $out = array('color', $r*255, $g*255, $b*255);
+ if (count($color) > 4) $out[] = $color[4]; // copy alpha
+ return $out;
+ }
+
+ protected function clamp($v, $max = 1, $min = 0) {
+ return min($max, max($min, $v));
+ }
+
+ /**
+ * Convert the rgb, rgba, hsl color literals of function type
+ * as returned by the parser into values of color type.
+ */
+ protected function funcToColor($func) {
+ $fname = $func[1];
+ if ($func[2][0] != 'list') return false; // need a list of arguments
+ $rawComponents = $func[2][2];
+
+ if ($fname == 'hsl' || $fname == 'hsla') {
+ $hsl = array('hsl');
+ $i = 0;
+ foreach ($rawComponents as $c) {
+ $val = $this->reduce($c);
+ $val = isset($val[1]) ? floatval($val[1]) : 0;
+
+ if ($i == 0) $clamp = 360;
+ elseif ($i < 3) $clamp = 100;
+ else $clamp = 1;
+
+ $hsl[] = $this->clamp($val, $clamp);
+ $i++;
+ }
+
+ while (count($hsl) < 4) $hsl[] = 0;
+ return $this->toRGB($hsl);
+
+ } elseif ($fname == 'rgb' || $fname == 'rgba') {
+ $components = array();
+ $i = 1;
+ foreach ($rawComponents as $c) {
+ $c = $this->reduce($c);
+ if ($i < 4) {
+ if ($c[0] == "number" && $c[2] == "%") {
+ $components[] = 255 * ($c[1] / 100);
+ } else {
+ $components[] = floatval($c[1]);
+ }
+ } elseif ($i == 4) {
+ if ($c[0] == "number" && $c[2] == "%") {
+ $components[] = 1.0 * ($c[1] / 100);
+ } else {
+ $components[] = floatval($c[1]);
+ }
+ } else break;
+
+ $i++;
+ }
+ while (count($components) < 3) $components[] = 0;
+ array_unshift($components, 'color');
+ return $this->fixColor($components);
+ }
+
+ return false;
+ }
+
+ protected function reduce($value, $forExpression = false) {
+ switch ($value[0]) {
+ case "interpolate":
+ $reduced = $this->reduce($value[1]);
+ $var = $this->compileValue($reduced);
+ $res = $this->reduce(array("variable", $this->vPrefix . $var));
+
+ if ($res[0] == "raw_color") {
+ $res = $this->coerceColor($res);
+ }
+
+ if (empty($value[2])) $res = $this->lib_e($res);
+
+ return $res;
+ case "variable":
+ $key = $value[1];
+ if (is_array($key)) {
+ $key = $this->reduce($key);
+ $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
+ }
+
+ $seen =& $this->env->seenNames;
+
+ if (!empty($seen[$key])) {
+ $this->throwError("infinite loop detected: $key");
+ }
+
+ $seen[$key] = true;
+ $out = $this->reduce($this->get($key));
+ $seen[$key] = false;
+ return $out;
+ case "list":
+ foreach ($value[2] as &$item) {
+ $item = $this->reduce($item, $forExpression);
+ }
+ return $value;
+ case "expression":
+ return $this->evaluate($value);
+ case "string":
+ foreach ($value[2] as &$part) {
+ if (is_array($part)) {
+ $strip = $part[0] == "variable";
+ $part = $this->reduce($part);
+ if ($strip) $part = $this->lib_e($part);
+ }
+ }
+ return $value;
+ case "escape":
+ list(,$inner) = $value;
+ return $this->lib_e($this->reduce($inner));
+ case "function":
+ $color = $this->funcToColor($value);
+ if ($color) return $color;
+
+ list(, $name, $args) = $value;
+ if ($name == "%") $name = "_sprintf";
+ $f = isset($this->libFunctions[$name]) ?
+ $this->libFunctions[$name] : array($this, 'lib_'.$name);
+
+ if (is_callable($f)) {
+ if ($args[0] == 'list')
+ $args = self::compressList($args[2], $args[1]);
+
+ $ret = call_user_func($f, $this->reduce($args, true), $this);
+
+ if (is_null($ret)) {
+ return array("string", "", array(
+ $name, "(", $args, ")"
+ ));
+ }
+
+ // convert to a typed value if the result is a php primitive
+ if (is_numeric($ret)) $ret = array('number', $ret, "");
+ elseif (!is_array($ret)) $ret = array('keyword', $ret);
+
+ return $ret;
+ }
+
+ // plain function, reduce args
+ $value[2] = $this->reduce($value[2]);
+ return $value;
+ case "unary":
+ list(, $op, $exp) = $value;
+ $exp = $this->reduce($exp);
+
+ if ($exp[0] == "number") {
+ switch ($op) {
+ case "+":
+ return $exp;
+ case "-":
+ $exp[1] *= -1;
+ return $exp;
+ }
+ }
+ return array("string", "", array($op, $exp));
+ }
+
+ if ($forExpression) {
+ switch ($value[0]) {
+ case "keyword":
+ if ($color = $this->coerceColor($value)) {
+ return $color;
+ }
+ break;
+ case "raw_color":
+ return $this->coerceColor($value);
+ }
+ }
+
+ return $value;
+ }
+
+
+ // coerce a value for use in color operation
+ protected function coerceColor($value) {
+ switch($value[0]) {
+ case 'color': return $value;
+ case 'raw_color':
+ $c = array("color", 0, 0, 0);
+ $colorStr = substr($value[1], 1);
+ $num = hexdec($colorStr);
+ $width = strlen($colorStr) == 3 ? 16 : 256;
+
+ for ($i = 3; $i > 0; $i--) { // 3 2 1
+ $t = $num % $width;
+ $num /= $width;
+
+ $c[$i] = $t * (256/$width) + $t * floor(16/$width);
+ }
+
+ return $c;
+ case 'keyword':
+ $name = $value[1];
+ if (isset(self::$cssColors[$name])) {
+ $rgba = explode(',', self::$cssColors[$name]);
+
+ if(isset($rgba[3]))
+ return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
+
+ return array('color', $rgba[0], $rgba[1], $rgba[2]);
+ }
+ return null;
+ }
+ }
+
+ // make something string like into a string
+ protected function coerceString($value) {
+ switch ($value[0]) {
+ case "string":
+ return $value;
+ case "keyword":
+ return array("string", "", array($value[1]));
+ }
+ return null;
+ }
+
+ // turn list of length 1 into value type
+ protected function flattenList($value) {
+ if ($value[0] == "list" && count($value[2]) == 1) {
+ return $this->flattenList($value[2][0]);
+ }
+ return $value;
+ }
+
+ public function toBool($a) {
+ if ($a) return self::$TRUE;
+ else return self::$FALSE;
+ }
+
+ // evaluate an expression
+ protected function evaluate($exp) {
+ list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
+
+ $left = $this->reduce($left, true);
+ $right = $this->reduce($right, true);
+
+ if ($leftColor = $this->coerceColor($left)) {
+ $left = $leftColor;
+ }
+
+ if ($rightColor = $this->coerceColor($right)) {
+ $right = $rightColor;
+ }
+
+ $ltype = $left[0];
+ $rtype = $right[0];
+
+ // operators that work on all types
+ if ($op == "and") {
+ return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
+ }
+
+ if ($op == "=") {
+ return $this->toBool($this->eq($left, $right) );
+ }
+
+ if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
+ return $str;
+ }
+
+ // type based operators
+ $fname = "op_${ltype}_${rtype}";
+ if (is_callable(array($this, $fname))) {
+ $out = $this->$fname($op, $left, $right);
+ if (!is_null($out)) return $out;
+ }
+
+ // make the expression look it did before being parsed
+ $paddedOp = $op;
+ if ($whiteBefore) $paddedOp = " " . $paddedOp;
+ if ($whiteAfter) $paddedOp .= " ";
+
+ return array("string", "", array($left, $paddedOp, $right));
+ }
+
+ protected function stringConcatenate($left, $right) {
+ if ($strLeft = $this->coerceString($left)) {
+ if ($right[0] == "string") {
+ $right[1] = "";
+ }
+ $strLeft[2][] = $right;
+ return $strLeft;
+ }
+
+ if ($strRight = $this->coerceString($right)) {
+ array_unshift($strRight[2], $left);
+ return $strRight;
+ }
+ }
+
+
+ // make sure a color's components don't go out of bounds
+ protected function fixColor($c) {
+ foreach (range(1, 3) as $i) {
+ if ($c[$i] < 0) $c[$i] = 0;
+ if ($c[$i] > 255) $c[$i] = 255;
+ }
+
+ return $c;
+ }
+
+ protected function op_number_color($op, $lft, $rgt) {
+ if ($op == '+' || $op == '*') {
+ return $this->op_color_number($op, $rgt, $lft);
+ }
+ }
+
+ protected function op_color_number($op, $lft, $rgt) {
+ if ($rgt[0] == '%') $rgt[1] /= 100;
+
+ return $this->op_color_color($op, $lft,
+ array_fill(1, count($lft) - 1, $rgt[1]));
+ }
+
+ protected function op_color_color($op, $left, $right) {
+ $out = array('color');
+ $max = count($left) > count($right) ? count($left) : count($right);
+ foreach (range(1, $max - 1) as $i) {
+ $lval = isset($left[$i]) ? $left[$i] : 0;
+ $rval = isset($right[$i]) ? $right[$i] : 0;
+ switch ($op) {
+ case '+':
+ $out[] = $lval + $rval;
+ break;
+ case '-':
+ $out[] = $lval - $rval;
+ break;
+ case '*':
+ $out[] = $lval * $rval;
+ break;
+ case '%':
+ $out[] = $lval % $rval;
+ break;
+ case '/':
+ if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
+ $out[] = $lval / $rval;
+ break;
+ default:
+ $this->throwError('evaluate error: color op number failed on op '.$op);
+ }
+ }
+ return $this->fixColor($out);
+ }
+
+ function lib_red($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for red()');
+ }
+
+ return $color[1];
+ }
+
+ function lib_green($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for green()');
+ }
+
+ return $color[2];
+ }
+
+ function lib_blue($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for blue()');
+ }
+
+ return $color[3];
+ }
+
+
+ // operator on two numbers
+ protected function op_number_number($op, $left, $right) {
+ $unit = empty($left[2]) ? $right[2] : $left[2];
+
+ $value = 0;
+ switch ($op) {
+ case '+':
+ $value = $left[1] + $right[1];
+ break;
+ case '*':
+ $value = $left[1] * $right[1];
+ break;
+ case '-':
+ $value = $left[1] - $right[1];
+ break;
+ case '%':
+ $value = $left[1] % $right[1];
+ break;
+ case '/':
+ if ($right[1] == 0) $this->throwError('parse error: divide by zero');
+ $value = $left[1] / $right[1];
+ break;
+ case '<':
+ return $this->toBool($left[1] < $right[1]);
+ case '>':
+ return $this->toBool($left[1] > $right[1]);
+ case '>=':
+ return $this->toBool($left[1] >= $right[1]);
+ case '=<':
+ return $this->toBool($left[1] <= $right[1]);
+ default:
+ $this->throwError('parse error: unknown number operator: '.$op);
+ }
+
+ return array("number", $value, $unit);
+ }
+
+
+ /* environment functions */
+
+ protected function makeOutputBlock($type, $selectors = null) {
+ $b = new stdclass;
+ $b->lines = array();
+ $b->children = array();
+ $b->selectors = $selectors;
+ $b->type = $type;
+ $b->parent = $this->scope;
+ return $b;
+ }
+
+ // the state of execution
+ protected function pushEnv($block = null) {
+ $e = new stdclass;
+ $e->parent = $this->env;
+ $e->store = array();
+ $e->block = $block;
+
+ $this->env = $e;
+ return $e;
+ }
+
+ // pop something off the stack
+ protected function popEnv() {
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ return $old;
+ }
+
+ // set something in the current env
+ protected function set($name, $value) {
+ $this->env->store[$name] = $value;
+ }
+
+
+ // get the highest occurrence entry for a name
+ protected function get($name) {
+ $current = $this->env;
+
+ $isArguments = $name == $this->vPrefix . 'arguments';
+ while ($current) {
+ if ($isArguments && isset($current->arguments)) {
+ return array('list', ' ', $current->arguments);
+ }
+
+ if (isset($current->store[$name]))
+ return $current->store[$name];
+ else {
+ $current = isset($current->storeParent) ?
+ $current->storeParent : $current->parent;
+ }
+ }
+
+ $this->throwError("variable $name is undefined");
+ }
+
+ // inject array of unparsed strings into environment as variables
+ protected function injectVariables($args) {
+ $this->pushEnv();
+ $parser = new lessc_parser($this, __METHOD__);
+ foreach ($args as $name => $strValue) {
+ if ($name{0} != '@') $name = '@'.$name;
+ $parser->count = 0;
+ $parser->buffer = (string)$strValue;
+ if (!$parser->propertyValue($value)) {
+ throw new Exception("failed to parse passed in variable $name: $strValue");
+ }
+
+ $this->set($name, $value);
+ }
+ }
+
+ /**
+ * Initialize any static state, can initialize parser for a file
+ * $opts isn't used yet
+ */
+ public function __construct($fname = null) {
+ if ($fname !== null) {
+ // used for deprecated parse method
+ $this->_parseFile = $fname;
+ }
+ }
+
+ public function compile($string, $name = null) {
+ $locale = setlocale(LC_NUMERIC, 0);
+ setlocale(LC_NUMERIC, "C");
+
+ $this->parser = $this->makeParser($name);
+ $root = $this->parser->parse($string);
+
+ $this->env = null;
+ $this->scope = null;
+
+ $this->formatter = $this->newFormatter();
+
+ if (!empty($this->registeredVars)) {
+ $this->injectVariables($this->registeredVars);
+ }
+
+ $this->sourceParser = $this->parser; // used for error messages
+ $this->compileBlock($root);
+
+ ob_start();
+ $this->formatter->block($this->scope);
+ $out = ob_get_clean();
+ setlocale(LC_NUMERIC, $locale);
+ return $out;
+ }
+
+ public function compileFile($fname, $outFname = null) {
+ if (!is_readable($fname)) {
+ throw new Exception('load error: failed to find '.$fname);
+ }
+
+ $pi = pathinfo($fname);
+
+ $oldImport = $this->importDir;
+
+ $this->importDir = (array)$this->importDir;
+ $this->importDir[] = $pi['dirname'].'/';
+
+ $this->addParsedFile($fname);
+
+ $out = $this->compile(file_get_contents($fname), $fname);
+
+ $this->importDir = $oldImport;
+
+ if ($outFname !== null) {
+ return file_put_contents($outFname, $out);
+ }
+
+ return $out;
+ }
+
+ // compile only if changed input has changed or output doesn't exist
+ public function checkedCompile($in, $out) {
+ if (!is_file($out) || filemtime($in) > filemtime($out)) {
+ $this->compileFile($in, $out);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Execute lessphp on a .less file or a lessphp cache structure
+ *
+ * The lessphp cache structure contains information about a specific
+ * less file having been parsed. It can be used as a hint for future
+ * calls to determine whether or not a rebuild is required.
+ *
+ * The cache structure contains two important keys that may be used
+ * externally:
+ *
+ * compiled: The final compiled CSS
+ * updated: The time (in seconds) the CSS was last compiled
+ *
+ * The cache structure is a plain-ol' PHP associative array and can
+ * be serialized and unserialized without a hitch.
+ *
+ * @param mixed $in Input
+ * @param bool $force Force rebuild?
+ * @return array lessphp cache structure
+ */
+ public function cachedCompile($in, $force = false) {
+ // assume no root
+ $root = null;
+
+ if (is_string($in)) {
+ $root = $in;
+ } elseif (is_array($in) and isset($in['root'])) {
+ if ($force or ! isset($in['files'])) {
+ // If we are forcing a recompile or if for some reason the
+ // structure does not contain any file information we should
+ // specify the root to trigger a rebuild.
+ $root = $in['root'];
+ } elseif (isset($in['files']) and is_array($in['files'])) {
+ foreach ($in['files'] as $fname => $ftime ) {
+ if (!file_exists($fname) or filemtime($fname) > $ftime) {
+ // One of the files we knew about previously has changed
+ // so we should look at our incoming root again.
+ $root = $in['root'];
+ break;
+ }
+ }
+ }
+ } else {
+ // TODO: Throw an exception? We got neither a string nor something
+ // that looks like a compatible lessphp cache structure.
+ return null;
+ }
+
+ if ($root !== null) {
+ // If we have a root value which means we should rebuild.
+ $out = array();
+ $out['root'] = $root;
+ $out['compiled'] = $this->compileFile($root);
+ $out['files'] = $this->allParsedFiles();
+ $out['updated'] = time();
+ return $out;
+ } else {
+ // No changes, pass back the structure
+ // we were given initially.
+ return $in;
+ }
+
+ }
+
+ // parse and compile buffer
+ // This is deprecated
+ public function parse($str = null, $initialVariables = null) {
+ if (is_array($str)) {
+ $initialVariables = $str;
+ $str = null;
+ }
+
+ $oldVars = $this->registeredVars;
+ if ($initialVariables !== null) {
+ $this->setVariables($initialVariables);
+ }
+
+ if ($str == null) {
+ if (empty($this->_parseFile)) {
+ throw new exception("nothing to parse");
+ }
+
+ $out = $this->compileFile($this->_parseFile);
+ } else {
+ $out = $this->compile($str);
+ }
+
+ $this->registeredVars = $oldVars;
+ return $out;
+ }
+
+ protected function makeParser($name) {
+ $parser = new lessc_parser($this, $name);
+ $parser->writeComments = $this->preserveComments;
+
+ return $parser;
+ }
+
+ public function setFormatter($name) {
+ $this->formatterName = $name;
+ }
+
+ protected function newFormatter() {
+ $className = "lessc_formatter_lessjs";
+ if (!empty($this->formatterName)) {
+ if (!is_string($this->formatterName))
+ return $this->formatterName;
+ $className = "lessc_formatter_$this->formatterName";
+ }
+
+ return new $className;
+ }
+
+ public function setPreserveComments($preserve) {
+ $this->preserveComments = $preserve;
+ }
+
+ public function registerFunction($name, $func) {
+ $this->libFunctions[$name] = $func;
+ }
+
+ public function unregisterFunction($name) {
+ unset($this->libFunctions[$name]);
+ }
+
+ public function setVariables($variables) {
+ $this->registeredVars = array_merge($this->registeredVars, $variables);
+ }
+
+ public function unsetVariable($name) {
+ unset($this->registeredVars[$name]);
+ }
+
+ public function setImportDir($dirs) {
+ $this->importDir = (array)$dirs;
+ }
+
+ public function addImportDir($dir) {
+ $this->importDir = (array)$this->importDir;
+ $this->importDir[] = $dir;
+ }
+
+ public function allParsedFiles() {
+ return $this->allParsedFiles;
+ }
+
+ public function addParsedFile($file) {
+ $this->allParsedFiles[realpath($file)] = filemtime($file);
+ }
+
+ /**
+ * Uses the current value of $this->count to show line and line number
+ */
+ public function throwError($msg = null) {
+ if ($this->sourceLoc >= 0) {
+ $this->sourceParser->throwError($msg, $this->sourceLoc);
+ }
+ throw new exception($msg);
+ }
+
+ // compile file $in to file $out if $in is newer than $out
+ // returns true when it compiles, false otherwise
+ public static function ccompile($in, $out, $less = null) {
+ if ($less === null) {
+ $less = new self;
+ }
+ return $less->checkedCompile($in, $out);
+ }
+
+ public static function cexecute($in, $force = false, $less = null) {
+ if ($less === null) {
+ $less = new self;
+ }
+ return $less->cachedCompile($in, $force);
+ }
+
+ static protected $cssColors = array(
+ 'aliceblue' => '240,248,255',
+ 'antiquewhite' => '250,235,215',
+ 'aqua' => '0,255,255',
+ 'aquamarine' => '127,255,212',
+ 'azure' => '240,255,255',
+ 'beige' => '245,245,220',
+ 'bisque' => '255,228,196',
+ 'black' => '0,0,0',
+ 'blanchedalmond' => '255,235,205',
+ 'blue' => '0,0,255',
+ 'blueviolet' => '138,43,226',
+ 'brown' => '165,42,42',
+ 'burlywood' => '222,184,135',
+ 'cadetblue' => '95,158,160',
+ 'chartreuse' => '127,255,0',
+ 'chocolate' => '210,105,30',
+ 'coral' => '255,127,80',
+ 'cornflowerblue' => '100,149,237',
+ 'cornsilk' => '255,248,220',
+ 'crimson' => '220,20,60',
+ 'cyan' => '0,255,255',
+ 'darkblue' => '0,0,139',
+ 'darkcyan' => '0,139,139',
+ 'darkgoldenrod' => '184,134,11',
+ 'darkgray' => '169,169,169',
+ 'darkgreen' => '0,100,0',
+ 'darkgrey' => '169,169,169',
+ 'darkkhaki' => '189,183,107',
+ 'darkmagenta' => '139,0,139',
+ 'darkolivegreen' => '85,107,47',
+ 'darkorange' => '255,140,0',
+ 'darkorchid' => '153,50,204',
+ 'darkred' => '139,0,0',
+ 'darksalmon' => '233,150,122',
+ 'darkseagreen' => '143,188,143',
+ 'darkslateblue' => '72,61,139',
+ 'darkslategray' => '47,79,79',
+ 'darkslategrey' => '47,79,79',
+ 'darkturquoise' => '0,206,209',
+ 'darkviolet' => '148,0,211',
+ 'deeppink' => '255,20,147',
+ 'deepskyblue' => '0,191,255',
+ 'dimgray' => '105,105,105',
+ 'dimgrey' => '105,105,105',
+ 'dodgerblue' => '30,144,255',
+ 'firebrick' => '178,34,34',
+ 'floralwhite' => '255,250,240',
+ 'forestgreen' => '34,139,34',
+ 'fuchsia' => '255,0,255',
+ 'gainsboro' => '220,220,220',
+ 'ghostwhite' => '248,248,255',
+ 'gold' => '255,215,0',
+ 'goldenrod' => '218,165,32',
+ 'gray' => '128,128,128',
+ 'green' => '0,128,0',
+ 'greenyellow' => '173,255,47',
+ 'grey' => '128,128,128',
+ 'honeydew' => '240,255,240',
+ 'hotpink' => '255,105,180',
+ 'indianred' => '205,92,92',
+ 'indigo' => '75,0,130',
+ 'ivory' => '255,255,240',
+ 'khaki' => '240,230,140',
+ 'lavender' => '230,230,250',
+ 'lavenderblush' => '255,240,245',
+ 'lawngreen' => '124,252,0',
+ 'lemonchiffon' => '255,250,205',
+ 'lightblue' => '173,216,230',
+ 'lightcoral' => '240,128,128',
+ 'lightcyan' => '224,255,255',
+ 'lightgoldenrodyellow' => '250,250,210',
+ 'lightgray' => '211,211,211',
+ 'lightgreen' => '144,238,144',
+ 'lightgrey' => '211,211,211',
+ 'lightpink' => '255,182,193',
+ 'lightsalmon' => '255,160,122',
+ 'lightseagreen' => '32,178,170',
+ 'lightskyblue' => '135,206,250',
+ 'lightslategray' => '119,136,153',
+ 'lightslategrey' => '119,136,153',
+ 'lightsteelblue' => '176,196,222',
+ 'lightyellow' => '255,255,224',
+ 'lime' => '0,255,0',
+ 'limegreen' => '50,205,50',
+ 'linen' => '250,240,230',
+ 'magenta' => '255,0,255',
+ 'maroon' => '128,0,0',
+ 'mediumaquamarine' => '102,205,170',
+ 'mediumblue' => '0,0,205',
+ 'mediumorchid' => '186,85,211',
+ 'mediumpurple' => '147,112,219',
+ 'mediumseagreen' => '60,179,113',
+ 'mediumslateblue' => '123,104,238',
+ 'mediumspringgreen' => '0,250,154',
+ 'mediumturquoise' => '72,209,204',
+ 'mediumvioletred' => '199,21,133',
+ 'midnightblue' => '25,25,112',
+ 'mintcream' => '245,255,250',
+ 'mistyrose' => '255,228,225',
+ 'moccasin' => '255,228,181',
+ 'navajowhite' => '255,222,173',
+ 'navy' => '0,0,128',
+ 'oldlace' => '253,245,230',
+ 'olive' => '128,128,0',
+ 'olivedrab' => '107,142,35',
+ 'orange' => '255,165,0',
+ 'orangered' => '255,69,0',
+ 'orchid' => '218,112,214',
+ 'palegoldenrod' => '238,232,170',
+ 'palegreen' => '152,251,152',
+ 'paleturquoise' => '175,238,238',
+ 'palevioletred' => '219,112,147',
+ 'papayawhip' => '255,239,213',
+ 'peachpuff' => '255,218,185',
+ 'peru' => '205,133,63',
+ 'pink' => '255,192,203',
+ 'plum' => '221,160,221',
+ 'powderblue' => '176,224,230',
+ 'purple' => '128,0,128',
+ 'red' => '255,0,0',
+ 'rosybrown' => '188,143,143',
+ 'royalblue' => '65,105,225',
+ 'saddlebrown' => '139,69,19',
+ 'salmon' => '250,128,114',
+ 'sandybrown' => '244,164,96',
+ 'seagreen' => '46,139,87',
+ 'seashell' => '255,245,238',
+ 'sienna' => '160,82,45',
+ 'silver' => '192,192,192',
+ 'skyblue' => '135,206,235',
+ 'slateblue' => '106,90,205',
+ 'slategray' => '112,128,144',
+ 'slategrey' => '112,128,144',
+ 'snow' => '255,250,250',
+ 'springgreen' => '0,255,127',
+ 'steelblue' => '70,130,180',
+ 'tan' => '210,180,140',
+ 'teal' => '0,128,128',
+ 'thistle' => '216,191,216',
+ 'tomato' => '255,99,71',
+ 'transparent' => '0,0,0,0',
+ 'turquoise' => '64,224,208',
+ 'violet' => '238,130,238',
+ 'wheat' => '245,222,179',
+ 'white' => '255,255,255',
+ 'whitesmoke' => '245,245,245',
+ 'yellow' => '255,255,0',
+ 'yellowgreen' => '154,205,50'
+ );
+}
+
+// responsible for taking a string of LESS code and converting it into a
+// syntax tree
+class lessc_parser {
+ static protected $nextBlockId = 0; // used to uniquely identify blocks
+
+ static protected $precedence = array(
+ '=<' => 0,
+ '>=' => 0,
+ '=' => 0,
+ '<' => 0,
+ '>' => 0,
+
+ '+' => 1,
+ '-' => 1,
+ '*' => 2,
+ '/' => 2,
+ '%' => 2,
+ );
+
+ static protected $whitePattern;
+ static protected $commentMulti;
+
+ static protected $commentSingle = "//";
+ static protected $commentMultiLeft = "/*";
+ static protected $commentMultiRight = "*/";
+
+ // regex string to match any of the operators
+ static protected $operatorString;
+
+ // these properties will supress division unless it's inside parenthases
+ static protected $supressDivisionProps =
+ array('/border-radius$/i', '/^font$/i');
+
+ protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
+ protected $lineDirectives = array("charset");
+
+ /**
+ * if we are in parens we can be more liberal with whitespace around
+ * operators because it must evaluate to a single value and thus is less
+ * ambiguous.
+ *
+ * Consider:
+ * property1: 10 -5; // is two numbers, 10 and -5
+ * property2: (10 -5); // should evaluate to 5
+ */
+ protected $inParens = false;
+
+ // caches preg escaped literals
+ static protected $literalCache = array();
+
+ public function __construct($lessc, $sourceName = null) {
+ $this->eatWhiteDefault = true;
+ // reference to less needed for vPrefix, mPrefix, and parentSelector
+ $this->lessc = $lessc;
+
+ $this->sourceName = $sourceName; // name used for error messages
+
+ $this->writeComments = false;
+
+ if (!self::$operatorString) {
+ self::$operatorString =
+ '('.implode('|', array_map(array('lessc', 'preg_quote'),
+ array_keys(self::$precedence))).')';
+
+ $commentSingle = lessc::preg_quote(self::$commentSingle);
+ $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
+ $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
+
+ self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
+ self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
+ }
+ }
+
+ public function parse($buffer) {
+ $this->count = 0;
+ $this->line = 1;
+
+ $this->env = null; // block stack
+ $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
+ $this->pushSpecialBlock("root");
+ $this->eatWhiteDefault = true;
+ $this->seenComments = array();
+
+ // trim whitespace on head
+ // if (preg_match('/^\s+/', $this->buffer, $m)) {
+ // $this->line += substr_count($m[0], "\n");
+ // $this->buffer = ltrim($this->buffer);
+ // }
+ $this->whitespace();
+
+ // parse the entire file
+ while (false !== $this->parseChunk());
+
+ if ($this->count != strlen($this->buffer))
+ $this->throwError();
+
+ // TODO report where the block was opened
+ if (!is_null($this->env->parent))
+ throw new exception('parse error: unclosed block');
+
+ return $this->env;
+ }
+
+ /**
+ * Parse a single chunk off the head of the buffer and append it to the
+ * current parse environment.
+ * Returns false when the buffer is empty, or when there is an error.
+ *
+ * This function is called repeatedly until the entire document is
+ * parsed.
+ *
+ * This parser is most similar to a recursive descent parser. Single
+ * functions represent discrete grammatical rules for the language, and
+ * they are able to capture the text that represents those rules.
+ *
+ * Consider the function lessc::keyword(). (all parse functions are
+ * structured the same)
+ *
+ * The function takes a single reference argument. When calling the
+ * function it will attempt to match a keyword on the head of the buffer.
+ * If it is successful, it will place the keyword in the referenced
+ * argument, advance the position in the buffer, and return true. If it
+ * fails then it won't advance the buffer and it will return false.
+ *
+ * All of these parse functions are powered by lessc::match(), which behaves
+ * the same way, but takes a literal regular expression. Sometimes it is
+ * more convenient to use match instead of creating a new function.
+ *
+ * Because of the format of the functions, to parse an entire string of
+ * grammatical rules, you can chain them together using &&.
+ *
+ * But, if some of the rules in the chain succeed before one fails, then
+ * the buffer position will be left at an invalid state. In order to
+ * avoid this, lessc::seek() is used to remember and set buffer positions.
+ *
+ * Before parsing a chain, use $s = $this->seek() to remember the current
+ * position into $s. Then if a chain fails, use $this->seek($s) to
+ * go back where we started.
+ */
+ protected function parseChunk() {
+ if (empty($this->buffer)) return false;
+ $s = $this->seek();
+
+ if ($this->whitespace()) {
+ return true;
+ }
+
+ // setting a property
+ if ($this->keyword($key) && $this->assign() &&
+ $this->propertyValue($value, $key) && $this->end())
+ {
+ $this->append(array('assign', $key, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+
+ // look for special css blocks
+ if ($this->literal('@', false)) {
+ $this->count--;
+
+ // media
+ if ($this->literal('@media')) {
+ if (($this->mediaQueryList($mediaQueries) || true)
+ && $this->literal('{'))
+ {
+ $media = $this->pushSpecialBlock("media");
+ $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
+ return true;
+ } else {
+ $this->seek($s);
+ return false;
+ }
+ }
+
+ if ($this->literal("@", false) && $this->keyword($dirName)) {
+ if ($this->isDirective($dirName, $this->blockDirectives)) {
+ if (($this->openString("{", $dirValue, null, array(";")) || true) &&
+ $this->literal("{"))
+ {
+ $dir = $this->pushSpecialBlock("directive");
+ $dir->name = $dirName;
+ if (isset($dirValue)) $dir->value = $dirValue;
+ return true;
+ }
+ } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
+ if ($this->propertyValue($dirValue) && $this->end()) {
+ $this->append(array("directive", $dirName, $dirValue));
+ return true;
+ }
+ }
+ }
+
+ $this->seek($s);
+ }
+
+ // setting a variable
+ if ($this->variable($var) && $this->assign() &&
+ $this->propertyValue($value) && $this->end())
+ {
+ $this->append(array('assign', $var, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->import($importValue)) {
+ $this->append($importValue, $s);
+ return true;
+ }
+
+ // opening parametric mixin
+ if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
+ ($this->guards($guards) || true) &&
+ $this->literal('{'))
+ {
+ $block = $this->pushBlock($this->fixTags(array($tag)));
+ $block->args = $args;
+ $block->isVararg = $isVararg;
+ if (!empty($guards)) $block->guards = $guards;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // opening a simple block
+ if ($this->tags($tags) && $this->literal('{', false)) {
+ $tags = $this->fixTags($tags);
+ $this->pushBlock($tags);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // closing a block
+ if ($this->literal('}', false)) {
+ try {
+ $block = $this->pop();
+ } catch (exception $e) {
+ $this->seek($s);
+ $this->throwError($e->getMessage());
+ }
+
+ $hidden = false;
+ if (is_null($block->type)) {
+ $hidden = true;
+ if (!isset($block->args)) {
+ foreach ($block->tags as $tag) {
+ if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) {
+ $hidden = false;
+ break;
+ }
+ }
+ }
+
+ foreach ($block->tags as $tag) {
+ if (is_string($tag)) {
+ $this->env->children[$tag][] = $block;
+ }
+ }
+ }
+
+ if (!$hidden) {
+ $this->append(array('block', $block), $s);
+ }
+
+ // this is done here so comments aren't bundled into he block that
+ // was just closed
+ $this->whitespace();
+ return true;
+ }
+
+ // mixin
+ if ($this->mixinTags($tags) &&
+ ($this->argumentDef($argv, $isVararg) || true) &&
+ ($this->keyword($suffix) || true) && $this->end())
+ {
+ $tags = $this->fixTags($tags);
+ $this->append(array('mixin', $tags, $argv, $suffix), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // spare ;
+ if ($this->literal(';')) return true;
+
+ return false; // got nothing, throw error
+ }
+
+ protected function isDirective($dirname, $directives) {
+ // TODO: cache pattern in parser
+ $pattern = implode("|",
+ array_map(array("lessc", "preg_quote"), $directives));
+ $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
+
+ return preg_match($pattern, $dirname);
+ }
+
+ protected function fixTags($tags) {
+ // move @ tags out of variable namespace
+ foreach ($tags as &$tag) {
+ if ($tag{0} == $this->lessc->vPrefix)
+ $tag[0] = $this->lessc->mPrefix;
+ }
+ return $tags;
+ }
+
+ // a list of expressions
+ protected function expressionList(&$exps) {
+ $values = array();
+
+ while ($this->expression($exp)) {
+ $values[] = $exp;
+ }
+
+ if (count($values) == 0) return false;
+
+ $exps = lessc::compressList($values, ' ');
+ return true;
+ }
+
+ /**
+ * Attempt to consume an expression.
+ * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
+ */
+ protected function expression(&$out) {
+ if ($this->value($lhs)) {
+ $out = $this->expHelper($lhs, 0);
+
+ // look for / shorthand
+ if (!empty($this->env->supressedDivision)) {
+ unset($this->env->supressedDivision);
+ $s = $this->seek();
+ if ($this->literal("/") && $this->value($rhs)) {
+ $out = array("list", "",
+ array($out, array("keyword", "/"), $rhs));
+ } else {
+ $this->seek($s);
+ }
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * recursively parse infix equation with $lhs at precedence $minP
+ */
+ protected function expHelper($lhs, $minP) {
+ $this->inExp = true;
+ $ss = $this->seek();
+
+ while (true) {
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ // If there is whitespace before the operator, then we require
+ // whitespace after the operator for it to be an expression
+ $needWhite = $whiteBefore && !$this->inParens;
+
+ if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
+ if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
+ foreach (self::$supressDivisionProps as $pattern) {
+ if (preg_match($pattern, $this->env->currentProperty)) {
+ $this->env->supressedDivision = true;
+ break 2;
+ }
+ }
+ }
+
+
+ $whiteAfter = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ if (!$this->value($rhs)) break;
+
+ // peek for next operator to see what to do with rhs
+ if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
+ }
+
+ $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
+ $ss = $this->seek();
+
+ continue;
+ }
+
+ break;
+ }
+
+ $this->seek($ss);
+
+ return $lhs;
+ }
+
+ // consume a list of values for a property
+ public function propertyValue(&$value, $keyName = null) {
+ $values = array();
+
+ if ($keyName !== null) $this->env->currentProperty = $keyName;
+
+ $s = null;
+ while ($this->expressionList($v)) {
+ $values[] = $v;
+ $s = $this->seek();
+ if (!$this->literal(',')) break;
+ }
+
+ if ($s) $this->seek($s);
+
+ if ($keyName !== null) unset($this->env->currentProperty);
+
+ if (count($values) == 0) return false;
+
+ $value = lessc::compressList($values, ', ');
+ return true;
+ }
+
+ protected function parenValue(&$out) {
+ $s = $this->seek();
+
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
+ return false;
+ }
+
+ $inParens = $this->inParens;
+ if ($this->literal("(") &&
+ ($this->inParens = true) && $this->expression($exp) &&
+ $this->literal(")"))
+ {
+ $out = $exp;
+ $this->inParens = $inParens;
+ return true;
+ } else {
+ $this->inParens = $inParens;
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ // a single value
+ protected function value(&$value) {
+ $s = $this->seek();
+
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
+ // negation
+ if ($this->literal("-", false) &&
+ (($this->variable($inner) && $inner = array("variable", $inner)) ||
+ $this->unit($inner) ||
+ $this->parenValue($inner)))
+ {
+ $value = array("unary", "-", $inner);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+ }
+
+ if ($this->parenValue($value)) return true;
+ if ($this->unit($value)) return true;
+ if ($this->color($value)) return true;
+ if ($this->func($value)) return true;
+ if ($this->string($value)) return true;
+
+ if ($this->keyword($word)) {
+ $value = array('keyword', $word);
+ return true;
+ }
+
+ // try a variable
+ if ($this->variable($var)) {
+ $value = array('variable', $var);
+ return true;
+ }
+
+ // unquote string (should this work on any type?
+ if ($this->literal("~") && $this->string($str)) {
+ $value = array("escape", $str);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // css hack: \0
+ if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
+ $value = array('keyword', '\\'.$m[1]);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ // an import statement
+ protected function import(&$out) {
+ if (!$this->literal('@import')) return false;
+
+ // @import "something.css" media;
+ // @import url("something.css") media;
+ // @import url(something.css) media;
+
+ if ($this->propertyValue($value)) {
+ $out = array("import", $value);
+ return true;
+ }
+ }
+
+ protected function mediaQueryList(&$out) {
+ if ($this->genericList($list, "mediaQuery", ",", false)) {
+ $out = $list[2];
+ return true;
+ }
+ return false;
+ }
+
+ protected function mediaQuery(&$out) {
+ $s = $this->seek();
+
+ $expressions = null;
+ $parts = array();
+
+ if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
+ $prop = array("mediaType");
+ if (isset($only)) $prop[] = "only";
+ if (isset($not)) $prop[] = "not";
+ $prop[] = $mediaType;
+ $parts[] = $prop;
+ } else {
+ $this->seek($s);
+ }
+
+
+ if (!empty($mediaType) && !$this->literal("and")) {
+ // ~
+ } else {
+ $this->genericList($expressions, "mediaExpression", "and", false);
+ if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
+ }
+
+ if (count($parts) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ $out = $parts;
+ return true;
+ }
+
+ protected function mediaExpression(&$out) {
+ $s = $this->seek();
+ $value = null;
+ if ($this->literal("(") &&
+ $this->keyword($feature) &&
+ ($this->literal(":") && $this->expression($value) || true) &&
+ $this->literal(")"))
+ {
+ $out = array("mediaExp", $feature);
+ if ($value) $out[] = $value;
+ return true;
+ } elseif ($this->variable($variable)) {
+ $out = array('variable', $variable);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // an unbounded string stopped by $end
+ protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $stop = array("'", '"', "@{", $end);
+ $stop = array_map(array("lessc", "preg_quote"), $stop);
+ // $stop[] = self::$commentMulti;
+
+ if (!is_null($rejectStrs)) {
+ $stop = array_merge($stop, $rejectStrs);
+ }
+
+ $patt = '(.*?)('.implode("|", $stop).')';
+
+ $nestingLevel = 0;
+
+ $content = array();
+ while ($this->match($patt, $m, false)) {
+ if (!empty($m[1])) {
+ $content[] = $m[1];
+ if ($nestingOpen) {
+ $nestingLevel += substr_count($m[1], $nestingOpen);
+ }
+ }
+
+ $tok = $m[2];
+
+ $this->count-= strlen($tok);
+ if ($tok == $end) {
+ if ($nestingLevel == 0) {
+ break;
+ } else {
+ $nestingLevel--;
+ }
+ }
+
+ if (($tok == "'" || $tok == '"') && $this->string($str)) {
+ $content[] = $str;
+ continue;
+ }
+
+ if ($tok == "@{" && $this->interpolation($inter)) {
+ $content[] = $inter;
+ continue;
+ }
+
+ if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
+ break;
+ }
+
+ $content[] = $tok;
+ $this->count+= strlen($tok);
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($content) == 0) return false;
+
+ // trim the end
+ if (is_string(end($content))) {
+ $content[count($content) - 1] = rtrim(end($content));
+ }
+
+ $out = array("string", "", $content);
+ return true;
+ }
+
+ protected function string(&$out) {
+ $s = $this->seek();
+ if ($this->literal('"', false)) {
+ $delim = '"';
+ } elseif ($this->literal("'", false)) {
+ $delim = "'";
+ } else {
+ return false;
+ }
+
+ $content = array();
+
+ // look for either ending delim , escape, or string interpolation
+ $patt = '([^\n]*?)(@\{|\\\\|' .
+ lessc::preg_quote($delim).')';
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while ($this->match($patt, $m, false)) {
+ $content[] = $m[1];
+ if ($m[2] == "@{") {
+ $this->count -= strlen($m[2]);
+ if ($this->interpolation($inter, false)) {
+ $content[] = $inter;
+ } else {
+ $this->count += strlen($m[2]);
+ $content[] = "@{"; // ignore it
+ }
+ } elseif ($m[2] == '\\') {
+ $content[] = $m[2];
+ if ($this->literal($delim, false)) {
+ $content[] = $delim;
+ }
+ } else {
+ $this->count -= strlen($delim);
+ break; // delim
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if ($this->literal($delim)) {
+ $out = array("string", $delim, $content);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function interpolation(&$out) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = true;
+
+ $s = $this->seek();
+ if ($this->literal("@{") &&
+ $this->openString("}", $interp, null, array("'", '"', ";")) &&
+ $this->literal("}", false))
+ {
+ $out = array("interpolate", $interp);
+ $this->eatWhiteDefault = $oldWhite;
+ if ($this->eatWhiteDefault) $this->whitespace();
+ return true;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ $this->seek($s);
+ return false;
+ }
+
+ protected function unit(&$unit) {
+ // speed shortcut
+ if (isset($this->buffer[$this->count])) {
+ $char = $this->buffer[$this->count];
+ if (!ctype_digit($char) && $char != ".") return false;
+ }
+
+ if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
+ $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
+ return true;
+ }
+ return false;
+ }
+
+ // a # color
+ protected function color(&$out) {
+ if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
+ if (strlen($m[1]) > 7) {
+ $out = array("string", "", array($m[1]));
+ } else {
+ $out = array("raw_color", $m[1]);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ // consume an argument definition list surrounded by ()
+ // each argument is a variable name with optional value
+ // or at the end a ... or a variable named followed by ...
+ // arguments are separated by , unless a ; is in the list, then ; is the
+ // delimiter.
+ protected function argumentDef(&$args, &$isVararg) {
+ $s = $this->seek();
+ if (!$this->literal('(')) return false;
+
+ $values = array();
+ $delim = ",";
+ $method = "expressionList";
+
+ $isVararg = false;
+ while (true) {
+ if ($this->literal("...")) {
+ $isVararg = true;
+ break;
+ }
+
+ if ($this->$method($value)) {
+ if ($value[0] == "variable") {
+ $arg = array("arg", $value[1]);
+ $ss = $this->seek();
+
+ if ($this->assign() && $this->$method($rhs)) {
+ $arg[] = $rhs;
+ } else {
+ $this->seek($ss);
+ if ($this->literal("...")) {
+ $arg[0] = "rest";
+ $isVararg = true;
+ }
+ }
+
+ $values[] = $arg;
+ if ($isVararg) break;
+ continue;
+ } else {
+ $values[] = array("lit", $value);
+ }
+ }
+
+
+ if (!$this->literal($delim)) {
+ if ($delim == "," && $this->literal(";")) {
+ // found new delim, convert existing args
+ $delim = ";";
+ $method = "propertyValue";
+
+ // transform arg list
+ if (isset($values[1])) { // 2 items
+ $newList = array();
+ foreach ($values as $i => $arg) {
+ switch($arg[0]) {
+ case "arg":
+ if ($i) {
+ $this->throwError("Cannot mix ; and , as delimiter types");
+ }
+ $newList[] = $arg[2];
+ break;
+ case "lit":
+ $newList[] = $arg[1];
+ break;
+ case "rest":
+ $this->throwError("Unexpected rest before semicolon");
+ }
+ }
+
+ $newList = array("list", ", ", $newList);
+
+ switch ($values[0][0]) {
+ case "arg":
+ $newArg = array("arg", $values[0][1], $newList);
+ break;
+ case "lit":
+ $newArg = array("lit", $newList);
+ break;
+ }
+
+ } elseif ($values) { // 1 item
+ $newArg = $values[0];
+ }
+
+ if ($newArg) {
+ $values = array($newArg);
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (!$this->literal(')')) {
+ $this->seek($s);
+ return false;
+ }
+
+ $args = $values;
+
+ return true;
+ }
+
+ // consume a list of tags
+ // this accepts a hanging delimiter
+ protected function tags(&$tags, $simple = false, $delim = ',') {
+ $tags = array();
+ while ($this->tag($tt, $simple)) {
+ $tags[] = $tt;
+ if (!$this->literal($delim)) break;
+ }
+ if (count($tags) == 0) return false;
+
+ return true;
+ }
+
+ // list of tags of specifying mixin path
+ // optionally separated by > (lazy, accepts extra >)
+ protected function mixinTags(&$tags) {
+ $tags = array();
+ while ($this->tag($tt, true)) {
+ $tags[] = $tt;
+ $this->literal(">");
+ }
+
+ if (count($tags) == 0) return false;
+
+ return true;
+ }
+
+ // a bracketed value (contained within in a tag definition)
+ protected function tagBracket(&$parts, &$hasExpression) {
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
+ return false;
+ }
+
+ $s = $this->seek();
+
+ $hasInterpolation = false;
+
+ if ($this->literal("[", false)) {
+ $attrParts = array("[");
+ // keyword, string, operator
+ while (true) {
+ if ($this->literal("]", false)) {
+ $this->count--;
+ break; // get out early
+ }
+
+ if ($this->match('\s+', $m)) {
+ $attrParts[] = " ";
+ continue;
+ }
+ if ($this->string($str)) {
+ // escape parent selector, (yuck)
+ foreach ($str[2] as &$chunk) {
+ $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
+ }
+
+ $attrParts[] = $str;
+ $hasInterpolation = true;
+ continue;
+ }
+
+ if ($this->keyword($word)) {
+ $attrParts[] = $word;
+ continue;
+ }
+
+ if ($this->interpolation($inter, false)) {
+ $attrParts[] = $inter;
+ $hasInterpolation = true;
+ continue;
+ }
+
+ // operator, handles attr namespace too
+ if ($this->match('[|-~\$\*\^=]+', $m)) {
+ $attrParts[] = $m[0];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->literal("]", false)) {
+ $attrParts[] = "]";
+ foreach ($attrParts as $part) {
+ $parts[] = $part;
+ }
+ $hasExpression = $hasExpression || $hasInterpolation;
+ return true;
+ }
+ $this->seek($s);
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // a space separated list of selectors
+ protected function tag(&$tag, $simple = false) {
+ if ($simple)
+ $chars = '^@,:;{}\][>\(\) "\'';
+ else
+ $chars = '^@,;{}["\'';
+
+ $s = $this->seek();
+
+ $hasExpression = false;
+ $parts = array();
+ while ($this->tagBracket($parts, $hasExpression));
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
+ $parts[] = $m[1];
+ if ($simple) break;
+
+ while ($this->tagBracket($parts, $hasExpression));
+ continue;
+ }
+
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
+ if ($this->interpolation($interp)) {
+ $hasExpression = true;
+ $interp[2] = true; // don't unescape
+ $parts[] = $interp;
+ continue;
+ }
+
+ if ($this->literal("@")) {
+ $parts[] = "@";
+ continue;
+ }
+ }
+
+ if ($this->unit($unit)) { // for keyframes
+ $parts[] = $unit[1];
+ $parts[] = $unit[2];
+ continue;
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ if (!$parts) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($hasExpression) {
+ $tag = array("exp", array("string", "", $parts));
+ } else {
+ $tag = trim(implode($parts));
+ }
+
+ $this->whitespace();
+ return true;
+ }
+
+ // a css function
+ protected function func(&$func) {
+ $s = $this->seek();
+
+ if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
+ $fname = $m[1];
+
+ $sPreArgs = $this->seek();
+
+ $args = array();
+ while (true) {
+ $ss = $this->seek();
+ // this ugly nonsense is for ie filter properties
+ if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
+ $args[] = array("string", "", array($name, "=", $value));
+ } else {
+ $this->seek($ss);
+ if ($this->expressionList($value)) {
+ $args[] = $value;
+ }
+ }
+
+ if (!$this->literal(',')) break;
+ }
+ $args = array('list', ',', $args);
+
+ if ($this->literal(')')) {
+ $func = array('function', $fname, $args);
+ return true;
+ } elseif ($fname == 'url') {
+ // couldn't parse and in url? treat as string
+ $this->seek($sPreArgs);
+ if ($this->openString(")", $string) && $this->literal(")")) {
+ $func = array('function', $fname, $string);
+ return true;
+ }
+ }
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // consume a less variable
+ protected function variable(&$name) {
+ $s = $this->seek();
+ if ($this->literal($this->lessc->vPrefix, false) &&
+ ($this->variable($sub) || $this->keyword($name)))
+ {
+ if (!empty($sub)) {
+ $name = array('variable', $sub);
+ } else {
+ $name = $this->lessc->vPrefix.$name;
+ }
+ return true;
+ }
+
+ $name = null;
+ $this->seek($s);
+ return false;
+ }
+
+ /**
+ * Consume an assignment operator
+ * Can optionally take a name that will be set to the current property name
+ */
+ protected function assign($name = null) {
+ if ($name) $this->currentProperty = $name;
+ return $this->literal(':') || $this->literal('=');
+ }
+
+ // consume a keyword
+ protected function keyword(&$word) {
+ if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
+ $word = $m[1];
+ return true;
+ }
+ return false;
+ }
+
+ // consume an end of statement delimiter
+ protected function end() {
+ if ($this->literal(';', false)) {
+ return true;
+ } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
+ // if there is end of file or a closing block next then we don't need a ;
+ return true;
+ }
+ return false;
+ }
+
+ protected function guards(&$guards) {
+ $s = $this->seek();
+
+ if (!$this->literal("when")) {
+ $this->seek($s);
+ return false;
+ }
+
+ $guards = array();
+
+ while ($this->guardGroup($g)) {
+ $guards[] = $g;
+ if (!$this->literal(",")) break;
+ }
+
+ if (count($guards) == 0) {
+ $guards = null;
+ $this->seek($s);
+ return false;
+ }
+
+ return true;
+ }
+
+ // a bunch of guards that are and'd together
+ // TODO rename to guardGroup
+ protected function guardGroup(&$guardGroup) {
+ $s = $this->seek();
+ $guardGroup = array();
+ while ($this->guard($guard)) {
+ $guardGroup[] = $guard;
+ if (!$this->literal("and")) break;
+ }
+
+ if (count($guardGroup) == 0) {
+ $guardGroup = null;
+ $this->seek($s);
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function guard(&$guard) {
+ $s = $this->seek();
+ $negate = $this->literal("not");
+
+ if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
+ $guard = $exp;
+ if ($negate) $guard = array("negate", $guard);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ /* raw parsing functions */
+
+ protected function literal($what, $eatWhitespace = null) {
+ if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
+
+ // shortcut on single letter
+ if (!isset($what[1]) && isset($this->buffer[$this->count])) {
+ if ($this->buffer[$this->count] == $what) {
+ if (!$eatWhitespace) {
+ $this->count++;
+ return true;
+ }
+ // goes below...
+ } else {
+ return false;
+ }
+ }
+
+ if (!isset(self::$literalCache[$what])) {
+ self::$literalCache[$what] = lessc::preg_quote($what);
+ }
+
+ return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
+ }
+
+ protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
+ $s = $this->seek();
+ $items = array();
+ while ($this->$parseItem($value)) {
+ $items[] = $value;
+ if ($delim) {
+ if (!$this->literal($delim)) break;
+ }
+ }
+
+ if (count($items) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($flatten && count($items) == 1) {
+ $out = $items[0];
+ } else {
+ $out = array("list", $delim, $items);
+ }
+
+ return true;
+ }
+
+
+ // advance counter to next occurrence of $what
+ // $until - don't include $what in advance
+ // $allowNewline, if string, will be used as valid char set
+ protected function to($what, &$out, $until = false, $allowNewline = false) {
+ if (is_string($allowNewline)) {
+ $validChars = $allowNewline;
+ } else {
+ $validChars = $allowNewline ? "." : "[^\n]";
+ }
+ if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
+ if ($until) $this->count -= strlen($what); // give back $what
+ $out = $m[1];
+ return true;
+ }
+
+ // try to match something on head of buffer
+ protected function match($regex, &$out, $eatWhitespace = null) {
+ if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
+
+ $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
+ $this->count += strlen($out[0]);
+ if ($eatWhitespace && $this->writeComments) $this->whitespace();
+ return true;
+ }
+ return false;
+ }
+
+ // match some whitespace
+ protected function whitespace() {
+ if ($this->writeComments) {
+ $gotWhite = false;
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ if (isset($m[1]) && empty($this->seenComments[$this->count])) {
+ $this->append(array("comment", $m[1]));
+ $this->seenComments[$this->count] = true;
+ }
+ $this->count += strlen($m[0]);
+ $gotWhite = true;
+ }
+ return $gotWhite;
+ } else {
+ $this->match("", $m);
+ return strlen($m[0]) > 0;
+ }
+ }
+
+ // match something without consuming it
+ protected function peek($regex, &$out = null, $from=null) {
+ if (is_null($from)) $from = $this->count;
+ $r = '/'.$regex.'/Ais';
+ $result = preg_match($r, $this->buffer, $out, null, $from);
+
+ return $result;
+ }
+
+ // seek to a spot in the buffer or return where we are on no argument
+ protected function seek($where = null) {
+ if ($where === null) return $this->count;
+ else $this->count = $where;
+ return true;
+ }
+
+ /* misc functions */
+
+ public function throwError($msg = "parse error", $count = null) {
+ $count = is_null($count) ? $this->count : $count;
+
+ $line = $this->line +
+ substr_count(substr($this->buffer, 0, $count), "\n");
+
+ if (!empty($this->sourceName)) {
+ $loc = "$this->sourceName on line $line";
+ } else {
+ $loc = "line: $line";
+ }
+
+ // TODO this depends on $this->count
+ if ($this->peek("(.*?)(\n|$)", $m, $count)) {
+ throw new exception("$msg: failed at `$m[1]` $loc");
+ } else {
+ throw new exception("$msg: $loc");
+ }
+ }
+
+ protected function pushBlock($selectors=null, $type=null) {
+ $b = new stdclass;
+ $b->parent = $this->env;
+
+ $b->type = $type;
+ $b->id = self::$nextBlockId++;
+
+ $b->isVararg = false; // TODO: kill me from here
+ $b->tags = $selectors;
+
+ $b->props = array();
+ $b->children = array();
+
+ $this->env = $b;
+ return $b;
+ }
+
+ // push a block that doesn't multiply tags
+ protected function pushSpecialBlock($type) {
+ return $this->pushBlock(null, $type);
+ }
+
+ // append a property to the current block
+ protected function append($prop, $pos = null) {
+ if ($pos !== null) $prop[-1] = $pos;
+ $this->env->props[] = $prop;
+ }
+
+ // pop something off the stack
+ protected function pop() {
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ return $old;
+ }
+
+ // remove comments from $text
+ // todo: make it work for all functions, not just url
+ protected function removeComments($text) {
+ $look = array(
+ 'url(', '//', '/*', '"', "'"
+ );
+
+ $out = '';
+ $min = null;
+ while (true) {
+ // find the next item
+ foreach ($look as $token) {
+ $pos = strpos($text, $token);
+ if ($pos !== false) {
+ if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
+ }
+ }
+
+ if (is_null($min)) break;
+
+ $count = $min[1];
+ $skip = 0;
+ $newlines = 0;
+ switch ($min[0]) {
+ case 'url(':
+ if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
+ $count += strlen($m[0]) - strlen($min[0]);
+ break;
+ case '"':
+ case "'":
+ if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
+ $count += strlen($m[0]) - 1;
+ break;
+ case '//':
+ $skip = strpos($text, "\n", $count);
+ if ($skip === false) $skip = strlen($text) - $count;
+ else $skip -= $count;
+ break;
+ case '/*':
+ if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
+ $skip = strlen($m[0]);
+ $newlines = substr_count($m[0], "\n");
+ }
+ break;
+ }
+
+ if ($skip == 0) $count += strlen($min[0]);
+
+ $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
+ $text = substr($text, $count + $skip);
+
+ $min = null;
+ }
+
+ return $out.$text;
+ }
+
+}
+
+class lessc_formatter_classic {
+ public $indentChar = " ";
+
+ public $break = "\n";
+ public $open = " {";
+ public $close = "}";
+ public $selectorSeparator = ", ";
+ public $assignSeparator = ":";
+
+ public $openSingle = " { ";
+ public $closeSingle = " }";
+
+ public $disableSingle = false;
+ public $breakSelectors = false;
+
+ public $compressColors = false;
+
+ public function __construct() {
+ $this->indentLevel = 0;
+ }
+
+ public function indentStr($n = 0) {
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+ }
+
+ public function property($name, $value) {
+ return $name . $this->assignSeparator . $value . ";";
+ }
+
+ protected function isEmpty($block) {
+ if (empty($block->lines)) {
+ foreach ($block->children as $child) {
+ if (!$this->isEmpty($child)) return false;
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ public function block($block) {
+ if ($this->isEmpty($block)) return;
+
+ $inner = $pre = $this->indentStr();
+
+ $isSingle = !$this->disableSingle &&
+ is_null($block->type) && count($block->lines) == 1;
+
+ if (!empty($block->selectors)) {
+ $this->indentLevel++;
+
+ if ($this->breakSelectors) {
+ $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
+ } else {
+ $selectorSeparator = $this->selectorSeparator;
+ }
+
+ echo $pre .
+ implode($selectorSeparator, $block->selectors);
+ if ($isSingle) {
+ echo $this->openSingle;
+ $inner = "";
+ } else {
+ echo $this->open . $this->break;
+ $inner = $this->indentStr();
+ }
+
+ }
+
+ if (!empty($block->lines)) {
+ $glue = $this->break.$inner;
+ echo $inner . implode($glue, $block->lines);
+ if (!$isSingle && !empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ foreach ($block->children as $child) {
+ $this->block($child);
+ }
+
+ if (!empty($block->selectors)) {
+ if (!$isSingle && empty($block->children)) echo $this->break;
+
+ if ($isSingle) {
+ echo $this->closeSingle . $this->break;
+ } else {
+ echo $pre . $this->close . $this->break;
+ }
+
+ $this->indentLevel--;
+ }
+ }
+}
+
+class lessc_formatter_compressed extends lessc_formatter_classic {
+ public $disableSingle = true;
+ public $open = "{";
+ public $selectorSeparator = ",";
+ public $assignSeparator = ":";
+ public $break = "";
+ public $compressColors = true;
+
+ public function indentStr($n = 0) {
+ return "";
+ }
+}
+
+class lessc_formatter_lessjs extends lessc_formatter_classic {
+ public $disableSingle = true;
+ public $breakSelectors = true;
+ public $assignSeparator = ": ";
+ public $selectorSeparator = ",";
+}
+
+
diff --git a/includes/limit.sh b/includes/limit.sh
new file mode 100644
index 00000000..2a1545b6
--- /dev/null
+++ b/includes/limit.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+#
+# Resource limiting wrapper for command execution
+#
+# Why is this in shell script? Because bash has a setrlimit() wrapper
+# and is available on most Linux systems. If Perl was distributed with
+# BSD::Resource included, we would happily use that instead, but it isn't.
+
+MW_INCLUDE_STDERR=
+MW_CPU_LIMIT=0
+MW_CGROUP=
+MW_MEM_LIMIT=0
+MW_FILE_SIZE_LIMIT=0
+MW_WALL_CLOCK_LIMIT=0
+
+# Override settings
+eval "$2"
+
+if [ -n "$MW_INCLUDE_STDERR" ]; then
+ exec 2>&1
+fi
+
+if [ "$MW_CPU_LIMIT" -gt 0 ]; then
+ ulimit -t "$MW_CPU_LIMIT"
+fi
+if [ "$MW_MEM_LIMIT" -gt 0 ]; then
+ if [ -n "$MW_CGROUP" ]; then
+ # Create cgroup
+ if ! mkdir -m 0700 "$MW_CGROUP"/$$; then
+ echo "limit.sh: failed to create the cgroup." 1>&2
+ exit 1
+ fi
+ echo $$ > "$MW_CGROUP"/$$/tasks
+ if [ -n "$MW_CGROUP_NOTIFY" ]; then
+ echo "1" > "$MW_CGROUP"/$$/notify_on_release
+ fi
+ # Memory
+ echo $(($MW_MEM_LIMIT*1024)) > "$MW_CGROUP"/$$/memory.limit_in_bytes
+ # Memory+swap
+ echo $(($MW_MEM_LIMIT*1024)) > "$MW_CGROUP"/$$/memory.memsw.limit_in_bytes
+ else
+ ulimit -v "$MW_MEM_LIMIT"
+ fi
+else
+ MW_CGROUP=""
+fi
+if [ "$MW_FILE_SIZE_LIMIT" -gt 0 ]; then
+ ulimit -f "$MW_FILE_SIZE_LIMIT"
+fi
+if [ "$MW_WALL_CLOCK_LIMIT" -gt 0 -a -x "/usr/bin/timeout" ]; then
+ /usr/bin/timeout $MW_WALL_CLOCK_LIMIT /bin/bash -c "$1"
+ STATUS="$?"
+ if [ "$STATUS" == 124 ]; then
+ echo "limit.sh: timed out." 1>&2
+ fi
+else
+ eval "$1"
+ STATUS="$?"
+fi
+
+# Clean up cgroup
+cleanup() {
+ # First we have to move the current task into a "garbage" group, otherwise
+ # the cgroup will not be empty, and attempting to remove it will fail with
+ # "Device or resource busy"
+ if [ -w "$MW_CGROUP"/tasks ]; then
+ GARBAGE="$MW_CGROUP"
+ else
+ GARBAGE="$MW_CGROUP"/garbage-"$USER"
+ if [ ! -e "$GARBAGE" ]; then
+ mkdir -m 0700 "$GARBAGE"
+ fi
+ fi
+ echo $BASHPID > "$GARBAGE"/tasks
+
+ # Suppress errors in case the cgroup has disappeared due to a release script
+ rmdir "$MW_CGROUP"/$$ 2>/dev/null
+}
+
+updateTaskCount() {
+ # There are lots of ways to count lines in a file in shell script, but this
+ # is one of the few that doesn't create another process, which would
+ # increase the returned number of tasks.
+ readarray < "$MW_CGROUP"/$$/tasks
+ NUM_TASKS=${#MAPFILE[*]}
+}
+
+if [ -n "$MW_CGROUP" ]; then
+ updateTaskCount
+
+ if [ $NUM_TASKS -gt 1 ]; then
+ # Spawn a monitor process which will continue to poll for completion
+ # of all processes in the cgroup after termination of the parent shell
+ (
+ while [ $NUM_TASKS -gt 1 ]; do
+ sleep 10
+ updateTaskCount
+ done
+ cleanup
+ ) >&/dev/null < /dev/null &
+ disown -a
+ else
+ cleanup
+ fi
+fi
+exit "$STATUS"
+
diff --git a/includes/logging/DeleteLogFormatter.php b/includes/logging/DeleteLogFormatter.php
new file mode 100644
index 00000000..01528b7e
--- /dev/null
+++ b/includes/logging/DeleteLogFormatter.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * Formatter for delete log entries.
+ *
+ * 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
+ * @author Niklas Laxström
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @since 1.22
+ */
+
+/**
+ * This class formats delete log entries.
+ *
+ * @since 1.19
+ */
+class DeleteLogFormatter extends LogFormatter {
+ protected function getMessageKey() {
+ $key = parent::getMessageKey();
+ if ( in_array( $this->entry->getSubtype(), array( 'event', 'revision' ) ) ) {
+ if ( count( $this->getMessageParameters() ) < 5 ) {
+ return "$key-legacy";
+ }
+ }
+ return $key;
+ }
+
+ protected function getMessageParameters() {
+ if ( isset( $this->parsedParametersDeleteLog ) ) {
+ return $this->parsedParametersDeleteLog;
+ }
+
+ $params = parent::getMessageParameters();
+ $subtype = $this->entry->getSubtype();
+ if ( in_array( $subtype, array( 'event', 'revision' ) ) ) {
+ // $params[3] here is 'revision' for page revisions, 'oldimage' for file versions, or a comma-separated list of log_ids for log entries.
+ // $subtype here is 'revision' for page revisions and file versions, or 'event' for log entries.
+ if (
+ ( $subtype === 'event' && count( $params ) === 6 ) ||
+ ( $subtype === 'revision' && isset( $params[3] ) && ( $params[3] === 'revision' || $params[3] === 'oldimage' ) )
+ ) {
+ $paramStart = $subtype === 'revision' ? 4 : 3;
+
+ $old = $this->parseBitField( $params[$paramStart + 1] );
+ $new = $this->parseBitField( $params[$paramStart + 2] );
+ list( $hid, $unhid, $extra ) = RevisionDeleter::getChanges( $new, $old );
+ $changes = array();
+ foreach ( $hid as $v ) {
+ $changes[] = $this->msg( "$v-hid" )->plain();
+ }
+ foreach ( $unhid as $v ) {
+ $changes[] = $this->msg( "$v-unhid" )->plain();
+ }
+ foreach ( $extra as $v ) {
+ $changes[] = $this->msg( $v )->plain();
+ }
+ $changeText = $this->context->getLanguage()->listToText( $changes );
+
+ $newParams = array_slice( $params, 0, 3 );
+ $newParams[3] = $changeText;
+ $count = count( explode( ',', $params[$paramStart] ) );
+ $newParams[4] = $this->context->getLanguage()->formatNum( $count );
+ return $this->parsedParametersDeleteLog = $newParams;
+ } else {
+ return $this->parsedParametersDeleteLog = array_slice( $params, 0, 3 );
+ }
+ }
+
+ return $this->parsedParametersDeleteLog = $params;
+ }
+
+ protected function parseBitField( $string ) {
+ // Input is like ofield=2134 or just the number
+ if ( strpos( $string, 'field=' ) === 1 ) {
+ list( , $field ) = explode( '=', $string );
+ return (int)$field;
+ } else {
+ return (int)$string;
+ }
+ }
+
+ public function getActionLinks() {
+ $user = $this->context->getUser();
+ if ( !$user->isAllowed( 'deletedhistory' ) || $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
+ return '';
+ }
+
+ switch ( $this->entry->getSubtype() ) {
+ case 'delete': // Show undelete link
+ if ( $user->isAllowed( 'undelete' ) ) {
+ $message = 'undeletelink';
+ } else {
+ $message = 'undeleteviewlink';
+ }
+ $revert = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Undelete' ),
+ $this->msg( $message )->escaped(),
+ array(),
+ array( 'target' => $this->entry->getTarget()->getPrefixedDBkey() )
+ );
+ return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
+
+ case 'revision': // If an edit was hidden from a page give a review link to the history
+ $params = $this->extractParameters();
+ if ( !isset( $params[3] ) || !isset( $params[4] ) ) {
+ return '';
+ }
+
+ // Different revision types use different URL params...
+ $key = $params[3];
+ // This is a CSV of the IDs
+ $ids = explode( ',', $params[4] );
+
+ $links = array();
+
+ // If there's only one item, we can show a diff link
+ if ( count( $ids ) == 1 ) {
+ // Live revision diffs...
+ if ( $key == 'oldid' || $key == 'revision' ) {
+ $links[] = Linker::linkKnown(
+ $this->entry->getTarget(),
+ $this->msg( 'diff' )->escaped(),
+ array(),
+ array(
+ 'diff' => intval( $ids[0] ),
+ 'unhide' => 1
+ )
+ );
+ // Deleted revision diffs...
+ } elseif ( $key == 'artimestamp' || $key == 'archive' ) {
+ $links[] = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Undelete' ),
+ $this->msg( 'diff' )->escaped(),
+ array(),
+ array(
+ 'target' => $this->entry->getTarget()->getPrefixedDBkey(),
+ 'diff' => 'prev',
+ 'timestamp' => $ids[0]
+ )
+ );
+ }
+ }
+
+ // View/modify link...
+ $links[] = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Revisiondelete' ),
+ $this->msg( 'revdel-restore' )->escaped(),
+ array(),
+ array(
+ 'target' => $this->entry->getTarget()->getPrefixedText(),
+ 'type' => $key,
+ 'ids' => implode( ',', $ids ),
+ )
+ );
+
+ return $this->msg( 'parentheses' )->rawParams(
+ $this->context->getLanguage()->pipeList( $links ) )->escaped();
+
+ case 'event': // Hidden log items, give review link
+ $params = $this->extractParameters();
+ if ( !isset( $params[3] ) ) {
+ return '';
+ }
+ // This is a CSV of the IDs
+ $query = $params[3];
+ // Link to each hidden object ID, $params[1] is the url param
+ $revert = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Revisiondelete' ),
+ $this->msg( 'revdel-restore' )->escaped(),
+ array(),
+ array(
+ 'target' => $this->entry->getTarget()->getPrefixedText(),
+ 'type' => 'logging',
+ 'ids' => $query
+ )
+ );
+ return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
+ default:
+ return '';
+ }
+ }
+}
diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php
index 37560d80..b2a8e50d 100644
--- a/includes/logging/LogEntry.php
+++ b/includes/logging/LogEntry.php
@@ -151,7 +151,7 @@ class DatabaseLogEntry extends LogEntryBase {
return array(
'tables' => $tables,
'fields' => $fields,
- 'conds' => array(),
+ 'conds' => array(),
'options' => array(),
'join_conds' => $joins,
);
@@ -165,7 +165,7 @@ class DatabaseLogEntry extends LogEntryBase {
*/
public static function newFromRow( $row ) {
if ( is_array( $row ) && isset( $row['rc_logid'] ) ) {
- return new RCDatabaseLogEntry( (object) $row );
+ return new RCDatabaseLogEntry( (object)$row );
} else {
return new self( $row );
}
@@ -175,6 +175,7 @@ class DatabaseLogEntry extends LogEntryBase {
/// Database result row.
protected $row;
+ protected $performer;
protected function __construct( $row ) {
$this->row = $row;
@@ -232,17 +233,20 @@ class DatabaseLogEntry extends LogEntryBase {
}
public function getPerformer() {
- $userId = (int) $this->row->log_user;
- if ( $userId !== 0 ) { // logged-in users
- if ( isset( $this->row->user_name ) ) {
- return User::newFromRow( $this->row );
- } else {
- return User::newFromId( $userId );
+ if ( !$this->performer ) {
+ $userId = (int)$this->row->log_user;
+ if ( $userId !== 0 ) { // logged-in users
+ if ( isset( $this->row->user_name ) ) {
+ $this->performer = User::newFromRow( $this->row );
+ } else {
+ $this->performer = User::newFromId( $userId );
+ }
+ } else { // IP users
+ $userText = $this->row->log_user_text;
+ $this->performer = User::newFromName( $userText, false );
}
- } else { // IP users
- $userText = $this->row->log_user_text;
- return User::newFromName( $userText, false );
}
+ return $this->performer;
}
public function getTarget() {
@@ -287,14 +291,17 @@ class RCDatabaseLogEntry extends DatabaseLogEntry {
}
public function getPerformer() {
- $userId = (int) $this->row->rc_user;
- if ( $userId !== 0 ) {
- return User::newFromId( $userId );
- } else {
- $userText = $this->row->rc_user_text;
- // Might be an IP, don't validate the username
- return User::newFromName( $userText, false );
+ if ( !$this->performer ) {
+ $userId = (int)$this->row->rc_user;
+ if ( $userId !== 0 ) {
+ $this->performer = User::newFromId( $userId );
+ } else {
+ $userText = $this->row->rc_user_text;
+ // Might be an IP, don't validate the username
+ $this->performer = User::newFromName( $userText, false );
+ }
}
+ return $this->performer;
}
public function getTarget() {
@@ -327,6 +334,7 @@ class ManualLogEntry extends LogEntryBase {
protected $type; ///!< @var string
protected $subtype; ///!< @var string
protected $parameters = array(); ///!< @var array
+ protected $relations = array(); ///!< @var array
protected $performer; ///!< @var User
protected $target; ///!< @var Title
protected $timestamp; ///!< @var string
@@ -335,9 +343,9 @@ class ManualLogEntry extends LogEntryBase {
/**
* Constructor.
- *
+ *
* @since 1.19
- *
+ *
* @param string $type
* @param string $subtype
*/
@@ -357,20 +365,31 @@ class ManualLogEntry extends LogEntryBase {
* '4:color' => 'blue',
* 'animal' => 'dog'
* );
- *
+ *
* @since 1.19
- *
- * @param $parameters array Associative array
+ *
+ * @param array $parameters Associative array
*/
public function setParameters( $parameters ) {
$this->parameters = $parameters;
}
/**
+ * Declare arbitrary tag/value relations to this log entry.
+ * These can be used to filter log entries later on.
+ *
+ * @param array Map of (tag => (list of values))
+ * @since 1.22
+ */
+ public function setRelations( array $relations ) {
+ $this->relations = $relations;
+ }
+
+ /**
* Set the user that performed the action being logged.
- *
+ *
* @since 1.19
- *
+ *
* @param User $performer
*/
public function setPerformer( User $performer ) {
@@ -379,9 +398,9 @@ class ManualLogEntry extends LogEntryBase {
/**
* Set the title of the object changed.
- *
+ *
* @since 1.19
- *
+ *
* @param Title $target
*/
public function setTarget( Title $target ) {
@@ -390,9 +409,9 @@ class ManualLogEntry extends LogEntryBase {
/**
* Set the timestamp of when the logged action took place.
- *
+ *
* @since 1.19
- *
+ *
* @param string $timestamp
*/
public function setTimestamp( $timestamp ) {
@@ -401,9 +420,9 @@ class ManualLogEntry extends LogEntryBase {
/**
* Set a comment associated with the action being logged.
- *
+ *
* @since 1.19
- *
+ *
* @param string $comment
*/
public function setComment( $comment ) {
@@ -412,9 +431,9 @@ class ManualLogEntry extends LogEntryBase {
/**
* TODO: document
- *
+ *
* @since 1.19
- *
+ *
* @param integer $deleted
*/
public function setDeleted( $deleted ) {
@@ -423,20 +442,24 @@ class ManualLogEntry extends LogEntryBase {
/**
* Inserts the entry into the logging table.
+ * @param IDatabase $dbw
* @return int If of the log entry
*/
- public function insert() {
+ public function insert( IDatabase $dbw = null ) {
global $wgContLang;
- $dbw = wfGetDB( DB_MASTER );
+ $dbw = $dbw ?: wfGetDB( DB_MASTER );
$id = $dbw->nextSequenceValue( 'logging_log_id_seq' );
if ( $this->timestamp === null ) {
$this->timestamp = wfTimestampNow();
}
+ # Trim spaces on user supplied text
+ $comment = trim( $this->getComment() );
+
# Truncate for whole multibyte characters.
- $comment = $wgContLang->truncate( $this->getComment(), 255 );
+ $comment = $wgContLang->truncate( $comment, 255 );
$data = array(
'log_id' => $id,
@@ -449,17 +472,35 @@ class ManualLogEntry extends LogEntryBase {
'log_title' => $this->getTarget()->getDBkey(),
'log_page' => $this->getTarget()->getArticleID(),
'log_comment' => $comment,
- 'log_params' => serialize( (array) $this->getParameters() ),
+ 'log_params' => serialize( (array)$this->getParameters() ),
);
$dbw->insert( 'logging', $data, __METHOD__ );
$this->id = !is_null( $id ) ? $id : $dbw->insertId();
+
+ $rows = array();
+ foreach ( $this->relations as $tag => $values ) {
+ if ( !strlen( $tag ) ) {
+ throw new MWException( "Got empty log search tag." );
+ }
+ foreach ( $values as $value ) {
+ $rows[] = array(
+ 'ls_field' => $tag,
+ 'ls_value' => $value,
+ 'ls_log_id' => $this->id
+ );
+ }
+ }
+ if ( count( $rows ) ) {
+ $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' );
+ }
+
return $this->id;
}
/**
* Publishes the log entry.
- * @param $newId int id of the log entry.
- * @param $to string: rcandudp (default), rc, udp
+ * @param int $newId id of the log entry.
+ * @param string $to rcandudp (default), rc, udp
*/
public function publish( $newId, $to = 'rcandudp' ) {
$log = new LogPage( $this->getType() );
@@ -493,7 +534,7 @@ class ManualLogEntry extends LogEntryBase {
$this->getSubtype(),
$this->getTarget(),
$this->getComment(),
- serialize( (array) $this->getParameters() ),
+ serialize( (array)$this->getParameters() ),
$newId,
$formatter->getIRCActionComment() // Used for IRC feeds
);
@@ -503,7 +544,7 @@ class ManualLogEntry extends LogEntryBase {
}
if ( $to === 'udp' || $to === 'rcandudp' ) {
- $rc->notifyRC2UDP();
+ $rc->notifyRCFeeds();
}
}
@@ -545,7 +586,7 @@ class ManualLogEntry extends LogEntryBase {
}
public function getDeleted() {
- return (int) $this->deleted;
+ return (int)$this->deleted;
}
}
diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php
index 4de1a974..c27b57af 100644
--- a/includes/logging/LogEventsList.php
+++ b/includes/logging/LogEventsList.php
@@ -42,7 +42,7 @@ class LogEventsList extends ContextSource {
*
* @param $context IContextSource Context to use; formerly it was Skin object.
* @param $unused void Unused; used to be an OutputPage object.
- * @param $flags int flags; can be a combinaison of self::NO_ACTION_LINK,
+ * @param int $flags flags; can be a combinaison of self::NO_ACTION_LINK,
* self::NO_EXTRA_USER_LINKS or self::USE_REVDEL_CHECKBOXES.
*/
public function __construct( $context, $unused = null, $flags = 0 ) {
@@ -74,9 +74,9 @@ class LogEventsList extends ContextSource {
public function showHeader( $type ) {
wfDeprecated( __METHOD__, '1.19' );
// If only one log type is used, then show a special message...
- $headerType = (count($type) == 1) ? $type[0] : '';
+ $headerType = count( $type ) == 1 ? $type[0] : '';
$out = $this->getOutput();
- if( LogPage::isLogType( $headerType ) ) {
+ if ( LogPage::isLogType( $headerType ) ) {
$page = new LogPage( $headerType );
$out->setPageTitle( $page->getName()->text() );
$out->addHTML( $page->getDescription()->parseAsBlock() );
@@ -88,7 +88,7 @@ class LogEventsList extends ContextSource {
/**
* Show options for the log list
*
- * @param $types string or Array
+ * @param string $types or Array
* @param $user String
* @param $page String
* @param $pattern String
@@ -97,14 +97,14 @@ class LogEventsList extends ContextSource {
* @param $filter: array
* @param $tagFilter: array?
*/
- public function showOptions( $types=array(), $user='', $page='', $pattern='', $year='',
- $month = '', $filter = null, $tagFilter='' ) {
+ public function showOptions( $types = array(), $user = '', $page = '', $pattern = '', $year = '',
+ $month = '', $filter = null, $tagFilter = '' ) {
global $wgScript, $wgMiserMode;
$title = SpecialPage::getTitleFor( 'Log' );
// For B/C, we take strings, but make sure they are converted...
- $types = ($types === '') ? array() : (array)$types;
+ $types = ( $types === '' ) ? array() : (array)$types;
$tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
@@ -117,7 +117,7 @@ class LogEventsList extends ContextSource {
$html .= $this->getExtraInputs( $types ) . "\n";
// Title pattern, if allowed
- if (!$wgMiserMode) {
+ if ( !$wgMiserMode ) {
$html .= $this->getTitlePattern( $pattern ) . "\n";
}
@@ -125,12 +125,12 @@ class LogEventsList extends ContextSource {
$html .= Xml::tags( 'p', null, Xml::dateMenu( $year, $month ) );
// Tag filter
- if ($tagSelector) {
+ if ( $tagSelector ) {
$html .= Xml::tags( 'p', null, implode( '&#160;', $tagSelector ) );
}
// Filter links
- if ($filter) {
+ if ( $filter ) {
$html .= Xml::tags( 'p', null, $this->getFilterLinks( $filter ) );
}
@@ -156,13 +156,13 @@ class LogEventsList extends ContextSource {
// Option value -> message mapping
$links = array();
$hiddens = ''; // keep track for "go" button
- foreach( $filter as $type => $val ) {
+ foreach ( $filter as $type => $val ) {
// Should the below assignment be outside the foreach?
// Then it would have to be copied. Not certain what is more expensive.
$query = $this->getDefaultQuery();
$queryKey = "hide_{$type}_log";
- $hideVal = 1 - intval($val);
+ $hideVal = 1 - intval( $val );
$query[$queryKey] = $hideVal;
$link = Linker::linkKnown(
@@ -172,11 +172,12 @@ class LogEventsList extends ContextSource {
$query
);
+ // Message: log-show-hide-patrol
$links[$type] = $this->msg( "log-show-hide-{$type}" )->rawParams( $link )->escaped();
$hiddens .= Html::hidden( "hide_{$type}_log", $val ) . "\n";
}
// Build links
- return '<small>'.$this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens;
+ return '<small>' . $this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens;
}
private function getDefaultQuery() {
@@ -198,7 +199,7 @@ class LogEventsList extends ContextSource {
* @return String: Formatted HTML
*/
private function getTypeMenu( $queryTypes ) {
- $queryType = count($queryTypes) == 1 ? $queryTypes[0] : '';
+ $queryType = count( $queryTypes ) == 1 ? $queryTypes[0] : '';
$selector = $this->getTypeSelector();
$selector->setDefault( $queryType );
return $selector->getHtml();
@@ -212,7 +213,7 @@ class LogEventsList extends ContextSource {
public function getTypeSelector() {
$typesByName = array(); // Temporary array
// First pass to load the log names
- foreach( LogPage::validTypes() as $type ) {
+ foreach ( LogPage::validTypes() as $type ) {
$page = new LogPage( $type );
$restriction = $page->getRestriction();
if ( $this->getUser()->isAllowed( $restriction ) ) {
@@ -221,7 +222,7 @@ class LogEventsList extends ContextSource {
}
// Second pass to sort by name
- asort($typesByName);
+ asort( $typesByName );
// Always put "All public logs" on top
$public = $typesByName[''];
@@ -229,7 +230,7 @@ class LogEventsList extends ContextSource {
$typesByName = array( '' => $public ) + $typesByName;
$select = new XmlSelect( 'type' );
- foreach( $typesByName as $type => $name ) {
+ foreach ( $typesByName as $type => $name ) {
$select->addOption( $name, $type );
}
@@ -273,10 +274,10 @@ class LogEventsList extends ContextSource {
private function getExtraInputs( $types ) {
$offender = $this->getRequest()->getVal( 'offender' );
$user = User::newFromName( $offender, false );
- if( !$user || ($user->getId() == 0 && !IP::isIPAddress($offender) ) ) {
+ if ( !$user || ( $user->getId() == 0 && !IP::isIPAddress( $offender ) ) ) {
$offender = ''; // Blank field if invalid
}
- if( count($types) == 1 && $types[0] == 'suppress' ) {
+ if ( count( $types ) == 1 && $types[0] == 'suppress' ) {
return Xml::inputLabel( $this->msg( 'revdelete-offender' )->text(), 'offender',
'mw-log-offender', 20, $offender );
}
@@ -307,7 +308,6 @@ class LogEventsList extends ContextSource {
$formatter->setContext( $this->getContext() );
$formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
- $title = $entry->getTarget();
$time = htmlspecialchars( $this->getLanguage()->userTimeAndDate(
$entry->getTimestamp(), $this->getUser() ) );
@@ -343,17 +343,17 @@ class LogEventsList extends ContextSource {
* @return string
*/
private function getShowHideLinks( $row ) {
- if( ( $this->flags == self::NO_ACTION_LINK ) // we don't want to see the links
+ if ( ( $this->flags == self::NO_ACTION_LINK ) // we don't want to see the links
|| $row->log_type == 'suppress' ) { // no one can hide items from the suppress log
return '';
}
$del = '';
$user = $this->getUser();
// Don't show useless checkbox to people who cannot hide log entries
- if( $user->isAllowed( 'deletedhistory' ) ) {
- if( $row->log_deleted || $user->isAllowed( 'deletelogentry' ) ) {
- $canHide = $user->isAllowed( 'deletelogentry' );
- if ( $this->flags & self::USE_REVDEL_CHECKBOXES ) { // Show checkboxes instead of links.
+ if ( $user->isAllowed( 'deletedhistory' ) ) {
+ $canHide = $user->isAllowed( 'deletelogentry' );
+ if ( $row->log_deleted || $canHide ) {
+ if ( $canHide && $this->flags & self::USE_REVDEL_CHECKBOXES ) { // Show checkboxes instead of links.
if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { // If event was hidden from sysops
$del = Xml::check( 'deleterevisions', false, array( 'disabled' => 'disabled' ) );
} else {
@@ -365,8 +365,8 @@ class LogEventsList extends ContextSource {
} else {
$query = array(
'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
- 'type' => 'logging',
- 'ids' => $row->log_id,
+ 'type' => 'logging',
+ 'ids' => $row->log_id,
);
$del = Linker::revDeleteLink( $query, self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide );
}
@@ -383,13 +383,13 @@ class LogEventsList extends ContextSource {
* @param $right string
* @return Boolean
*/
- public static function typeAction( $row, $type, $action, $right='' ) {
- $match = is_array($type) ?
+ public static function typeAction( $row, $type, $action, $right = '' ) {
+ $match = is_array( $type ) ?
in_array( $row->log_type, $type ) : $row->log_type == $type;
- if( $match ) {
+ if ( $match ) {
$match = is_array( $action ) ?
in_array( $row->log_action, $action ) : $row->log_action == $action;
- if( $match && $right ) {
+ if ( $match && $right ) {
global $wgUser;
$match = $wgUser->isAllowed( $right );
}
@@ -420,7 +420,7 @@ class LogEventsList extends ContextSource {
* @return Boolean
*/
public static function userCanBitfield( $bitfield, $field, User $user = null ) {
- if( $bitfield & $field ) {
+ if ( $bitfield & $field ) {
if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
$permission = 'suppressrevision';
} else {
@@ -450,10 +450,10 @@ class LogEventsList extends ContextSource {
* Show log extract. Either with text and a box (set $msgKey) or without (don't set $msgKey)
*
* @param $out OutputPage|String-by-reference
- * @param $types String|Array Log types to show
- * @param $page String|Title The page title to show log entries for
- * @param $user String The user who made the log entries
- * @param $param array Associative Array with the following additional options:
+ * @param string|array $types Log types to show
+ * @param string|Title $page The page title to show log entries for
+ * @param string $user The user who made the log entries
+ * @param array $param Associative Array with the following additional options:
* - lim Integer Limit of items to show, default is 50
* - conds Array Extra conditions for the query (e.g. "log_action != 'revision'")
* - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty
@@ -465,18 +465,20 @@ class LogEventsList extends ContextSource {
* set to '' to unset offset
* - wrap String Wrap the message in html (usually something like "<div ...>$1</div>").
* - flags Integer display flags (NO_ACTION_LINK,NO_EXTRA_USER_LINKS)
+ * - useRequestParams boolean Set true to use Pager-related parameters in the WebRequest
* @return Integer Number of total log items (not limited by $lim)
*/
public static function showLogExtract(
- &$out, $types=array(), $page='', $user='', $param = array()
+ &$out, $types = array(), $page = '', $user = '', $param = array()
) {
$defaultParameters = array(
'lim' => 25,
'conds' => array(),
'showIfEmpty' => true,
- 'msgKey' => array(''),
+ 'msgKey' => array( '' ),
'wrap' => "$1",
- 'flags' => 0
+ 'flags' => 0,
+ 'useRequestParams' => false,
);
# The + operator appends elements of remaining keys from the right
# handed array to the left handed, whereas duplicated keys are NOT overwritten.
@@ -488,6 +490,7 @@ class LogEventsList extends ContextSource {
$msgKey = $param['msgKey'];
$wrap = $param['wrap'];
$flags = $param['flags'];
+ $useRequestParams = $param['useRequestParams'];
if ( !is_array( $msgKey ) ) {
$msgKey = array( $msgKey );
}
@@ -501,13 +504,22 @@ class LogEventsList extends ContextSource {
# Insert list of top 50 (or top $lim) items
$loglist = new LogEventsList( $context, null, $flags );
$pager = new LogPager( $loglist, $types, $user, $page, '', $conds );
+ if ( !$useRequestParams ) {
+ # Reset vars that may have been taken from the request
+ $pager->mLimit = 50;
+ $pager->mDefaultLimit = 50;
+ $pager->mOffset = "";
+ $pager->mIsBackwards = false;
+ }
if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
$pager->setOffset( $param['offset'] );
}
- if( $lim > 0 ) $pager->mLimit = $lim;
+ if ( $lim > 0 ) {
+ $pager->mLimit = $lim;
+ }
$logBody = $pager->getBody();
$s = '';
- if( $logBody ) {
+ if ( $logBody ) {
if ( $msgKey[0] ) {
$s = '<div class="mw-warning-with-logexcerpt">';
@@ -520,28 +532,29 @@ class LogEventsList extends ContextSource {
}
}
$s .= $loglist->beginLogEventsList() .
- $logBody .
- $loglist->endLogEventsList();
- } else {
- if ( $showIfEmpty ) {
- $s = Html::rawElement( 'div', array( 'class' => 'mw-warning-logempty' ),
- $context->msg( 'logempty' )->parse() );
- }
+ $logBody .
+ $loglist->endLogEventsList();
+ } elseif ( $showIfEmpty ) {
+ $s = Html::rawElement( 'div', array( 'class' => 'mw-warning-logempty' ),
+ $context->msg( 'logempty' )->parse() );
}
- if( $pager->getNumRows() > $pager->mLimit ) { # Show "Full log" link
+ if ( $pager->getNumRows() > $pager->mLimit ) { # Show "Full log" link
$urlParam = array();
if ( $page instanceof Title ) {
$urlParam['page'] = $page->getPrefixedDBkey();
} elseif ( $page != '' ) {
$urlParam['page'] = $page;
}
- if ( $user != '')
+ if ( $user != '' ) {
$urlParam['user'] = $user;
- if ( !is_array( $types ) ) # Make it an array, if it isn't
+ }
+ if ( !is_array( $types ) ) { # Make it an array, if it isn't
$types = array( $types );
+ }
# If there is exactly one log type, we can link to Special:Log?type=foo
- if ( count( $types ) == 1 )
+ if ( count( $types ) == 1 ) {
$urlParam['type'] = $types[0];
+ }
$s .= Linker::link(
SpecialPage::getTitleFor( 'Log' ),
$context->msg( 'log-fulllog' )->escaped(),
@@ -560,7 +573,7 @@ class LogEventsList extends ContextSource {
/* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
if ( wfRunHooks( 'LogEventsListShowLogExtract', array( &$s, $types, $page, $user, $param ) ) ) {
// $out can be either an OutputPage object or a String-by-reference
- if ( $out instanceof OutputPage ){
+ if ( $out instanceof OutputPage ) {
$out->addHTML( $s );
} else {
$out = $s;
@@ -575,24 +588,31 @@ class LogEventsList extends ContextSource {
*
* @param $db DatabaseBase
* @param $audience string, public/user
+ * @param $user User object to check, or null to use $wgUser
* @return Mixed: string or false
*/
- public static function getExcludeClause( $db, $audience = 'public' ) {
- global $wgLogRestrictions, $wgUser;
+ public static function getExcludeClause( $db, $audience = 'public', User $user = null ) {
+ global $wgLogRestrictions;
+
+ if ( $audience != 'public' && $user === null ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+
// Reset the array, clears extra "where" clauses when $par is used
$hiddenLogs = array();
+
// Don't show private logs to unprivileged users
- foreach( $wgLogRestrictions as $logType => $right ) {
- if( $audience == 'public' || !$wgUser->isAllowed($right) ) {
- $safeType = $db->strencode( $logType );
- $hiddenLogs[] = $safeType;
+ foreach ( $wgLogRestrictions as $logType => $right ) {
+ if ( $audience == 'public' || !$user->isAllowed( $right ) ) {
+ $hiddenLogs[] = $logType;
}
}
- if( count($hiddenLogs) == 1 ) {
+ if ( count( $hiddenLogs ) == 1 ) {
return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
- } elseif( $hiddenLogs ) {
- return 'log_type NOT IN (' . $db->makeList($hiddenLogs) . ')';
+ } elseif ( $hiddenLogs ) {
+ return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
}
return false;
}
- }
+}
diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php
index 7586bb65..8f60aee1 100644
--- a/includes/logging/LogFormatter.php
+++ b/includes/logging/LogFormatter.php
@@ -192,18 +192,18 @@ class LogFormatter {
$parameters = $entry->getParameters();
// @see LogPage::actionText()
// Text of title the action is aimed at.
- $target = $entry->getTarget()->getPrefixedText() ;
+ $target = $entry->getTarget()->getPrefixedText();
$text = null;
- switch( $entry->getType() ) {
+ switch ( $entry->getType() ) {
case 'move':
- switch( $entry->getSubtype() ) {
+ switch ( $entry->getSubtype() ) {
case 'move':
- $movesource = $parameters['4::target'];
+ $movesource = $parameters['4::target'];
$text = wfMessage( '1movedto2' )
->rawParams( $target, $movesource )->inContentLanguage()->escaped();
break;
case 'move_redir':
- $movesource = $parameters['4::target'];
+ $movesource = $parameters['4::target'];
$text = wfMessage( '1movedto2_redir' )
->rawParams( $target, $movesource )->inContentLanguage()->escaped();
break;
@@ -215,7 +215,7 @@ class LogFormatter {
break;
case 'delete':
- switch( $entry->getSubtype() ) {
+ switch ( $entry->getSubtype() ) {
case 'delete':
$text = wfMessage( 'deletedarticle' )
->rawParams( $target )->inContentLanguage()->escaped();
@@ -246,7 +246,7 @@ class LogFormatter {
break;
case 'protect':
- switch( $entry->getSubtype() ) {
+ switch ( $entry->getSubtype() ) {
case 'protect':
$text = wfMessage( 'protectedarticle' )
->rawParams( $target . ' ' . $parameters[0] )->inContentLanguage()->escaped();
@@ -263,13 +263,14 @@ class LogFormatter {
break;
case 'newusers':
- switch( $entry->getSubtype() ) {
+ switch ( $entry->getSubtype() ) {
case 'newusers':
case 'create':
$text = wfMessage( 'newuserlog-create-entry' )
->inContentLanguage()->escaped();
break;
case 'create2':
+ case 'byemail':
$text = wfMessage( 'newuserlog-create2-entry' )
->rawParams( $target )->inContentLanguage()->escaped();
break;
@@ -281,7 +282,7 @@ class LogFormatter {
break;
case 'upload':
- switch( $entry->getSubtype() ) {
+ switch ( $entry->getSubtype() ) {
case 'upload':
$text = wfMessage( 'uploadedimage' )
->rawParams( $target )->inContentLanguage()->escaped();
@@ -293,11 +294,33 @@ class LogFormatter {
}
break;
+ case 'rights':
+ if ( count( $parameters['4::oldgroups'] ) ) {
+ $oldgroups = implode( ', ', $parameters['4::oldgroups'] );
+ } else {
+ $oldgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
+ }
+ if ( count( $parameters['5::newgroups'] ) ) {
+ $newgroups = implode( ', ', $parameters['5::newgroups'] );
+ } else {
+ $newgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
+ }
+ switch ( $entry->getSubtype() ) {
+ case 'rights':
+ $text = wfMessage( 'rightslogentry' )
+ ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
+ break;
+ case 'autopromote':
+ $text = wfMessage( 'rightslogentry-autopromote' )
+ ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
+ break;
+ }
+ break;
// case 'suppress' --private log -- aaron (sign your messages so we know who to blame in a few years :-D)
// default:
}
- if( is_null( $text ) ) {
+ if ( is_null( $text ) ) {
$text = $this->getPlainActionText();
}
@@ -379,9 +402,11 @@ class LogFormatter {
// Filter out parameters which are not in format #:foo
foreach ( $entry->getParameters() as $key => $value ) {
- if ( strpos( $key, ':' ) === false ) continue;
- list( $index, $type, $name ) = explode( ':', $key, 3 );
- $params[$index - 1] = $value;
+ if ( strpos( $key, ':' ) === false ) {
+ continue;
+ }
+ list( $index, $type, ) = explode( ':', $key, 3 );
+ $params[$index - 1] = $this->formatParameterValue( $type, $value );
}
/* Message class doesn't like non consecutive numbering.
@@ -416,7 +441,7 @@ class LogFormatter {
$entry = $this->entry;
$params = $this->extractParameters();
$params[0] = Message::rawParam( $this->getPerformerElement() );
- $params[1] = $entry->getPerformer()->getName();
+ $params[1] = $this->canView( LogPage::DELETED_USER ) ? $entry->getPerformer()->getName() : '';
$params[2] = Message::rawParam( $this->makePageLink( $entry->getTarget() ) );
// Bad things happens if the numbers are not in correct order
@@ -425,10 +450,83 @@ class LogFormatter {
}
/**
+ * Formats parameters values dependent to their type
+ * @param string $type The type of the value.
+ * Valid are currently:
+ * * - (empty) or plain: The value is returned as-is
+ * * raw: The value will be added to the log message
+ * as raw parameter (e.g. no escaping)
+ * Use this only if there is no other working
+ * type like user-link or title-link
+ * * msg: The value is a message-key, the output is
+ * the message in user language
+ * * msg-content: The value is a message-key, the output
+ * is the message in content language
+ * * user: The value is a user name, e.g. for GENDER
+ * * user-link: The value is a user name, returns a
+ * link for the user
+ * * title: The value is a page title,
+ * returns name of page
+ * * title-link: The value is a page title,
+ * returns link to this page
+ * * number: Format value as number
+ * @param string $value The parameter value that should
+ * be formated
+ * @return string or Message::numParam or Message::rawParam
+ * Formated value
+ * @since 1.21
+ */
+ protected function formatParameterValue( $type, $value ) {
+ $saveLinkFlood = $this->linkFlood;
+
+ switch ( strtolower( trim( $type ) ) ) {
+ case 'raw':
+ $value = Message::rawParam( $value );
+ break;
+ case 'msg':
+ $value = $this->msg( $value )->text();
+ break;
+ case 'msg-content':
+ $value = $this->msg( $value )->inContentLanguage()->text();
+ break;
+ case 'number':
+ $value = Message::numParam( $value );
+ break;
+ case 'user':
+ $user = User::newFromName( $value );
+ $value = $user->getName();
+ break;
+ case 'user-link':
+ $this->setShowUserToolLinks( false );
+
+ $user = User::newFromName( $value );
+ $value = Message::rawParam( $this->makeUserLink( $user ) );
+
+ $this->setShowUserToolLinks( $saveLinkFlood );
+ break;
+ case 'title':
+ $title = Title::newFromText( $value );
+ $value = $title->getPrefixedText();
+ break;
+ case 'title-link':
+ $title = Title::newFromText( $value );
+ $value = Message::rawParam( $this->makePageLink( $title ) );
+ break;
+ case 'plain':
+ // Plain text, nothing to do
+ default:
+ // Catch other types and use the old behavior (return as-is)
+ }
+
+ return $value;
+ }
+
+ /**
* Helper to make a link to the page, taking the plaintext
* value in consideration.
* @param $title Title the page
- * @param $parameters array query parameters
+ * @param array $parameters query parameters
+ * @throws MWException
* @return String
*/
protected function makePageLink( Title $title = null, $parameters = array() ) {
@@ -492,7 +590,7 @@ class LogFormatter {
return $this->msg( $message )->text();
}
- $content = $this->msg( $message )->escaped();
+ $content = $this->msg( $message )->escaped();
$attribs = array( 'class' => 'history-deleted' );
return Html::rawElement( 'span', $attribs, $content );
}
@@ -547,6 +645,16 @@ class LogFormatter {
return array();
}
+ /**
+ * @return Output of getMessageParameters() for testing
+ */
+ public function getMessageParametersForTesting() {
+ // This function was added because getMessageParameters() is
+ // protected and a change from protected to public caused
+ // problems with extensions
+ return $this->getMessageParameters();
+ }
+
}
/**
@@ -607,7 +715,7 @@ class LegacyLogFormatter extends LogFormatter {
$performer = $this->getPerformerElement();
if ( !$this->irctext ) {
- $action = $performer . $this->msg( 'word-separator' )->text() . $action;
+ $action = $performer . $this->msg( 'word-separator' )->text() . $action;
}
return $action;
@@ -667,7 +775,7 @@ class LegacyLogFormatter extends LogFormatter {
return $this->msg( 'parentheses' )->rawParams(
$this->context->getLanguage()->pipeList( $links ) )->escaped();
// Show unmerge link
- } elseif( $type == 'merge' && $subtype == 'merge' ) {
+ } elseif ( $type == 'merge' && $subtype == 'merge' ) {
if ( !$this->context->getUser()->isAllowed( 'mergehistory' ) ) {
return '';
}
@@ -706,305 +814,3 @@ class LegacyLogFormatter extends LogFormatter {
}
}
-/**
- * This class formats move log entries.
- * @since 1.19
- */
-class MoveLogFormatter extends LogFormatter {
- public function getPreloadTitles() {
- $params = $this->extractParameters();
- return array( Title::newFromText( $params[3] ) );
- }
-
- protected function getMessageKey() {
- $key = parent::getMessageKey();
- $params = $this->getMessageParameters();
- if ( isset( $params[4] ) && $params[4] === '1' ) {
- $key .= '-noredirect';
- }
- return $key;
- }
-
- protected function getMessageParameters() {
- $params = parent::getMessageParameters();
- $oldname = $this->makePageLink( $this->entry->getTarget(), array( 'redirect' => 'no' ) );
- $newname = $this->makePageLink( Title::newFromText( $params[3] ) );
- $params[2] = Message::rawParam( $oldname );
- $params[3] = Message::rawParam( $newname );
- return $params;
- }
-
- public function getActionLinks() {
- if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden
- || $this->entry->getSubtype() !== 'move'
- || !$this->context->getUser()->isAllowed( 'move' ) )
- {
- return '';
- }
-
- $params = $this->extractParameters();
- $destTitle = Title::newFromText( $params[3] );
- if ( !$destTitle ) {
- return '';
- }
-
- $revert = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Movepage' ),
- $this->msg( 'revertmove' )->escaped(),
- array(),
- array(
- 'wpOldTitle' => $destTitle->getPrefixedDBkey(),
- 'wpNewTitle' => $this->entry->getTarget()->getPrefixedDBkey(),
- 'wpReason' => $this->msg( 'revertmove' )->inContentLanguage()->text(),
- 'wpMovetalk' => 0
- )
- );
- return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
- }
-}
-
-/**
- * This class formats delete log entries.
- * @since 1.19
- */
-class DeleteLogFormatter extends LogFormatter {
- protected function getMessageKey() {
- $key = parent::getMessageKey();
- if ( in_array( $this->entry->getSubtype(), array( 'event', 'revision' ) ) ) {
- if ( count( $this->getMessageParameters() ) < 5 ) {
- return "$key-legacy";
- }
- }
- return $key;
- }
-
- protected function getMessageParameters() {
- if ( isset( $this->parsedParametersDeleteLog ) ) {
- return $this->parsedParametersDeleteLog;
- }
-
- $params = parent::getMessageParameters();
- $subtype = $this->entry->getSubtype();
- if ( in_array( $subtype, array( 'event', 'revision' ) ) ) {
- if (
- ($subtype === 'event' && count( $params ) === 6 ) ||
- ($subtype === 'revision' && isset( $params[3] ) && $params[3] === 'revision' )
- ) {
- $paramStart = $subtype === 'revision' ? 4 : 3;
-
- $old = $this->parseBitField( $params[$paramStart+1] );
- $new = $this->parseBitField( $params[$paramStart+2] );
- list( $hid, $unhid, $extra ) = RevisionDeleter::getChanges( $new, $old );
- $changes = array();
- foreach ( $hid as $v ) {
- $changes[] = $this->msg( "$v-hid" )->plain();
- }
- foreach ( $unhid as $v ) {
- $changes[] = $this->msg( "$v-unhid" )->plain();
- }
- foreach ( $extra as $v ) {
- $changes[] = $this->msg( $v )->plain();
- }
- $changeText = $this->context->getLanguage()->listToText( $changes );
-
-
- $newParams = array_slice( $params, 0, 3 );
- $newParams[3] = $changeText;
- $count = count( explode( ',', $params[$paramStart] ) );
- $newParams[4] = $this->context->getLanguage()->formatNum( $count );
- return $this->parsedParametersDeleteLog = $newParams;
- } else {
- return $this->parsedParametersDeleteLog = array_slice( $params, 0, 3 );
- }
- }
-
- return $this->parsedParametersDeleteLog = $params;
- }
-
- protected function parseBitField( $string ) {
- // Input is like ofield=2134 or just the number
- if ( strpos( $string, 'field=' ) === 1 ) {
- list( , $field ) = explode( '=', $string );
- return (int) $field;
- } else {
- return (int) $string;
- }
- }
-
- public function getActionLinks() {
- $user = $this->context->getUser();
- if ( !$user->isAllowed( 'deletedhistory' ) || $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
- return '';
- }
-
- switch ( $this->entry->getSubtype() ) {
- case 'delete': // Show undelete link
- if( $user->isAllowed( 'undelete' ) ) {
- $message = 'undeletelink';
- } else {
- $message = 'undeleteviewlink';
- }
- $revert = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Undelete' ),
- $this->msg( $message )->escaped(),
- array(),
- array( 'target' => $this->entry->getTarget()->getPrefixedDBkey() )
- );
- return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
-
- case 'revision': // If an edit was hidden from a page give a review link to the history
- $params = $this->extractParameters();
- if ( !isset( $params[3] ) || !isset( $params[4] ) ) {
- return '';
- }
-
- // Different revision types use different URL params...
- $key = $params[3];
- // This is a CSV of the IDs
- $ids = explode( ',', $params[4] );
-
- $links = array();
-
- // If there's only one item, we can show a diff link
- if ( count( $ids ) == 1 ) {
- // Live revision diffs...
- if ( $key == 'oldid' || $key == 'revision' ) {
- $links[] = Linker::linkKnown(
- $this->entry->getTarget(),
- $this->msg( 'diff' )->escaped(),
- array(),
- array(
- 'diff' => intval( $ids[0] ),
- 'unhide' => 1
- )
- );
- // Deleted revision diffs...
- } elseif ( $key == 'artimestamp' || $key == 'archive' ) {
- $links[] = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Undelete' ),
- $this->msg( 'diff' )->escaped(),
- array(),
- array(
- 'target' => $this->entry->getTarget()->getPrefixedDBKey(),
- 'diff' => 'prev',
- 'timestamp' => $ids[0]
- )
- );
- }
- }
-
- // View/modify link...
- $links[] = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Revisiondelete' ),
- $this->msg( 'revdel-restore' )->escaped(),
- array(),
- array(
- 'target' => $this->entry->getTarget()->getPrefixedText(),
- 'type' => $key,
- 'ids' => implode( ',', $ids ),
- )
- );
-
- return $this->msg( 'parentheses' )->rawParams(
- $this->context->getLanguage()->pipeList( $links ) )->escaped();
-
- case 'event': // Hidden log items, give review link
- $params = $this->extractParameters();
- if ( !isset( $params[3] ) ) {
- return '';
- }
- // This is a CSV of the IDs
- $query = $params[3];
- // Link to each hidden object ID, $params[1] is the url param
- $revert = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Revisiondelete' ),
- $this->msg( 'revdel-restore' )->escaped(),
- array(),
- array(
- 'target' => $this->entry->getTarget()->getPrefixedText(),
- 'type' => 'logging',
- 'ids' => $query
- )
- );
- return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
- default:
- return '';
- }
- }
-}
-
-/**
- * This class formats patrol log entries.
- * @since 1.19
- */
-class PatrolLogFormatter extends LogFormatter {
- protected function getMessageKey() {
- $key = parent::getMessageKey();
- $params = $this->getMessageParameters();
- if ( isset( $params[5] ) && $params[5] ) {
- $key .= '-auto';
- }
- return $key;
- }
-
- protected function getMessageParameters() {
- $params = parent::getMessageParameters();
-
- $target = $this->entry->getTarget();
- $oldid = $params[3];
- $revision = $this->context->getLanguage()->formatNum( $oldid, true );
-
- if ( $this->plaintext ) {
- $revlink = $revision;
- } elseif ( $target->exists() ) {
- $query = array(
- 'oldid' => $oldid,
- 'diff' => 'prev'
- );
- $revlink = Linker::link( $target, htmlspecialchars( $revision ), array(), $query );
- } else {
- $revlink = htmlspecialchars( $revision );
- }
-
- $params[3] = Message::rawParam( $revlink );
- return $params;
- }
-}
-
-/**
- * This class formats new user log entries.
- * @since 1.19
- */
-class NewUsersLogFormatter extends LogFormatter {
- protected function getMessageParameters() {
- $params = parent::getMessageParameters();
- if ( $this->entry->getSubtype() === 'create2' ) {
- if ( isset( $params[3] ) ) {
- $target = User::newFromId( $params[3] );
- } else {
- $target = User::newFromName( $this->entry->getTarget()->getText(), false );
- }
- $params[2] = Message::rawParam( $this->makeUserLink( $target ) );
- $params[3] = $target->getName();
- }
- return $params;
- }
-
- public function getComment() {
- $timestamp = wfTimestamp( TS_MW, $this->entry->getTimestamp() );
- if ( $timestamp < '20080129000000' ) {
- # Suppress $comment from old entries (before 2008-01-29),
- # not needed and can contain incorrect links
- return '';
- }
- return parent::getComment();
- }
-
- public function getPreloadTitles() {
- if ( $this->entry->getSubtype() === 'create2' ) {
- //add the user talk to LinkBatch for the userLink
- return array( Title::makeTitle( NS_USER_TALK, $this->entry->getTarget()->getText() ) );
- }
- return array();
- }
-}
diff --git a/includes/logging/LogPage.php b/includes/logging/LogPage.php
index d96a5ea5..cc473c18 100644
--- a/includes/logging/LogPage.php
+++ b/includes/logging/LogPage.php
@@ -50,16 +50,16 @@ class LogPage {
*/
var $target;
- /* @acess public */
+ /* @access public */
var $updateRecentChanges, $sendToUDP;
/**
* Constructor
*
- * @param $type String: one of '', 'block', 'protect', 'rights', 'delete',
+ * @param string $type one of '', 'block', 'protect', 'rights', 'delete',
* 'upload', 'move'
* @param $rc Boolean: whether to update recent changes as well as the logging table
- * @param $udp String: pass 'UDP' to send to the UDP feed if NOT sent to RC
+ * @param string $udp pass 'UDP' to send to the UDP feed if NOT sent to RC
*/
public function __construct( $type, $rc = true, $udp = 'skipUDP' ) {
$this->type = $type;
@@ -94,7 +94,7 @@ class LogPage {
$newId = !is_null( $log_id ) ? $log_id : $dbw->insertId();
# And update recentchanges
- if( $this->updateRecentChanges ) {
+ if ( $this->updateRecentChanges ) {
$titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
RecentChange::notifyLog(
@@ -102,9 +102,9 @@ class LogPage {
$this->type, $this->action, $this->target, $this->comment,
$this->params, $newId, $this->getRcCommentIRC()
);
- } elseif( $this->sendToUDP ) {
+ } elseif ( $this->sendToUDP ) {
# Don't send private logs to UDP
- if( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) {
+ if ( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) {
return $newId;
}
@@ -129,7 +129,7 @@ class LogPage {
public function getRcComment() {
$rcComment = $this->actionText;
- if( $this->comment != '' ) {
+ if ( $this->comment != '' ) {
if ( $rcComment == '' ) {
$rcComment = $this->comment;
} else {
@@ -149,7 +149,7 @@ class LogPage {
public function getRcCommentIRC() {
$rcComment = $this->ircActionText;
- if( $this->comment != '' ) {
+ if ( $this->comment != '' ) {
if ( $rcComment == '' ) {
$rcComment = $this->comment;
} else {
@@ -181,7 +181,7 @@ class LogPage {
/**
* Is $type a valid log type
*
- * @param $type String: log type to check
+ * @param string $type log type to check
* @return Boolean
*/
public static function isLogType( $type ) {
@@ -191,14 +191,14 @@ class LogPage {
/**
* Get the name for the given log type
*
- * @param $type String: logtype
+ * @param string $type logtype
* @return String: log name
* @deprecated in 1.19, warnings in 1.21. Use getName()
*/
public static function logName( $type ) {
global $wgLogNames;
- if( isset( $wgLogNames[$type] ) ) {
+ if ( isset( $wgLogNames[$type] ) ) {
return str_replace( '_', ' ', wfMessage( $wgLogNames[$type] )->text() );
} else {
// Bogus log types? Perhaps an extension was removed.
@@ -210,7 +210,7 @@ class LogPage {
* Get the log header for the given log type
*
* @todo handle missing log types
- * @param $type String: logtype
+ * @param string $type logtype
* @return String: headertext of this logtype
* @deprecated in 1.19, warnings in 1.21. Use getDescription()
*/
@@ -220,15 +220,15 @@ class LogPage {
}
/**
- * Generate text for a log entry.
+ * Generate text for a log entry.
* Only LogFormatter should call this function.
*
- * @param $type String: log type
- * @param $action String: log action
+ * @param string $type log type
+ * @param string $action log action
* @param $title Mixed: Title object or null
* @param $skin Mixed: Skin object or null. If null, we want to use the wiki
* content language, since that will go to the IRC feed.
- * @param $params Array: parameters
+ * @param array $params parameters
* @param $filterWikilinks Boolean: whether to filter wiki links
* @return HTML string
*/
@@ -247,36 +247,13 @@ class LogPage {
$key = "$type/$action";
- if( isset( $wgLogActions[$key] ) ) {
- if( is_null( $title ) ) {
+ if ( isset( $wgLogActions[$key] ) ) {
+ if ( is_null( $title ) ) {
$rv = wfMessage( $wgLogActions[$key] )->inLanguage( $langObj )->escaped();
} else {
$titleLink = self::getTitleLink( $type, $langObjOrNull, $title, $params );
- if( preg_match( '/^rights\/(rights|autopromote)/', $key ) ) {
- $rightsnone = wfMessage( 'rightsnone' )->inLanguage( $langObj )->text();
-
- if( $skin ) {
- $username = $title->getText();
- foreach ( $params as &$param ) {
- $groupArray = array_map( 'trim', explode( ',', $param ) );
- foreach( $groupArray as &$group ) {
- $group = User::getGroupMember( $group, $username );
- }
- $param = $wgLang->listToText( $groupArray );
- }
- }
-
- if( !isset( $params[0] ) || trim( $params[0] ) == '' ) {
- $params[0] = $rightsnone;
- }
-
- if( !isset( $params[1] ) || trim( $params[1] ) == '' ) {
- $params[1] = $rightsnone;
- }
- }
-
- if( count( $params ) == 0 ) {
+ if ( count( $params ) == 0 ) {
$rv = wfMessage( $wgLogActions[$key] )->rawParams( $titleLink )->inLanguage( $langObj )->escaped();
} else {
$details = '';
@@ -285,7 +262,7 @@ class LogPage {
// User suppression
if ( preg_match( '/^(block|suppress)\/(block|reblock)$/', $key ) ) {
if ( $skin ) {
- $params[1] = '<span class="blockExpiry" dir="ltr" title="' . htmlspecialchars( $params[1] ). '">' .
+ $params[1] = '<span class="blockExpiry" title="&lrm;' . htmlspecialchars( $params[1] ) . '">' .
$wgLang->translateBlockExpiry( $params[1] ) . '</span>';
} else {
$params[1] = $wgContLang->translateBlockExpiry( $params[1] );
@@ -294,16 +271,16 @@ class LogPage {
$params[2] = isset( $params[2] ) ?
self::formatBlockFlags( $params[2], $langObj ) : '';
// Page protections
- } elseif ( $type == 'protect' && count($params) == 3 ) {
+ } elseif ( $type == 'protect' && count( $params ) == 3 ) {
// Restrictions and expiries
- if( $skin ) {
+ if ( $skin ) {
$details .= $wgLang->getDirMark() . htmlspecialchars( " {$params[1]}" );
} else {
$details .= " {$params[1]}";
}
// Cascading flag...
- if( $params[2] ) {
+ if ( $params[2] ) {
$details .= ' [' . wfMessage( 'protect-summary-cascade' )->inLanguage( $langObj )->text() . ']';
}
}
@@ -314,7 +291,7 @@ class LogPage {
} else {
global $wgLogActionsHandlers;
- if( isset( $wgLogActionsHandlers[$key] ) ) {
+ if ( isset( $wgLogActionsHandlers[$key] ) ) {
$args = func_get_args();
$rv = call_user_func_array( $wgLogActionsHandlers[$key], $args );
} else {
@@ -333,7 +310,7 @@ class LogPage {
// you want to link to something OTHER than the title of the log entry.
// The real problem, which Erik was trying to fix (and it sort-of works now) is
// that the same messages are being treated as both wikitext *and* HTML.
- if( $filterWikilinks ) {
+ if ( $filterWikilinks ) {
$rv = str_replace( '[[', '', $rv );
$rv = str_replace( ']]', '', $rv );
}
@@ -350,13 +327,11 @@ class LogPage {
* @return String
*/
protected static function getTitleLink( $type, $lang, $title, &$params ) {
- global $wgContLang, $wgUserrightsInterwikiDelimiter;
-
- if( !$lang ) {
+ if ( !$lang ) {
return $title->getPrefixedText();
}
- switch( $type ) {
+ switch ( $type ) {
case 'move':
$titleLink = Linker::link(
$title,
@@ -378,7 +353,7 @@ class LogPage {
}
break;
case 'block':
- if( substr( $title->getText(), 0, 1 ) == '#' ) {
+ if ( substr( $title->getText(), 0, 1 ) == '#' ) {
$titleLink = $title->getText();
} else {
// @todo Store the user identifier in the parameters
@@ -388,20 +363,6 @@ class LogPage {
. Linker::userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK );
}
break;
- case 'rights':
- $text = $wgContLang->ucfirst( $title->getText() );
- $parts = explode( $wgUserrightsInterwikiDelimiter, $text, 2 );
-
- if ( count( $parts ) == 2 ) {
- $titleLink = WikiMap::foreignUserLink( $parts[1], $parts[0],
- htmlspecialchars( $title->getPrefixedText() ) );
-
- if ( $titleLink !== false ) {
- break;
- }
- }
- $titleLink = Linker::link( Title::makeTitle( NS_USER, $text ) );
- break;
case 'merge':
$titleLink = Linker::link(
$title,
@@ -416,11 +377,11 @@ class LogPage {
$params[1] = $lang->timeanddate( $params[1] );
break;
default:
- if( $title->isSpecialPage() ) {
+ if ( $title->isSpecialPage() ) {
list( $name, $par ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
# Use the language name for log titles, rather than Log/X
- if( $name == 'Log' ) {
+ if ( $name == 'Log' ) {
$logPage = new LogPage( $par );
$titleLink = Linker::link( $title, $logPage->getName()->escaped() );
$titleLink = wfMessage( 'parentheses' )
@@ -441,10 +402,10 @@ class LogPage {
/**
* Add a log entry
*
- * @param $action String: one of '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'move_redir'
+ * @param string $action one of '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'move_redir'
* @param $target Title object
- * @param $comment String: description associated
- * @param $params Array: parameters passed later to wfMessage function
+ * @param string $comment description associated
+ * @param array $params parameters passed later to wfMessage function
* @param $doer User object: the user doing the action
*
* @return int log_id of the inserted log entry
@@ -460,6 +421,9 @@ class LogPage {
$comment = '';
}
+ # Trim spaces on user supplied text
+ $comment = trim( $comment );
+
# Truncate for whole multibyte characters.
$comment = $wgContLang->truncate( $comment, 255 );
@@ -501,13 +465,13 @@ class LogPage {
* @return Boolean
*/
public function addRelations( $field, $values, $logid ) {
- if( !strlen( $field ) || empty( $values ) ) {
+ if ( !strlen( $field ) || empty( $values ) ) {
return false; // nothing
}
$data = array();
- foreach( $values as $value ) {
+ foreach ( $values as $value ) {
$data[] = array(
'ls_field' => $field,
'ls_value' => $value,
@@ -549,35 +513,35 @@ class LogPage {
* Convert a comma-delimited list of block log flags
* into a more readable (and translated) form
*
- * @param $flags string Flags to format
+ * @param string $flags Flags to format
* @param $lang Language object to use
* @return String
*/
public static function formatBlockFlags( $flags, $lang ) {
- $flags = explode( ',', trim( $flags ) );
+ $flags = trim( $flags );
+ if ( $flags === '' ) {
+ return ''; //nothing to do
+ }
+ $flags = explode( ',', $flags );
- if( count( $flags ) > 0 ) {
- for( $i = 0; $i < count( $flags ); $i++ ) {
- $flags[$i] = self::formatBlockFlag( $flags[$i], $lang );
- }
- return wfMessage( 'parentheses' )->inLanguage( $lang )
- ->rawParams( $lang->commaList( $flags ) )->escaped();
- } else {
- return '';
+ for ( $i = 0; $i < count( $flags ); $i++ ) {
+ $flags[$i] = self::formatBlockFlag( $flags[$i], $lang );
}
+ return wfMessage( 'parentheses' )->inLanguage( $lang )
+ ->rawParams( $lang->commaList( $flags ) )->escaped();
}
/**
* Translate a block log flag if possible
*
- * @param $flag int Flag to translate
+ * @param int $flag Flag to translate
* @param $lang Language object to use
* @return String
*/
public static function formatBlockFlag( $flag, $lang ) {
static $messages = array();
- if( !isset( $messages[$flag] ) ) {
+ if ( !isset( $messages[$flag] ) ) {
$messages[$flag] = htmlspecialchars( $flag ); // Fallback
// For grepping. The following core messages can be used here:
@@ -598,7 +562,6 @@ class LogPage {
return $messages[$flag];
}
-
/**
* Name of the log.
* @return Message
diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php
index ea1be8e0..09ae3b8c 100644
--- a/includes/logging/LogPager.php
+++ b/includes/logging/LogPager.php
@@ -34,15 +34,15 @@ class LogPager extends ReverseChronologicalPager {
/**
* Constructor
*
- * @param $list LogEventsList
- * @param $types String or Array: log types to show
- * @param $performer String: the user who made the log entries
- * @param $title String|Title: the page title the log entries are for
- * @param $pattern String: do a prefix search rather than an exact title match
- * @param $conds Array: extra conditions for the query
- * @param $year Integer: the year to start from
- * @param $month Integer: the month to start from
- * @param $tagFilter String: tag
+ * @param LogEventsList $list
+ * @param string $types or Array: log types to show
+ * @param string $performer the user who made the log entries
+ * @param string|Title $title the page title the log entries are for
+ * @param string $pattern do a prefix search rather than an exact title match
+ * @param array $conds extra conditions for the query
+ * @param int $year The year to start from
+ * @param int $month The month to start from
+ * @param string $tagFilter tag
*/
public function __construct( $list, $types = array(), $performer = '', $title = '', $pattern = '',
$conds = array(), $year = false, $month = false, $tagFilter = '' ) {
@@ -71,16 +71,17 @@ class LogPager extends ReverseChronologicalPager {
public function getFilterParams() {
global $wgFilterLogTypes;
$filters = array();
- if( count($this->types) ) {
+ if ( count( $this->types ) ) {
return $filters;
}
- foreach( $wgFilterLogTypes as $type => $default ) {
+ foreach ( $wgFilterLogTypes as $type => $default ) {
// Avoid silly filtering
- if( $type !== 'patrol' || $this->getUser()->useNPPatrol() ) {
+ if ( $type !== 'patrol' || $this->getUser()->useNPPatrol() ) {
$hide = $this->getRequest()->getInt( "hide_{$type}_log", $default );
$filters[$type] = $hide;
- if( $hide )
+ if ( $hide ) {
$this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
+ }
}
}
return $filters;
@@ -90,18 +91,20 @@ class LogPager extends ReverseChronologicalPager {
* Set the log reader to return only entries of the given type.
* Type restrictions enforced here
*
- * @param $types String or array: Log types ('upload', 'delete', etc);
+ * @param string $types or array: Log types ('upload', 'delete', etc);
* empty string means no restriction
*/
private function limitType( $types ) {
global $wgLogRestrictions;
+
+ $user = $this->getUser();
// If $types is not an array, make it an array
- $types = ($types === '') ? array() : (array)$types;
+ $types = ( $types === '' ) ? array() : (array)$types;
// Don't even show header for private logs; don't recognize it...
$needReindex = false;
foreach ( $types as $type ) {
- if( isset( $wgLogRestrictions[$type] )
- && !$this->getUser()->isAllowed($wgLogRestrictions[$type])
+ if ( isset( $wgLogRestrictions[$type] )
+ && !$user->isAllowed( $wgLogRestrictions[$type] )
) {
$needReindex = true;
$types = array_diff( $types, array( $type ) );
@@ -116,34 +119,36 @@ class LogPager extends ReverseChronologicalPager {
// Don't show private logs to unprivileged users.
// Also, only show them upon specific request to avoid suprises.
$audience = $types ? 'user' : 'public';
- $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience );
- if( $hideLogs !== false ) {
+ $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience, $user );
+ if ( $hideLogs !== false ) {
$this->mConds[] = $hideLogs;
}
- if( count($types) ) {
+ if ( count( $types ) ) {
$this->mConds['log_type'] = $types;
// Set typeCGI; used in url param for paging
- if( count($types) == 1 ) $this->typeCGI = $types[0];
+ if ( count( $types ) == 1 ) {
+ $this->typeCGI = $types[0];
+ }
}
}
/**
* Set the log reader to return only entries by the given user.
*
- * @param $name String: (In)valid user name
+ * @param string $name (In)valid user name
* @return bool
*/
private function limitPerformer( $name ) {
- if( $name == '' ) {
+ if ( $name == '' ) {
return false;
}
$usertitle = Title::makeTitleSafe( NS_USER, $name );
- if( is_null($usertitle) ) {
+ if ( is_null( $usertitle ) ) {
return false;
}
/* Fetch userid at first, if known, provides awesome query plan afterwards */
$userid = User::idFromName( $name );
- if( !$userid ) {
+ if ( !$userid ) {
/* It should be nicer to abort query at all,
but for now it won't pass anywhere behind the optimizer */
$this->mConds[] = "NULL";
@@ -151,10 +156,10 @@ class LogPager extends ReverseChronologicalPager {
$this->mConds['log_user'] = $userid;
// Paranoia: avoid brute force searches (bug 17342)
$user = $this->getUser();
- if( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::DELETED_USER) . ' = 0';
- } elseif( !$user->isAllowed( 'suppressrevision' ) ) {
- $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::SUPPRESSED_USER) .
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
+ } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
' != ' . LogPage::SUPPRESSED_USER;
}
$this->performer = $usertitle->getText();
@@ -165,7 +170,7 @@ class LogPager extends ReverseChronologicalPager {
* Set the log reader to return only entries affecting the given page.
* (For the block and rights logs, this is a user page.)
*
- * @param $page String or Title object: Title name
+ * @param string $page or Title object: Title name
* @param $pattern String
* @return bool
*/
@@ -176,7 +181,7 @@ class LogPager extends ReverseChronologicalPager {
$title = $page;
} else {
$title = Title::newFromText( $page );
- if( strlen( $page ) == 0 || !$title instanceof Title ) {
+ if ( strlen( $page ) == 0 || !$title instanceof Title ) {
return false;
}
}
@@ -196,7 +201,7 @@ class LogPager extends ReverseChronologicalPager {
# use the page_time index. That should have no more than a few hundred
# log entries for even the busiest pages, so it can be safely scanned
# in full to satisfy an impossible condition on user or similar.
- if( $pattern && !$wgMiserMode ) {
+ if ( $pattern && !$wgMiserMode ) {
$this->mConds['log_namespace'] = $ns;
$this->mConds[] = 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() );
$this->pattern = $pattern;
@@ -206,10 +211,10 @@ class LogPager extends ReverseChronologicalPager {
}
// Paranoia: avoid brute force searches (bug 17342)
$user = $this->getUser();
- if( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->mConds[] = $db->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0';
- } elseif( !$user->isAllowed( 'suppressrevision' ) ) {
- $this->mConds[] = $db->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) .
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
+ $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
+ } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+ $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
' != ' . LogPage::SUPPRESSED_ACTION;
}
}
@@ -232,7 +237,7 @@ class LogPager extends ReverseChronologicalPager {
# Add log_search table if there are conditions on it.
# This filters the results to only include log rows that have
# log_search records with the specified ls_field and ls_value values.
- if( array_key_exists( 'ls_field', $this->mConds ) ) {
+ if ( array_key_exists( 'ls_field', $this->mConds ) ) {
$tables[] = 'log_search';
$index['log_search'] = 'ls_field_val';
$index['logging'] = 'PRIMARY';
@@ -244,28 +249,18 @@ class LogPager extends ReverseChronologicalPager {
# no duplicate log rows. Otherwise, we need to remove the duplicates.
$options[] = 'DISTINCT';
}
- # Avoid usage of the wrong index by limiting
- # the choices of available indexes. This mainly
- # avoids site-breaking filesorts.
- } elseif( $this->title || $this->pattern || $this->performer ) {
- $index['logging'] = array( 'page_time', 'user_time' );
- if( count($this->types) == 1 ) {
- $index['logging'][] = 'log_user_type_time';
- }
- } elseif( count($this->types) == 1 ) {
- $index['logging'] = 'type_time';
- } else {
- $index['logging'] = 'times';
}
- $options['USE INDEX'] = $index;
+ if ( count( $index ) ) {
+ $options['USE INDEX'] = $index;
+ }
# Don't show duplicate rows when using log_search
$joins['log_search'] = array( 'INNER JOIN', 'ls_log_id=log_id' );
$info = array(
- 'tables' => $tables,
- 'fields' => $fields,
- 'conds' => array_merge( $conds, $this->mConds ),
- 'options' => $options,
+ 'tables' => $tables,
+ 'fields' => $fields,
+ 'conds' => array_merge( $conds, $this->mConds ),
+ 'options' => $options,
'join_conds' => $joins,
);
# Add ChangeTags filter query
@@ -293,7 +288,7 @@ class LogPager extends ReverseChronologicalPager {
public function getStartBody() {
wfProfileIn( __METHOD__ );
# Do a link batch query
- if( $this->getNumRows() > 0 ) {
+ if ( $this->getNumRows() > 0 ) {
$lb = new LinkBatch;
foreach ( $this->mResult as $row ) {
$lb->add( $row->log_namespace, $row->log_title );
diff --git a/includes/logging/MoveLogFormatter.php b/includes/logging/MoveLogFormatter.php
new file mode 100644
index 00000000..0978f976
--- /dev/null
+++ b/includes/logging/MoveLogFormatter.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Formatter for move log entries.
+ *
+ * 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
+ * @author Niklas Laxström
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @since 1.22
+ */
+
+/**
+ * This class formats move log entries.
+ *
+ * @since 1.19
+ */
+class MoveLogFormatter extends LogFormatter {
+ public function getPreloadTitles() {
+ $params = $this->extractParameters();
+ return array( Title::newFromText( $params[3] ) );
+ }
+
+ protected function getMessageKey() {
+ $key = parent::getMessageKey();
+ $params = $this->getMessageParameters();
+ if ( isset( $params[4] ) && $params[4] === '1' ) {
+ $key .= '-noredirect';
+ }
+ return $key;
+ }
+
+ protected function getMessageParameters() {
+ $params = parent::getMessageParameters();
+ $oldname = $this->makePageLink( $this->entry->getTarget(), array( 'redirect' => 'no' ) );
+ $newname = $this->makePageLink( Title::newFromText( $params[3] ) );
+ $params[2] = Message::rawParam( $oldname );
+ $params[3] = Message::rawParam( $newname );
+ return $params;
+ }
+
+ public function getActionLinks() {
+ if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden
+ || $this->entry->getSubtype() !== 'move'
+ || !$this->context->getUser()->isAllowed( 'move' ) )
+ {
+ return '';
+ }
+
+ $params = $this->extractParameters();
+ $destTitle = Title::newFromText( $params[3] );
+ if ( !$destTitle ) {
+ return '';
+ }
+
+ $revert = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Movepage' ),
+ $this->msg( 'revertmove' )->escaped(),
+ array(),
+ array(
+ 'wpOldTitle' => $destTitle->getPrefixedDBkey(),
+ 'wpNewTitle' => $this->entry->getTarget()->getPrefixedDBkey(),
+ 'wpReason' => $this->msg( 'revertmove' )->inContentLanguage()->text(),
+ 'wpMovetalk' => 0
+ )
+ );
+ return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
+ }
+}
diff --git a/includes/logging/NewUsersLogFormatter.php b/includes/logging/NewUsersLogFormatter.php
new file mode 100644
index 00000000..602728b4
--- /dev/null
+++ b/includes/logging/NewUsersLogFormatter.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Formatter for new user log entries.
+ *
+ * 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
+ * @author Niklas Laxström
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @since 1.22
+ */
+
+/**
+ * This class formats new user log entries.
+ *
+ * @since 1.19
+ */
+class NewUsersLogFormatter extends LogFormatter {
+ protected function getMessageParameters() {
+ $params = parent::getMessageParameters();
+ $subtype = $this->entry->getSubtype();
+ if ( $subtype === 'create2' || $subtype === 'byemail' ) {
+ if ( isset( $params[3] ) ) {
+ $target = User::newFromId( $params[3] );
+ } else {
+ $target = User::newFromName( $this->entry->getTarget()->getText(), false );
+ }
+ $params[2] = Message::rawParam( $this->makeUserLink( $target ) );
+ $params[3] = $target->getName();
+ }
+ return $params;
+ }
+
+ public function getComment() {
+ $timestamp = wfTimestamp( TS_MW, $this->entry->getTimestamp() );
+ if ( $timestamp < '20080129000000' ) {
+ # Suppress $comment from old entries (before 2008-01-29),
+ # not needed and can contain incorrect links
+ return '';
+ }
+ return parent::getComment();
+ }
+
+ public function getPreloadTitles() {
+ $subtype = $this->entry->getSubtype();
+ if ( $subtype === 'create2' || $subtype === 'byemail' ) {
+ //add the user talk to LinkBatch for the userLink
+ return array( Title::makeTitle( NS_USER_TALK, $this->entry->getTarget()->getText() ) );
+ }
+ return array();
+ }
+}
diff --git a/includes/logging/PatrolLog.php b/includes/logging/PatrolLog.php
index 911fffc0..bb76d5a9 100644
--- a/includes/logging/PatrolLog.php
+++ b/includes/logging/PatrolLog.php
@@ -38,6 +38,13 @@ class PatrolLog {
* @return bool
*/
public static function record( $rc, $auto = false, User $user = null ) {
+ global $wgLogAutopatrol;
+
+ // do not log autopatrolled edits if setting disables it
+ if ( $auto && !$wgLogAutopatrol ) {
+ return false;
+ }
+
if ( !$rc instanceof RecentChange ) {
$rc = RecentChange::newFromId( $rc );
if ( !is_object( $rc ) ) {
diff --git a/includes/logging/PatrolLogFormatter.php b/includes/logging/PatrolLogFormatter.php
new file mode 100644
index 00000000..507039ba
--- /dev/null
+++ b/includes/logging/PatrolLogFormatter.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Formatter for new user log entries.
+ *
+ * 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
+ * @author Niklas Laxström
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @since 1.22
+ */
+
+/**
+ * This class formats patrol log entries.
+ *
+ * @since 1.19
+ */
+class PatrolLogFormatter extends LogFormatter {
+ protected function getMessageKey() {
+ $key = parent::getMessageKey();
+ $params = $this->getMessageParameters();
+ if ( isset( $params[5] ) && $params[5] ) {
+ $key .= '-auto';
+ }
+ return $key;
+ }
+
+ protected function getMessageParameters() {
+ $params = parent::getMessageParameters();
+
+ $target = $this->entry->getTarget();
+ $oldid = $params[3];
+ $revision = $this->context->getLanguage()->formatNum( $oldid, true );
+
+ if ( $this->plaintext ) {
+ $revlink = $revision;
+ } elseif ( $target->exists() ) {
+ $query = array(
+ 'oldid' => $oldid,
+ 'diff' => 'prev'
+ );
+ $revlink = Linker::link( $target, htmlspecialchars( $revision ), array(), $query );
+ } else {
+ $revlink = htmlspecialchars( $revision );
+ }
+
+ $params[3] = Message::rawParam( $revlink );
+ return $params;
+ }
+}
diff --git a/includes/logging/RightsLogFormatter.php b/includes/logging/RightsLogFormatter.php
new file mode 100644
index 00000000..d3daf6ee
--- /dev/null
+++ b/includes/logging/RightsLogFormatter.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Formatter for user rights log entries.
+ *
+ * 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
+ * @author Alexandre Emsenhuber
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @since 1.22
+ */
+
+/**
+ * This class formats rights log entries.
+ *
+ * @since 1.21
+ */
+class RightsLogFormatter extends LogFormatter {
+ protected function makePageLink( Title $title = null, $parameters = array() ) {
+ global $wgContLang, $wgUserrightsInterwikiDelimiter;
+
+ if ( !$this->plaintext ) {
+ $text = $wgContLang->ucfirst( $title->getText() );
+ $parts = explode( $wgUserrightsInterwikiDelimiter, $text, 2 );
+
+ if ( count( $parts ) === 2 ) {
+ $titleLink = WikiMap::foreignUserLink( $parts[1], $parts[0],
+ htmlspecialchars( $title->getPrefixedText() ) );
+
+ if ( $titleLink !== false ) {
+ return $titleLink;
+ }
+ }
+ }
+
+ return parent::makePageLink( $title, $parameters );
+ }
+
+ protected function getMessageKey() {
+ $key = parent::getMessageKey();
+ $params = $this->getMessageParameters();
+ if ( !isset( $params[3] ) && !isset( $params[4] ) ) {
+ $key .= '-legacy';
+ }
+ return $key;
+ }
+
+ protected function getMessageParameters() {
+ $params = parent::getMessageParameters();
+
+ // Really old entries
+ if ( !isset( $params[3] ) && !isset( $params[4] ) ) {
+ return $params;
+ }
+
+ $oldGroups = $params[3];
+ $newGroups = $params[4];
+
+ // Less old entries
+ if ( $oldGroups === '' ) {
+ $oldGroups = array();
+ } elseif ( is_string( $oldGroups ) ) {
+ $oldGroups = array_map( 'trim', explode( ',', $oldGroups ) );
+ }
+ if ( $newGroups === '' ) {
+ $newGroups = array();
+ } elseif ( is_string( $newGroups ) ) {
+ $newGroups = array_map( 'trim', explode( ',', $newGroups ) );
+ }
+
+ $userName = $this->entry->getTarget()->getText();
+ if ( !$this->plaintext && count( $oldGroups ) ) {
+ foreach ( $oldGroups as &$group ) {
+ $group = User::getGroupMember( $group, $userName );
+ }
+ }
+ if ( !$this->plaintext && count( $newGroups ) ) {
+ foreach ( $newGroups as &$group ) {
+ $group = User::getGroupMember( $group, $userName );
+ }
+ }
+
+ $lang = $this->context->getLanguage();
+ if ( count( $oldGroups ) ) {
+ $params[3] = $lang->listToText( $oldGroups );
+ } else {
+ $params[3] = $this->msg( 'rightsnone' )->text();
+ }
+ if ( count( $newGroups ) ) {
+ // Array_values is used here because of bug 42211
+ // see use of array_unique in UserrightsPage::doSaveUserGroups on $newGroups.
+ $params[4] = $lang->listToText( array_values( $newGroups ) );
+ } else {
+ $params[4] = $this->msg( 'rightsnone' )->text();
+ }
+
+ return $params;
+ }
+}
diff --git a/includes/media/BMP.php b/includes/media/BMP.php
index a515c635..99b7741a 100644
--- a/includes/media/BMP.php
+++ b/includes/media/BMP.php
@@ -58,15 +58,15 @@ class BmpHandler extends BitmapHandler {
*/
function getImageSize( $image, $filename ) {
$f = fopen( $filename, 'rb' );
- if( !$f ) {
+ if ( !$f ) {
return false;
}
$header = fread( $f, 54 );
- fclose($f);
+ fclose( $f );
// Extract binary form of width and height from the header
- $w = substr( $header, 18, 4);
- $h = substr( $header, 22, 4);
+ $w = substr( $header, 18, 4 );
+ $h = substr( $header, 22, 4 );
// Convert the unsigned long 32 bits (little endian):
try {
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index 99ac854b..e2444a11 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -29,7 +29,7 @@
class BitmapHandler extends ImageHandler {
/**
* @param $image File
- * @param $params array Transform parameters. Entries with the keys 'width'
+ * @param array $params Transform parameters. Entries with the keys 'width'
* and 'height' are the respective screen width and height, while the keys
* 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
* @return bool
@@ -75,7 +75,6 @@ class BitmapHandler extends ImageHandler {
return true;
}
-
/**
* Extracts the width/height if the image will be scaled before rotating
*
@@ -84,8 +83,8 @@ class BitmapHandler extends ImageHandler {
* stored as raw landscape with 90-degress rotation, the resulting size
* will be wider than it is tall.
*
- * @param $params array Parameters as returned by normaliseParams
- * @param $rotation int The rotation angle that will be applied
+ * @param array $params Parameters as returned by normaliseParams
+ * @param int $rotation The rotation angle that will be applied
* @return array ($width, $height) array
*/
public function extractPreRotationDimensions( $params, $rotation ) {
@@ -100,18 +99,6 @@ class BitmapHandler extends ImageHandler {
return array( $width, $height );
}
-
- /**
- * Function that returns the number of pixels to be thumbnailed.
- * Intended for animated GIFs to multiply by the number of frames.
- *
- * @param File $image
- * @return int
- */
- function getImageArea( $image ) {
- return $image->getWidth() * $image->getHeight();
- }
-
/**
* @param $image File
* @param $dstPath
@@ -133,7 +120,7 @@ class BitmapHandler extends ImageHandler {
# The size of the image on the page
'clientWidth' => $params['width'],
'clientHeight' => $params['height'],
- # Comment as will be added to the EXIF of the thumbnail
+ # Comment as will be added to the Exif of the thumbnail
'comment' => isset( $params['descriptionUrl'] ) ?
"File source: {$params['descriptionUrl']}" : '',
# Properties of the original image
@@ -158,7 +145,6 @@ class BitmapHandler extends ImageHandler {
return $this->getClientScalingThumbnailImage( $image, $scalerParams );
}
-
if ( $scaler == 'client' ) {
# Client-side image scaling, use the source URL
# Using the destination URL in a TRANSFORM_LATER request would be incorrect
@@ -264,7 +250,7 @@ class BitmapHandler extends ImageHandler {
* client side
*
* @param $image File File associated with this thumbnail
- * @param $scalerParams array Array with scaler params
+ * @param array $scalerParams Array with scaler params
* @return ThumbnailImage
*
* @todo fixme: no rotation support
@@ -281,7 +267,7 @@ class BitmapHandler extends ImageHandler {
* Transform an image using ImageMagick
*
* @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param array $params Array with scaler params
*
* @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
@@ -341,7 +327,7 @@ class BitmapHandler extends ImageHandler {
$rotation = $this->getRotation( $image );
list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
- $cmd =
+ $cmd =
wfEscapeShellArg( $wgImageMagickConvertCommand ) .
// Specify white background color, will be used for transparent images
// in Internet Explorer/Windows instead of default black.
@@ -360,12 +346,12 @@ class BitmapHandler extends ImageHandler {
" -depth 8 $sharpen " .
" -rotate -$rotation " .
" {$animation_post} " .
- wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1";
+ wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
wfProfileIn( 'convert' );
$retval = 0;
- $err = wfShellExec( $cmd, $retval, $env );
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
wfProfileOut( 'convert' );
if ( $retval !== 0 ) {
@@ -380,7 +366,7 @@ class BitmapHandler extends ImageHandler {
* Transform an image using the Imagick PHP extension
*
* @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param array $params Array with scaler params
*
* @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
@@ -401,7 +387,7 @@ class BitmapHandler extends ImageHandler {
$im->sharpenImage( $radius, $sigma );
}
$im->setCompressionQuality( 80 );
- } elseif( $params['mimeType'] == 'image/png' ) {
+ } elseif ( $params['mimeType'] == 'image/png' ) {
$im->setCompressionQuality( 95 );
} elseif ( $params['mimeType'] == 'image/gif' ) {
if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
@@ -457,7 +443,7 @@ class BitmapHandler extends ImageHandler {
* Transform an image using a custom command
*
* @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param array $params Array with scaler params
*
* @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
@@ -475,7 +461,7 @@ class BitmapHandler extends ImageHandler {
wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
wfProfileIn( 'convert' );
$retval = 0;
- $err = wfShellExec( $cmd, $retval );
+ $err = wfShellExecWithStderr( $cmd, $retval );
wfProfileOut( 'convert' );
if ( $retval !== 0 ) {
@@ -500,8 +486,8 @@ class BitmapHandler extends ImageHandler {
/**
* Get a MediaTransformError with error 'thumbnail_error'
*
- * @param $params array Parameter array as passed to the transform* functions
- * @param $errMsg string Error message
+ * @param array $params Parameter array as passed to the transform* functions
+ * @param string $errMsg Error message
* @return MediaTransformError
*/
public function getMediaTransformError( $params, $errMsg ) {
@@ -513,7 +499,7 @@ class BitmapHandler extends ImageHandler {
* Transform an image using the built in GD library
*
* @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param array $params Array with scaler params
*
* @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
@@ -622,8 +608,9 @@ class BitmapHandler extends ImageHandler {
* 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
+ * @param string $path The file path
+ * @param bool|string $scene The scene specification, or false if there is none
+ * @throws MWException
* @return string
*/
function escapeMagickInput( $path, $scene = false ) {
@@ -653,8 +640,9 @@ class BitmapHandler extends ImageHandler {
* 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
+ * @param string $path The file path
+ * @param bool|string $scene The scene specification, or false if there is none
+ * @throws MWException
* @return string
*/
protected function escapeMagickPath( $path, $scene = false ) {
@@ -713,24 +701,6 @@ class BitmapHandler extends ImageHandler {
imagejpeg( $dst_image, $thumbPath, 95 );
}
- /**
- * On supporting image formats, try to read out the low-level orientation
- * of the file and return the angle that the file needs to be rotated to
- * be viewed.
- *
- * This information is only useful when manipulating the original file;
- * the width and height we normally work with is logical, and will match
- * any produced output views.
- *
- * The base BitmapHandler doesn't understand any metadata formats, so this
- * is left up to child classes to implement.
- *
- * @param $file File
- * @return int 0, 90, 180 or 270
- */
- public function getRotation( $file ) {
- return 0;
- }
/**
* Returns whether the current scaler supports rotation (im and gd do)
@@ -757,6 +727,55 @@ class BitmapHandler extends ImageHandler {
}
/**
+ * @param $file File
+ * @param array $params Rotate parameters.
+ * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
+ * @since 1.21
+ * @return bool
+ */
+ public function rotate( $file, $params ) {
+ global $wgImageMagickConvertCommand;
+
+ $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
+ $scene = false;
+
+ $scaler = self::getScalerType( null, false );
+ switch ( $scaler ) {
+ case 'im':
+ $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
+ wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
+ " -rotate -$rotation " .
+ wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
+ wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
+ wfProfileIn( 'convert' );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
+ wfProfileOut( 'convert' );
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+ return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
+ }
+ return false;
+ case 'imext':
+ $im = new Imagick();
+ $im->readImage( $params['srcPath'] );
+ if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "Error rotating $rotation degrees" );
+ }
+ $result = $im->writeImage( $params['dstPath'] );
+ if ( !$result ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "Unable to write image to {$params['dstPath']}" );
+ }
+ return false;
+ default:
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "$scaler rotation not implemented" );
+ }
+ }
+
+ /**
* Rerurns whether the file needs to be rendered. Returns true if the
* file requires rotation and we are able to rotate it.
*
diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php
index 0a195547..7c39c814 100644
--- a/includes/media/BitmapMetadataHandler.php
+++ b/includes/media/BitmapMetadataHandler.php
@@ -47,14 +47,14 @@ class BitmapMetadataHandler {
private $iptcType = 'iptc-no-hash';
/**
- * This does the photoshop image resource app13 block
- * of interest, IPTC-IIM metadata is stored here.
- *
- * Mostly just calls doPSIR and doIPTC
- *
- * @param String $app13 String containing app13 block from jpeg file
- */
- private function doApp13 ( $app13 ) {
+ * This does the photoshop image resource app13 block
+ * of interest, IPTC-IIM metadata is stored here.
+ *
+ * Mostly just calls doPSIR and doIPTC
+ *
+ * @param string $app13 String containing app13 block from jpeg file
+ */
+ private function doApp13( $app13 ) {
try {
$this->iptcType = JpegMetadataExtractor::doPSIR( $app13 );
} catch ( MWException $e ) {
@@ -69,7 +69,6 @@ class BitmapMetadataHandler {
$this->addMetadata( $iptc, $this->iptcType );
}
-
/**
* Get exif info using exif class.
* Basically what used to be in BitmapHandler::getMetadata().
@@ -80,7 +79,7 @@ class BitmapMetadataHandler {
* @param $filename string
* @param $byteOrder string
*/
- function getExif ( $filename, $byteOrder ) {
+ function getExif( $filename, $byteOrder ) {
global $wgShowEXIF;
if ( file_exists( $filename ) && $wgShowEXIF ) {
$exif = new Exif( $filename, $byteOrder );
@@ -91,12 +90,12 @@ class BitmapMetadataHandler {
}
}
/** Add misc metadata. Warning: atm if the metadata category
- * doesn't have a priority, it will be silently discarded.
- *
- * @param Array $metaArray array of metadata values
- * @param string $type type. defaults to other. if two things have the same type they're merged
- */
- function addMetadata ( $metaArray, $type = 'other' ) {
+ * doesn't have a priority, it will be silently discarded.
+ *
+ * @param array $metaArray array of metadata values
+ * @param string $type type. defaults to other. if two things have the same type they're merged
+ */
+ function addMetadata( $metaArray, $type = 'other' ) {
if ( isset( $this->metadata[$type] ) ) {
/* merge with old data */
$metaArray = $metaArray + $this->metadata[$type];
@@ -106,15 +105,15 @@ class BitmapMetadataHandler {
}
/**
- * Merge together the various types of metadata
- * the different types have different priorites,
- * and are merged in order.
- *
- * This function is generally called by the media handlers' getMetadata()
- *
- * @return Array metadata array
- */
- function getMetadataArray () {
+ * Merge together the various types of metadata
+ * the different types have different priorites,
+ * and are merged in order.
+ *
+ * This function is generally called by the media handlers' getMetadata()
+ *
+ * @return Array metadata array
+ */
+ function getMetadataArray() {
// this seems a bit ugly... This is all so its merged in right order
// based on the MWG recomendation.
$temp = Array();
@@ -144,11 +143,11 @@ class BitmapMetadataHandler {
/** Main entry point for jpeg's.
*
- * @param $filename string filename (with full path)
+ * @param string $filename filename (with full path)
* @return array metadata result array.
* @throws MWException on invalid file.
*/
- static function Jpeg ( $filename ) {
+ static function Jpeg( $filename ) {
$showXMP = function_exists( 'xml_parser_create_ns' );
$meta = new self();
@@ -157,7 +156,7 @@ class BitmapMetadataHandler {
$meta->addMetadata( Array( 'JPEGFileComment' => $seg['COM'] ), 'native' );
}
if ( isset( $seg['PSIR'] ) && count( $seg['PSIR'] ) > 0 ) {
- foreach( $seg['PSIR'] as $curPSIRValue ) {
+ foreach ( $seg['PSIR'] as $curPSIRValue ) {
$meta->doApp13( $curPSIRValue );
}
}
@@ -187,10 +186,10 @@ class BitmapMetadataHandler {
* merge the png various tEXt chunks to that
* are interesting, but for now it only does XMP
*
- * @param $filename String full path to file
+ * @param string $filename full path to file
* @return Array Array for storage in img_metadata.
*/
- static public function PNG ( $filename ) {
+ public static function PNG( $filename ) {
$showXMP = function_exists( 'xml_parser_create_ns' );
$meta = new self();
@@ -216,10 +215,10 @@ class BitmapMetadataHandler {
* They don't really have native metadata, so just merges together
* XMP and image comment.
*
- * @param $filename string full path to file
+ * @param string $filename full path to file
* @return Array metadata array
*/
- static public function GIF ( $filename ) {
+ public static function GIF( $filename ) {
$meta = new self();
$baseArray = GIFMetadataExtractor::getMetadata( $filename );
@@ -240,7 +239,7 @@ class BitmapMetadataHandler {
unset( $baseArray['comment'] );
unset( $baseArray['xmp'] );
-
+
$baseArray['metadata'] = $meta->getMetadataArray();
$baseArray['metadata']['_MW_GIF_VERSION'] = GIFMetadataExtractor::VERSION;
return $baseArray;
@@ -257,9 +256,10 @@ class BitmapMetadataHandler {
*
* The various exceptions this throws are caught later.
* @param $filename String
+ * @throws MWException
* @return Array The metadata.
*/
- static public function Tiff ( $filename ) {
+ public static function Tiff( $filename ) {
if ( file_exists( $filename ) ) {
$byteOrder = self::getTiffByteOrder( $filename );
if ( !$byteOrder ) {
@@ -281,16 +281,18 @@ class BitmapMetadataHandler {
* Read the first 2 bytes of a tiff file to figure out
* Little Endian or Big Endian. Needed for exif stuff.
*
- * @param $filename String The filename
+ * @param string $filename The filename
* @return String 'BE' or 'LE' or false
*/
static function getTiffByteOrder( $filename ) {
$fh = fopen( $filename, 'rb' );
- if ( !$fh ) return false;
+ if ( !$fh ) {
+ return false;
+ }
$head = fread( $fh, 2 );
fclose( $fh );
- switch( $head ) {
+ switch ( $head ) {
case 'II':
return 'LE'; // II for intel.
case 'MM':
@@ -300,6 +302,4 @@ class BitmapMetadataHandler {
}
}
-
-
}
diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php
index 84672e05..b9e89d9d 100644
--- a/includes/media/DjVu.php
+++ b/includes/media/DjVu.php
@@ -183,9 +183,9 @@ class DjVuHandler extends ImageHandler {
if ( $wgDjvuPostProcessor ) {
$cmd .= " | {$wgDjvuPostProcessor}";
}
- $cmd .= ' > ' . wfEscapeShellArg($dstPath) . ') 2>&1';
+ $cmd .= ' > ' . wfEscapeShellArg( $dstPath ) . ') 2>&1';
wfProfileIn( 'ddjvu' );
- wfDebug( __METHOD__.": $cmd\n" );
+ wfDebug( __METHOD__ . ": $cmd\n" );
$retval = '';
$err = wfShellExec( $cmd, $retval );
wfProfileOut( 'ddjvu' );
@@ -194,7 +194,7 @@ class DjVuHandler extends ImageHandler {
if ( $retval != 0 || $removed ) {
wfDebugLog( 'thumbnail',
sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
- wfHostname(), $retval, trim($err), $cmd ) );
+ wfHostname(), $retval, trim( $err ), $cmd ) );
return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
} else {
$params = array(
@@ -228,7 +228,7 @@ class DjVuHandler extends ImageHandler {
* @param $gettext Boolean: DOCUMENT (Default: false)
* @return bool
*/
- function getMetaTree( $image , $gettext = false ) {
+ function getMetaTree( $image, $gettext = false ) {
if ( isset( $image->dejaMetaTree ) ) {
return $image->dejaMetaTree;
}
@@ -246,24 +246,23 @@ class DjVuHandler extends ImageHandler {
$image->dejaMetaTree = false;
$image->djvuTextTree = false;
$tree = new SimpleXMLElement( $metadata );
- if( $tree->getName() == 'mw-djvu' ) {
- foreach($tree->children() as $b){
- if( $b->getName() == 'DjVuTxt' ) {
+ if ( $tree->getName() == 'mw-djvu' ) {
+ foreach ( $tree->children() as $b ) {
+ if ( $b->getName() == 'DjVuTxt' ) {
$image->djvuTextTree = $b;
- }
- elseif ( $b->getName() == 'DjVuXML' ) {
+ } elseif ( $b->getName() == 'DjVuXML' ) {
$image->dejaMetaTree = $b;
}
}
} else {
$image->dejaMetaTree = $tree;
}
- } catch( Exception $e ) {
+ } catch ( Exception $e ) {
wfDebug( "Bogus multipage XML metadata on '{$image->getName()}'\n" );
}
wfRestoreWarnings();
wfProfileOut( __METHOD__ );
- if( $gettext ) {
+ if ( $gettext ) {
return $image->djvuTextTree;
} else {
return $image->dejaMetaTree;
@@ -294,7 +293,7 @@ class DjVuHandler extends ImageHandler {
}
function isMetadataValid( $image, $metadata ) {
- return !empty( $metadata ) && $metadata != serialize(array());
+ return !empty( $metadata ) && $metadata != serialize( array() );
}
function pageCount( $image ) {
@@ -311,7 +310,7 @@ class DjVuHandler extends ImageHandler {
return false;
}
- $o = $tree->BODY[0]->OBJECT[$page-1];
+ $o = $tree->BODY[0]->OBJECT[$page - 1];
if ( $o ) {
return array(
'width' => intval( $o['width'] ),
@@ -322,13 +321,13 @@ class DjVuHandler extends ImageHandler {
}
}
- function getPageText( $image, $page ){
+ function getPageText( $image, $page ) {
$tree = $this->getMetaTree( $image, true );
if ( !$tree ) {
return false;
}
- $o = $tree->BODY[0]->PAGE[$page-1];
+ $o = $tree->BODY[0]->PAGE[$page - 1];
if ( $o ) {
$txt = $o['value'];
return $txt;
diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php
index 6aef562b..54efe7a8 100644
--- a/includes/media/DjVuImage.php
+++ b/includes/media/DjVuImage.php
@@ -34,11 +34,21 @@
* @ingroup Media
*/
class DjVuImage {
+ /**
+ * Constructor
+ *
+ * @param string $filename The DjVu file name.
+ */
function __construct( $filename ) {
$this->mFilename = $filename;
}
/**
+ * @const DJVUTXT_MEMORY_LIMIT Memory limit for the DjVu description software
+ */
+ const DJVUTXT_MEMORY_LIMIT = 300000;
+
+ /**
* Check if the given file is indeed a valid DjVu image file
* @return bool
*/
@@ -47,7 +57,6 @@ class DjVuImage {
return $info !== false;
}
-
/**
* Return data in the style of getimagesize()
* @return array or false on failure
@@ -55,8 +64,8 @@ class DjVuImage {
public function getImageSize() {
$data = $this->getInfo();
- if( $data !== false ) {
- $width = $data['width'];
+ if ( $data !== false ) {
+ $width = $data['width'];
$height = $data['height'];
return array( $width, $height, 'DjVu',
@@ -84,20 +93,20 @@ class DjVuImage {
$start = ftell( $file );
$secondary = fread( $file, 4 );
echo str_repeat( ' ', $indent * 4 ) . "($secondary)\n";
- while( ftell( $file ) - $start < $length ) {
+ while ( ftell( $file ) - $start < $length ) {
$chunkHeader = fread( $file, 8 );
- if( $chunkHeader == '' ) {
+ if ( $chunkHeader == '' ) {
break;
}
// @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) );
echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n";
- if( $chunk == 'FORM' ) {
+ if ( $chunk == 'FORM' ) {
$this->dumpForm( $file, $chunkLength, $indent + 1 );
} else {
fseek( $file, $chunkLength, SEEK_CUR );
- if( $chunkLength & 1 == 1 ) {
+ if ( $chunkLength & 1 == 1 ) {
// Padding byte between chunks
fseek( $file, 1, SEEK_CUR );
}
@@ -109,7 +118,7 @@ class DjVuImage {
wfSuppressWarnings();
$file = fopen( $this->mFilename, 'rb' );
wfRestoreWarnings();
- if( $file === false ) {
+ if ( $file === false ) {
wfDebug( __METHOD__ . ": missing or failed file read\n" );
return false;
}
@@ -117,21 +126,21 @@ class DjVuImage {
$header = fread( $file, 16 );
$info = false;
- if( strlen( $header ) < 16 ) {
+ if ( strlen( $header ) < 16 ) {
wfDebug( __METHOD__ . ": too short file header\n" );
} else {
// @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) );
- if( $magic != 'AT&T' ) {
+ if ( $magic != 'AT&T' ) {
wfDebug( __METHOD__ . ": not a DjVu file\n" );
- } elseif( $subtype == 'DJVU' ) {
+ } elseif ( $subtype == 'DJVU' ) {
// Single-page document
$info = $this->getPageInfo( $file, $formLength );
- } elseif( $subtype == 'DJVM' ) {
+ } elseif ( $subtype == 'DJVM' ) {
// Multi-page document
$info = $this->getMultiPageInfo( $file, $formLength );
- } else {
+ } else {
wfDebug( __METHOD__ . ": unrecognized DJVU file type '$formType'\n" );
}
}
@@ -141,7 +150,7 @@ class DjVuImage {
private function readChunk( $file ) {
$header = fread( $file, 8 );
- if( strlen( $header ) < 8 ) {
+ if ( strlen( $header ) < 8 ) {
return array( false, 0 );
} else {
// @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
@@ -153,7 +162,7 @@ class DjVuImage {
private function skipChunk( $file, $chunkLength ) {
fseek( $file, $chunkLength, SEEK_CUR );
- if( $chunkLength & 0x01 == 1 && !feof( $file ) ) {
+ if ( $chunkLength & 0x01 == 1 && !feof( $file ) ) {
// padding byte
fseek( $file, 1, SEEK_CUR );
}
@@ -165,13 +174,13 @@ class DjVuImage {
$start = ftell( $file );
do {
list( $chunk, $length ) = $this->readChunk( $file );
- if( !$chunk ) {
+ if ( !$chunk ) {
break;
}
- if( $chunk == 'FORM' ) {
+ if ( $chunk == 'FORM' ) {
$subtype = fread( $file, 4 );
- if( $subtype == 'DJVU' ) {
+ if ( $subtype == 'DJVU' ) {
wfDebug( __METHOD__ . ": found first subpage\n" );
return $this->getPageInfo( $file, $length );
}
@@ -180,7 +189,7 @@ class DjVuImage {
wfDebug( __METHOD__ . ": skipping '$chunk' chunk\n" );
$this->skipChunk( $file, $length );
}
- } while( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength );
+ } while ( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength );
wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" );
return false;
@@ -188,17 +197,17 @@ class DjVuImage {
private function getPageInfo( $file, $formLength ) {
list( $chunk, $length ) = $this->readChunk( $file );
- if( $chunk != 'INFO' ) {
+ if ( $chunk != 'INFO' ) {
wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" );
return false;
}
- if( $length < 9 ) {
+ if ( $length < 9 ) {
wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" );
return false;
}
$data = fread( $file, $length );
- if( strlen( $data ) < $length ) {
+ if ( strlen( $data ) < $length ) {
wfDebug( __METHOD__ . ": INFO chunk cut off\n" );
return false;
}
@@ -228,7 +237,7 @@ class DjVuImage {
function retrieveMetaData() {
global $wgDjvuToXML, $wgDjvuDump, $wgDjvuTxt;
wfProfileIn( __METHOD__ );
-
+
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
@@ -247,20 +256,20 @@ class DjVuImage {
$xml = null;
}
# Text layer
- if ( isset( $wgDjvuTxt ) ) {
+ if ( isset( $wgDjvuTxt ) ) {
wfProfileIn( 'djvutxt' );
- $cmd = wfEscapeShellArg( $wgDjvuTxt ) . ' --detail=page ' . wfEscapeShellArg( $this->mFilename ) ;
- wfDebug( __METHOD__.": $cmd\n" );
+ $cmd = wfEscapeShellArg( $wgDjvuTxt ) . ' --detail=page ' . wfEscapeShellArg( $this->mFilename );
+ wfDebug( __METHOD__ . ": $cmd\n" );
$retval = '';
- $txt = wfShellExec( $cmd, $retval );
+ $txt = wfShellExec( $cmd, $retval, array(), array( 'memory' => self::DJVUTXT_MEMORY_LIMIT ) );
wfProfileOut( 'djvutxt' );
- if( $retval == 0) {
+ if ( $retval == 0 ) {
# Strip some control characters
$txt = preg_replace( "/[\013\035\037]/", "", $txt );
$reg = <<<EOR
/\(page\s[\d-]*\s[\d-]*\s[\d-]*\s[\d-]*\s*"
((?> # Text to match is composed of atoms of either:
- \\\\. # - any escaped character
+ \\\\. # - any escaped character
| # - any character different from " and \
[^"\\\\]+
)*?)
@@ -271,7 +280,7 @@ EOR;
$txt = preg_replace_callback( $reg, array( $this, 'pageTextCallback' ), $txt );
$txt = "<DjVuTxt>\n<HEAD></HEAD>\n<BODY>\n" . $txt . "</BODY>\n</DjVuTxt>\n";
$xml = preg_replace( "/<DjVuXML>/", "<mw-djvu><DjVuXML>", $xml, 1 );
- $xml = $xml . $txt. '</mw-djvu>' ;
+ $xml = $xml . $txt . '</mw-djvu>';
}
}
wfProfileOut( __METHOD__ );
diff --git a/includes/media/Exif.php b/includes/media/Exif.php
index 784a6018..9a2794a5 100644
--- a/includes/media/Exif.php
+++ b/includes/media/Exif.php
@@ -31,15 +31,16 @@
*/
class Exif {
- const BYTE = 1; //!< An 8-bit (1-byte) unsigned integer.
- const ASCII = 2; //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.
- const SHORT = 3; //!< A 16-bit (2-byte) unsigned integer.
- const LONG = 4; //!< A 32-bit (4-byte) unsigned integer.
- const RATIONAL = 5; //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator
- const UNDEFINED = 7; //!< An 8-bit byte that can take any value depending on the field definition
- const SLONG = 9; //!< A 32-bit (4-byte) signed integer (2's complement notation),
- const SRATIONAL = 10; //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
- const IGNORE = -1; // A fake value for things we don't want or don't support.
+ const BYTE = 1; //!< An 8-bit (1-byte) unsigned integer.
+ const ASCII = 2; //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.
+ const SHORT = 3; //!< A 16-bit (2-byte) unsigned integer.
+ const LONG = 4; //!< A 32-bit (4-byte) unsigned integer.
+ const RATIONAL = 5; //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator
+ const SHORT_OR_LONG = 6; //!< A 16-bit (2-byte) or 32-bit (4-byte) unsigned integer.
+ const UNDEFINED = 7; //!< An 8-bit byte that can take any value depending on the field definition
+ const SLONG = 9; //!< A 32-bit (4-byte) signed integer (2's complement notation),
+ const SRATIONAL = 10; //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
+ const IGNORE = -1; // A fake value for things we don't want or don't support.
//@{
/* @var array
@@ -102,8 +103,9 @@ class Exif {
/**
* Constructor
*
- * @param $file String: filename.
- * @param $byteOrder String Type of byte ordering either 'BE' (Big Endian) or 'LE' (Little Endian). Default ''.
+ * @param string $file filename.
+ * @param string $byteOrder Type of byte ordering either 'BE' (Big Endian) or 'LE' (Little Endian). Default ''.
+ * @throws MWException
* @todo FIXME: The following are broke:
* SubjectArea. Need to test the more obscure tags.
*
@@ -112,7 +114,7 @@ class Exif {
*/
function __construct( $file, $byteOrder = '' ) {
/**
- * Page numbers here refer to pages in the EXIF 2.2 standard
+ * Page numbers here refer to pages in the Exif 2.2 standard
*
* Note, Exif::UNDEFINED is treated as a string, not as an array of bytes
* so don't put a count parameter for any UNDEFINED values.
@@ -123,8 +125,8 @@ class Exif {
# TIFF Rev. 6.0 Attribute Information (p22)
'IFD0' => array(
# Tags relating to image structure
- 'ImageWidth' => Exif::SHORT.','.Exif::LONG, # Image width
- 'ImageLength' => Exif::SHORT.','.Exif::LONG, # Image height
+ 'ImageWidth' => Exif::SHORT_OR_LONG, # Image width
+ 'ImageLength' => Exif::SHORT_OR_LONG, # Image height
'BitsPerSample' => array( Exif::SHORT, 3 ), # Number of bits per component
# "When a primary image is JPEG compressed, this designation is not"
# "necessary and is omitted." (p23)
@@ -133,25 +135,25 @@ class Exif {
'Orientation' => Exif::SHORT, # Orientation of image #p24
'SamplesPerPixel' => Exif::SHORT, # Number of components
'PlanarConfiguration' => Exif::SHORT, # Image data arrangement #p24
- 'YCbCrSubSampling' => array( Exif::SHORT, 2), # Subsampling ratio of Y to C #p24
+ 'YCbCrSubSampling' => array( Exif::SHORT, 2 ), # Subsampling ratio of Y to C #p24
'YCbCrPositioning' => Exif::SHORT, # Y and C positioning #p24-25
'XResolution' => Exif::RATIONAL, # Image resolution in width direction
'YResolution' => Exif::RATIONAL, # Image resolution in height direction
'ResolutionUnit' => Exif::SHORT, # Unit of X and Y resolution #(p26)
# Tags relating to recording offset
- 'StripOffsets' => Exif::SHORT.','.Exif::LONG, # Image data location
- 'RowsPerStrip' => Exif::SHORT.','.Exif::LONG, # Number of rows per strip
- 'StripByteCounts' => Exif::SHORT.','.Exif::LONG, # Bytes per compressed strip
- 'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG, # Offset to JPEG SOI
- 'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG, # Bytes of JPEG data
+ 'StripOffsets' => Exif::SHORT_OR_LONG, # Image data location
+ 'RowsPerStrip' => Exif::SHORT_OR_LONG, # Number of rows per strip
+ 'StripByteCounts' => Exif::SHORT_OR_LONG, # Bytes per compressed strip
+ 'JPEGInterchangeFormat' => Exif::SHORT_OR_LONG, # Offset to JPEG SOI
+ 'JPEGInterchangeFormatLength' => Exif::SHORT_OR_LONG, # Bytes of JPEG data
# Tags relating to image data characteristics
'TransferFunction' => Exif::IGNORE, # Transfer function
- 'WhitePoint' => array( Exif::RATIONAL, 2), # White point chromaticity
- 'PrimaryChromaticities' => array( Exif::RATIONAL, 6), # Chromaticities of primarities
- 'YCbCrCoefficients' => array( Exif::RATIONAL, 3), # Color space transformation matrix coefficients #p27
- 'ReferenceBlackWhite' => array( Exif::RATIONAL, 6), # Pair of black and white reference values
+ 'WhitePoint' => array( Exif::RATIONAL, 2 ), # White point chromaticity
+ 'PrimaryChromaticities' => array( Exif::RATIONAL, 6 ), # Chromaticities of primarities
+ 'YCbCrCoefficients' => array( Exif::RATIONAL, 3 ), # Color space transformation matrix coefficients #p27
+ 'ReferenceBlackWhite' => array( Exif::RATIONAL, 6 ), # Pair of black and white reference values
# Other tags
'DateTime' => Exif::ASCII, # File change date and time
@@ -166,8 +168,8 @@ class Exif {
# Exif IFD Attribute Information (p30-31)
'EXIF' => array(
# TODO: NOTE: Nonexistence of this field is taken to mean nonconformance
- # to the EXIF 2.1 AND 2.2 standards
- 'ExifVersion' => Exif::UNDEFINED, # Exif version
+ # to the Exif 2.1 AND 2.2 standards
+ 'ExifVersion' => Exif::UNDEFINED, # Exif version
'FlashPixVersion' => Exif::UNDEFINED, # Supported Flashpix version #p32
# Tags relating to Image Data Characteristics
@@ -176,8 +178,8 @@ class Exif {
# Tags relating to image configuration
'ComponentsConfiguration' => Exif::UNDEFINED, # Meaning of each component #p33
'CompressedBitsPerPixel' => Exif::RATIONAL, # Image compression mode
- 'PixelYDimension' => Exif::SHORT.','.Exif::LONG, # Valid image width
- 'PixelXDimension' => Exif::SHORT.','.Exif::LONG, # Valid image height
+ 'PixelYDimension' => Exif::SHORT_OR_LONG, # Valid image width
+ 'PixelXDimension' => Exif::SHORT_OR_LONG, # Valid image height
# Tags relating to related user information
'MakerNote' => Exif::IGNORE, # Manufacturer notes
@@ -217,7 +219,7 @@ class Exif {
'FocalPlaneXResolution' => Exif::RATIONAL, # Focal plane X resolution
'FocalPlaneYResolution' => Exif::RATIONAL, # Focal plane Y resolution
'FocalPlaneResolutionUnit' => Exif::SHORT, # Focal plane resolution unit #p46
- 'SubjectLocation' => array( Exif::SHORT, 2), # Subject location
+ 'SubjectLocation' => array( Exif::SHORT, 2 ), # Subject location
'ExposureIndex' => Exif::RATIONAL, # Exposure index
'SensingMethod' => Exif::SHORT, # Sensing method #p46
'FileSource' => Exif::UNDEFINED, # File source #p47
@@ -249,12 +251,12 @@ class Exif {
'GPSLatitudeRef' => Exif::ASCII, # North or South Latitude #p52-53
'GPSLatitude' => array( Exif::RATIONAL, 3 ), # Latitude
'GPSLongitudeRef' => Exif::ASCII, # East or West Longitude #p53
- 'GPSLongitude' => array( Exif::RATIONAL, 3), # Longitude
+ 'GPSLongitude' => array( Exif::RATIONAL, 3 ), # Longitude
'GPSAltitudeRef' => Exif::UNDEFINED,
# Altitude reference. Note, the exif standard says this should be an EXIF::Byte,
# but php seems to disagree.
'GPSAltitude' => Exif::RATIONAL, # Altitude
- 'GPSTimeStamp' => array( Exif::RATIONAL, 3), # GPS time (atomic clock)
+ 'GPSTimeStamp' => array( Exif::RATIONAL, 3 ), # GPS time (atomic clock)
'GPSSatellites' => Exif::ASCII, # Satellites used for measurement
'GPSStatus' => Exif::ASCII, # Receiver status #p54
'GPSMeasureMode' => Exif::ASCII, # Measurement mode #p54-55
@@ -289,13 +291,13 @@ class Exif {
// Only give a warning for b/c, since originally we didn't
// require this. The number of things affected by this is
// rather small.
- wfWarn( 'Exif class did not have byte order specified. '
- . 'Some properties may be decoded incorrectly.' );
+ wfWarn( 'Exif class did not have byte order specified. ' .
+ 'Some properties may be decoded incorrectly.' );
$this->byteOrder = 'BE'; // BE seems about twice as popular as LE in jpg's.
}
$this->debugFile( $this->basename, __FUNCTION__, true );
- if( function_exists( 'exif_read_data' ) ) {
+ if ( function_exists( 'exif_read_data' ) ) {
wfSuppressWarnings();
$data = exif_read_data( $this->file, 0, true );
wfRestoreWarnings();
@@ -321,7 +323,7 @@ class Exif {
foreach ( array_keys( $this->mRawExifData ) as $section ) {
if ( !in_array( $section, array_keys( $this->mExifTags ) ) ) {
- $this->debug( $section , __FUNCTION__, "'$section' is not a valid Exif section" );
+ $this->debug( $section, __FUNCTION__, "'$section' is not a valid Exif section" );
continue;
}
@@ -345,24 +347,24 @@ class Exif {
}
/**
- * Collapse some fields together.
- * This converts some fields from exif form, to a more friendly form.
- * For example GPS latitude to a single number.
- *
- * The rationale behind this is that we're storing data, not presenting to the user
- * For example a longitude is a single number describing how far away you are from
- * the prime meridian. Well it might be nice to split it up into minutes and seconds
- * for the user, it doesn't really make sense to split a single number into 4 parts
- * for storage. (degrees, minutes, second, direction vs single floating point number).
- *
- * Other things this might do (not really sure if they make sense or not):
- * Dates -> mediawiki date format.
- * convert values that can be in different units to be in one standardized unit.
- *
- * As an alternative approach, some of this could be done in the validate phase
- * if we make up our own types like Exif::DATE.
- */
- function collapseData( ) {
+ * Collapse some fields together.
+ * This converts some fields from exif form, to a more friendly form.
+ * For example GPS latitude to a single number.
+ *
+ * The rationale behind this is that we're storing data, not presenting to the user
+ * For example a longitude is a single number describing how far away you are from
+ * the prime meridian. Well it might be nice to split it up into minutes and seconds
+ * for the user, it doesn't really make sense to split a single number into 4 parts
+ * for storage. (degrees, minutes, second, direction vs single floating point number).
+ *
+ * Other things this might do (not really sure if they make sense or not):
+ * Dates -> mediawiki date format.
+ * convert values that can be in different units to be in one standardized unit.
+ *
+ * As an alternative approach, some of this could be done in the validate phase
+ * if we make up our own types like Exif::DATE.
+ */
+ function collapseData() {
$this->exifGPStoNumber( 'GPSLatitude' );
$this->exifGPStoNumber( 'GPSDestLatitude' );
@@ -386,22 +388,22 @@ class Exif {
$this->exifPropToOrd( 'SceneType' );
$this->charCodeString( 'UserComment' );
- $this->charCodeString( 'GPSProcessingMethod');
+ $this->charCodeString( 'GPSProcessingMethod' );
$this->charCodeString( 'GPSAreaInformation' );
-
+
//ComponentsConfiguration should really be an array instead of a string...
//This turns a string of binary numbers into an array of numbers.
- if ( isset ( $this->mFilteredExifData['ComponentsConfiguration'] ) ) {
+ if ( isset( $this->mFilteredExifData['ComponentsConfiguration'] ) ) {
$val = $this->mFilteredExifData['ComponentsConfiguration'];
$ccVals = array();
- for ($i = 0; $i < strlen($val); $i++) {
- $ccVals[$i] = ord( substr($val, $i, 1) );
+ for ( $i = 0; $i < strlen( $val ); $i++ ) {
+ $ccVals[$i] = ord( substr( $val, $i, 1 ) );
}
$ccVals['_type'] = 'ol'; //this is for formatting later.
$this->mFilteredExifData['ComponentsConfiguration'] = $ccVals;
}
-
+
//GPSVersion(ID) is treated as the wrong type by php exif support.
//Go through each byte turning it into a version string.
//For example: "\x02\x02\x00\x00" -> "2.2.0.0"
@@ -409,14 +411,14 @@ class Exif {
//Also change exif tag name from GPSVersion (what php exif thinks it is)
//to GPSVersionID (what the exif standard thinks it is).
- if ( isset ( $this->mFilteredExifData['GPSVersion'] ) ) {
+ if ( isset( $this->mFilteredExifData['GPSVersion'] ) ) {
$val = $this->mFilteredExifData['GPSVersion'];
$newVal = '';
- for ($i = 0; $i < strlen($val); $i++) {
+ for ( $i = 0; $i < strlen( $val ); $i++ ) {
if ( $i !== 0 ) {
$newVal .= '.';
}
- $newVal .= ord( substr($val, $i, 1) );
+ $newVal .= ord( substr( $val, $i, 1 ) );
}
if ( $this->byteOrder === 'LE' ) {
// Need to reverse the string
@@ -433,26 +435,25 @@ class Exif {
}
/**
- * Do userComment tags and similar. See pg. 34 of exif standard.
- * basically first 8 bytes is charset, rest is value.
- * This has not been tested on any shift-JIS strings.
- * @param $prop String prop name.
- */
- private function charCodeString ( $prop ) {
+ * Do userComment tags and similar. See pg. 34 of exif standard.
+ * basically first 8 bytes is charset, rest is value.
+ * This has not been tested on any shift-JIS strings.
+ * @param string $prop prop name.
+ */
+ private function charCodeString( $prop ) {
if ( isset( $this->mFilteredExifData[$prop] ) ) {
- if ( strlen($this->mFilteredExifData[$prop]) <= 8 ) {
+ if ( strlen( $this->mFilteredExifData[$prop] ) <= 8 ) {
//invalid. Must be at least 9 bytes long.
- $this->debug( $this->mFilteredExifData[$prop] , __FUNCTION__, false );
- unset($this->mFilteredExifData[$prop]);
+ $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, false );
+ unset( $this->mFilteredExifData[$prop] );
return;
}
- $charCode = substr( $this->mFilteredExifData[$prop], 0, 8);
- $val = substr( $this->mFilteredExifData[$prop], 8);
-
-
- switch ($charCode) {
+ $charCode = substr( $this->mFilteredExifData[$prop], 0, 8 );
+ $val = substr( $this->mFilteredExifData[$prop], 8 );
+
+ switch ( $charCode ) {
case "\x4A\x49\x53\x00\x00\x00\x00\x00":
//JIS
$charset = "Shift-JIS";
@@ -466,9 +467,9 @@ class Exif {
}
// This could possibly check to see if iconv is really installed
// or if we're using the compatibility wrapper in globalFunctions.php
- if ($charset) {
+ if ( $charset ) {
wfSuppressWarnings();
- $val = iconv($charset, 'UTF-8//IGNORE', $val);
+ $val = iconv( $charset, 'UTF-8//IGNORE', $val );
wfRestoreWarnings();
} else {
// if valid utf-8, assume that, otherwise assume windows-1252
@@ -476,17 +477,17 @@ class Exif {
UtfNormal::quickIsNFCVerify( $valCopy ); //validates $valCopy.
if ( $valCopy !== $val ) {
wfSuppressWarnings();
- $val = iconv('Windows-1252', 'UTF-8//IGNORE', $val);
+ $val = iconv( 'Windows-1252', 'UTF-8//IGNORE', $val );
wfRestoreWarnings();
}
}
-
+
//trim and check to make sure not only whitespace.
- $val = trim($val);
+ $val = trim( $val );
if ( strlen( $val ) === 0 ) {
//only whitespace.
- $this->debug( $this->mFilteredExifData[$prop] , __FUNCTION__, "$prop: Is only whitespace" );
- unset($this->mFilteredExifData[$prop]);
+ $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, "$prop: Is only whitespace" );
+ unset( $this->mFilteredExifData[$prop] );
return;
}
@@ -495,22 +496,22 @@ class Exif {
}
}
/**
- * Convert an Exif::UNDEFINED from a raw binary string
- * to its value. This is sometimes needed depending on
- * the type of UNDEFINED field
- * @param $prop String name of property
- */
- private function exifPropToOrd ( $prop ) {
+ * Convert an Exif::UNDEFINED from a raw binary string
+ * to its value. This is sometimes needed depending on
+ * the type of UNDEFINED field
+ * @param string $prop name of property
+ */
+ private function exifPropToOrd( $prop ) {
if ( isset( $this->mFilteredExifData[$prop] ) ) {
$this->mFilteredExifData[$prop] = ord( $this->mFilteredExifData[$prop] );
}
}
/**
- * Convert gps in exif form to a single floating point number
- * for example 10 degress 20`40`` S -> -10.34444
- * @param String $prop a gps coordinate exif tag name (like GPSLongitude)
- */
- private function exifGPStoNumber ( $prop ) {
+ * Convert gps in exif form to a single floating point number
+ * for example 10 degress 20`40`` S -> -10.34444
+ * @param string $prop a gps coordinate exif tag name (like GPSLongitude)
+ */
+ private function exifGPStoNumber( $prop ) {
$loc =& $this->mFilteredExifData[$prop];
$dir =& $this->mFilteredExifData[$prop . 'Ref'];
$res = false;
@@ -545,7 +546,7 @@ class Exif {
*
* @deprecated since 1.18
*/
- function makeFormattedData( ) {
+ function makeFormattedData() {
wfDeprecated( __METHOD__, '1.18' );
$this->mFormattedExifData = FormatMetadata::getFormattedData(
$this->mFilteredExifData );
@@ -580,7 +581,7 @@ class Exif {
*/
function getFormattedData() {
wfDeprecated( __METHOD__, '1.18' );
- if (!$this->mFormattedExifData) {
+ if ( !$this->mFormattedExifData ) {
$this->makeFormattedData();
}
return $this->mFormattedExifData;
@@ -612,7 +613,7 @@ class Exif {
* @return bool
*/
private function isByte( $in ) {
- if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) {
+ if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 255 ) {
$this->debug( $in, __FUNCTION__, true );
return true;
} else {
@@ -648,7 +649,7 @@ class Exif {
* @return bool
*/
private function isShort( $in ) {
- if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) {
+ if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 65536 ) {
$this->debug( $in, __FUNCTION__, true );
return true;
} else {
@@ -662,7 +663,7 @@ class Exif {
* @return bool
*/
private function isLong( $in ) {
- if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) {
+ if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 4294967296 ) {
$this->debug( $in, __FUNCTION__, true );
return true;
} else {
@@ -677,7 +678,7 @@ class Exif {
*/
private function isRational( $in ) {
$m = array();
- if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
+ if ( !is_array( $in ) && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
} else {
$this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
@@ -727,8 +728,8 @@ class Exif {
* Validates if a tag has a legal value according to the Exif spec
*
* @private
- * @param $section String: section where tag is located.
- * @param $tag String: the tag to check.
+ * @param string $section section where tag is located.
+ * @param string $tag the tag to check.
* @param $val Mixed: the value of the tag.
* @param $recursive Boolean: true if called recursively for array types.
* @return bool
@@ -737,26 +738,27 @@ class Exif {
$debug = "tag is '$tag'";
$etype = $this->mExifTags[$section][$tag];
$ecount = 1;
- if( is_array( $etype ) ) {
+ if ( is_array( $etype ) ) {
list( $etype, $ecount ) = $etype;
- if ( $recursive )
+ if ( $recursive ) {
$ecount = 1; // checking individual elements
+ }
}
$count = count( $val );
- if( $ecount != $count ) {
+ if ( $ecount != $count ) {
$this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
return false;
}
- if( $count > 1 ) {
- foreach( $val as $v ) {
- if( !$this->validate( $section, $tag, $v, true ) ) {
- return false;
- }
+ if ( $count > 1 ) {
+ foreach ( $val as $v ) {
+ if ( !$this->validate( $section, $tag, $v, true ) ) {
+ return false;
+ }
}
return true;
}
// Does not work if not typecast
- switch( (string)$etype ) {
+ switch ( (string)$etype ) {
case (string)Exif::BYTE:
$this->debug( $val, __FUNCTION__, $debug );
return $this->isByte( $val );
@@ -772,6 +774,9 @@ class Exif {
case (string)Exif::RATIONAL:
$this->debug( $val, __FUNCTION__, $debug );
return $this->isRational( $val );
+ case (string)Exif::SHORT_OR_LONG:
+ $this->debug( $val, __FUNCTION__, $debug );
+ return $this->isShort( $val ) || $this->isLong( $val );
case (string)Exif::UNDEFINED:
$this->debug( $val, __FUNCTION__, $debug );
return $this->isUndefined( $val );
@@ -781,9 +786,6 @@ class Exif {
case (string)Exif::SRATIONAL:
$this->debug( $val, __FUNCTION__, $debug );
return $this->isSrational( $val );
- case (string)Exif::SHORT.','.Exif::LONG:
- $this->debug( $val, __FUNCTION__, $debug );
- return $this->isShort( $val ) || $this->isLong( $val );
case (string)Exif::IGNORE:
$this->debug( $val, __FUNCTION__, $debug );
return false;
@@ -808,18 +810,18 @@ class Exif {
}
$type = gettype( $in );
$class = ucfirst( __CLASS__ );
- if ( $type === 'array' ) {
+ if ( is_array( $in ) ) {
$in = print_r( $in, true );
}
if ( $action === true ) {
- wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n");
+ wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n" );
} elseif ( $action === false ) {
- wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n");
+ wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n" );
} elseif ( $action === null ) {
- wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n");
+ wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n" );
} else {
- wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n");
+ wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n" );
}
}
@@ -828,7 +830,7 @@ class Exif {
*
* @private
*
- * @param $fname String: the name of the function calling this function
+ * @param string $fname the name of the function calling this function
* @param $io Boolean: Specify whether we're beginning or ending
*/
private function debugFile( $fname, $io ) {
@@ -843,4 +845,3 @@ class Exif {
}
}
}
-
diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php
index 34a1f511..d8d0bede 100644
--- a/includes/media/ExifBitmap.php
+++ b/includes/media/ExifBitmap.php
@@ -34,8 +34,8 @@ class ExifBitmapHandler extends BitmapHandler {
function convertMetadataVersion( $metadata, $version = 1 ) {
// basically flattens arrays.
- $version = explode(';', $version, 2);
- $version = intval($version[0]);
+ $version = explode( ';', $version, 2 );
+ $version = intval( $version[0] );
if ( $version < 1 || $version >= 2 ) {
return $metadata;
}
@@ -51,12 +51,12 @@ class ExifBitmapHandler extends BitmapHandler {
// Treat Software as a special case because in can contain
// an array of (SoftwareName, Version).
- if (isset( $metadata['Software'] )
+ if ( isset( $metadata['Software'] )
&& is_array( $metadata['Software'] )
- && is_array( $metadata['Software'][0])
+ && is_array( $metadata['Software'][0] )
&& isset( $metadata['Software'][0][0] )
- && isset( $metadata['Software'][0][1])
- ) {
+ && isset( $metadata['Software'][0][1] )
+ ) {
$metadata['Software'] = $metadata['Software'][0][0] . ' (Version '
. $metadata['Software'][0][1] . ')';
}
@@ -84,9 +84,9 @@ class ExifBitmapHandler extends BitmapHandler {
return self::METADATA_GOOD;
}
if ( $metadata === self::OLD_BROKEN_FILE ) {
- # Old special value indicating that there is no EXIF data in the file.
+ # Old special value indicating that there is no Exif data in the file.
# or that there was an error well extracting the metadata.
- wfDebug( __METHOD__ . ": back-compat version\n");
+ wfDebug( __METHOD__ . ": back-compat version\n" );
return self::METADATA_COMPATIBLE;
}
if ( $metadata === self::BROKEN_FILE ) {
@@ -102,11 +102,11 @@ class ExifBitmapHandler extends BitmapHandler {
$exif['MEDIAWIKI_EXIF_VERSION'] == 1 )
{
//back-compatible but old
- wfDebug( __METHOD__.": back-compat version\n" );
+ wfDebug( __METHOD__ . ": back-compat version\n" );
return self::METADATA_COMPATIBLE;
}
# Wrong (non-compatible) version
- wfDebug( __METHOD__.": wrong version\n" );
+ wfDebug( __METHOD__ . ": wrong version\n" );
return self::METADATA_BAD;
}
return self::METADATA_GOOD;
@@ -163,7 +163,7 @@ class ExifBitmapHandler extends BitmapHandler {
$rotation = 0;
}
- if ($rotation == 90 || $rotation == 270) {
+ if ( $rotation == 90 || $rotation == 270 ) {
$width = $gis[0];
$gis[0] = $gis[1];
$gis[1] = $width;
@@ -225,4 +225,3 @@ class ExifBitmapHandler extends BitmapHandler {
return 0;
}
}
-
diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php
index 843c1fa2..1c5136f5 100644
--- a/includes/media/FormatMetadata.php
+++ b/includes/media/FormatMetadata.php
@@ -1,6 +1,6 @@
<?php
/**
- * Formating of image metadata values into human readable form.
+ * Formatting of image metadata values into human readable form.
*
* 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
@@ -25,7 +25,6 @@
* @file
*/
-
/**
* Format Image metadata values into a human readable form.
*
@@ -53,7 +52,7 @@ class FormatMetadata {
* value which most of the time are plain integers. This function
* formats Exif (and other metadata) values into human readable form.
*
- * @param $tags Array: the Exif data to format ( as returned by
+ * @param array $tags the Exif data to format ( as returned by
* Exif::getFilteredData() or BitmapMetadataHandler )
* @return array
*/
@@ -80,20 +79,20 @@ class FormatMetadata {
}
//This is done differently as the tag is an array.
- if ($tag == 'GPSTimeStamp' && count($vals) === 3) {
+ if ( $tag == 'GPSTimeStamp' && count( $vals ) === 3 ) {
//hour min sec array
- $h = explode('/', $vals[0]);
- $m = explode('/', $vals[1]);
- $s = explode('/', $vals[2]);
+ $h = explode( '/', $vals[0] );
+ $m = explode( '/', $vals[1] );
+ $s = explode( '/', $vals[2] );
// this should already be validated
// when loaded from file, but it could
// come from a foreign repo, so be
// paranoid.
- if ( !isset($h[1])
- || !isset($m[1])
- || !isset($s[1])
+ if ( !isset( $h[1] )
+ || !isset( $m[1] )
+ || !isset( $s[1] )
|| $h[1] == 0
|| $m[1] == 0
|| $s[1] == 0
@@ -128,9 +127,9 @@ class FormatMetadata {
foreach ( $vals as &$val ) {
- switch( $tag ) {
+ switch ( $tag ) {
case 'Compression':
- switch( $val ) {
+ switch ( $val ) {
case 1: case 2: case 3: case 4:
case 5: case 6: case 7: case 8:
case 32773: case 32946: case 34712:
@@ -143,7 +142,7 @@ class FormatMetadata {
break;
case 'PhotometricInterpretation':
- switch( $val ) {
+ switch ( $val ) {
case 2: case 6:
$val = self::msg( $tag, $val );
break;
@@ -154,7 +153,7 @@ class FormatMetadata {
break;
case 'Orientation':
- switch( $val ) {
+ switch ( $val ) {
case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
$val = self::msg( $tag, $val );
break;
@@ -165,7 +164,7 @@ class FormatMetadata {
break;
case 'PlanarConfiguration':
- switch( $val ) {
+ switch ( $val ) {
case 1: case 2:
$val = self::msg( $tag, $val );
break;
@@ -190,7 +189,7 @@ class FormatMetadata {
case 'XResolution':
case 'YResolution':
- switch( $resolutionunit ) {
+ switch ( $resolutionunit ) {
case 2:
$val = self::msg( 'XYResolution', 'i', self::formatNum( $val ) );
break;
@@ -209,7 +208,7 @@ class FormatMetadata {
break;
case 'ColorSpace':
- switch( $val ) {
+ switch ( $val ) {
case 1: case 65535:
$val = self::msg( $tag, $val );
break;
@@ -220,7 +219,7 @@ class FormatMetadata {
break;
case 'ComponentsConfiguration':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6:
$val = self::msg( $tag, $val );
break;
@@ -268,7 +267,7 @@ class FormatMetadata {
break;
case 'ExposureProgram':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
$val = self::msg( $tag, $val );
break;
@@ -283,7 +282,7 @@ class FormatMetadata {
break;
case 'MeteringMode':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255:
$val = self::msg( $tag, $val );
break;
@@ -294,7 +293,7 @@ class FormatMetadata {
break;
case 'LightSource':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11:
case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20:
case 21: case 22: case 23: case 24: case 255:
@@ -308,11 +307,11 @@ class FormatMetadata {
case 'Flash':
$flashDecode = array(
- 'fired' => $val & bindec( '00000001' ),
- 'return' => ( $val & bindec( '00000110' ) ) >> 1,
- 'mode' => ( $val & bindec( '00011000' ) ) >> 3,
+ 'fired' => $val & bindec( '00000001' ),
+ 'return' => ( $val & bindec( '00000110' ) ) >> 1,
+ 'mode' => ( $val & bindec( '00011000' ) ) >> 3,
'function' => ( $val & bindec( '00100000' ) ) >> 5,
- 'redeye' => ( $val & bindec( '01000000' ) ) >> 6,
+ 'redeye' => ( $val & bindec( '01000000' ) ) >> 6,
// 'reserved' => ($val & bindec( '10000000' )) >> 7,
);
$flashMsgs = array();
@@ -322,14 +321,14 @@ class FormatMetadata {
if ( $subTag != 'fired' && $subValue == 0 ) {
continue;
}
- $fullTag = $tag . '-' . $subTag ;
+ $fullTag = $tag . '-' . $subTag;
$flashMsgs[] = self::msg( $fullTag, $subValue );
}
$val = $wgLang->commaList( $flashMsgs );
break;
case 'FocalPlaneResolutionUnit':
- switch( $val ) {
+ switch ( $val ) {
case 2:
$val = self::msg( $tag, $val );
break;
@@ -340,7 +339,7 @@ class FormatMetadata {
break;
case 'SensingMethod':
- switch( $val ) {
+ switch ( $val ) {
case 1: case 2: case 3: case 4: case 5: case 7: case 8:
$val = self::msg( $tag, $val );
break;
@@ -351,7 +350,7 @@ class FormatMetadata {
break;
case 'FileSource':
- switch( $val ) {
+ switch ( $val ) {
case 3:
$val = self::msg( $tag, $val );
break;
@@ -362,7 +361,7 @@ class FormatMetadata {
break;
case 'SceneType':
- switch( $val ) {
+ switch ( $val ) {
case 1:
$val = self::msg( $tag, $val );
break;
@@ -373,7 +372,7 @@ class FormatMetadata {
break;
case 'CustomRendered':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1:
$val = self::msg( $tag, $val );
break;
@@ -384,7 +383,7 @@ class FormatMetadata {
break;
case 'ExposureMode':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2:
$val = self::msg( $tag, $val );
break;
@@ -395,7 +394,7 @@ class FormatMetadata {
break;
case 'WhiteBalance':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1:
$val = self::msg( $tag, $val );
break;
@@ -406,7 +405,7 @@ class FormatMetadata {
break;
case 'SceneCaptureType':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3:
$val = self::msg( $tag, $val );
break;
@@ -417,7 +416,7 @@ class FormatMetadata {
break;
case 'GainControl':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3: case 4:
$val = self::msg( $tag, $val );
break;
@@ -428,7 +427,7 @@ class FormatMetadata {
break;
case 'Contrast':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2:
$val = self::msg( $tag, $val );
break;
@@ -439,7 +438,7 @@ class FormatMetadata {
break;
case 'Saturation':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2:
$val = self::msg( $tag, $val );
break;
@@ -450,7 +449,7 @@ class FormatMetadata {
break;
case 'Sharpness':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2:
$val = self::msg( $tag, $val );
break;
@@ -461,7 +460,7 @@ class FormatMetadata {
break;
case 'SubjectDistanceRange':
- switch( $val ) {
+ switch ( $val ) {
case 0: case 1: case 2: case 3:
$val = self::msg( $tag, $val );
break;
@@ -474,7 +473,7 @@ class FormatMetadata {
//The GPS...Ref values are kept for compatibility, probably won't be reached.
case 'GPSLatitudeRef':
case 'GPSDestLatitudeRef':
- switch( $val ) {
+ switch ( $val ) {
case 'N': case 'S':
$val = self::msg( 'GPSLatitude', $val );
break;
@@ -486,7 +485,7 @@ class FormatMetadata {
case 'GPSLongitudeRef':
case 'GPSDestLongitudeRef':
- switch( $val ) {
+ switch ( $val ) {
case 'E': case 'W':
$val = self::msg( 'GPSLongitude', $val );
break;
@@ -505,7 +504,7 @@ class FormatMetadata {
break;
case 'GPSStatus':
- switch( $val ) {
+ switch ( $val ) {
case 'A': case 'V':
$val = self::msg( $tag, $val );
break;
@@ -516,7 +515,7 @@ class FormatMetadata {
break;
case 'GPSMeasureMode':
- switch( $val ) {
+ switch ( $val ) {
case 2: case 3:
$val = self::msg( $tag, $val );
break;
@@ -526,11 +525,10 @@ class FormatMetadata {
}
break;
-
case 'GPSTrackRef':
case 'GPSImgDirectionRef':
case 'GPSDestBearingRef':
- switch( $val ) {
+ switch ( $val ) {
case 'T': case 'M':
$val = self::msg( 'GPSDirection', $val );
break;
@@ -550,7 +548,7 @@ class FormatMetadata {
break;
case 'GPSSpeedRef':
- switch( $val ) {
+ switch ( $val ) {
case 'K': case 'M': case 'N':
$val = self::msg( 'GPSSpeed', $val );
break;
@@ -561,7 +559,7 @@ class FormatMetadata {
break;
case 'GPSDestDistanceRef':
- switch( $val ) {
+ switch ( $val ) {
case 'K': case 'M': case 'N':
$val = self::msg( 'GPSDestDistance', $val );
break;
@@ -631,7 +629,7 @@ class FormatMetadata {
case 'MaxApertureValue':
if ( strpos( $val, '/' ) !== false ) {
// need to expand this earlier to calculate fNumber
- list($n, $d) = explode('/', $val);
+ list( $n, $d ) = explode( '/', $val );
if ( is_numeric( $n ) && is_numeric( $d ) ) {
$val = $n / $d;
}
@@ -648,7 +646,7 @@ class FormatMetadata {
break;
case 'iimCategory':
- switch( strtolower( $val ) ) {
+ switch ( strtolower( $val ) ) {
// See pg 29 of IPTC photo
// metadata standard.
case 'ace': case 'clj':
@@ -684,7 +682,7 @@ class FormatMetadata {
$urgency = 'high';
} elseif ( $val == 5 ) {
$urgency = 'normal';
- } elseif ( $val <= 8 && $val > 5) {
+ } elseif ( $val <= 8 && $val > 5 ) {
$urgency = 'low';
}
@@ -793,7 +791,7 @@ class FormatMetadata {
}
break;
case 'Copyrighted':
- switch( $val ) {
+ switch ( $val ) {
case 'True': case 'False':
$val = self::msg( $tag, $val );
break;
@@ -809,7 +807,7 @@ class FormatMetadata {
case 'LanguageCode':
$lang = Language::fetchLanguageName( strtolower( $val ), $wgLang->getCode() );
- if ($lang) {
+ if ( $lang ) {
$val = htmlspecialchars( $lang );
} else {
$val = htmlspecialchars( $val );
@@ -829,20 +827,20 @@ class FormatMetadata {
}
/**
- * A function to collapse multivalued tags into a single value.
- * This turns an array of (for example) authors into a bulleted list.
- *
- * This is public on the basis it might be useful outside of this class.
- *
- * @param $vals Array array of values
- * @param $type String Type of array (either lang, ul, ol).
- * lang = language assoc array with keys being the lang code
- * ul = unordered list, ol = ordered list
- * type can also come from the '_type' member of $vals.
- * @param $noHtml Boolean If to avoid returning anything resembling
- * html. (Ugly hack for backwards compatibility with old mediawiki).
- * @return String single value (in wiki-syntax).
- */
+ * A function to collapse multivalued tags into a single value.
+ * This turns an array of (for example) authors into a bulleted list.
+ *
+ * This is public on the basis it might be useful outside of this class.
+ *
+ * @param array $vals array of values
+ * @param string $type Type of array (either lang, ul, ol).
+ * lang = language assoc array with keys being the lang code
+ * ul = unordered list, ol = ordered list
+ * type can also come from the '_type' member of $vals.
+ * @param $noHtml Boolean If to avoid returning anything resembling
+ * html. (Ugly hack for backwards compatibility with old mediawiki).
+ * @return String single value (in wiki-syntax).
+ */
public static function flattenArray( $vals, $type = 'ul', $noHtml = false ) {
if ( isset( $vals['_type'] ) ) {
$type = $vals['_type'];
@@ -850,13 +848,13 @@ class FormatMetadata {
}
if ( !is_array( $vals ) ) {
- return $vals; // do nothing if not an array;
+ return $vals; // do nothing if not an array;
}
elseif ( count( $vals ) === 1 && $type !== 'lang' ) {
return $vals[0];
}
elseif ( count( $vals ) === 0 ) {
- wfDebug( __METHOD__ . ' metadata array with 0 elements!' );
+ wfDebug( __METHOD__ . " metadata array with 0 elements!\n" );
return ""; // paranoia. This should never happen
}
/* @todo FIXME: This should hide some of the list entries if there are
@@ -865,7 +863,7 @@ class FormatMetadata {
*/
else {
global $wgContLang;
- switch( $type ) {
+ switch ( $type ) {
case 'lang':
// Display default, followed by ContLang,
// followed by the rest in no particular
@@ -899,7 +897,7 @@ class FormatMetadata {
}
$content .= self::langItem(
$vals[$cLang], $cLang,
- $isDefault, $noHtml );
+ $isDefault, $noHtml );
unset( $vals[$cLang] );
}
@@ -915,8 +913,8 @@ class FormatMetadata {
}
if ( $defaultItem !== false ) {
$content = self::langItem( $defaultItem,
- $defaultLang, true, $noHtml )
- . $content;
+ $defaultLang, true, $noHtml ) .
+ $content;
}
if ( $noHtml ) {
return $content;
@@ -941,8 +939,8 @@ class FormatMetadata {
/** Helper function for creating lists of translations.
*
- * @param $value String value (this is not escaped)
- * @param $lang String lang code of item or false
+ * @param string $value value (this is not escaped)
+ * @param string $lang lang code of item or false
* @param $default Boolean if it is default value.
* @param $noHtml Boolean If to avoid html (for back-compat)
* @throws MWException
@@ -950,9 +948,9 @@ class FormatMetadata {
* this is treated as wikitext not html).
*/
private static function langItem( $value, $lang, $default = false, $noHtml = false ) {
- if ( $lang === false && $default === false) {
- throw new MWException('$lang and $default cannot both '
- . 'be false.');
+ if ( $lang === false && $default === false ) {
+ throw new MWException( '$lang and $default cannot both '
+ . 'be false.' );
}
if ( $noHtml ) {
@@ -1008,17 +1006,18 @@ class FormatMetadata {
*
* @private
*
- * @param $tag String: the tag name to pass on
- * @param $val String: the value of the tag
- * @param $arg String: an argument to pass ($1)
- * @param $arg2 String: a 2nd argument to pass ($2)
+ * @param string $tag the tag name to pass on
+ * @param string $val the value of the tag
+ * @param string $arg an argument to pass ($1)
+ * @param string $arg2 a 2nd argument to pass ($2)
* @return string A wfMessage of "exif-$tag-$val" in lower case
*/
static function msg( $tag, $val, $arg = null, $arg2 = null ) {
global $wgContLang;
- if ($val === '')
+ if ( $val === '' ) {
$val = 'value';
+ }
return wfMessage( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text();
}
@@ -1033,10 +1032,10 @@ class FormatMetadata {
static function formatNum( $num, $round = false ) {
global $wgLang;
$m = array();
- if( is_array($num) ) {
+ if ( is_array( $num ) ) {
$out = array();
- foreach( $num as $number ) {
- $out[] = self::formatNum($number);
+ foreach ( $num as $number ) {
+ $out[] = self::formatNum( $number );
}
return $wgLang->commaList( $out );
}
@@ -1073,7 +1072,7 @@ class FormatMetadata {
$numerator = intval( $m[1] );
$denominator = intval( $m[2] );
$gcd = self::gcd( abs( $numerator ), $denominator );
- if( $gcd != 0 ) {
+ if ( $gcd != 0 ) {
// 0 shouldn't happen! ;)
return self::formatNum( $numerator / $gcd ) . '/' . self::formatNum( $denominator / $gcd );
}
@@ -1098,7 +1097,7 @@ class FormatMetadata {
else
return gcd( $b, $a % $b );
*/
- while( $b != 0 ) {
+ while ( $b != 0 ) {
$remainder = $a % $b;
// tail recursion...
@@ -1117,16 +1116,16 @@ class FormatMetadata {
* Note, leading 0's are significant, so this is
* a string, not an int.
*
- * @param $val String: The 8 digit news code.
+ * @param string $val The 8 digit news code.
* @return string The human readable form
*/
- static private function convertNewsCode( $val ) {
+ private static function convertNewsCode( $val ) {
if ( !preg_match( '/^\d{8}$/D', $val ) ) {
// Not a valid news code.
return $val;
}
$cat = '';
- switch( substr( $val , 0, 2 ) ) {
+ switch ( substr( $val, 0, 2 ) ) {
case '01':
$cat = 'ace';
break;
@@ -1190,8 +1189,8 @@ class FormatMetadata {
* Format a coordinate value, convert numbers from floating point
* into degree minute second representation.
*
- * @param $coord int degrees, minutes and seconds
- * @param $type String: latitude or longitude (for if its a NWS or E)
+ * @param int $coord degrees, minutes and seconds
+ * @param string $type latitude or longitude (for if its a NWS or E)
* @return mixed A floating point number or whatever we were fed
*/
static function formatCoords( $coord, $type ) {
@@ -1226,7 +1225,7 @@ class FormatMetadata {
/**
* Format the contact info field into a single value.
*
- * @param $vals Array array with fields of the ContactInfo
+ * @param array $vals array with fields of the ContactInfo
* struct defined in the IPTC4XMP spec. Or potentially
* an array with one element that is a free form text
* value from the older iptc iim 1:118 prop.
@@ -1238,7 +1237,7 @@ class FormatMetadata {
* @return String of html-ish looking wikitext
*/
public static function collapseContactInfo( $vals ) {
- if( ! ( isset( $vals['CiAdrExtadr'] )
+ if ( !( isset( $vals['CiAdrExtadr'] )
|| isset( $vals['CiAdrCity'] )
|| isset( $vals['CiAdrCtry'] )
|| isset( $vals['CiEmailWork'] )
@@ -1256,7 +1255,7 @@ class FormatMetadata {
// because people often insert >, etc into
// the metadata which should not be interpreted
// but we still want to auto-link urls.
- foreach( $vals as &$val ) {
+ foreach ( $vals as &$val ) {
$val = htmlspecialchars( $val );
}
return self::flattenArray( $vals );
@@ -1353,7 +1352,7 @@ class FormatMetadata {
*
* @deprecated since 1.18
*
-**/
+ */
class FormatExif {
var $meta;
@@ -1361,7 +1360,7 @@ class FormatExif {
* @param $meta array
*/
function FormatExif( $meta ) {
- wfDeprecated(__METHOD__);
+ wfDeprecated( __METHOD__, '1.18' );
$this->meta = $meta;
}
diff --git a/includes/media/GIF.php b/includes/media/GIF.php
index 84b9b8ca..608fb257 100644
--- a/includes/media/GIF.php
+++ b/includes/media/GIF.php
@@ -29,17 +29,17 @@
class GIFHandler extends BitmapHandler {
const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
-
+
function getMetadata( $image, $filename ) {
try {
$parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename );
- } catch( Exception $e ) {
+ } catch ( Exception $e ) {
// Broken file?
wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
return self::BROKEN_FILE;
}
- return serialize($parsedGIFMetadata);
+ return serialize( $parsedGIFMetadata );
}
/**
@@ -53,7 +53,7 @@ class GIFHandler extends BitmapHandler {
return false;
}
$meta = unserialize( $meta );
- if ( !isset( $meta['metadata'] ) || count( $meta['metadata'] ) <= 1 ) {
+ if ( !isset( $meta['metadata'] ) || count( $meta['metadata'] ) <= 1 ) {
return false;
}
@@ -85,8 +85,8 @@ class GIFHandler extends BitmapHandler {
function isAnimatedImage( $image ) {
$ser = $image->getMetadata();
if ( $ser ) {
- $metadata = unserialize($ser);
- if( $metadata['frameCount'] > 1 ) {
+ $metadata = unserialize( $ser );
+ if ( $metadata['frameCount'] > 1 ) {
return true;
}
}
@@ -119,13 +119,13 @@ class GIFHandler extends BitmapHandler {
wfRestoreWarnings();
if ( !$data || !is_array( $data ) ) {
- wfDebug(__METHOD__ . ' invalid GIF metadata' );
+ wfDebug( __METHOD__ . " invalid GIF metadata\n" );
return self::METADATA_BAD;
}
if ( !isset( $data['metadata']['_MW_GIF_VERSION'] )
|| $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION ) {
- wfDebug(__METHOD__ . ' old but compatible GIF metadata' );
+ wfDebug( __METHOD__ . " old but compatible GIF metadata\n" );
return self::METADATA_COMPATIBLE;
}
return self::METADATA_GOOD;
@@ -141,29 +141,29 @@ class GIFHandler extends BitmapHandler {
$original = parent::getLongDesc( $image );
wfSuppressWarnings();
- $metadata = unserialize($image->getMetadata());
+ $metadata = unserialize( $image->getMetadata() );
wfRestoreWarnings();
-
- if (!$metadata || $metadata['frameCount'] <= 1) {
+
+ if ( !$metadata || $metadata['frameCount'] <= 1 ) {
return $original;
}
/* Preserve original image info string, but strip the last char ')' so we can add even more */
$info = array();
$info[] = $original;
-
+
if ( $metadata['looped'] ) {
$info[] = wfMessage( 'file-info-gif-looped' )->parse();
}
-
+
if ( $metadata['frameCount'] > 1 ) {
$info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse();
}
-
+
if ( $metadata['duration'] ) {
$info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
}
-
+
return $wgLang->commaList( $info );
}
}
diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php
index 5fc5c1a7..887afa3f 100644
--- a/includes/media/GIFMetadataExtractor.php
+++ b/includes/media/GIFMetadataExtractor.php
@@ -49,16 +49,16 @@ class GIFMetadataExtractor {
* @return array
*/
static function getMetadata( $filename ) {
- self::$gif_frame_sep = pack( "C", ord("," ) );
- self::$gif_extension_sep = pack( "C", ord("!" ) );
- self::$gif_term = pack( "C", ord(";" ) );
+ self::$gif_frame_sep = pack( "C", ord( "," ) );
+ self::$gif_extension_sep = pack( "C", ord( "!" ) );
+ self::$gif_term = pack( "C", ord( ";" ) );
$frameCount = 0;
$duration = 0.0;
$isLooped = false;
$xmp = "";
$comment = array();
-
+
if ( !$filename ) {
throw new Exception( "No file name specified" );
} elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
@@ -73,7 +73,7 @@ class GIFMetadataExtractor {
// Check for the GIF header
$buf = fread( $fh, 6 );
- if ( !($buf == 'GIF87a' || $buf == 'GIF89a') ) {
+ if ( !( $buf == 'GIF87a' || $buf == 'GIF89a' ) ) {
throw new Exception( "Not a valid GIF file; header: $buf" );
}
@@ -90,10 +90,10 @@ class GIFMetadataExtractor {
// Skip over the GCT
self::readGCT( $fh, $bpp );
- while( !feof( $fh ) ) {
+ while ( !feof( $fh ) ) {
$buf = fread( $fh, 1 );
- if ($buf == self::$gif_frame_sep) {
+ if ( $buf == self::$gif_frame_sep ) {
// Found a frame
$frameCount++;
@@ -107,21 +107,25 @@ class GIFMetadataExtractor {
## Read GCT
self::readGCT( $fh, $bpp );
fread( $fh, 1 );
- self::skipBlock( $fh );
+ self::skipBlock( $fh );
} elseif ( $buf == self::$gif_extension_sep ) {
$buf = fread( $fh, 1 );
- if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $buf ) < 1 ) {
+ throw new Exception( "Ran out of input" );
+ }
$extension_code = unpack( 'C', $buf );
$extension_code = $extension_code[1];
- if ($extension_code == 0xF9) {
+ if ( $extension_code == 0xF9 ) {
// Graphics Control Extension.
fread( $fh, 1 ); // Block size
fread( $fh, 1 ); // Transparency, disposal method, user input
$buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
- if ( strlen( $buf ) < 2 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $buf ) < 2 ) {
+ throw new Exception( "Ran out of input" );
+ }
$delay = unpack( 'v', $buf );
$delay = $delay[1];
$duration += $delay * 0.01;
@@ -129,13 +133,15 @@ class GIFMetadataExtractor {
fread( $fh, 1 ); // Transparent colour index
$term = fread( $fh, 1 ); // Should be a terminator
- if ( strlen( $term ) < 1 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $term ) < 1 ) {
+ throw new Exception( "Ran out of input" );
+ }
$term = unpack( 'C', $term );
$term = $term[1];
- if ($term != 0 ) {
+ if ( $term != 0 ) {
throw new Exception( "Malformed Graphics Control Extension block" );
}
- } elseif ($extension_code == 0xFE) {
+ } elseif ( $extension_code == 0xFE ) {
// Comment block(s).
$data = self::readBlock( $fh );
if ( $data === "" ) {
@@ -157,48 +163,51 @@ class GIFMetadataExtractor {
$commentCount = count( $comment );
if ( $commentCount === 0
- || $comment[$commentCount-1] !== $data )
+ || $comment[$commentCount - 1] !== $data )
{
// Some applications repeat the same comment on each
// frame of an animated GIF image, so if this comment
// is identical to the last, only extract once.
$comment[] = $data;
}
- } elseif ($extension_code == 0xFF) {
+ } elseif ( $extension_code == 0xFF ) {
// Application extension (Netscape info about the animated gif)
// or XMP (or theoretically any other type of extension block)
$blockLength = fread( $fh, 1 );
- if ( strlen( $blockLength ) < 1 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $blockLength ) < 1 ) {
+ throw new Exception( "Ran out of input" );
+ }
$blockLength = unpack( 'C', $blockLength );
$blockLength = $blockLength[1];
$data = fread( $fh, $blockLength );
- if ($blockLength != 11 ) {
- wfDebug( __METHOD__ . ' GIF application block with wrong length' );
- fseek( $fh, -($blockLength + 1), SEEK_CUR );
+ if ( $blockLength != 11 ) {
+ wfDebug( __METHOD__ . " GIF application block with wrong length\n" );
+ fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
self::skipBlock( $fh );
continue;
}
// NETSCAPE2.0 (application name for animated gif)
if ( $data == 'NETSCAPE2.0' ) {
-
$data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
- if ($data != "\x03\x01") {
+ if ( $data != "\x03\x01" ) {
throw new Exception( "Expected \x03\x01, got $data" );
}
-
+
// Unsigned little-endian integer, loop count or zero for "forever"
$loopData = fread( $fh, 2 );
- if ( strlen( $loopData ) < 2 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $loopData ) < 2 ) {
+ throw new Exception( "Ran out of input" );
+ }
$loopData = unpack( 'v', $loopData );
$loopCount = $loopData[1];
-
- if ($loopCount != 1) {
+
+ if ( $loopCount != 1 ) {
$isLooped = true;
}
-
+
// Read out terminator byte
fread( $fh, 1 );
} elseif ( $data == 'XMP DataXMP' ) {
@@ -219,7 +228,7 @@ class GIFMetadataExtractor {
} else {
// unrecognized extension block
- fseek( $fh, -($blockLength + 1), SEEK_CUR );
+ fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
self::skipBlock( $fh );
continue;
}
@@ -229,10 +238,12 @@ class GIFMetadataExtractor {
} elseif ( $buf == self::$gif_term ) {
break;
} else {
- if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $buf ) < 1 ) {
+ throw new Exception( "Ran out of input" );
+ }
$byte = unpack( 'C', $buf );
$byte = $byte[1];
- throw new Exception( "At position: ".ftell($fh). ", Unknown byte ".$byte );
+ throw new Exception( "At position: " . ftell( $fh ) . ", Unknown byte " . $byte );
}
}
@@ -252,7 +263,7 @@ class GIFMetadataExtractor {
*/
static function readGCT( $fh, $bpp ) {
if ( $bpp > 0 ) {
- for( $i=1; $i<=pow( 2, $bpp ); ++$i ) {
+ for ( $i = 1; $i <= pow( 2, $bpp ); ++$i ) {
fread( $fh, 3 );
}
}
@@ -260,10 +271,13 @@ class GIFMetadataExtractor {
/**
* @param $data
+ * @throws Exception
* @return int
*/
static function decodeBPP( $data ) {
- if ( strlen( $data ) < 1 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $data ) < 1 ) {
+ throw new Exception( "Ran out of input" );
+ }
$buf = unpack( 'C', $data );
$buf = $buf[1];
$bpp = ( $buf & 7 ) + 1;
@@ -276,20 +290,23 @@ class GIFMetadataExtractor {
/**
* @param $fh
- * @return
+ * @throws Exception
*/
static function skipBlock( $fh ) {
while ( !feof( $fh ) ) {
$buf = fread( $fh, 1 );
- if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
+ if ( strlen( $buf ) < 1 ) {
+ throw new Exception( "Ran out of input" );
+ }
$block_len = unpack( 'C', $buf );
$block_len = $block_len[1];
- if ($block_len == 0) {
+ if ( $block_len == 0 ) {
return;
}
fread( $fh, $block_len );
}
}
+
/**
* Read a block. In the GIF format, a block is made up of
* several sub-blocks. Each sub block starts with one byte
@@ -301,6 +318,7 @@ class GIFMetadataExtractor {
* sub-blocks in the returned value. Normally this is false,
* except XMP is weird and does a hack where you need to keep
* these length bytes.
+ * @throws Exception
* @return string The data.
*/
static function readBlock( $fh, $includeLengths = false ) {
@@ -308,7 +326,7 @@ class GIFMetadataExtractor {
$subLength = fread( $fh, 1 );
$blocks = 0;
- while( $subLength !== "\0" ) {
+ while ( $subLength !== "\0" ) {
$blocks++;
if ( $blocks > self::MAX_SUBBLOCKS ) {
throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php
index 8fd3552f..544dd211 100644
--- a/includes/media/IPTC.php
+++ b/includes/media/IPTC.php
@@ -29,27 +29,27 @@
class IPTC {
/**
- * This takes the results of iptcparse() and puts it into a
- * form that can be handled by mediawiki. Generally called from
- * BitmapMetadataHandler::doApp13.
- *
- * @see http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf
- *
- * @param $rawData String app13 block from jpeg containing iptc/iim data
- * @return Array iptc metadata array
- */
+ * This takes the results of iptcparse() and puts it into a
+ * form that can be handled by mediawiki. Generally called from
+ * BitmapMetadataHandler::doApp13.
+ *
+ * @see http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf
+ *
+ * @param string $rawData app13 block from jpeg containing iptc/iim data
+ * @return Array iptc metadata array
+ */
static function parse( $rawData ) {
$parsed = iptcparse( $rawData );
$data = Array();
- if (!is_array($parsed)) {
+ if ( !is_array( $parsed ) ) {
return $data;
}
$c = '';
//charset info contained in tag 1:90.
- if (isset($parsed['1#090']) && isset($parsed['1#090'][0])) {
- $c = self::getCharset($parsed['1#090'][0]);
- if ($c === false) {
+ if ( isset( $parsed['1#090'] ) && isset( $parsed['1#090'][0] ) ) {
+ $c = self::getCharset( $parsed['1#090'][0] );
+ if ( $c === false ) {
//Unknown charset. refuse to parse.
//note: There is a different between
//unknown and no charset specified.
@@ -59,11 +59,11 @@ class IPTC {
}
foreach ( $parsed as $tag => $val ) {
- if ( isset( $val[0] ) && trim($val[0]) == '' ) {
- wfDebugLog('iptc', "IPTC tag $tag had only whitespace as its value.");
+ if ( isset( $val[0] ) && trim( $val[0] ) == '' ) {
+ wfDebugLog( 'iptc', "IPTC tag $tag had only whitespace as its value." );
continue;
}
- switch( $tag ) {
+ switch ( $tag ) {
case '2#120': /*IPTC caption. mapped with exif ImageDescription*/
$data['ImageDescription'] = self::convIPTC( $val, $c );
break;
@@ -175,7 +175,7 @@ class IPTC {
if ( isset( $parsed['2#070'] ) ) {
//if a version is set for the software.
$softwareVersion = self::convIPTC( $parsed['2#070'], $c );
- unset($parsed['2#070']);
+ unset( $parsed['2#070'] );
$data['Software'] = array( array( $software[0], $softwareVersion[0] ) );
} else {
$data['Software'] = $software;
@@ -198,7 +198,7 @@ class IPTC {
/* original transmission ref.
* "A code representing the location of original transmission ac-
* cording to practises of the provider."
- */
+ */
$data['OriginalTransmissionRef'] = self::convIPTC( $val, $c );
break;
case '2#118': /*contact*/
@@ -227,8 +227,8 @@ class IPTC {
} else {
$time = Array();
}
- $timestamp = self::timeHelper( $val, $time, $c );
- if ($timestamp) {
+ $timestamp = self::timeHelper( $val, $time, $c );
+ if ( $timestamp ) {
$data['DateTimeOriginal'] = $timestamp;
}
break;
@@ -241,8 +241,8 @@ class IPTC {
} else {
$time = Array();
}
- $timestamp = self::timeHelper( $val, $time, $c );
- if ($timestamp) {
+ $timestamp = self::timeHelper( $val, $time, $c );
+ if ( $timestamp ) {
$data['DateTimeDigitized'] = $timestamp;
}
break;
@@ -254,8 +254,8 @@ class IPTC {
} else {
$time = Array();
}
- $timestamp = self::timeHelper( $val, $time, $c );
- if ($timestamp) {
+ $timestamp = self::timeHelper( $val, $time, $c );
+ if ( $timestamp ) {
$data['DateTimeReleased'] = $timestamp;
}
break;
@@ -267,8 +267,8 @@ class IPTC {
} else {
$time = Array();
}
- $timestamp = self::timeHelper( $val, $time, $c );
- if ($timestamp) {
+ $timestamp = self::timeHelper( $val, $time, $c );
+ if ( $timestamp ) {
$data['DateTimeExpires'] = $timestamp;
}
break;
@@ -313,7 +313,7 @@ class IPTC {
// describing the subject matter of the content.
$codes = self::convIPTC( $val, $c );
foreach ( $codes as $ic ) {
- $fields = explode(':', $ic, 3 );
+ $fields = explode( ':', $ic, 3 );
if ( count( $fields ) < 2 ||
$fields[0] !== 'IPTC' )
@@ -350,43 +350,43 @@ class IPTC {
}
/**
- * Convert an iptc date and time tags into the exif format
- *
- * @todo Potentially this should also capture the timezone offset.
- * @param Array $date The date tag
- * @param Array $time The time tag
- * @param $c
- * @return String Date in exif format.
- */
+ * Convert an iptc date and time tags into the exif format
+ *
+ * @todo Potentially this should also capture the timezone offset.
+ * @param array $date The date tag
+ * @param array $time The time tag
+ * @param $c
+ * @return String Date in exif format.
+ */
private static function timeHelper( $date, $time, $c ) {
if ( count( $date ) === 1 ) {
//the standard says this should always be 1
//just double checking.
- list($date) = self::convIPTC( $date, $c );
+ list( $date ) = self::convIPTC( $date, $c );
} else {
return null;
}
if ( count( $time ) === 1 ) {
- list($time) = self::convIPTC( $time, $c );
+ list( $time ) = self::convIPTC( $time, $c );
$dateOnly = false;
} else {
$time = '000000+0000'; //placeholder
$dateOnly = true;
}
- if ( ! ( preg_match('/\d\d\d\d\d\d[-+]\d\d\d\d/', $time)
- && preg_match('/\d\d\d\d\d\d\d\d/', $date)
- && substr($date, 0, 4) !== '0000'
- && substr($date, 4, 2) !== '00'
- && substr($date, 6, 2) !== '00'
- ) ) {
+ if ( !( preg_match( '/\d\d\d\d\d\d[-+]\d\d\d\d/', $time )
+ && preg_match( '/\d\d\d\d\d\d\d\d/', $date )
+ && substr( $date, 0, 4 ) !== '0000'
+ && substr( $date, 4, 2 ) !== '00'
+ && substr( $date, 6, 2 ) !== '00'
+ ) ) {
//something wrong.
// Note, this rejects some valid dates according to iptc spec
// for example: the date 00000400 means the photo was taken in
// April, but the year and day is unknown. We don't process these
// types of incomplete dates atm.
- wfDebugLog( 'iptc', "IPTC: invalid time ( $time ) or date ( $date )");
+ wfDebugLog( 'iptc', "IPTC: invalid time ( $time ) or date ( $date )" );
return null;
}
@@ -396,7 +396,7 @@ class IPTC {
return null;
}
- $tz = ( intval( substr( $time, 7, 2 ) ) *60*60 )
+ $tz = ( intval( substr( $time, 7, 2 ) ) * 60 * 60 )
+ ( intval( substr( $time, 9, 2 ) ) * 60 );
if ( substr( $time, 6, 1 ) === '-' ) {
@@ -417,15 +417,15 @@ class IPTC {
}
/**
- * Helper function to convert charset for iptc values.
- * @param $data string|array The iptc string
- * @param $charset String: The charset
+ * Helper function to convert charset for iptc values.
+ * @param string|array $data The iptc string
+ * @param string $charset The charset
*
* @return string|array
- */
- private static function convIPTC ( $data, $charset ) {
+ */
+ private static function convIPTC( $data, $charset ) {
if ( is_array( $data ) ) {
- foreach ($data as &$val) {
+ foreach ( $data as &$val ) {
$val = self::convIPTCHelper( $val, $charset );
}
} else {
@@ -435,27 +435,27 @@ class IPTC {
return $data;
}
/**
- * Helper function of a helper function to convert charset for iptc values.
- * @param $data Mixed String or Array: The iptc string
- * @param $charset String: The charset
- *
- * @return string
- */
- private static function convIPTCHelper ( $data, $charset ) {
+ * Helper function of a helper function to convert charset for iptc values.
+ * @param $data Mixed String or Array: The iptc string
+ * @param string $charset The charset
+ *
+ * @return string
+ */
+ private static function convIPTCHelper( $data, $charset ) {
if ( $charset ) {
wfSuppressWarnings();
- $data = iconv($charset, "UTF-8//IGNORE", $data);
+ $data = iconv( $charset, "UTF-8//IGNORE", $data );
wfRestoreWarnings();
- if ($data === false) {
+ if ( $data === false ) {
$data = "";
- wfDebugLog('iptc', __METHOD__ . " Error converting iptc data charset $charset to utf-8");
+ wfDebugLog( 'iptc', __METHOD__ . " Error converting iptc data charset $charset to utf-8" );
}
} else {
//treat as utf-8 if is valid utf-8. otherwise pretend its windows-1252
// most of the time if there is no 1:90 tag, it is either ascii, latin1, or utf-8
$oldData = $data;
UtfNormal::quickIsNFCVerify( $data ); //make $data valid utf-8
- if ($data === $oldData) {
+ if ( $data === $oldData ) {
return $data; //if validation didn't change $data
} else {
return self::convIPTCHelper( $oldData, 'Windows-1252' );
@@ -465,14 +465,14 @@ class IPTC {
}
/**
- * take the value of 1:90 tag and returns a charset
- * @param String $tag 1:90 tag.
- * @return string charset name or "?"
- * Warning, this function does not (and is not intended to) detect
- * all iso 2022 escape codes. In practise, the code for utf-8 is the
- * only code that seems to have wide use. It does detect that code.
- */
- static function getCharset($tag) {
+ * take the value of 1:90 tag and returns a charset
+ * @param string $tag 1:90 tag.
+ * @return string charset name or "?"
+ * Warning, this function does not (and is not intended to) detect
+ * all iso 2022 escape codes. In practise, the code for utf-8 is the
+ * only code that seems to have wide use. It does detect that code.
+ */
+ static function getCharset( $tag ) {
//According to iim standard, charset is defined by the tag 1:90.
//in which there are iso 2022 escape sequences to specify the character set.
@@ -530,7 +530,7 @@ class IPTC {
case "\x1b(K":
$c = "ISO646-DE";
break;
- case "\x1b(N": //crylic
+ case "\x1b(N": //crylic
$c = "ISO_5427";
break;
case "\x1b(`": //iso646-NO
@@ -590,7 +590,7 @@ class IPTC {
$c = 'CSN_369103';
break;
default:
- wfDebugLog('iptc', __METHOD__ . 'Unknown charset in iptc 1:90: ' . bin2hex( $tag ) );
+ wfDebugLog( 'iptc', __METHOD__ . 'Unknown charset in iptc 1:90: ' . bin2hex( $tag ) );
//at this point just give up and refuse to parse iptc?
$c = false;
}
diff --git a/includes/media/ImageHandler.php b/includes/media/ImageHandler.php
index 61759074..e079003b 100644
--- a/includes/media/ImageHandler.php
+++ b/includes/media/ImageHandler.php
@@ -58,7 +58,7 @@ abstract class ImageHandler extends MediaHandler {
} elseif ( isset( $params['width'] ) ) {
$width = $params['width'];
} else {
- throw new MWException( 'No width specified to '.__METHOD__ );
+ throw new MWException( 'No width specified to ' . __METHOD__ );
}
# Removed for ProofreadPage
#$width = intval( $width );
@@ -92,7 +92,7 @@ abstract class ImageHandler extends MediaHandler {
if ( !isset( $params['page'] ) ) {
$params['page'] = 1;
- } else {
+ } else {
if ( $params['page'] > $image->pageCount() ) {
$params['page'] = $image->pageCount();
}
@@ -139,7 +139,6 @@ abstract class ImageHandler extends MediaHandler {
$params['height'] = $params['physicalHeight'];
}
-
if ( !$this->validateThumbParams( $params['physicalWidth'],
$params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) ) {
return false;
@@ -161,12 +160,12 @@ abstract class ImageHandler extends MediaHandler {
$width = intval( $width );
# Sanity check $width
- if( $width <= 0) {
- wfDebug( __METHOD__.": Invalid destination width: $width\n" );
+ if ( $width <= 0 ) {
+ wfDebug( __METHOD__ . ": Invalid destination width: $width\n" );
return false;
}
if ( $srcWidth <= 0 ) {
- wfDebug( __METHOD__.": Invalid source width: $srcWidth\n" );
+ wfDebug( __METHOD__ . ": Invalid source width: $srcWidth\n" );
return false;
}
@@ -188,9 +187,9 @@ abstract class ImageHandler extends MediaHandler {
if ( !$this->normaliseParams( $image, $params ) ) {
return false;
}
- $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) );
+ $url = wfAppendQuery( $script, $this->getScriptParams( $params ) );
- if( $image->mustRender() || $params['width'] < $image->getWidth() ) {
+ if ( $image->mustRender() || $params['width'] < $image->getWidth() ) {
return new ThumbnailImage( $image, $url, false, $params );
}
}
@@ -201,6 +200,19 @@ abstract class ImageHandler extends MediaHandler {
wfRestoreWarnings();
return $gis;
}
+ /**
+ * Function that returns the number of pixels to be thumbnailed.
+ * Intended for animated GIFs to multiply by the number of frames.
+ *
+ * If the file doesn't support a notion of "area" return 0.
+ *
+ * @param File $image
+ * @return int
+ */
+ function getImageArea( $image ) {
+ return $image->getWidth() * $image->getHeight();
+ }
+
/**
* @param $file File
diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php
index a15b6524..fa763668 100644
--- a/includes/media/Jpeg.php
+++ b/includes/media/Jpeg.php
@@ -32,12 +32,12 @@
*/
class JpegHandler extends ExifBitmapHandler {
- function getMetadata ( $image, $filename ) {
+ function getMetadata( $image, $filename ) {
try {
$meta = BitmapMetadataHandler::Jpeg( $filename );
if ( !is_array( $meta ) ) {
// This should never happen, but doesn't hurt to be paranoid.
- throw new MWException('Metadata array is not an array');
+ throw new MWException( 'Metadata array is not an array' );
}
$meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
return serialize( $meta );
@@ -59,5 +59,36 @@ class JpegHandler extends ExifBitmapHandler {
}
}
-}
+ /**
+ * @param $file File
+ * @param array $params Rotate parameters.
+ * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
+ * @since 1.21
+ * @return bool
+ */
+ public function rotate( $file, $params ) {
+ global $wgJpegTran;
+
+ $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
+ if ( $wgJpegTran && is_file( $wgJpegTran ) ) {
+ $cmd = wfEscapeShellArg( $wgJpegTran ) .
+ " -rotate " . wfEscapeShellArg( $rotation ) .
+ " -outfile " . wfEscapeShellArg( $params['dstPath'] ) .
+ " " . wfEscapeShellArg( $params['srcPath'] );
+ wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" );
+ wfProfileIn( 'jpegtran' );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
+ wfProfileOut( 'jpegtran' );
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+ return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
+ }
+ return false;
+ } else {
+ return parent::rotate( $file, $params );
+ }
+ }
+
+}
diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php
index 8d7e43b9..c7030eba 100644
--- a/includes/media/JpegMetadataExtractor.php
+++ b/includes/media/JpegMetadataExtractor.php
@@ -25,7 +25,7 @@
* Class for reading jpegs and extracting metadata.
* see also BitmapMetadataHandler.
*
- * Based somewhat on GIFMetadataExtrator.
+ * Based somewhat on GIFMetadataExtractor.
*
* @ingroup Media
*/
@@ -37,17 +37,17 @@ class JpegMetadataExtractor {
// that many segments. Your average file has about 10.
/** Function to extract metadata segments of interest from jpeg files
- * based on GIFMetadataExtractor.
- *
- * we can almost use getimagesize to do this
- * but gis doesn't support having multiple app1 segments
- * and those can't extract xmp on files containing both exif and xmp data
- *
- * @param String $filename name of jpeg file
- * @return Array of interesting segments.
- * @throws MWException if given invalid file.
- */
- static function segmentSplitter ( $filename ) {
+ * based on GIFMetadataExtractor.
+ *
+ * we can almost use getimagesize to do this
+ * but gis doesn't support having multiple app1 segments
+ * and those can't extract xmp on files containing both exif and xmp data
+ *
+ * @param string $filename name of jpeg file
+ * @return Array of interesting segments.
+ * @throws MWException if given invalid file.
+ */
+ static function segmentSplitter( $filename ) {
$showXMP = function_exists( 'xml_parser_create_ns' );
$segmentCount = 0;
@@ -87,7 +87,7 @@ class JpegMetadataExtractor {
}
$buffer = fread( $fh, 1 );
- while( $buffer === "\xFF" && !feof( $fh ) ) {
+ while ( $buffer === "\xFF" && !feof( $fh ) ) {
// Skip through any 0xFF padding bytes.
$buffer = fread( $fh, 1 );
}
@@ -111,7 +111,7 @@ class JpegMetadataExtractor {
if ( $com === $oldCom ) {
$segments["COM"][] = $oldCom;
} else {
- wfDebug( __METHOD__ . ' Ignoring JPEG comment as is garbage.' );
+ wfDebug( __METHOD__ . " Ignoring JPEG comment as is garbage.\n" );
}
} elseif ( $buffer === "\xE1" ) {
@@ -129,7 +129,7 @@ class JpegMetadataExtractor {
// whatever...
$segments["XMP"] = substr( $temp, 29 );
wfDebug( __METHOD__ . ' Found XMP section with wrong app identifier '
- . "Using anyways.\n" );
+ . "Using anyways.\n" );
} elseif ( substr( $temp, 0, 6 ) === "Exif\0\0" ) {
// Just need to find out what the byte order is.
// because php's exif plugin sucks...
@@ -140,7 +140,7 @@ class JpegMetadataExtractor {
} elseif ( $byteOrderMarker === 'II' ) {
$segments['byteOrder'] = 'LE';
} else {
- wfDebug( __METHOD__ . ' Invalid byte ordering?!' );
+ wfDebug( __METHOD__ . " Invalid byte ordering?!\n" );
}
}
} elseif ( $buffer === "\xED" ) {
@@ -155,7 +155,9 @@ class JpegMetadataExtractor {
} else {
// segment we don't care about, so skip
$size = wfUnpack( "nint", fread( $fh, 2 ), 2 );
- if ( $size['int'] <= 2 ) throw new MWException( "invalid marker size in jpeg" );
+ if ( $size['int'] <= 2 ) {
+ throw new MWException( "invalid marker size in jpeg" );
+ }
fseek( $fh, $size['int'] - 2, SEEK_CUR );
}
@@ -165,10 +167,11 @@ class JpegMetadataExtractor {
}
/**
- * Helper function for jpegSegmentSplitter
- * @param &$fh FileHandle for jpeg file
- * @return string data content of segment.
- */
+ * Helper function for jpegSegmentSplitter
+ * @param &$fh FileHandle for jpeg file
+ * @throws MWException
+ * @return string data content of segment.
+ */
private static function jpegExtractMarker( &$fh ) {
$size = wfUnpack( "nint", fread( $fh, 2 ), 2 );
if ( $size['int'] <= 2 ) {
@@ -182,19 +185,19 @@ class JpegMetadataExtractor {
}
/**
- * This reads the photoshop image resource.
- * Currently it only compares the iptc/iim hash
- * with the stored hash, which is used to determine the precedence
- * of the iptc data. In future it may extract some other info, like
- * url of copyright license.
- *
- * This should generally be called by BitmapMetadataHandler::doApp13()
- *
- * @param String $app13 photoshop psir app13 block from jpg.
- * @throws MWException (It gets caught next level up though)
- * @return String if the iptc hash is good or not.
- */
- public static function doPSIR ( $app13 ) {
+ * This reads the photoshop image resource.
+ * Currently it only compares the iptc/iim hash
+ * with the stored hash, which is used to determine the precedence
+ * of the iptc data. In future it may extract some other info, like
+ * url of copyright license.
+ *
+ * This should generally be called by BitmapMetadataHandler::doApp13()
+ *
+ * @param string $app13 photoshop psir app13 block from jpg.
+ * @throws MWException (It gets caught next level up though)
+ * @return String if the iptc hash is good or not.
+ */
+ public static function doPSIR( $app13 ) {
if ( !$app13 ) {
throw new MWException( "No App13 segment given" );
}
@@ -242,7 +245,9 @@ class JpegMetadataExtractor {
// PHP can take issue with very large unsigned ints and make them negative.
// Which should never ever happen, as this has to be inside a segment
// which is limited to a 16 bit number.
- if ( $lenData['len'] < 0 ) throw new MWException( "Too big PSIR (" . $lenData['len'] . ')' );
+ if ( $lenData['len'] < 0 ) {
+ throw new MWException( "Too big PSIR (" . $lenData['len'] . ')' );
+ }
$offset += 4; // 4bytes length field;
@@ -266,7 +271,9 @@ class JpegMetadataExtractor {
// if odd, add 1 to length to account for
// null pad byte.
- if ( $lenData['len'] % 2 == 1 ) $lenData['len']++;
+ if ( $lenData['len'] % 2 == 1 ) {
+ $lenData['len']++;
+ }
$offset += $lenData['len'];
}
diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php
index 965099fd..779e23c9 100644
--- a/includes/media/MediaHandler.php
+++ b/includes/media/MediaHandler.php
@@ -46,7 +46,7 @@ abstract class MediaHandler {
static function getHandler( $type ) {
global $wgMediaHandlers;
if ( !isset( $wgMediaHandlers[$type] ) ) {
- wfDebug( __METHOD__ . ": no handler found for $type.\n");
+ wfDebug( __METHOD__ . ": no handler found for $type.\n" );
return false;
}
$class = $wgMediaHandlers[$type];
@@ -78,14 +78,16 @@ abstract class MediaHandler {
/**
* Merge a parameter array into a string appropriate for inclusion in filenames
*
- * @param $params array
+ * @param $params array Array of parameters that have been through normaliseParams.
+ * @return String
*/
abstract function makeParamString( $params );
/**
* Parse a param string made with makeParamString back into an array
*
- * @param $str string
+ * @param $str string The parameter string without file name (e.g. 122px)
+ * @return Array|Boolean Array of parameters or false on failure.
*/
abstract function parseParamString( $str );
@@ -103,7 +105,7 @@ abstract class MediaHandler {
* can't be determined.
*
* @param $image File: the image object, or false if there isn't one
- * @param $path String: the filename
+ * @param string $path the filename
* @return Array Follow the format of PHP getimagesize() internal function. See http://www.php.net/getimagesize
*/
abstract function getImageSize( $image, $path );
@@ -113,42 +115,44 @@ abstract class MediaHandler {
*
* @param $image File: the image object, or false if there isn't one.
* Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!)
- * @param $path String: the filename
+ * @param string $path the filename
* @return String
*/
- function getMetadata( $image, $path ) { return ''; }
-
- /**
- * Get metadata version.
- *
- * This is not used for validating metadata, this is used for the api when returning
- * metadata, since api content formats should stay the same over time, and so things
- * using ForiegnApiRepo can keep backwards compatibility
- *
- * All core media handlers share a common version number, and extensions can
- * use the GetMetadataVersion hook to append to the array (they should append a unique
- * string so not to get confusing). If there was a media handler named 'foo' with metadata
- * version 3 it might add to the end of the array the element 'foo=3'. if the core metadata
- * version is 2, the end version string would look like '2;foo=3'.
- *
- * @return string version string
- */
- static function getMetadataVersion () {
+ function getMetadata( $image, $path ) {
+ return '';
+ }
+
+ /**
+ * Get metadata version.
+ *
+ * This is not used for validating metadata, this is used for the api when returning
+ * metadata, since api content formats should stay the same over time, and so things
+ * using ForiegnApiRepo can keep backwards compatibility
+ *
+ * All core media handlers share a common version number, and extensions can
+ * use the GetMetadataVersion hook to append to the array (they should append a unique
+ * string so not to get confusing). If there was a media handler named 'foo' with metadata
+ * version 3 it might add to the end of the array the element 'foo=3'. if the core metadata
+ * version is 2, the end version string would look like '2;foo=3'.
+ *
+ * @return string version string
+ */
+ static function getMetadataVersion() {
$version = Array( '2' ); // core metadata version
- wfRunHooks('GetMetadataVersion', Array(&$version));
- return implode( ';', $version);
- }
-
- /**
- * Convert metadata version.
- *
- * By default just returns $metadata, but can be used to allow
- * media handlers to convert between metadata versions.
- *
- * @param $metadata Mixed String or Array metadata array (serialized if string)
- * @param $version Integer target version
- * @return Array serialized metadata in specified version, or $metadata on fail.
- */
+ wfRunHooks( 'GetMetadataVersion', Array( &$version ) );
+ return implode( ';', $version );
+ }
+
+ /**
+ * Convert metadata version.
+ *
+ * By default just returns $metadata, but can be used to allow
+ * media handlers to convert between metadata versions.
+ *
+ * @param $metadata Mixed String or Array metadata array (serialized if string)
+ * @param $version Integer target version
+ * @return Array serialized metadata in specified version, or $metadata on fail.
+ */
function convertMetadataVersion( $metadata, $version = 1 ) {
if ( !is_array( $metadata ) ) {
@@ -166,7 +170,9 @@ abstract class MediaHandler {
*
* @return string
*/
- function getMetadataType( $image ) { return false; }
+ function getMetadataType( $image ) {
+ return false;
+ }
/**
* Check if the metadata string is valid for this handler.
@@ -181,7 +187,6 @@ abstract class MediaHandler {
return self::METADATA_GOOD;
}
-
/**
* Get a MediaTransformOutput object representing an alternate of the transformed
* output which will call an intermediary thumbnail assist script.
@@ -200,9 +205,9 @@ abstract class MediaHandler {
* actually do the transform.
*
* @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 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()
* @return MediaTransformOutput
*/
final function getTransform( $image, $dstPath, $dstUrl, $params ) {
@@ -214,9 +219,10 @@ abstract class MediaHandler {
* transform unless $flags contains 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 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()
+ * Note: These parameters have *not* gone through $this->normaliseParams()
* @param $flags Integer: a bitfield, may contain self::TRANSFORM_LATER
*
* @return MediaTransformOutput
@@ -225,6 +231,10 @@ abstract class MediaHandler {
/**
* Get the thumbnail extension and MIME type for a given source MIME type
+ *
+ * @param String $ext Extension of original file
+ * @param String $mime Mime type of original file
+ * @param Array $params Handler specific rendering parameters
* @return array thumbnail extension and MIME type
*/
function getThumbType( $ext, $mime, $params = null ) {
@@ -244,66 +254,108 @@ abstract class MediaHandler {
}
/**
+ * Get useful response headers for GET/HEAD requests for a file with the given metadata
+ * @param $metadata mixed Result of the getMetadata() function of this handler for a file
+ * @return Array
+ */
+ public function getStreamHeaders( $metadata ) {
+ return array();
+ }
+
+ /**
* True if the handled types can be transformed
* @return bool
*/
- function canRender( $file ) { return true; }
+ function canRender( $file ) {
+ return true;
+ }
+
/**
* True if handled types cannot be displayed directly in a browser
* but can be rendered
* @return bool
*/
- function mustRender( $file ) { return false; }
+ function mustRender( $file ) {
+ return false;
+ }
+
/**
* True if the type has multi-page capabilities
* @return bool
*/
- function isMultiPage( $file ) { return false; }
+ function isMultiPage( $file ) {
+ return false;
+ }
+
/**
* Page count for a multi-page document, false if unsupported or unknown
* @return bool
*/
- function pageCount( $file ) { return false; }
+ function pageCount( $file ) {
+ return false;
+ }
+
/**
* The material is vectorized and thus scaling is lossless
* @return bool
*/
- function isVectorized( $file ) { return false; }
+ function isVectorized( $file ) {
+ return false;
+ }
+
/**
* The material is an image, and is animated.
* In particular, video material need not return true.
* @note Before 1.20, this was a method of ImageHandler only
* @return bool
*/
- function isAnimatedImage( $file ) { return false; }
+ function isAnimatedImage( $file ) {
+ return false;
+ }
+
/**
* If the material is animated, we can animate the thumbnail
* @since 1.20
* @return bool If material is not animated, handler may return any value.
*/
- function canAnimateThumbnail( $file ) { return true; }
+ function canAnimateThumbnail( $file ) {
+ return true;
+ }
+
/**
* False if the handler is disabled for all files
* @return bool
*/
- function isEnabled() { return true; }
+ function isEnabled() {
+ return true;
+ }
/**
* Get an associative array of page dimensions
* Currently "width" and "height" are understood, but this might be
* expanded in the future.
- * Returns false if unknown or if the document is not multi-page.
+ * Returns false if unknown.
+ *
+ * It is expected that handlers for paged media (e.g. DjVuHandler)
+ * will override this method so that it gives the correct results
+ * for each specific page of the file, using the $page argument.
+ *
+ * @note For non-paged media, use getImageSize.
*
* @param $image File
- * @param $page Unused, left for backcompatibility?
- * @return array
+ * @param $page What page to get dimensions of
+ * @return array|bool
*/
function getPageDimensions( $image, $page ) {
$gis = $this->getImageSize( $image, $image->getLocalRefPath() );
- return array(
- 'width' => $gis[0],
- 'height' => $gis[1]
- );
+ if ( $gis ) {
+ return array(
+ 'width' => $gis[0],
+ 'height' => $gis[1]
+ );
+ } else {
+ return false;
+ }
}
/**
@@ -352,11 +404,11 @@ abstract class MediaHandler {
*
* This is used by the media handlers that use the FormatMetadata class
*
- * @param $metadataArray Array metadata array
+ * @param array $metadataArray metadata array
* @return array for use displaying metadata.
*/
function formatMetadataHelper( $metadataArray ) {
- $result = array(
+ $result = array(
'visible' => array(),
'collapsed' => array()
);
@@ -386,9 +438,9 @@ abstract class MediaHandler {
function visibleMetadataFields() {
$fields = array();
$lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() );
- foreach( $lines as $line ) {
+ foreach ( $lines as $line ) {
$matches = array();
- if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
+ if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
$fields[] = $matches[1];
}
}
@@ -396,7 +448,6 @@ abstract class MediaHandler {
return $fields;
}
-
/**
* This is used to generate an array element for each metadata value
* That array is then used to generate the table of metadata values
@@ -405,17 +456,17 @@ abstract class MediaHandler {
* @param &$array Array An array containing elements for each type of visibility
* and each of those elements being an array of metadata items. This function adds
* a value to that array.
- * @param $visibility string ('visible' or 'collapsed') if this value is hidden
+ * @param string $visibility ('visible' or 'collapsed') if this value is hidden
* by default.
- * @param $type String type of metadata tag (currently always 'exif')
- * @param $id String the name of the metadata tag (like 'artist' for example).
+ * @param string $type type of metadata tag (currently always 'exif')
+ * @param string $id the name of the metadata tag (like 'artist' for example).
* its name in the table displayed is the message "$type-$id" (Ex exif-artist ).
- * @param $value String thingy goes into a wikitext table; it used to be escaped but
+ * @param string $value thingy goes into a wikitext table; it used to be escaped but
* that was incompatible with previous practise of customized display
* with wikitext formatting via messages such as 'exif-model-value'.
* So the escaping is taken back out, but generally this seems a confusing
* interface.
- * @param $param String value to pass to the message for the name of the field
+ * @param string $param value to pass to the message for the name of the field
* as $1. Currently this parameter doesn't seem to ever be used.
*
* Note, everything here is passed through the parser later on (!)
@@ -441,6 +492,8 @@ abstract class MediaHandler {
}
/**
+ * Used instead of getLongDesc if there is no handler registered for file.
+ *
* @param $file File
* @return string
*/
@@ -450,6 +503,8 @@ abstract class MediaHandler {
}
/**
+ * Short description. Shown on Special:Search results.
+ *
* @param $file File
* @return string
*/
@@ -460,6 +515,8 @@ abstract class MediaHandler {
}
/**
+ * Long description. Shown under image on image description page surounded by ().
+ *
* @param $file File
* @return string
*/
@@ -469,6 +526,8 @@ abstract class MediaHandler {
}
/**
+ * Used instead of getShortDesc if there is no handler registered for file.
+ *
* @param $file File
* @return string
*/
@@ -489,19 +548,32 @@ abstract class MediaHandler {
public static function fitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) {
$idealWidth = $boxWidth * $maxHeight / $boxHeight;
$roundedUp = ceil( $idealWidth );
- if( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight ) {
+ if ( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight ) {
return floor( $idealWidth );
} else {
return $roundedUp;
}
}
+ /**
+ * Shown in file history box on image description page.
+ *
+ * @param File $file
+ * @return String Dimensions
+ */
function getDimensionsString( $file ) {
return '';
}
/**
- * Modify the parser object post-transform
+ * Modify the parser object post-transform.
+ *
+ * This is often used to do $parser->addOutputHook(),
+ * in order to add some javascript to render a viewer.
+ * See TimedMediaHandler or OggHandler for an example.
+ *
+ * @param Parser $parser
+ * @param File $file
*/
function parserTransformHook( $parser, $file ) {}
@@ -512,7 +584,7 @@ abstract class MediaHandler {
* match the handler class, a Status object should be returned containing
* relevant errors.
*
- * @param $fileName string The local path to the file.
+ * @param string $fileName The local path to the file.
* @return Status object
*/
function verifyUpload( $fileName ) {
@@ -523,14 +595,14 @@ abstract class MediaHandler {
* Check for zero-sized thumbnails. These can be generated when
* no disk space is available or some other error occurs
*
- * @param $dstPath string The location of the suspect file
- * @param $retval int Return value of some shell process, file will be deleted if this is non-zero
+ * @param string $dstPath The location of the suspect file
+ * @param int $retval Return value of some shell process, file will be deleted if this is non-zero
* @return bool True if removed, false otherwise
*/
function removeBadFile( $dstPath, $retval = 0 ) {
- if( file_exists( $dstPath ) ) {
+ if ( file_exists( $dstPath ) ) {
$thumbstat = stat( $dstPath );
- if( $thumbstat['size'] == 0 || $retval != 0 ) {
+ if ( $thumbstat['size'] == 0 || $retval != 0 ) {
$result = unlink( $dstPath );
if ( $result ) {
@@ -549,12 +621,47 @@ abstract class MediaHandler {
}
/**
- * Remove files from the purge list
+ * Remove files from the purge list.
+ *
+ * This is used by some video handlers to prevent ?action=purge
+ * from removing a transcoded video, which is expensive to
+ * regenerate.
+ *
+ * @see LocalFile::purgeThumbnails
*
* @param array $files
- * @param array $options
+ * @param array $options Purge options. Currently will always be
+ * an array with a single key 'forThumbRefresh' set to true.
*/
public function filterThumbnailPurgeList( &$files, $options ) {
// Do nothing
}
+
+ /*
+ * True if the handler can rotate the media
+ * @since 1.21
+ * @return bool
+ */
+ public static function canRotate() {
+ return false;
+ }
+
+ /**
+ * On supporting image formats, try to read out the low-level orientation
+ * of the file and return the angle that the file needs to be rotated to
+ * be viewed.
+ *
+ * This information is only useful when manipulating the original file;
+ * the width and height we normally work with is logical, and will match
+ * any produced output views.
+ *
+ * For files we don't know, we return 0.
+ *
+ * @param $file File
+ * @return int 0, 90, 180 or 270
+ */
+ public function getRotation( $file ) {
+ return 0;
+ }
+
}
diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php
index 773824cb..c49d3f20 100644
--- a/includes/media/MediaTransformOutput.php
+++ b/includes/media/MediaTransformOutput.php
@@ -32,7 +32,14 @@ abstract class MediaTransformOutput {
*/
var $file;
- var $width, $height, $url, $page, $path;
+ var $width, $height, $url, $page, $path, $lang;
+
+ /**
+ * @var array Associative array mapping optional supplementary image files
+ * from pixel density (eg 1.5 or 2) to additional URLs.
+ */
+ public $responsiveUrls = array();
+
protected $storagePath = false;
/**
@@ -73,7 +80,7 @@ abstract class MediaTransformOutput {
}
/**
- * @param $storagePath string The permanent storage path
+ * @param string $storagePath The permanent storage path
* @return void
*/
public function setStoragePath( $storagePath ) {
@@ -83,7 +90,7 @@ abstract class MediaTransformOutput {
/**
* Fetch HTML for this transform output
*
- * @param $options array Associative array of options. Boolean options
+ * @param array $options Associative array of options. Boolean options
* should be indicated with a value of true for true, and false or
* absent for false.
*
@@ -144,7 +151,12 @@ abstract class MediaTransformOutput {
if ( $this->isError() ) {
return false;
} elseif ( $this->path === null ) {
- return $this->file->getLocalRefPath();
+ return $this->file->getLocalRefPath(); // assume thumb was not scaled
+ } elseif ( FileBackend::isStoragePath( $this->path ) ) {
+ $be = $this->file->getRepo()->getBackend();
+ // The temp file will be process cached by FileBackend
+ $fsFile = $be->getLocalReference( array( 'src' => $this->path ) );
+ return $fsFile ? $fsFile->getPath() : false;
} else {
return $this->path; // may return false
}
@@ -153,7 +165,7 @@ abstract class MediaTransformOutput {
/**
* Stream the file if there were no errors
*
- * @param $headers Array Additional HTTP headers to send on success
+ * @param array $headers Additional HTTP headers to send on success
* @return Bool success
*/
public function streamFile( $headers = array() ) {
@@ -185,14 +197,26 @@ abstract class MediaTransformOutput {
/**
* @param $title string
- * @param $params array
+ * @param $params string|array Query parameters to add
* @return array
*/
- public function getDescLinkAttribs( $title = null, $params = '' ) {
- $query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : '';
- if( $params ) {
- $query .= $query ? '&'.$params : $params;
+ public function getDescLinkAttribs( $title = null, $params = array() ) {
+ if ( is_array( $params ) ) {
+ $query = $params;
+ } else {
+ $query = array();
+ }
+ if ( $this->page && $this->page !== 1 ) {
+ $query['page'] = $this->page;
+ }
+ if ( $this->lang ) {
+ $query['lang'] = $this->lang;
+ }
+
+ if ( is_string( $params ) && $params !== '' ) {
+ $query = $params . '&' . wfArrayToCgi( $query );
}
+
$attribs = array(
'href' => $this->file->getTitle()->getLocalURL( $query ),
'class' => 'image',
@@ -218,19 +242,21 @@ class ThumbnailImage extends MediaTransformOutput {
* It may also include a 'page' parameter for multipage files.
*
* @param $file File object
- * @param $url String: URL path to the thumb
+ * @param string $url URL path to the thumb
* @param $path String|bool|null: filesystem path to the thumb
- * @param $parameters Array: Associative array of parameters
+ * @param array $parameters Associative array of parameters
* @private
*/
function __construct( $file, $url, $path = false, $parameters = array() ) {
# Previous parameters:
# $file, $url, $width, $height, $path = false, $page = false
- if( is_array( $parameters ) ){
- $defaults = array(
- 'page' => false
- );
+ $defaults = array(
+ 'page' => false,
+ 'lang' => false
+ );
+
+ if ( is_array( $parameters ) ) {
$actualParams = $parameters + $defaults;
} else {
# Using old format, should convert. Later a warning could be added here.
@@ -239,7 +265,7 @@ class ThumbnailImage extends MediaTransformOutput {
'width' => $path,
'height' => $parameters,
'page' => ( $numArgs > 5 ) ? func_get_arg( 5 ) : false
- );
+ ) + $defaults;
$path = ( $numArgs > 4 ) ? func_get_arg( 4 ) : false;
}
@@ -254,13 +280,14 @@ class ThumbnailImage extends MediaTransformOutput {
$this->height = round( $actualParams['height'] );
$this->page = $actualParams['page'];
+ $this->lang = $actualParams['lang'];
}
/**
* Return HTML <img ... /> tag for the thumbnail, will include
* width and height attributes and a blank alt text (as required).
*
- * @param $options array Associative array of options. Boolean options
+ * @param array $options Associative array of options. Boolean options
* should be indicated with a value of true for true, and false or
* absent for false.
*
@@ -271,6 +298,8 @@ class ThumbnailImage extends MediaTransformOutput {
* valign vertical-align property, if the output is an inline element
* img-class Class applied to the \<img\> tag, if there is such a tag
* desc-query String, description link query params
+ * override-width Override width attribute. Should generally not set
+ * override-height Override height attribute. Should generally not set
* custom-url-link Custom URL to link to
* custom-title-link Custom Title object to link to
* custom target-link Value of the target attribute, for custom-target-link
@@ -281,16 +310,17 @@ class ThumbnailImage extends MediaTransformOutput {
* For images, desc-link and file-link are implemented as a click-through. For
* sounds and videos, they may be displayed in other ways.
*
+ * @throws MWException
* @return string
*/
function toHtml( $options = array() ) {
if ( count( func_get_args() ) == 2 ) {
- throw new MWException( __METHOD__ .' called in the old style' );
+ throw new MWException( __METHOD__ . ' called in the old style' );
}
$alt = empty( $options['alt'] ) ? '' : $options['alt'];
- $query = empty( $options['desc-query'] ) ? '' : $options['desc-query'];
+ $query = empty( $options['desc-query'] ) ? '' : $options['desc-query'];
if ( !empty( $options['custom-url-link'] ) ) {
$linkAttribs = array( 'href' => $options['custom-url-link'] );
@@ -323,7 +353,7 @@ class ThumbnailImage extends MediaTransformOutput {
'alt' => $alt,
'src' => $this->url,
'width' => $this->width,
- 'height' => $this->height,
+ 'height' => $this->height
);
if ( !empty( $options['valign'] ) ) {
$attribs['style'] = "vertical-align: {$options['valign']}";
@@ -331,6 +361,20 @@ class ThumbnailImage extends MediaTransformOutput {
if ( !empty( $options['img-class'] ) ) {
$attribs['class'] = $options['img-class'];
}
+ if ( isset( $options['override-height'] ) ) {
+ $attribs['height'] = $options['override-height'];
+ }
+ if ( isset( $options['override-width'] ) ) {
+ $attribs['width'] = $options['override-width'];
+ }
+
+ // Additional densities for responsive images, if specified.
+ if ( !empty( $this->responsiveUrls ) ) {
+ $attribs['srcset'] = Html::srcSet( $this->responsiveUrls );
+ }
+
+ wfRunHooks( 'ThumbnailBeforeProduceHTML', array( $this, &$attribs, &$linkAttribs ) );
+
return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) );
}
@@ -385,7 +429,7 @@ class MediaTransformError extends MediaTransformOutput {
class TransformParameterError extends MediaTransformError {
function __construct( $params ) {
parent::__construct( 'thumbnail_error',
- max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
+ max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
max( isset( $params['height'] ) ? $params['height'] : 0, 120 ),
wfMessage( 'thumbnail_invalid_params' )->text() );
}
diff --git a/includes/media/PNG.php b/includes/media/PNG.php
index 1b329e57..98f13861 100644
--- a/includes/media/PNG.php
+++ b/includes/media/PNG.php
@@ -38,13 +38,13 @@ class PNGHandler extends BitmapHandler {
function getMetadata( $image, $filename ) {
try {
$metadata = BitmapMetadataHandler::PNG( $filename );
- } catch( Exception $e ) {
+ } catch ( Exception $e ) {
// Broken file?
wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
return self::BROKEN_FILE;
}
- return serialize($metadata);
+ return serialize( $metadata );
}
/**
@@ -74,9 +74,11 @@ class PNGHandler extends BitmapHandler {
*/
function isAnimatedImage( $image ) {
$ser = $image->getMetadata();
- if ($ser) {
- $metadata = unserialize($ser);
- if( $metadata['frameCount'] > 1 ) return true;
+ if ( $ser ) {
+ $metadata = unserialize( $ser );
+ if ( $metadata['frameCount'] > 1 ) {
+ return true;
+ }
}
return false;
}
@@ -88,11 +90,11 @@ class PNGHandler extends BitmapHandler {
function canAnimateThumbnail( $image ) {
return false;
}
-
+
function getMetadataType( $image ) {
return 'parsed-png';
}
-
+
function isMetadataValid( $image, $metadata ) {
if ( $metadata === self::BROKEN_FILE ) {
@@ -105,13 +107,13 @@ class PNGHandler extends BitmapHandler {
wfRestoreWarnings();
if ( !$data || !is_array( $data ) ) {
- wfDebug(__METHOD__ . ' invalid png metadata' );
+ wfDebug( __METHOD__ . " invalid png metadata\n" );
return self::METADATA_BAD;
}
if ( !isset( $data['metadata']['_MW_PNG_VERSION'] )
|| $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION ) {
- wfDebug(__METHOD__ . ' old but compatible png metadata' );
+ wfDebug( __METHOD__ . " old but compatible png metadata\n" );
return self::METADATA_COMPATIBLE;
}
return self::METADATA_GOOD;
@@ -126,29 +128,30 @@ class PNGHandler extends BitmapHandler {
$original = parent::getLongDesc( $image );
wfSuppressWarnings();
- $metadata = unserialize($image->getMetadata());
+ $metadata = unserialize( $image->getMetadata() );
wfRestoreWarnings();
- if( !$metadata || $metadata['frameCount'] <= 0 )
+ if ( !$metadata || $metadata['frameCount'] <= 0 ) {
return $original;
+ }
$info = array();
$info[] = $original;
-
+
if ( $metadata['loopCount'] == 0 ) {
$info[] = wfMessage( 'file-info-png-looped' )->parse();
} elseif ( $metadata['loopCount'] > 1 ) {
$info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse();
}
-
+
if ( $metadata['frameCount'] > 0 ) {
$info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse();
}
-
+
if ( $metadata['duration'] ) {
$info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
}
-
+
return $wgLang->commaList( $info );
}
diff --git a/includes/media/PNGMetadataExtractor.php b/includes/media/PNGMetadataExtractor.php
index 9dcde406..845d212a 100644
--- a/includes/media/PNGMetadataExtractor.php
+++ b/includes/media/PNGMetadataExtractor.php
@@ -124,7 +124,7 @@ class PNGMetadataExtractor {
case 0:
$colorType = 'greyscale';
break;
- case 2:
+ case 2:
$colorType = 'truecolour';
break;
case 3:
@@ -142,7 +142,7 @@ class PNGMetadataExtractor {
}
} elseif ( $chunk_type == "acTL" ) {
$buf = fread( $fh, $chunk_size );
- if( !$buf || strlen( $buf ) < $chunk_size || $chunk_size < 4 ) {
+ if ( !$buf || strlen( $buf ) < $chunk_size || $chunk_size < 4 ) {
throw new Exception( __METHOD__ . ": Read error" );
}
@@ -202,21 +202,21 @@ class PNGMetadataExtractor {
if ( $items[5] === false ) {
// decompression failed
- wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] );
+ wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n" );
fseek( $fh, self::$CRC_size, SEEK_CUR );
continue;
}
} else {
wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,'
- . ' or potentially invalid compression method' );
+ . " or potentially invalid compression method\n" );
fseek( $fh, self::$CRC_size, SEEK_CUR );
continue;
}
}
- $finalKeyword = self::$text_chunks[ $items[1] ];
- $text[ $finalKeyword ][ $items[3] ] = $items[5];
- $text[ $finalKeyword ]['_type'] = 'lang';
+ $finalKeyword = self::$text_chunks[$items[1]];
+ $text[$finalKeyword][$items[3]] = $items[5];
+ $text[$finalKeyword]['_type'] = 'lang';
} else {
// Error reading iTXt chunk
@@ -251,9 +251,9 @@ class PNGMetadataExtractor {
throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
}
- $finalKeyword = self::$text_chunks[ $keyword ];
- $text[ $finalKeyword ][ 'x-default' ] = $content;
- $text[ $finalKeyword ]['_type'] = 'lang';
+ $finalKeyword = self::$text_chunks[$keyword];
+ $text[$finalKeyword]['x-default'] = $content;
+ $text[$finalKeyword]['_type'] = 'lang';
} elseif ( $chunk_type == 'zTXt' ) {
if ( function_exists( 'gzuncompress' ) ) {
@@ -279,7 +279,7 @@ class PNGMetadataExtractor {
$compression = substr( $postKeyword, 0, 1 );
$content = substr( $postKeyword, 1 );
if ( $compression !== "\x00" ) {
- wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping." );
+ wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping.\n" );
fseek( $fh, self::$CRC_size, SEEK_CUR );
continue;
}
@@ -290,7 +290,7 @@ class PNGMetadataExtractor {
if ( $content === false ) {
// decompression failed
- wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword );
+ wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n" );
fseek( $fh, self::$CRC_size, SEEK_CUR );
continue;
}
@@ -303,12 +303,12 @@ class PNGMetadataExtractor {
throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
}
- $finalKeyword = self::$text_chunks[ $keyword ];
- $text[ $finalKeyword ][ 'x-default' ] = $content;
- $text[ $finalKeyword ]['_type'] = 'lang';
+ $finalKeyword = self::$text_chunks[$keyword];
+ $text[$finalKeyword]['x-default'] = $content;
+ $text[$finalKeyword]['_type'] = 'lang';
} else {
- wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping." );
+ wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping.\n" );
fseek( $fh, $chunk_size, SEEK_CUR );
}
} elseif ( $chunk_type == 'tIME' ) {
@@ -417,7 +417,7 @@ class PNGMetadataExtractor {
* @throws Exception if too big.
* @return String The chunk.
*/
- static private function read( $fh, $size ) {
+ private static function read( $fh, $size ) {
if ( $size > self::MAX_CHUNK_SIZE ) {
throw new Exception( __METHOD__ . ': Chunk size of ' . $size .
' too big. Max size is: ' . self::MAX_CHUNK_SIZE );
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index 55fa5547..72a9696c 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -56,7 +56,7 @@ class SvgHandler extends ImageHandler {
$metadata = $file->getMetadata();
if ( $metadata ) {
$metadata = $this->unpackMetadata( $metadata );
- if( isset( $metadata['animated'] ) ) {
+ if ( isset( $metadata['animated'] ) ) {
return $metadata['animated'];
}
}
@@ -115,19 +115,26 @@ class SvgHandler extends ImageHandler {
$clientHeight = $params['height'];
$physicalWidth = $params['physicalWidth'];
$physicalHeight = $params['physicalHeight'];
+ $lang = isset( $params['lang'] ) ? $params['lang'] : 'en';
if ( $flags & self::TRANSFORM_LATER ) {
return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
}
+ $metadata = $this->unpackMetadata( $image->getMetadata() );
+ if ( isset( $metadata['error'] ) ) { // sanity check
+ $err = wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
+ }
+
if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
wfMessage( 'thumbnail_dest_directory' )->text() );
}
$srcPath = $image->getLocalRefPath();
- $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight );
- if( $status === true ) {
+ $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
+ if ( $status === true ) {
return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
} else {
return $status; // MediaTransformError
@@ -135,15 +142,17 @@ class SvgHandler extends ImageHandler {
}
/**
- * Transform an SVG file to PNG
- * This function can be called outside of thumbnail contexts
- * @param string $srcPath
- * @param string $dstPath
- * @param string $width
- * @param string $height
- * @return bool|MediaTransformError
- */
- public function rasterize( $srcPath, $dstPath, $width, $height ) {
+ * Transform an SVG file to PNG
+ * This function can be called outside of thumbnail contexts
+ * @param string $srcPath
+ * @param string $dstPath
+ * @param string $width
+ * @param string $height
+ * @param string $lang Language code of the language to render the SVG in
+ * @throws MWException
+ * @return bool|MediaTransformError
+ */
+ public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
$err = false;
$retval = '';
@@ -151,7 +160,7 @@ class SvgHandler extends ImageHandler {
if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
// This is a PHP callable
$func = $wgSVGConverters[$wgSVGConverter][0];
- $args = array_merge( array( $srcPath, $dstPath, $width, $height ),
+ $args = array_merge( array( $srcPath, $dstPath, $width, $height, $lang ),
array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
if ( !is_callable( $func ) ) {
throw new MWException( "$func is not callable" );
@@ -163,22 +172,28 @@ class SvgHandler extends ImageHandler {
$cmd = str_replace(
array( '$path/', '$width', '$height', '$input', '$output' ),
array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
- intval( $width ),
- intval( $height ),
- wfEscapeShellArg( $srcPath ),
- wfEscapeShellArg( $dstPath ) ),
+ intval( $width ),
+ intval( $height ),
+ wfEscapeShellArg( $srcPath ),
+ wfEscapeShellArg( $dstPath ) ),
$wgSVGConverters[$wgSVGConverter]
- ) . " 2>&1";
+ );
+
+ $env = array();
+ if ( $lang !== false ) {
+ $env['LANG'] = $lang;
+ }
+
wfProfileIn( 'rsvg' );
- wfDebug( __METHOD__.": $cmd\n" );
- $err = wfShellExec( $cmd, $retval );
+ wfDebug( __METHOD__ . ": $cmd\n" );
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
wfProfileOut( 'rsvg' );
}
}
$removed = $this->removeBadFile( $dstPath, $retval );
if ( $retval != 0 || $removed ) {
wfDebugLog( 'thumbnail', sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
- wfHostname(), $retval, trim($err), $cmd ) );
+ wfHostname(), $retval, trim( $err ), $cmd ) );
return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
}
return true;
@@ -213,6 +228,8 @@ class SvgHandler extends ImageHandler {
if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
return array( $metadata['width'], $metadata['height'], 'SVG',
"width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" );
+ } else { // error
+ return array( 0, 0, 'SVG', "width=\"0\" height=\"0\"" );
}
}
@@ -231,6 +248,12 @@ class SvgHandler extends ImageHandler {
*/
function getLongDesc( $file ) {
global $wgLang;
+
+ $metadata = $this->unpackMetadata( $file->getMetadata() );
+ if ( isset( $metadata['error'] ) ) {
+ return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
+ }
+
$size = $wgLang->formatSize( $file->getSize() );
if ( $this->isAnimatedImage( $file ) ) {
@@ -239,23 +262,23 @@ class SvgHandler extends ImageHandler {
$msg = wfMessage( 'svg-long-desc' );
}
- $msg->numParams(
- $file->getWidth(),
- $file->getHeight()
- );
- $msg->Params( $size );
+ $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
+
return $msg->parse();
}
function getMetadata( $file, $filename ) {
+ $metadata = array( 'version' => self::SVG_METADATA_VERSION );
try {
- $metadata = SVGMetadataExtractor::getMetadata( $filename );
- } catch( Exception $e ) {
- // Broken file?
+ $metadata += SVGMetadataExtractor::getMetadata( $filename );
+ } catch ( MWException $e ) { // @todo SVG specific exceptions
+ // File not found, broken, etc.
+ $metadata['error'] = array(
+ 'message' => $e->getMessage(),
+ 'code' => $e->getCode()
+ );
wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
- return '0';
}
- $metadata['version'] = self::SVG_METADATA_VERSION;
return serialize( $metadata );
}
@@ -305,7 +328,7 @@ class SvgHandler extends ImageHandler {
return false;
}
$metadata = $this->unpackMetadata( $metadata );
- if ( !$metadata ) {
+ if ( !$metadata || isset( $metadata['error'] ) ) {
return false;
}
@@ -325,6 +348,7 @@ class SvgHandler extends ImageHandler {
'description' => 'imagedescription',
'title' => 'objectname',
);
+ $showMeta = false;
foreach ( $metadata as $name => $value ) {
$tag = strtolower( $name );
if ( isset( $conversion[$tag] ) ) {
@@ -333,6 +357,7 @@ class SvgHandler extends ImageHandler {
// Do not output other metadata not in list
continue;
}
+ $showMeta = true;
self::addMeta( $result,
in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
'exif',
@@ -340,6 +365,70 @@ class SvgHandler extends ImageHandler {
$value
);
}
- return $result;
+ return $showMeta ? $result : false;
+ }
+
+
+ /**
+ * @param string $name Parameter name
+ * @param $string $value Parameter value
+ * @return bool Validity
+ */
+ function validateParam( $name, $value ) {
+ if ( in_array( $name, array( 'width', 'height' ) ) ) {
+ // Reject negative heights, widths
+ return ( $value > 0 );
+ } elseif ( $name == 'lang' ) {
+ // Validate $code
+ if ( !Language::isValidBuiltinCode( $value ) ) {
+ wfDebug( "Invalid user language code\n" );
+ return false;
+ }
+ return true;
+ }
+ // Only lang, width and height are acceptable keys
+ return false;
+ }
+
+ /**
+ * @param array $params name=>value pairs of parameters
+ * @return string Filename to use
+ */
+ function makeParamString( $params ) {
+ $lang = '';
+ if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
+ $params['lang'] = mb_strtolower( $params['lang'] );
+ $lang = "lang{$params['lang']}-";
+ }
+ if ( !isset( $params['width'] ) ) {
+ return false;
+ }
+ return "$lang{$params['width']}px";
+ }
+
+ function parseParamString( $str ) {
+ $m = false;
+ if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
+ return array( 'width' => array_pop( $m ), 'lang' => $m[1] );
+ } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
+ return array( 'width' => $m[1], 'lang' => 'en' );
+ } else {
+ return false;
+ }
+ }
+
+ function getParamMap() {
+ return array( 'img_lang' => 'lang', 'img_width' => 'width' );
+ }
+
+ /**
+ * @param $params
+ * @return array
+ */
+ function getScriptParams( $params ) {
+ return array(
+ 'width' => $params['width'],
+ 'lang' => $params['lang'],
+ );
}
}
diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php
index c6f63fd4..2e33bb98 100644
--- a/includes/media/SVGMetadataExtractor.php
+++ b/includes/media/SVGMetadataExtractor.php
@@ -51,7 +51,8 @@ class SVGReader {
* Constructor
*
* Creates an SVGReader drawing from the source provided
- * @param $source String: URI from which to read
+ * @param string $source URI from which to read
+ * @throws MWException|Exception
*/
function __construct( $source ) {
global $wgSVGMetadataCutoff;
@@ -66,7 +67,7 @@ class SVGReader {
if ( $size > $wgSVGMetadataCutoff ) {
$this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." );
$contents = file_get_contents( $source, false, null, -1, $wgSVGMetadataCutoff );
- if ($contents === false) {
+ if ( $contents === false ) {
throw new MWException( 'Error reading SVG file.' );
}
$this->reader->XML( $contents, null, LIBXML_NOERROR | LIBXML_NOWARNING );
@@ -100,7 +101,7 @@ class SVGReader {
wfSuppressWarnings();
try {
$this->read();
- } catch( Exception $e ) {
+ } catch ( Exception $e ) {
// Note, if this happens, the width/height will be taken to be 0x0.
// Should we consider it the default 512x512 instead?
wfRestoreWarnings();
@@ -120,29 +121,30 @@ class SVGReader {
/**
* Read the SVG
+ * @throws MWException
* @return bool
*/
protected function read() {
$keepReading = $this->reader->read();
/* Skip until first element */
- while( $keepReading && $this->reader->nodeType != XmlReader::ELEMENT ) {
+ while ( $keepReading && $this->reader->nodeType != XmlReader::ELEMENT ) {
$keepReading = $this->reader->read();
}
if ( $this->reader->localName != 'svg' || $this->reader->namespaceURI != self::NS_SVG ) {
- throw new MWException( "Expected <svg> tag, got ".
+ throw new MWException( "Expected <svg> tag, got " .
$this->reader->localName . " in NS " . $this->reader->namespaceURI );
}
$this->debug( "<svg> tag is correct." );
$this->handleSVGAttribs();
- $exitDepth = $this->reader->depth;
+ $exitDepth = $this->reader->depth;
$keepReading = $this->reader->read();
while ( $keepReading ) {
$tag = $this->reader->localName;
$type = $this->reader->nodeType;
- $isSVG = ($this->reader->namespaceURI == self::NS_SVG);
+ $isSVG = ( $this->reader->namespaceURI == self::NS_SVG );
$this->debug( "$tag" );
@@ -180,19 +182,19 @@ class SVGReader {
/**
* Read a textelement from an element
*
- * @param String $name of the element that we are reading from
- * @param String $metafield that we will fill with the result
+ * @param string $name of the element that we are reading from
+ * @param string $metafield that we will fill with the result
*/
- private function readField( $name, $metafield=null ) {
- $this->debug ( "Read field $metafield" );
- if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
+ private function readField( $name, $metafield = null ) {
+ $this->debug( "Read field $metafield" );
+ if ( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
return;
}
$keepReading = $this->reader->read();
- while( $keepReading ) {
- if( $this->reader->localName == $name && $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
+ while ( $keepReading ) {
+ if ( $this->reader->localName == $name && $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
break;
- } elseif( $this->reader->nodeType == XmlReader::TEXT ){
+ } elseif ( $this->reader->nodeType == XmlReader::TEXT ) {
$this->metadata[$metafield] = trim( $this->reader->value );
}
$keepReading = $this->reader->read();
@@ -202,15 +204,16 @@ class SVGReader {
/**
* Read an XML snippet from an element
*
- * @param String $metafield that we will fill with the result
+ * @param string $metafield that we will fill with the result
+ * @throws MWException
*/
- private function readXml( $metafield=null ) {
- $this->debug ( "Read top level metadata" );
- if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
+ private function readXml( $metafield = null ) {
+ $this->debug( "Read top level metadata" );
+ if ( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
return;
}
// TODO: find and store type of xml snippet. metadata['metadataType'] = "rdf"
- if( method_exists( $this->reader, 'readInnerXML' ) ) {
+ if ( method_exists( $this->reader, 'readInnerXML' ) ) {
$this->metadata[$metafield] = trim( $this->reader->readInnerXML() );
} else {
throw new MWException( "The PHP XMLReader extension does not come with readInnerXML() method. Your libxml is probably out of date (need 2.6.20 or later)." );
@@ -221,24 +224,24 @@ class SVGReader {
/**
* Filter all children, looking for animate elements
*
- * @param String $name of the element that we are reading from
+ * @param string $name of the element that we are reading from
*/
private function animateFilter( $name ) {
- $this->debug ( "animate filter for tag $name" );
- if( $this->reader->nodeType != XmlReader::ELEMENT ) {
+ $this->debug( "animate filter for tag $name" );
+ if ( $this->reader->nodeType != XmlReader::ELEMENT ) {
return;
}
if ( $this->reader->isEmptyElement ) {
return;
}
- $exitDepth = $this->reader->depth;
+ $exitDepth = $this->reader->depth;
$keepReading = $this->reader->read();
- while( $keepReading ) {
- if( $this->reader->localName == $name && $this->reader->depth <= $exitDepth
+ while ( $keepReading ) {
+ if ( $this->reader->localName == $name && $this->reader->depth <= $exitDepth
&& $this->reader->nodeType == XmlReader::END_ELEMENT ) {
break;
} elseif ( $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::ELEMENT ) {
- switch( $this->reader->localName ) {
+ switch ( $this->reader->localName ) {
case 'script':
// Normally we disallow files with
// <script>, but its possible
@@ -264,7 +267,7 @@ class SVGReader {
}
private function debug( $data ) {
- if( $this->mDebug ) {
+ if ( $this->mDebug ) {
wfDebug( "SVGReader: $data\n" );
}
}
@@ -282,44 +285,44 @@ class SVGReader {
*
* The parser has to be in the start element of "<svg>"
*/
- private function handleSVGAttribs( ) {
+ private function handleSVGAttribs() {
$defaultWidth = self::DEFAULT_WIDTH;
$defaultHeight = self::DEFAULT_HEIGHT;
$aspect = 1.0;
$width = null;
$height = null;
- if( $this->reader->getAttribute('viewBox') ) {
+ if ( $this->reader->getAttribute( 'viewBox' ) ) {
// min-x min-y width height
- $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute('viewBox') ) );
- if( count( $viewBox ) == 4 ) {
+ $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute( 'viewBox' ) ) );
+ if ( count( $viewBox ) == 4 ) {
$viewWidth = $this->scaleSVGUnit( $viewBox[2] );
$viewHeight = $this->scaleSVGUnit( $viewBox[3] );
- if( $viewWidth > 0 && $viewHeight > 0 ) {
+ if ( $viewWidth > 0 && $viewHeight > 0 ) {
$aspect = $viewWidth / $viewHeight;
$defaultHeight = $defaultWidth / $aspect;
}
}
}
- if( $this->reader->getAttribute('width') ) {
- $width = $this->scaleSVGUnit( $this->reader->getAttribute('width'), $defaultWidth );
+ if ( $this->reader->getAttribute( 'width' ) ) {
+ $width = $this->scaleSVGUnit( $this->reader->getAttribute( 'width' ), $defaultWidth );
$this->metadata['originalWidth'] = $this->reader->getAttribute( 'width' );
}
- if( $this->reader->getAttribute('height') ) {
- $height = $this->scaleSVGUnit( $this->reader->getAttribute('height'), $defaultHeight );
+ if ( $this->reader->getAttribute( 'height' ) ) {
+ $height = $this->scaleSVGUnit( $this->reader->getAttribute( 'height' ), $defaultHeight );
$this->metadata['originalHeight'] = $this->reader->getAttribute( 'height' );
}
- if( !isset( $width ) && !isset( $height ) ) {
+ if ( !isset( $width ) && !isset( $height ) ) {
$width = $defaultWidth;
$height = $width / $aspect;
- } elseif( isset( $width ) && !isset( $height ) ) {
+ } elseif ( isset( $width ) && !isset( $height ) ) {
$height = $width / $aspect;
- } elseif( isset( $height ) && !isset( $width ) ) {
+ } elseif ( isset( $height ) && !isset( $width ) ) {
$width = $height * $aspect;
}
- if( $width > 0 && $height > 0 ) {
+ if ( $width > 0 && $height > 0 ) {
$this->metadata['width'] = intval( round( $width ) );
$this->metadata['height'] = intval( round( $height ) );
}
@@ -329,11 +332,11 @@ class SVGReader {
* Return a rounded pixel equivalent for a labeled CSS/SVG length.
* http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
*
- * @param $length String: CSS/SVG length.
+ * @param string $length CSS/SVG length.
* @param $viewportSize: Float optional scale for percentage units...
* @return float: length in pixels
*/
- static function scaleSVGUnit( $length, $viewportSize=512 ) {
+ static function scaleSVGUnit( $length, $viewportSize = 512 ) {
static $unitLength = array(
'px' => 1.0,
'pt' => 1.25,
@@ -343,13 +346,13 @@ class SVGReader {
'in' => 90.0,
'em' => 16.0, // fake it?
'ex' => 12.0, // fake it?
- '' => 1.0, // "User units" pixels by default
+ '' => 1.0, // "User units" pixels by default
);
$matches = array();
- if( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) {
+ if ( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) {
$length = floatval( $matches[1] );
$unit = $matches[2];
- if( $unit == '%' ) {
+ if ( $unit == '%' ) {
return $length * 0.01 * $viewportSize;
} else {
return $length * $unitLength[$unit];
diff --git a/includes/media/Tiff.php b/includes/media/Tiff.php
index d95c9074..55acb120 100644
--- a/includes/media/Tiff.php
+++ b/includes/media/Tiff.php
@@ -43,7 +43,7 @@ class TiffHandler extends ExifBitmapHandler {
function canRender( $file ) {
global $wgTiffThumbnailType;
return (bool)$wgTiffThumbnailType
- || ($file->getRepo() instanceof ForeignAPIRepo);
+ || $file->getRepo() instanceof ForeignAPIRepo;
}
/**
@@ -70,8 +70,9 @@ class TiffHandler extends ExifBitmapHandler {
}
/**
- * @param $image
- * @param $filename
+ * @param File $image
+ * @param string $filename
+ * @throws MWException
* @return string
*/
function getMetadata( $image, $filename ) {
@@ -81,7 +82,7 @@ class TiffHandler extends ExifBitmapHandler {
$meta = BitmapMetadataHandler::Tiff( $filename );
if ( !is_array( $meta ) ) {
// This should never happen, but doesn't hurt to be paranoid.
- throw new MWException('Metadata array is not an array');
+ throw new MWException( 'Metadata array is not an array' );
}
$meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
return serialize( $meta );
diff --git a/includes/media/XCF.php b/includes/media/XCF.php
index 555fa1fb..e77d3842 100644
--- a/includes/media/XCF.php
+++ b/includes/media/XCF.php
@@ -72,19 +72,19 @@ class XCFHandler extends BitmapHandler {
* @author Hexmode
* @author Hashar
*
- * @param $filename String Full path to a XCF file
+ * @param string $filename Full path to a XCF file
* @return bool|array metadata array just like PHP getimagesize()
*/
static function getXCFMetaData( $filename ) {
# Decode master structure
$f = fopen( $filename, 'rb' );
- if( !$f ) {
+ if ( !$f ) {
return false;
}
# The image structure always starts at offset 0 in the XCF file.
# So we just read it :-)
$binaryHeader = fread( $f, 26 );
- fclose($f);
+ fclose( $f );
# Master image structure:
#
@@ -110,12 +110,12 @@ class XCFHandler extends BitmapHandler {
. "/Nbase_type" # /
, $binaryHeader
);
- } catch( MWException $mwe ) {
+ } catch ( MWException $mwe ) {
return false;
}
# Check values
- if( $header['magic'] !== 'gimp xcf' ) {
+ if ( $header['magic'] !== 'gimp xcf' ) {
wfDebug( __METHOD__ . " '$filename' has invalid magic signature.\n" );
return false;
}
diff --git a/includes/media/XMP.php b/includes/media/XMP.php
index 36660b3d..7eb3d19e 100644
--- a/includes/media/XMP.php
+++ b/includes/media/XMP.php
@@ -22,30 +22,30 @@
*/
/**
-* Class for reading xmp data containing properties relevant to
-* images, and spitting out an array that FormatExif accepts.
-*
-* Note, this is not meant to recognize every possible thing you can
-* encode in XMP. It should recognize all the properties we want.
-* For example it doesn't have support for structures with multiple
-* nesting levels, as none of the properties we're supporting use that
-* feature. If it comes across properties it doesn't recognize, it should
-* ignore them.
-*
-* The public methods one would call in this class are
-* - parse( $content )
-* Reads in xmp content.
-* Can potentially be called multiple times with partial data each time.
-* - parseExtended( $content )
-* Reads XMPExtended blocks (jpeg files only).
-* - getResults
-* Outputs a results array.
-*
-* Note XMP kind of looks like rdf. They are not the same thing - XMP is
-* encoded as a specific subset of rdf. This class can read XMP. It cannot
-* read rdf.
-*
-*/
+ * Class for reading xmp data containing properties relevant to
+ * images, and spitting out an array that FormatExif accepts.
+ *
+ * Note, this is not meant to recognize every possible thing you can
+ * encode in XMP. It should recognize all the properties we want.
+ * For example it doesn't have support for structures with multiple
+ * nesting levels, as none of the properties we're supporting use that
+ * feature. If it comes across properties it doesn't recognize, it should
+ * ignore them.
+ *
+ * The public methods one would call in this class are
+ * - parse( $content )
+ * Reads in xmp content.
+ * Can potentially be called multiple times with partial data each time.
+ * - parseExtended( $content )
+ * Reads XMPExtended blocks (jpeg files only).
+ * - getResults
+ * Outputs a results array.
+ *
+ * Note XMP kind of looks like rdf. They are not the same thing - XMP is
+ * encoded as a specific subset of rdf. This class can read XMP. It cannot
+ * read rdf.
+ *
+ */
class XMPReader {
private $curItem = array(); // array to hold the current element (and previous element, and so on)
@@ -63,39 +63,38 @@ class XMPReader {
protected $items;
/**
- * These are various mode constants.
- * they are used to figure out what to do
- * with an element when its encountered.
- *
- * For example, MODE_IGNORE is used when processing
- * a property we're not interested in. So if a new
- * element pops up when we're in that mode, we ignore it.
- */
+ * These are various mode constants.
+ * they are used to figure out what to do
+ * with an element when its encountered.
+ *
+ * For example, MODE_IGNORE is used when processing
+ * a property we're not interested in. So if a new
+ * element pops up when we're in that mode, we ignore it.
+ */
const MODE_INITIAL = 0;
- const MODE_IGNORE = 1;
- const MODE_LI = 2;
+ const MODE_IGNORE = 1;
+ const MODE_LI = 2;
const MODE_LI_LANG = 3;
- const MODE_QDESC = 4;
+ const MODE_QDESC = 4;
// The following MODE constants are also used in the
// $items array to denote what type of property the item is.
- const MODE_SIMPLE = 10;
- const MODE_STRUCT = 11; // structure (associative array)
- const MODE_SEQ = 12; // ordered list
- const MODE_BAG = 13; // unordered list
- const MODE_LANG = 14;
- const MODE_ALT = 15; // non-language alt. Currently not implemented, and not needed atm.
+ const MODE_SIMPLE = 10;
+ const MODE_STRUCT = 11; // structure (associative array)
+ const MODE_SEQ = 12; // ordered list
+ const MODE_BAG = 13; // unordered list
+ const MODE_LANG = 14;
+ const MODE_ALT = 15; // non-language alt. Currently not implemented, and not needed atm.
const MODE_BAGSTRUCT = 16; // A BAG of Structs.
const NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
const NS_XML = 'http://www.w3.org/XML/1998/namespace';
-
/**
- * Constructor.
- *
- * Primary job is to initialize the XMLParser
- */
+ * Constructor.
+ *
+ * Primary job is to initialize the XMLParser
+ */
function __construct() {
if ( !function_exists( 'xml_parser_create_ns' ) ) {
@@ -109,12 +108,12 @@ class XMPReader {
}
/**
- * Main use is if a single item has multiple xmp documents describing it.
- * For example in jpeg's with extendedXMP
- */
+ * Main use is if a single item has multiple xmp documents describing it.
+ * For example in jpeg's with extendedXMP
+ */
private function resetXMLParser() {
- if ($this->xmlParser) {
+ if ( $this->xmlParser ) {
//is this needed?
xml_parser_free( $this->xmlParser );
}
@@ -131,20 +130,20 @@ class XMPReader {
}
/** Destroy the xml parser
- *
- * Not sure if this is actually needed.
- */
+ *
+ * Not sure if this is actually needed.
+ */
function __destruct() {
// not sure if this is needed.
xml_parser_free( $this->xmlParser );
}
/** Get the result array. Do some post-processing before returning
- * the array, and transform any metadata that is special-cased.
- *
- * @return Array array of results as an array of arrays suitable for
- * FormatMetadata::getFormattedData().
- */
+ * the array, and transform any metadata that is special-cased.
+ *
+ * @return Array array of results as an array of arrays suitable for
+ * FormatMetadata::getFormattedData().
+ */
public function getResults() {
// xmp-special is for metadata that affects how stuff
// is extracted. For example xmpNote:HasExtendedXMP.
@@ -156,7 +155,7 @@ class XMPReader {
$data = $this->results;
- wfRunHooks('XMPGetResults', Array(&$data));
+ wfRunHooks( 'XMPGetResults', Array( &$data ) );
if ( isset( $data['xmp-special']['AuthorsPosition'] )
&& is_string( $data['xmp-special']['AuthorsPosition'] )
@@ -181,12 +180,12 @@ class XMPReader {
) {
// the is_array is just paranoia. It should always
// be an array.
- foreach( $data['xmp-special']['LocationShown'] as $loc ) {
+ foreach ( $data['xmp-special']['LocationShown'] as $loc ) {
if ( !is_array( $loc ) ) {
// To avoid copying over the _type meta-fields.
continue;
}
- foreach( $loc as $field => $val ) {
+ foreach ( $loc as $field => $val ) {
$data['xmp-general'][$field . 'Dest'][] = $val;
}
}
@@ -196,18 +195,17 @@ class XMPReader {
) {
// the is_array is just paranoia. It should always
// be an array.
- foreach( $data['xmp-special']['LocationCreated'] as $loc ) {
+ foreach ( $data['xmp-special']['LocationCreated'] as $loc ) {
if ( !is_array( $loc ) ) {
// To avoid copying over the _type meta-fields.
continue;
}
- foreach( $loc as $field => $val ) {
+ foreach ( $loc as $field => $val ) {
$data['xmp-general'][$field . 'Created'][] = $val;
}
}
}
-
// We don't want to return the special values, since they're
// special and not info to be stored about the file.
unset( $data['xmp-special'] );
@@ -232,17 +230,18 @@ class XMPReader {
}
/**
- * Main function to call to parse XMP. Use getResults to
- * get results.
- *
- * Also catches any errors during processing, writes them to
- * debug log, blanks result array and returns false.
- *
- * @param $content String: XMP data
- * @param $allOfIt Boolean: If this is all the data (true) or if its split up (false). Default true
- * @param $reset Boolean: does xml parser need to be reset. Default false
- * @return Boolean success.
- */
+ * Main function to call to parse XMP. Use getResults to
+ * get results.
+ *
+ * Also catches any errors during processing, writes them to
+ * debug log, blanks result array and returns false.
+ *
+ * @param string $content XMP data
+ * @param $allOfIt Boolean: If this is all the data (true) or if its split up (false). Default true
+ * @param $reset Boolean: does xml parser need to be reset. Default false
+ * @throws MWException
+ * @return Boolean success.
+ */
public function parse( $content, $allOfIt = true, $reset = false ) {
if ( $reset ) {
$this->resetXMLParser();
@@ -254,7 +253,7 @@ class XMPReader {
if ( !$this->charset ) {
$bom = array();
if ( preg_match( '/\xEF\xBB\xBF|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\xFF\xFE/',
- $content, $bom )
+ $content, $bom )
) {
switch ( $bom[0] ) {
case "\xFE\xFF":
@@ -274,11 +273,8 @@ class XMPReader {
break;
default:
//this should be impossible to get to
- throw new MWException("Invalid BOM");
- break;
-
+ throw new MWException( "Invalid BOM" );
}
-
} else {
// standard specifically says, if no bom assume utf-8
$this->charset = 'UTF-8';
@@ -314,7 +310,7 @@ class XMPReader {
*
* @todo In serious need of testing
* @see http://www.adobe.ge/devnet/xmp/pdfs/XMPSpecificationPart3.pdf XMP spec part 3 page 20
- * @param String $content XMPExtended block minus the namespace signature
+ * @param string $content XMPExtended block minus the namespace signature
* @return Boolean If it succeeded.
*/
public function parseExtended( $content ) {
@@ -323,17 +319,16 @@ class XMPReader {
$guid = substr( $content, 0, 32 );
if ( !isset( $this->results['xmp-special']['HasExtendedXMP'] )
|| $this->results['xmp-special']['HasExtendedXMP'] !== $guid ) {
- wfDebugLog('XMP', __METHOD__ . " Ignoring XMPExtended block due to wrong guid (guid= '$guid' )");
+ wfDebugLog( 'XMP', __METHOD__ . " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" );
return false;
}
- $len = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) );
+ $len = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) );
- if (!$len || $len['length'] < 4 || $len['offset'] < 0 || $len['offset'] > $len['length'] ) {
- wfDebugLog('XMP', __METHOD__ . 'Error reading extended XMP block, invalid length or offset.');
+ if ( !$len || $len['length'] < 4 || $len['offset'] < 0 || $len['offset'] > $len['length'] ) {
+ wfDebugLog( 'XMP', __METHOD__ . 'Error reading extended XMP block, invalid length or offset.' );
return false;
}
-
// we're not very robust here. we should accept it in the wrong order. To quote
// the xmp standard:
// "A JPEG writer should write the ExtendedXMP marker segments in order, immediately following the
@@ -344,8 +339,8 @@ class XMPReader {
// so the probability that it will have > 128k, and be in the wrong order is very low...
if ( $len['offset'] !== $this->extendedXMPOffset ) {
- wfDebugLog('XMP', __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
- . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')');
+ wfDebugLog( 'XMP', __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
+ . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')' );
return false;
}
@@ -365,26 +360,26 @@ class XMPReader {
$atEnd = false;
}
- wfDebugLog('XMP', __METHOD__ . 'Parsing a XMPExtended block');
+ wfDebugLog( 'XMP', __METHOD__ . 'Parsing a XMPExtended block' );
return $this->parse( $actualContent, $atEnd );
}
/**
- * Character data handler
- * Called whenever character data is found in the xmp document.
- *
- * does nothing if we're in MODE_IGNORE or if the data is whitespace
- * throws an error if we're not in MODE_SIMPLE (as we're not allowed to have character
- * data in the other modes).
- *
- * As an example, this happens when we encounter XMP like:
- * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
- * and are processing the 0/10 bit.
- *
- * @param $parser XMLParser reference to the xml parser
- * @param $data String Character data
- * @throws MWException on invalid data
- */
+ * Character data handler
+ * Called whenever character data is found in the xmp document.
+ *
+ * does nothing if we're in MODE_IGNORE or if the data is whitespace
+ * throws an error if we're not in MODE_SIMPLE (as we're not allowed to have character
+ * data in the other modes).
+ *
+ * As an example, this happens when we encounter XMP like:
+ * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
+ * and are processing the 0/10 bit.
+ *
+ * @param $parser XMLParser reference to the xml parser
+ * @param string $data Character data
+ * @throws MWException on invalid data
+ */
function char( $parser, $data ) {
$data = trim( $data );
@@ -396,7 +391,9 @@ class XMPReader {
throw new MWException( 'Unexpected character data before first rdf:Description element' );
}
- if ( $this->mode[0] === self::MODE_IGNORE ) return;
+ if ( $this->mode[0] === self::MODE_IGNORE ) {
+ return;
+ }
if ( $this->mode[0] !== self::MODE_SIMPLE
&& $this->mode[0] !== self::MODE_QDESC
@@ -414,37 +411,34 @@ class XMPReader {
}
/** When we hit a closing element in MODE_IGNORE
- * Check to see if this is the element we started to ignore,
- * in which case we get out of MODE_IGNORE
- *
- * @param $elm String Namespace of element followed by a space and then tag name of element.
- */
- private function endElementModeIgnore ( $elm ) {
-
+ * Check to see if this is the element we started to ignore,
+ * in which case we get out of MODE_IGNORE
+ *
+ * @param string $elm Namespace of element followed by a space and then tag name of element.
+ */
+ private function endElementModeIgnore( $elm ) {
if ( $this->curItem[0] === $elm ) {
array_shift( $this->curItem );
array_shift( $this->mode );
}
- return;
-
}
/**
- * Hit a closing element when in MODE_SIMPLE.
- * This generally means that we finished processing a
- * property value, and now have to save the result to the
- * results array
- *
- * For example, when processing:
- * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
- * this deals with when we hit </exif:DigitalZoomRatio>.
- *
- * Or it could be if we hit the end element of a property
- * of a compound data structure (like a member of an array).
- *
- * @param $elm String namespace, space, and tag name.
- */
- private function endElementModeSimple ( $elm ) {
+ * Hit a closing element when in MODE_SIMPLE.
+ * This generally means that we finished processing a
+ * property value, and now have to save the result to the
+ * results array
+ *
+ * For example, when processing:
+ * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
+ * this deals with when we hit </exif:DigitalZoomRatio>.
+ *
+ * Or it could be if we hit the end element of a property
+ * of a compound data structure (like a member of an array).
+ *
+ * @param string $elm namespace, space, and tag name.
+ */
+ private function endElementModeSimple( $elm ) {
if ( $this->charContent !== false ) {
if ( $this->processingArray ) {
// if we're processing an array, use the original element
@@ -463,22 +457,23 @@ class XMPReader {
}
/**
- * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG
- * generally means we've finished processing a nested structure.
- * resets some internal variables to indicate that.
- *
- * Note this means we hit the closing element not the "</rdf:Seq>".
- *
- * @par For example, when processing:
- * @code{,xml}
- * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
- * </rdf:Seq> </exif:ISOSpeedRatings>
- * @endcode
- *
- * This method is called when we hit the "</exif:ISOSpeedRatings>" tag.
- *
- * @param $elm String namespace . space . tag name.
- */
+ * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG
+ * generally means we've finished processing a nested structure.
+ * resets some internal variables to indicate that.
+ *
+ * Note this means we hit the closing element not the "</rdf:Seq>".
+ *
+ * @par For example, when processing:
+ * @code{,xml}
+ * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+ * </rdf:Seq> </exif:ISOSpeedRatings>
+ * @endcode
+ *
+ * This method is called when we hit the "</exif:ISOSpeedRatings>" tag.
+ *
+ * @param string $elm namespace . space . tag name.
+ * @throws MWException
+ */
private function endElementNested( $elm ) {
/* cur item must be the same as $elm, unless if in MODE_STRUCT
@@ -486,7 +481,7 @@ class XMPReader {
if ( $this->curItem[0] !== $elm
&& !( $elm === self::NS_RDF . ' Description'
&& $this->mode[0] === self::MODE_STRUCT )
- ) {
+ ) {
throw new MWException( "nesting mismatch. got a </$elm> but expected a </" . $this->curItem[0] . '>' );
}
@@ -528,23 +523,24 @@ class XMPReader {
}
/**
- * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag )
- * Add information about what type of element this is.
- *
- * Note we still have to hit the outer "</property>"
- *
- * @par For example, when processing:
- * @code{,xml}
- * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
- * </rdf:Seq> </exif:ISOSpeedRatings>
- * @endcode
- *
- * This method is called when we hit the "</rdf:Seq>".
- * (For comparison, we call endElementModeSimple when we
- * hit the "</rdf:li>")
- *
- * @param $elm String namespace . ' ' . element name
- */
+ * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag )
+ * Add information about what type of element this is.
+ *
+ * Note we still have to hit the outer "</property>"
+ *
+ * @par For example, when processing:
+ * @code{,xml}
+ * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+ * </rdf:Seq> </exif:ISOSpeedRatings>
+ * @endcode
+ *
+ * This method is called when we hit the "</rdf:Seq>".
+ * (For comparison, we call endElementModeSimple when we
+ * hit the "</rdf:li>")
+ *
+ * @param string $elm namespace . ' ' . element name
+ * @throws MWException
+ */
private function endElementModeLi( $elm ) {
list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
@@ -575,15 +571,15 @@ class XMPReader {
}
/**
- * End element while in MODE_QDESC
- * mostly when ending an element when we have a simple value
- * that has qualifiers.
- *
- * Qualifiers aren't all that common, and we don't do anything
- * with them.
- *
- * @param $elm String namespace and element
- */
+ * End element while in MODE_QDESC
+ * mostly when ending an element when we have a simple value
+ * that has qualifiers.
+ *
+ * Qualifiers aren't all that common, and we don't do anything
+ * with them.
+ *
+ * @param string $elm namespace and element
+ */
private function endElementModeQDesc( $elm ) {
if ( $elm === self::NS_RDF . ' value' ) {
@@ -594,22 +590,21 @@ class XMPReader {
array_shift( $this->mode );
array_shift( $this->curItem );
}
-
-
}
/**
- * Handler for hitting a closing element.
- *
- * generally just calls a helper function depending on what
- * mode we're in.
- *
- * Ignores the outer wrapping elements that are optional in
- * xmp and have no meaning.
- *
- * @param $parser XMLParser
- * @param $elm String namespace . ' ' . element name
- */
+ * Handler for hitting a closing element.
+ *
+ * generally just calls a helper function depending on what
+ * mode we're in.
+ *
+ * Ignores the outer wrapping elements that are optional in
+ * xmp and have no meaning.
+ *
+ * @param $parser XMLParser
+ * @param string $elm namespace . ' ' . element name
+ * @throws MWException
+ */
function endElement( $parser, $elm ) {
if ( $elm === ( self::NS_RDF . ' RDF' )
|| $elm === 'adobe:ns:meta/ xmpmeta'
@@ -646,7 +641,7 @@ class XMPReader {
throw new MWException( "Hit end element </$elm> but no curItem" );
}
- switch( $this->mode[0] ) {
+ switch ( $this->mode[0] ) {
case self::MODE_IGNORE:
$this->endElementModeIgnore( $elm );
break;
@@ -681,16 +676,16 @@ class XMPReader {
}
/**
- * Hit an opening element while in MODE_IGNORE
- *
- * XMP is extensible, so ignore any tag we don't understand.
- *
- * Mostly ignores, unless we encounter the element that we are ignoring.
- * in which case we add it to the item stack, so we can ignore things
- * that are nested, correctly.
- *
- * @param $elm String namespace . ' ' . tag name
- */
+ * Hit an opening element while in MODE_IGNORE
+ *
+ * XMP is extensible, so ignore any tag we don't understand.
+ *
+ * Mostly ignores, unless we encounter the element that we are ignoring.
+ * in which case we add it to the item stack, so we can ignore things
+ * that are nested, correctly.
+ *
+ * @param string $elm namespace . ' ' . tag name
+ */
private function startElementModeIgnore( $elm ) {
if ( $elm === $this->curItem[0] ) {
array_unshift( $this->curItem, $elm );
@@ -699,12 +694,12 @@ class XMPReader {
}
/**
- * Start element in MODE_BAG (unordered array)
- * this should always be <rdf:Bag>
- *
- * @param $elm String namespace . ' ' . tag
- * @throws MWException if we have an element that's not <rdf:Bag>
- */
+ * Start element in MODE_BAG (unordered array)
+ * this should always be <rdf:Bag>
+ *
+ * @param string $elm namespace . ' ' . tag
+ * @throws MWException if we have an element that's not <rdf:Bag>
+ */
private function startElementModeBag( $elm ) {
if ( $elm === self::NS_RDF . ' Bag' ) {
array_unshift( $this->mode, self::MODE_LI );
@@ -715,12 +710,12 @@ class XMPReader {
}
/**
- * Start element in MODE_SEQ (ordered array)
- * this should always be <rdf:Seq>
- *
- * @param $elm String namespace . ' ' . tag
- * @throws MWException if we have an element that's not <rdf:Seq>
- */
+ * Start element in MODE_SEQ (ordered array)
+ * this should always be <rdf:Seq>
+ *
+ * @param string $elm namespace . ' ' . tag
+ * @throws MWException if we have an element that's not <rdf:Seq>
+ */
private function startElementModeSeq( $elm ) {
if ( $elm === self::NS_RDF . ' Seq' ) {
array_unshift( $this->mode, self::MODE_LI );
@@ -736,19 +731,19 @@ class XMPReader {
}
/**
- * Start element in MODE_LANG (language alternative)
- * this should always be <rdf:Alt>
- *
- * This tag tends to be used for metadata like describe this
- * picture, which can be translated into multiple languages.
- *
- * XMP supports non-linguistic alternative selections,
- * which are really only used for thumbnails, which
- * we don't care about.
- *
- * @param $elm String namespace . ' ' . tag
- * @throws MWException if we have an element that's not <rdf:Alt>
- */
+ * Start element in MODE_LANG (language alternative)
+ * this should always be <rdf:Alt>
+ *
+ * This tag tends to be used for metadata like describe this
+ * picture, which can be translated into multiple languages.
+ *
+ * XMP supports non-linguistic alternative selections,
+ * which are really only used for thumbnails, which
+ * we don't care about.
+ *
+ * @param string $elm namespace . ' ' . tag
+ * @throws MWException if we have an element that's not <rdf:Alt>
+ */
private function startElementModeLang( $elm ) {
if ( $elm === self::NS_RDF . ' Alt' ) {
array_unshift( $this->mode, self::MODE_LI_LANG );
@@ -759,22 +754,23 @@ class XMPReader {
}
/**
- * Handle an opening element when in MODE_SIMPLE
- *
- * This should not happen often. This is for if a simple element
- * already opened has a child element. Could happen for a
- * qualified element.
- *
- * For example:
- * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
- * <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
- * </exif:DigitalZoomRatio>
- *
- * This method is called when processing the <rdf:Description> element
- *
- * @param $elm String namespace and tag names separated by space.
- * @param $attribs Array Attributes of the element.
- */
+ * Handle an opening element when in MODE_SIMPLE
+ *
+ * This should not happen often. This is for if a simple element
+ * already opened has a child element. Could happen for a
+ * qualified element.
+ *
+ * For example:
+ * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
+ * <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
+ * </exif:DigitalZoomRatio>
+ *
+ * This method is called when processing the <rdf:Description> element
+ *
+ * @param string $elm namespace and tag names separated by space.
+ * @param array $attribs Attributes of the element.
+ * @throws MWException
+ */
private function startElementModeSimple( $elm, $attribs ) {
if ( $elm === self::NS_RDF . ' Description' ) {
// If this value has qualifiers
@@ -800,19 +796,19 @@ class XMPReader {
}
/**
- * Start an element when in MODE_QDESC.
- * This generally happens when a simple element has an inner
- * rdf:Description to hold qualifier elements.
- *
- * For example in:
- * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
- * <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
- * </exif:DigitalZoomRatio>
- * Called when processing the <rdf:value> or <foo:someQualifier>.
- *
- * @param $elm String namespace and tag name separated by a space.
- *
- */
+ * Start an element when in MODE_QDESC.
+ * This generally happens when a simple element has an inner
+ * rdf:Description to hold qualifier elements.
+ *
+ * For example in:
+ * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
+ * <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
+ * </exif:DigitalZoomRatio>
+ * Called when processing the <rdf:value> or <foo:someQualifier>.
+ *
+ * @param string $elm namespace and tag name separated by a space.
+ *
+ */
private function startElementModeQDesc( $elm ) {
if ( $elm === self::NS_RDF . ' value' ) {
return; // do nothing
@@ -824,16 +820,17 @@ class XMPReader {
}
/**
- * Starting an element when in MODE_INITIAL
- * This usually happens when we hit an element inside
- * the outer rdf:Description
- *
- * This is generally where most properties start.
- *
- * @param $ns String Namespace
- * @param $tag String tag name (without namespace prefix)
- * @param $attribs Array array of attributes
- */
+ * Starting an element when in MODE_INITIAL
+ * This usually happens when we hit an element inside
+ * the outer rdf:Description
+ *
+ * This is generally where most properties start.
+ *
+ * @param string $ns Namespace
+ * @param string $tag tag name (without namespace prefix)
+ * @param array $attribs array of attributes
+ * @throws MWException
+ */
private function startElementModeInitial( $ns, $tag, $attribs ) {
if ( $ns !== self::NS_RDF ) {
@@ -877,23 +874,24 @@ class XMPReader {
}
/**
- * Hit an opening element when in a Struct (MODE_STRUCT)
- * This is generally for fields of a compound property.
- *
- * Example of a struct (abbreviated; flash has more properties):
- *
- * <exif:Flash> <rdf:Description> <exif:Fired>True</exif:Fired>
- * <exif:Mode>1</exif:Mode></rdf:Description></exif:Flash>
- *
- * or:
- *
- * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired>
- * <exif:Mode>1</exif:Mode></exif:Flash>
- *
- * @param $ns String namespace
- * @param $tag String tag name (no ns)
- * @param $attribs Array array of attribs w/ values.
- */
+ * Hit an opening element when in a Struct (MODE_STRUCT)
+ * This is generally for fields of a compound property.
+ *
+ * Example of a struct (abbreviated; flash has more properties):
+ *
+ * <exif:Flash> <rdf:Description> <exif:Fired>True</exif:Fired>
+ * <exif:Mode>1</exif:Mode></rdf:Description></exif:Flash>
+ *
+ * or:
+ *
+ * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired>
+ * <exif:Mode>1</exif:Mode></exif:Flash>
+ *
+ * @param string $ns namespace
+ * @param string $tag tag name (no ns)
+ * @param array $attribs array of attribs w/ values.
+ * @throws MWException
+ */
private function startElementModeStruct( $ns, $tag, $attribs ) {
if ( $ns !== self::NS_RDF ) {
@@ -929,18 +927,18 @@ class XMPReader {
}
/**
- * opening element in MODE_LI
- * process elements of arrays.
- *
- * Example:
- * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
- * </rdf:Seq> </exif:ISOSpeedRatings>
- * This method is called when we hit the <rdf:li> element.
- *
- * @param $elm String: namespace . ' ' . tagname
- * @param $attribs Array: Attributes. (needed for BAGSTRUCTS)
- * @throws MWException if gets a tag other than <rdf:li>
- */
+ * opening element in MODE_LI
+ * process elements of arrays.
+ *
+ * Example:
+ * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+ * </rdf:Seq> </exif:ISOSpeedRatings>
+ * This method is called when we hit the <rdf:li> element.
+ *
+ * @param string $elm namespace . ' ' . tagname
+ * @param array $attribs Attributes. (needed for BAGSTRUCTS)
+ * @throws MWException if gets a tag other than <rdf:li>
+ */
private function startElementModeLi( $elm, $attribs ) {
if ( ( $elm ) !== self::NS_RDF . ' li' ) {
throw new MWException( "<rdf:li> expected but got $elm." );
@@ -980,32 +978,32 @@ class XMPReader {
}
/**
- * Opening element in MODE_LI_LANG.
- * process elements of language alternatives
- *
- * Example:
- * <dc:title> <rdf:Alt> <rdf:li xml:lang="x-default">My house
- * </rdf:li> </rdf:Alt> </dc:title>
- *
- * This method is called when we hit the <rdf:li> element.
- *
- * @param $elm String namespace . ' ' . tag
- * @param $attribs array array of elements (most importantly xml:lang)
- * @throws MWException if gets a tag other than <rdf:li> or if no xml:lang
- */
+ * Opening element in MODE_LI_LANG.
+ * process elements of language alternatives
+ *
+ * Example:
+ * <dc:title> <rdf:Alt> <rdf:li xml:lang="x-default">My house
+ * </rdf:li> </rdf:Alt> </dc:title>
+ *
+ * This method is called when we hit the <rdf:li> element.
+ *
+ * @param string $elm namespace . ' ' . tag
+ * @param array $attribs array of elements (most importantly xml:lang)
+ * @throws MWException if gets a tag other than <rdf:li> or if no xml:lang
+ */
private function startElementModeLiLang( $elm, $attribs ) {
if ( $elm !== self::NS_RDF . ' li' ) {
throw new MWException( __METHOD__ . " <rdf:li> expected but got $elm." );
}
- if ( !isset( $attribs[ self::NS_XML . ' lang'] )
- || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[ self::NS_XML . ' lang' ] ) )
+ if ( !isset( $attribs[self::NS_XML . ' lang'] )
+ || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[self::NS_XML . ' lang'] ) )
{
throw new MWException( __METHOD__
. " <rdf:li> did not contain, or has invalid xml:lang attribute in lang alternative" );
}
// Lang is case-insensitive.
- $this->itemLang = strtolower( $attribs[ self::NS_XML . ' lang' ] );
+ $this->itemLang = strtolower( $attribs[self::NS_XML . ' lang'] );
// need to add curItem[0] on again since one is for the specific item
// and one is for the entire group.
@@ -1015,19 +1013,20 @@ class XMPReader {
}
/**
- * Hits an opening element.
- * Generally just calls a helper based on what MODE we're in.
- * Also does some initial set up for the wrapper element
- *
- * @param $parser XMLParser
- * @param $elm String namespace "<space>" element
- * @param $attribs Array attribute name => value
- */
+ * Hits an opening element.
+ * Generally just calls a helper based on what MODE we're in.
+ * Also does some initial set up for the wrapper element
+ *
+ * @param $parser XMLParser
+ * @param string $elm namespace "<space>" element
+ * @param array $attribs attribute name => value
+ * @throws MWException
+ */
function startElement( $parser, $elm, $attribs ) {
if ( $elm === self::NS_RDF . ' RDF'
|| $elm === 'adobe:ns:meta/ xmpmeta'
- || $elm === 'adobe:ns:meta/ xapmeta')
+ || $elm === 'adobe:ns:meta/ xapmeta' )
{
/* ignore. */
return;
@@ -1057,11 +1056,11 @@ class XMPReader {
if ( count( $this->mode ) === 0 ) {
// This should not happen.
- throw new MWException('Error extracting XMP, '
+ throw new MWException( 'Error extracting XMP, '
. "encountered <$elm> with no mode" );
}
- switch( $this->mode[0] ) {
+ switch ( $this->mode[0] ) {
case self::MODE_IGNORE:
$this->startElementModeIgnore( $elm );
break;
@@ -1095,24 +1094,24 @@ class XMPReader {
break;
default:
throw new MWException( 'StartElement in unknown mode: ' . $this->mode[0] );
- break;
}
}
/**
- * Process attributes.
- * Simple values can be stored as either a tag or attribute
- *
- * Often the initial "<rdf:Description>" tag just has all the simple
- * properties as attributes.
- *
- * @par Example:
- * @code
- * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10">
- * @endcode
- *
- * @param $attribs Array attribute=>value array.
- */
+ * Process attributes.
+ * Simple values can be stored as either a tag or attribute
+ *
+ * Often the initial "<rdf:Description>" tag just has all the simple
+ * properties as attributes.
+ *
+ * @par Example:
+ * @code
+ * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10">
+ * @endcode
+ *
+ * @param array $attribs attribute=>value array.
+ * @throws MWException
+ */
private function doAttribs( $attribs ) {
// first check for rdf:parseType attribute, as that can change
@@ -1126,8 +1125,6 @@ class XMPReader {
$this->mode[0] = self::MODE_QDESC;
}
foreach ( $attribs as $name => $val ) {
-
-
if ( strpos( $name, ' ' ) === false ) {
// This shouldn't happen, but so far some old software forgets namespace
// on rdf:about.
@@ -1155,16 +1152,16 @@ class XMPReader {
}
/**
- * Given an extracted value, save it to results array
- *
- * note also uses $this->ancestorStruct and
- * $this->processingArray to determine what name to
- * save the value under. (in addition to $tag).
- *
- * @param $ns String namespace of tag this is for
- * @param $tag String tag name
- * @param $val String value to save
- */
+ * Given an extracted value, save it to results array
+ *
+ * note also uses $this->ancestorStruct and
+ * $this->processingArray to determine what name to
+ * save the value under. (in addition to $tag).
+ *
+ * @param string $ns namespace of tag this is for
+ * @param string $tag tag name
+ * @param string $val value to save
+ */
private function saveValue( $ns, $tag, $val ) {
$info =& $this->items[$ns][$tag];
diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php
index 83b8a102..f0b2cb5e 100644
--- a/includes/media/XMPInfo.php
+++ b/includes/media/XMPInfo.php
@@ -22,20 +22,20 @@
*/
/**
-* This class is just a container for a big array
-* used by XMPReader to determine which XMP items to
-* extract.
-*/
+ * This class is just a container for a big array
+ * used by XMPReader to determine which XMP items to
+ * extract.
+ */
class XMPInfo {
/** get the items array
* @return Array XMP item configuration array.
- */
- public static function getItems ( ) {
- if( !self::$ranHooks ) {
+ */
+ public static function getItems() {
+ if ( !self::$ranHooks ) {
// This is for if someone makes a custom metadata extension.
// For example, a medical wiki might want to decode DICOM xmp properties.
- wfRunHooks('XMPGetInfo', Array(&self::$items));
+ wfRunHooks( 'XMPGetInfo', Array( &self::$items ) );
self::$ranHooks = true; // Only want to do this once.
}
return self::$items;
@@ -44,26 +44,25 @@ class XMPInfo {
static private $ranHooks = false;
/**
- * XMPInfo::$items keeps a list of all the items
- * we are interested to extract, as well as
- * information about the item like what type
- * it is.
- *
- * Format is an array of namespaces,
- * each containing an array of tags
- * each tag is an array of information about the
- * tag, including:
- * * map_group - what group (used for precedence during conflicts)
- * * mode - What type of item (self::MODE_SIMPLE usually, see above for all values)
- * * validate - method to validate input. Could also post-process the input. A string value is assumed to be a static method of XMPValidate. Can also take a array( 'className', 'methodName' ).
- * * choices - array of potential values (format of 'value' => true ). Only used with validateClosed
- * * rangeLow and rangeHigh - alternative to choices for numeric ranges. Again for validateClosed only.
- * * children - for MODE_STRUCT items, allowed children.
- * * structPart - Indicates that this element can only appear as a member of a structure.
- *
- * currently this just has a bunch of exif values as this class is only half-done
- */
-
+ * XMPInfo::$items keeps a list of all the items
+ * we are interested to extract, as well as
+ * information about the item like what type
+ * it is.
+ *
+ * Format is an array of namespaces,
+ * each containing an array of tags
+ * each tag is an array of information about the
+ * tag, including:
+ * * map_group - what group (used for precedence during conflicts)
+ * * mode - What type of item (self::MODE_SIMPLE usually, see above for all values)
+ * * validate - method to validate input. Could also post-process the input. A string value is assumed to be a static method of XMPValidate. Can also take a array( 'className', 'methodName' ).
+ * * choices - array of potential values (format of 'value' => true ). Only used with validateClosed
+ * * rangeLow and rangeHigh - alternative to choices for numeric ranges. Again for validateClosed only.
+ * * children - for MODE_STRUCT items, allowed children.
+ * * structPart - Indicates that this element can only appear as a member of a structure.
+ *
+ * currently this just has a bunch of exif values as this class is only half-done
+ */
static private $items = array(
'http://ns.adobe.com/exif/1.0/' => array(
'ApertureValue' => array(
@@ -260,7 +259,7 @@ class XMPInfo {
'mode' => XMPReader::MODE_SIMPLE,
'validate' => 'validateDate',
),
- 'DateTimeDigitized' => array( /* xmp:CreateDate */
+ 'DateTimeDigitized' => array( /* xmp:CreateDate */
'map_group' => 'exif',
'mode' => XMPReader::MODE_SIMPLE,
'validate' => 'validateDate',
@@ -429,7 +428,7 @@ class XMPInfo {
'mode' => XMPReader::MODE_SIMPLE,
'validate' => 'validateClosed',
/* can't use a range, as it skips... */
- 'choices' => array( '0' => true, '1' => true,
+ 'choices' => array( '0' => true, '1' => true,
'2' => true, '3' => true, '4' => true,
'9' => true, '10' => true, '11' => true,
'12' => true, '13' => true,
@@ -552,12 +551,12 @@ class XMPInfo {
'map_group' => 'exif',
'mode' => XMPReader::MODE_LANG,
),
- 'DateTime' => array( /* proper prop is xmp:ModifyDate */
+ 'DateTime' => array( /* proper prop is xmp:ModifyDate */
'map_group' => 'exif',
'mode' => XMPReader::MODE_SIMPLE,
'validate' => 'validateDate',
),
- 'ImageDescription' => array( /* proper one is dc:description */
+ 'ImageDescription' => array( /* proper one is dc:description */
'map_group' => 'exif',
'mode' => XMPReader::MODE_LANG,
),
@@ -622,7 +621,7 @@ class XMPInfo {
'mode' => XMPReader::MODE_SIMPLE,
'validate' => 'validateInteger',
),
- 'Software' => array( /* see xmp:CreatorTool */
+ 'Software' => array( /* see xmp:CreatorTool */
'map_group' => 'exif',
'mode' => XMPReader::MODE_SIMPLE,
),
@@ -669,7 +668,7 @@ class XMPInfo {
* 'validate' => 'validateClosed',
* 'choices' => array( '1' => true, '2' => true ),
* ),
- */
+ */
),
'http://ns.adobe.com/exif/1.0/aux/' => array(
'Lens' => array(
diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php
index 5ce3c00b..87f8abfe 100644
--- a/includes/media/XMPValidate.php
+++ b/includes/media/XMPValidate.php
@@ -22,32 +22,32 @@
*/
/**
-* This contains some static methods for
-* validating XMP properties. See XMPInfo and XMPReader classes.
-*
-* Each of these functions take the same parameters
-* * an info array which is a subset of the XMPInfo::items array
-* * A value (passed as reference) to validate. This can be either a
-* simple value or an array
-* * A boolean to determine if this is validating a simple or complex values
-*
-* It should be noted that when an array is being validated, typically the validation
-* function is called once for each value, and then once at the end for the entire array.
-*
-* These validation functions can also be used to modify the data. See the gps and flash one's
-* for example.
-*
-* @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf starting at pg 28
-* @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf starting at pg 11
-*/
+ * This contains some static methods for
+ * validating XMP properties. See XMPInfo and XMPReader classes.
+ *
+ * Each of these functions take the same parameters
+ * * an info array which is a subset of the XMPInfo::items array
+ * * A value (passed as reference) to validate. This can be either a
+ * simple value or an array
+ * * A boolean to determine if this is validating a simple or complex values
+ *
+ * It should be noted that when an array is being validated, typically the validation
+ * function is called once for each value, and then once at the end for the entire array.
+ *
+ * These validation functions can also be used to modify the data. See the gps and flash one's
+ * for example.
+ *
+ * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf starting at pg 28
+ * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf starting at pg 11
+ */
class XMPValidate {
/**
- * function to validate boolean properties ( True or False )
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate boolean properties ( True or False )
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateBoolean( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
@@ -61,12 +61,12 @@ class XMPValidate {
}
/**
- * function to validate rational properties ( 12/10 )
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate rational properties ( 12/10 )
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateRational( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
@@ -80,38 +80,38 @@ class XMPValidate {
}
/**
- * function to validate rating properties -1, 0-5
- *
- * if its outside of range put it into range.
- *
- * @see MWG spec
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate rating properties -1, 0-5
+ *
+ * if its outside of range put it into range.
+ *
+ * @see MWG spec
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateRating( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
return;
}
if ( !preg_match( '/^[-+]?\d*(?:\.?\d*)$/D', $val )
- || !is_numeric($val)
- ) {
+ || !is_numeric( $val )
+ ) {
wfDebugLog( 'XMP', __METHOD__ . " Expected rating but got $val" );
$val = null;
return;
} else {
- $nVal = (float) $val;
+ $nVal = (float)$val;
if ( $nVal < 0 ) {
// We do < 0 here instead of < -1 here, since
// the values between 0 and -1 are also illegal
// as -1 is meant as a special reject rating.
- wfDebugLog( 'XMP', __METHOD__ . " Rating too low, setting to -1 (Rejected)");
+ wfDebugLog( 'XMP', __METHOD__ . " Rating too low, setting to -1 (Rejected)" );
$val = '-1';
return;
}
if ( $nVal > 5 ) {
- wfDebugLog( 'XMP', __METHOD__ . " Rating too high, setting to 5");
+ wfDebugLog( 'XMP', __METHOD__ . " Rating too high, setting to 5" );
$val = '5';
return;
}
@@ -119,12 +119,12 @@ class XMPValidate {
}
/**
- * function to validate integers
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate integers
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateInteger( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
@@ -138,13 +138,13 @@ class XMPValidate {
}
/**
- * function to validate properties with a fixed number of allowed
- * choices. (closed choice)
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate properties with a fixed number of allowed
+ * choices. (closed choice)
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateClosed( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
@@ -153,7 +153,7 @@ class XMPValidate {
//check if its in a numeric range
$inRange = false;
- if ( isset( $info['rangeLow'] )
+ if ( isset( $info['rangeLow'] )
&& isset( $info['rangeHigh'] )
&& is_numeric( $val )
&& ( intval( $val ) <= $info['rangeHigh'] )
@@ -169,12 +169,12 @@ class XMPValidate {
}
/**
- * function to validate and modify flash structure
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate and modify flash structure
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateFlash( $info, &$val, $standalone ) {
if ( $standalone ) {
// this only validates flash structs, not individual properties
@@ -198,23 +198,23 @@ class XMPValidate {
}
/**
- * function to validate LangCode properties ( en-GB, etc )
- *
- * This is just a naive check to make sure it somewhat looks like a lang code.
- *
- * @see rfc 3066
- * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf page 30 (section 8.2.2.5)
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate LangCode properties ( en-GB, etc )
+ *
+ * This is just a naive check to make sure it somewhat looks like a lang code.
+ *
+ * @see rfc 3066
+ * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf page 30 (section 8.2.2.5)
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateLangCode( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
return;
}
- if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val) ) {
+ if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val ) ) {
//this is a rather naive check.
wfDebugLog( 'XMP', __METHOD__ . " Expected Lang code but got $val" );
$val = null;
@@ -223,22 +223,22 @@ class XMPValidate {
}
/**
- * function to validate date properties, and convert to (partial) Exif format.
- *
- * Dates can be one of the following formats:
- * YYYY
- * YYYY-MM
- * YYYY-MM-DD
- * YYYY-MM-DDThh:mmTZD
- * YYYY-MM-DDThh:mm:ssTZD
- * YYYY-MM-DDThh:mm:ss.sTZD
- *
- * @param $info Array information about current property
- * @param &$val Mixed current value to validate. Converts to TS_EXIF as a side-effect.
- * in cases where there's only a partial date, it will give things like
- * 2011:04.
- * @param $standalone Boolean if this is a simple property or array
- */
+ * function to validate date properties, and convert to (partial) Exif format.
+ *
+ * Dates can be one of the following formats:
+ * YYYY
+ * YYYY-MM
+ * YYYY-MM-DD
+ * YYYY-MM-DDThh:mmTZD
+ * YYYY-MM-DDThh:mm:ssTZD
+ * YYYY-MM-DDThh:mm:ss.sTZD
+ *
+ * @param array $info information about current property
+ * @param &$val Mixed current value to validate. Converts to TS_EXIF as a side-effect.
+ * in cases where there's only a partial date, it will give things like
+ * 2011:04.
+ * @param $standalone Boolean if this is a simple property or array
+ */
public static function validateDate( $info, &$val, $standalone ) {
if ( !$standalone ) {
// this only validates standalone properties, not arrays, etc
@@ -247,8 +247,8 @@ class XMPValidate {
$res = array();
if ( !preg_match(
/* ahh! scary regex... */
- '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D'
- , $val, $res)
+ '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D',
+ $val, $res )
) {
wfDebugLog( 'XMP', __METHOD__ . " Expected date but got $val" );
$val = null;
@@ -295,7 +295,6 @@ class XMPValidate {
return;
}
-
// Extra check for empty string necessary due to TZ but no second case.
$stripSeconds = false;
if ( !isset( $res[6] ) || $res[6] === '' ) {
@@ -331,35 +330,35 @@ class XMPValidate {
* @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf
* section 1.2.7.4 on page 23
*
- * @param $info Array unused (info about prop)
+ * @param array $info unused (info about prop)
* @param &$val String GPS string in either DDD,MM,SSk or
* or DDD,MM.mmk form
* @param $standalone Boolean if its a simple prop (should always be true)
*/
- public static function validateGPS ( $info, &$val, $standalone ) {
+ public static function validateGPS( $info, &$val, $standalone ) {
if ( !$standalone ) {
return;
}
$m = array();
- if ( preg_match(
+ if ( preg_match(
'/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D',
$val, $m )
) {
$coord = intval( $m[1] );
- $coord += intval( $m[2] ) * (1/60);
- $coord += intval( $m[3] ) * (1/3600);
+ $coord += intval( $m[2] ) * ( 1 / 60 );
+ $coord += intval( $m[3] ) * ( 1 / 3600 );
if ( $m[4] === 'S' || $m[4] === 'W' ) {
$coord = -$coord;
}
$val = $coord;
return;
- } elseif ( preg_match(
+ } elseif ( preg_match(
'/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D',
$val, $m )
) {
$coord = intval( $m[1] );
- $coord += floatval( $m[2] ) * (1/60);
+ $coord += floatval( $m[2] ) * ( 1 / 60 );
if ( $m[3] === 'S' || $m[3] === 'W' ) {
$coord = -$coord;
}
@@ -367,7 +366,7 @@ class XMPValidate {
return;
} else {
- wfDebugLog( 'XMP', __METHOD__
+ wfDebugLog( 'XMP', __METHOD__
. " Expected GPSCoordinate, but got $val." );
$val = null;
return;
diff --git a/includes/mime.info b/includes/mime.info
index f7576e41..c7981871 100644
--- a/includes/mime.info
+++ b/includes/mime.info
@@ -30,6 +30,7 @@ application/x-dia-diagram [DRAWING]
audio/mpeg audio/mp3 audio/mpeg3 [AUDIO]
+audio/mp4 [AUDIO]
audio/wav audio/x-wav audio/wave [AUDIO]
audio/midi audio/mid [AUDIO]
audio/basic [AUDIO]
@@ -39,6 +40,8 @@ audio/x-pn-realaudio [AUDIO]
audio/x-realaudio [AUDIO]
audio/webm [AUDIO]
audio/x-matroska [AUDIO]
+audio/x-flac [AUDIO]
+audio/flac [AUDIO]
video/mpeg application/mpeg [VIDEO]
video/ogg [VIDEO]
@@ -46,6 +49,7 @@ video/x-sgi-video [VIDEO]
video/x-flv [VIDEO]
video/webm [VIDEO]
video/x-matroska [VIDEO]
+video/mp4 [VIDEO]
application/ogg application/x-ogg audio/ogg audio/x-ogg video/ogg video/x-ogg [MULTIMEDIA]
diff --git a/includes/mime.types b/includes/mime.types
index 5b64201f..61f7ff58 100644
--- a/includes/mime.types
+++ b/includes/mime.types
@@ -65,6 +65,7 @@ audio/basic au snd
audio/midi mid midi kar
audio/mpeg mpga mp2 mp3
audio/ogg oga ogg spx
+video/webm webm
audio/webm webm
audio/x-aiff aif aiff aifc
audio/x-matroska mka mkv
@@ -74,6 +75,9 @@ audio/x-pn-realaudio ram rm
audio/x-pn-realaudio-plugin rpm
audio/x-realaudio ra
audio/x-wav wav
+audio/wav wav
+audio/x-flac flac
+audio/flac flac
chemical/x-pdb pdb
chemical/x-xyz xyz
image/bmp bmp
@@ -117,10 +121,11 @@ text/xml xml xsl xslt rss rdf
text/x-setext etx
text/x-sawfish jl
video/mpeg mpeg mpg mpe
+video/mp4 mp4 m4a m4p m4b m4r m4v
+audio/mp4 m4a
video/ogg ogv ogm ogg
video/quicktime qt mov
video/vnd.mpegurl mxu
-video/webm webm
video/x-flv flv
video/x-matroska mkv mka
video/x-msvideo avi
diff --git a/includes/mobile/DeviceDetection.php b/includes/mobile/DeviceDetection.php
deleted file mode 100644
index 262665be..00000000
--- a/includes/mobile/DeviceDetection.php
+++ /dev/null
@@ -1,459 +0,0 @@
-<?php
-/**
- * Mobile device detection code
- *
- * Copyright © 2011 Patrick Reilly
- * 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
- */
-
-/**
- * Base for classes describing devices and their capabilities
- * @since 1.20
- */
-interface IDeviceProperties {
- /**
- * @return string: 'html' or 'wml'
- */
- function format();
-
- /**
- * @return bool
- */
- function supportsJavaScript();
-
- /**
- * @return bool
- */
- function supportsJQuery();
-
- /**
- * @return bool
- */
- function disableZoom();
-}
-
-/**
- * @since 1.20
- */
-interface IDeviceDetector {
- /**
- * @param $userAgent
- * @param string $acceptHeader
- * @return IDeviceProperties
- */
- function detectDeviceProperties( $userAgent, $acceptHeader = '' );
-
- /**
- * @param $deviceName
- * @return IDeviceProperties
- */
- function getDeviceProperties( $deviceName );
-
- /**
- * @param $userAgent string
- * @param $acceptHeader string
- * @return string
- */
- function detectDeviceName( $userAgent, $acceptHeader = '' );
-}
-
-/**
- * MediaWiki's default IDeviceProperties implementation
- */
-final class DeviceProperties implements IDeviceProperties {
- private $device;
-
- public function __construct( array $deviceCapabilities ) {
- $this->device = $deviceCapabilities;
- }
-
- /**
- * @return string
- */
- function format() {
- return $this->device['view_format'];
- }
-
- /**
- * @return bool
- */
- function supportsJavaScript() {
- return $this->device['supports_javascript'];
- }
-
- /**
- * @return bool
- */
- function supportsJQuery() {
- return $this->device['supports_jquery'];
- }
-
- /**
- * @return bool
- */
- function disableZoom() {
- return $this->device['disable_zoom'];
- }
-}
-
-/**
- * Provides abstraction for a device.
- * A device can select which format a request should receive and
- * may be extended to provide access to particular device functionality.
- * @since 1.20
- */
-class DeviceDetection implements IDeviceDetector {
-
- private static $formats = array (
- 'html' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'default',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'capable' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'default',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => true,
- ),
- 'webkit' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'webkit',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => false,
- ),
- 'ie' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'default',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => false,
- ),
- 'android' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'android',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => false,
- ),
- 'iphone' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'iphone',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => false,
- ),
- 'iphone2' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'iphone2',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => true,
- ),
- 'native_iphone' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'default',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => false,
- ),
- 'palm_pre' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'palm_pre',
- 'supports_javascript' => true,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'kindle' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'kindle',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'kindle2' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'kindle',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'blackberry' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'blackberry',
- 'supports_javascript' => true,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'blackberry-lt5' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'blackberry',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'netfront' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'simple',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'wap2' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'simple',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'psp' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'psp',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'ps3' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'simple',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'wii' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'wii',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => true,
- ),
- 'operamini' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'operamini',
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'operamobile' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'operamobile',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => true,
- ),
- 'nokia' => array (
- 'view_format' => 'html',
- 'css_file_name' => 'nokia',
- 'supports_javascript' => true,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- 'wml' => array (
- 'view_format' => 'wml',
- 'css_file_name' => null,
- 'supports_javascript' => false,
- 'supports_jquery' => false,
- 'disable_zoom' => true,
- ),
- );
-
- /**
- * Returns an instance of detection class, overridable by extensions
- * @return IDeviceDetector
- */
- public static function factory() {
- global $wgDeviceDetectionClass;
-
- static $instance = null;
- if ( !$instance ) {
- $instance = new $wgDeviceDetectionClass();
- }
- return $instance;
- }
-
- /**
- * @deprecated: Deprecated, will be removed once detectDeviceProperties() will be deployed everywhere on WMF
- * @param $userAgent
- * @param string $acceptHeader
- * @return array
- */
- public function detectDevice( $userAgent, $acceptHeader = '' ) {
- $formatName = $this->detectFormatName( $userAgent, $acceptHeader );
- return $this->getDevice( $formatName );
- }
-
- /**
- * @param $userAgent
- * @param string $acceptHeader
- * @return IDeviceProperties
- */
- public function detectDeviceProperties( $userAgent, $acceptHeader = '' ) {
- $deviceName = $this->detectDeviceName( $userAgent, $acceptHeader );
- return $this->getDeviceProperties( $deviceName );
- }
-
- /**
- * @deprecated: Deprecated, will be removed once detectDeviceProperties() will be deployed everywhere on WMF
- * @param $formatName
- * @return array
- */
- public function getDevice( $formatName ) {
- return ( isset( self::$formats[$formatName] ) ) ? self::$formats[$formatName] : array();
- }
-
- /**
- * @param $deviceName
- * @return IDeviceProperties
- */
- public function getDeviceProperties( $deviceName ) {
- if ( isset( self::$formats[$deviceName] ) ) {
- return new DeviceProperties( self::$formats[$deviceName] );
- } else {
- return new DeviceProperties( array(
- 'view_format' => 'html',
- 'css_file_name' => 'default',
- 'supports_javascript' => true,
- 'supports_jquery' => true,
- 'disable_zoom' => true,
- ) );
- }
- }
-
- /**
- * @deprecated: Renamed to detectDeviceName()
- * @param $userAgent string
- * @param $acceptHeader string
- * @return string
- */
- public function detectFormatName( $userAgent, $acceptHeader = '' ) {
- return $this->detectDeviceName( $userAgent, $acceptHeader );
- }
-
- /**
- * @param $userAgent string
- * @param $acceptHeader string
- * @return string
- */
- public function detectDeviceName( $userAgent, $acceptHeader = '' ) {
- wfProfileIn( __METHOD__ );
-
- $deviceName = '';
- if ( preg_match( '/Android/', $userAgent ) ) {
- $deviceName = 'android';
- if ( strpos( $userAgent, 'Opera Mini' ) !== false ) {
- $deviceName = 'operamini';
- } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) {
- $deviceName = 'operamobile';
- }
- } elseif ( preg_match( '/MSIE 9.0/', $userAgent ) ||
- preg_match( '/MSIE 8.0/', $userAgent ) ) {
- $deviceName = 'ie';
- } elseif( preg_match( '/MSIE/', $userAgent ) ) {
- $deviceName = 'html';
- } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) {
- $deviceName = 'operamobile';
- } elseif ( preg_match( '/iPad.* Safari/', $userAgent ) ) {
- $deviceName = 'iphone';
- } elseif ( preg_match( '/iPhone.* Safari/', $userAgent ) ) {
- if ( strpos( $userAgent, 'iPhone OS 2' ) !== false ) {
- $deviceName = 'iphone2';
- } else {
- $deviceName = 'iphone';
- }
- } elseif ( preg_match( '/iPhone/', $userAgent ) ) {
- if ( strpos( $userAgent, 'Opera' ) !== false ) {
- $deviceName = 'operamini';
- } else {
- $deviceName = 'native_iphone';
- }
- } elseif ( preg_match( '/WebKit/', $userAgent ) ) {
- if ( preg_match( '/Series60/', $userAgent ) ) {
- $deviceName = 'nokia';
- } elseif ( preg_match( '/webOS/', $userAgent ) ) {
- $deviceName = 'palm_pre';
- } else {
- $deviceName = 'webkit';
- }
- } elseif ( preg_match( '/Opera/', $userAgent ) ) {
- if ( strpos( $userAgent, 'Nintendo Wii' ) !== false ) {
- $deviceName = 'wii';
- } elseif ( strpos( $userAgent, 'Opera Mini' ) !== false ) {
- $deviceName = 'operamini';
- } else {
- $deviceName = 'operamobile';
- }
- } elseif ( preg_match( '/Kindle\/1.0/', $userAgent ) ) {
- $deviceName = 'kindle';
- } elseif ( preg_match( '/Kindle\/2.0/', $userAgent ) ) {
- $deviceName = 'kindle2';
- } elseif ( preg_match( '/Firefox/', $userAgent ) ) {
- $deviceName = 'capable';
- } elseif ( preg_match( '/NetFront/', $userAgent ) ) {
- $deviceName = 'netfront';
- } elseif ( preg_match( '/SEMC-Browser/', $userAgent ) ) {
- $deviceName = 'wap2';
- } elseif ( preg_match( '/Series60/', $userAgent ) ) {
- $deviceName = 'wap2';
- } elseif ( preg_match( '/PlayStation Portable/', $userAgent ) ) {
- $deviceName = 'psp';
- } elseif ( preg_match( '/PLAYSTATION 3/', $userAgent ) ) {
- $deviceName = 'ps3';
- } elseif ( preg_match( '/SAMSUNG/', $userAgent ) ) {
- $deviceName = 'capable';
- } elseif ( preg_match( '/BlackBerry/', $userAgent ) ) {
- if( preg_match( '/BlackBerry[^\/]*\/[1-4]\./', $userAgent ) ) {
- $deviceName = 'blackberry-lt5';
- } else {
- $deviceName = 'blackberry';
- }
- }
-
- if ( $deviceName === '' ) {
- if ( strpos( $acceptHeader, 'application/vnd.wap.xhtml+xml' ) !== false ) {
- // Should be wap2
- $deviceName = 'html';
- } elseif ( strpos( $acceptHeader, 'vnd.wap.wml' ) !== false ) {
- $deviceName = 'wml';
- } else {
- $deviceName = 'html';
- }
- }
- wfProfileOut( __METHOD__ );
- return $deviceName;
- }
-
- /**
- * @return array: List of all device-specific stylesheets
- */
- public function getCssFiles() {
- $files = array();
-
- foreach ( self::$formats as $dev ) {
- if ( isset( $dev['css_file_name'] ) ) {
- $files[] = $dev['css_file_name'];
- }
- }
- return array_unique( $files );
- }
-}
diff --git a/includes/normal/Makefile b/includes/normal/Makefile
index f0c340f6..66348ee3 100644
--- a/includes/normal/Makefile
+++ b/includes/normal/Makefile
@@ -8,7 +8,7 @@
# Explicitly using Unicode 6.0
BASE=http://www.unicode.org/Public/6.0.0/ucd
-# Can override to php-cli or php5 or whatevah
+# Can override to php-cli or php5 or whatever
PHP=php
#PHP=php-cli
diff --git a/includes/normal/README b/includes/normal/README
index a17aa7da..0f718d2c 100644
--- a/includes/normal/README
+++ b/includes/normal/README
@@ -48,12 +48,12 @@ grains of salt.
There's an experimental PHP extension module which wraps the ICU library's
normalization functions. This is *MUCH* faster than doing this work in pure
-PHP code. This is in the 'normal' directory in MediaWiki's CVS extensions
-module. It is known to work with PHP 4.3.8 and 5.0.2 on Linux/x86 but hasn't
-been thoroughly tested on other configurations.
+PHP code. This is at https://git.wikimedia.org/summary/mediawiki%2Fextensions%2Fnormal.git.
+It is used by the WMF, which currently runs PHP 5.3.10 on Linux. It hasn't been
+thoroughly tested on other configurations, but may work.
If the php_normal.so module is loaded in php.ini, the normalization functions
will automatically use it. If you can't (or don't want to) load it in php.ini,
-you may be able to load it using the dl() function before include()ing or
-require()ing UtfNormal.php, and it will be picked up.
+you may be able to load it using the dl() function before the inclusion of
+UtfNormal.php, and it will be picked up.
diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php
index 23471e94..06029868 100644
--- a/includes/normal/RandomTest.php
+++ b/includes/normal/RandomTest.php
@@ -26,15 +26,15 @@
* @ingroup UtfNormal
*/
-if( php_sapi_name() != 'cli' ) {
+if( PHP_SAPI != 'cli' ) {
die( "Run me from the command line please.\n" );
}
/** */
-require_once( 'UtfNormal.php' );
-require_once( '../diff/DifferenceEngine.php' );
+require_once 'UtfNormal.php';
+require_once '../diff/DifferenceEngine.php';
-dl('php_utfnormal.so' );
+dl( 'php_utfnormal.so' );
# mt_srand( 99999 );
diff --git a/includes/normal/Utf8CaseGenerate.php b/includes/normal/Utf8CaseGenerate.php
index 368d0bcd..adc3ef22 100644
--- a/includes/normal/Utf8CaseGenerate.php
+++ b/includes/normal/Utf8CaseGenerate.php
@@ -25,7 +25,7 @@
* @ingroup UtfNormal
*/
-if( php_sapi_name() != 'cli' ) {
+if( PHP_SAPI != 'cli' ) {
die( "Run me from the command line please.\n" );
}
@@ -49,7 +49,7 @@ while( false !== ($line = fgets( $in ) ) ) {
$name = $columns[1];
$simpleUpper = $columns[12];
$simpleLower = $columns[13];
-
+
$source = codepointToUtf8( hexdec( $codepoint ) );
if( $simpleUpper ) {
$wikiUpperChars[$source] = codepointToUtf8( hexdec( $simpleUpper ) );
@@ -60,7 +60,7 @@ while( false !== ($line = fgets( $in ) ) ) {
}
fclose( $in );
-$out = fopen("Utf8Case.php", "wt");
+$out = fopen( "Utf8Case.php", "wt" );
if( $out ) {
$outUpperChars = escapeArray( $wikiUpperChars );
$outLowerChars = escapeArray( $wikiLowerChars );
diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php
index 6eae6e72..c5c1be59 100644
--- a/includes/normal/Utf8Test.php
+++ b/includes/normal/Utf8Test.php
@@ -27,6 +27,10 @@
/** */
+if ( PHP_SAPI != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
require_once 'UtfNormalDefines.php';
require_once 'UtfNormalUtil.php';
require_once 'UtfNormal.php';
@@ -34,9 +38,6 @@ mb_internal_encoding( "utf-8" );
$verbose = false;
#$verbose = true;
-if( php_sapi_name() != 'cli' ) {
- die( "Run me from the command line please.\n" );
-}
$in = fopen( "UTF-8-test.txt", "rt" );
if( !$in ) {
diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php
index 08f85bd3..5a091afc 100644
--- a/includes/normal/UtfNormal.php
+++ b/includes/normal/UtfNormal.php
@@ -37,7 +37,7 @@ define( 'NORMALIZE_INTL', function_exists( 'normalizer_normalize' ) );
*
* Not as fast as I'd like, but should be usable for most purposes.
* UtfNormal::toNFC() will bail early if given ASCII text or text
- * it can quickly deterimine is already normalized.
+ * it can quickly determine is already normalized.
*
* All functions can be called static.
*
@@ -73,7 +73,7 @@ class UtfNormal {
* Fast return for pure ASCII strings; some lesser optimizations for
* strings containing only known-good characters. Not as fast as toNFC().
*
- * @param $string String: a UTF-8 string
+ * @param string $string a UTF-8 string
* @return string a clean, shiny, normalized UTF-8 string
*/
static function cleanUp( $string ) {
@@ -114,7 +114,7 @@ class UtfNormal {
* Fast return for pure ASCII strings; some lesser optimizations for
* strings containing only known-good characters.
*
- * @param $string String: a valid UTF-8 string. Input is not validated.
+ * @param string $string a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form C
*/
static function toNFC( $string ) {
@@ -132,7 +132,7 @@ class UtfNormal {
* Convert a UTF-8 string to normal form D, canonical decomposition.
* Fast return for pure ASCII strings.
*
- * @param $string String: a valid UTF-8 string. Input is not validated.
+ * @param string $string a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form D
*/
static function toNFD( $string ) {
@@ -151,7 +151,7 @@ class UtfNormal {
* This may cause irreversible information loss, use judiciously.
* Fast return for pure ASCII strings.
*
- * @param $string String: a valid UTF-8 string. Input is not validated.
+ * @param string $string a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form KC
*/
static function toNFKC( $string ) {
@@ -170,7 +170,7 @@ class UtfNormal {
* This may cause irreversible information loss, use judiciously.
* Fast return for pure ASCII strings.
*
- * @param $string String: a valid UTF-8 string. Input is not validated.
+ * @param string $string a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form KD
*/
static function toNFKD( $string ) {
@@ -190,14 +190,14 @@ class UtfNormal {
*/
static function loadData() {
if( !isset( self::$utfCombiningClass ) ) {
- require_once( __DIR__ . '/UtfNormalData.inc' );
+ require_once __DIR__ . '/UtfNormalData.inc';
}
}
/**
* Returns true if the string is _definitely_ in NFC.
* Returns false if not or uncertain.
- * @param $string String: a valid UTF-8 string. Input is not validated.
+ * @param string $string a valid UTF-8 string. Input is not validated.
* @return bool
*/
static function quickIsNFC( $string ) {
@@ -237,7 +237,7 @@ class UtfNormal {
/**
* Returns true if the string is _definitely_ in NFC.
* Returns false if not or uncertain.
- * @param $string String: a UTF-8 string, altered on output to be valid UTF-8 safe for XML.
+ * @param string $string a UTF-8 string, altered on output to be valid UTF-8 safe for XML.
* @return bool
*/
static function quickIsNFCVerify( &$string ) {
@@ -491,7 +491,7 @@ class UtfNormal {
*/
static function NFKD( $string ) {
if( !isset( self::$utfCompatibilityDecomp ) ) {
- require_once( 'UtfNormalDataK.inc' );
+ require_once 'UtfNormalDataK.inc';
}
return self::fastCombiningSort(
self::fastDecompose( $string, self::$utfCompatibilityDecomp ) );
@@ -503,8 +503,8 @@ class UtfNormal {
* (depending on which decomposition map is passed to us).
* Input is assumed to be *valid* UTF-8. Invalid code will break.
* @private
- * @param $string String: valid UTF-8 string
- * @param $map Array: hash of expanded decomposition map
+ * @param string $string valid UTF-8 string
+ * @param array $map hash of expanded decomposition map
* @return string a UTF-8 string decomposed, not yet normalized (needs sorting)
*/
static function fastDecompose( $string, $map ) {
@@ -564,7 +564,7 @@ class UtfNormal {
* Sorts combining characters into canonical order. This is the
* final step in creating decomposed normal forms D and KD.
* @private
- * @param $string String: a valid, decomposed UTF-8 string. Input is not validated.
+ * @param string $string a valid, decomposed UTF-8 string. Input is not validated.
* @return string a UTF-8 string with combining characters sorted in canonical order
*/
static function fastCombiningSort( $string ) {
@@ -616,7 +616,7 @@ class UtfNormal {
* Produces canonically composed sequences, i.e. normal form C or KC.
*
* @private
- * @param $string String: a valid UTF-8 string in sorted normal form D or KD. Input is not validated.
+ * @param string $string a valid UTF-8 string in sorted normal form D or KD. Input is not validated.
* @return string a UTF-8 string with canonical precomposed characters used where possible
*/
static function fastCompose( $string ) {
@@ -627,8 +627,8 @@ class UtfNormal {
$lastHangul = 0;
$startChar = '';
$combining = '';
- $x1 = ord(substr(UTF8_HANGUL_VBASE,0,1));
- $x2 = ord(substr(UTF8_HANGUL_TEND,0,1));
+ $x1 = ord(substr(UTF8_HANGUL_VBASE, 0, 1));
+ $x2 = ord(substr(UTF8_HANGUL_TEND, 0, 1));
for( $i = 0; $i < $len; $i++ ) {
$c = $string[$i];
$n = ord( $c );
@@ -762,10 +762,10 @@ class UtfNormal {
* Function to replace some characters that we don't want
* but most of the native normalize functions keep.
*
- * @param $string String The string
+ * @param string $string The string
* @return String String with the character codes replaced.
*/
- private static function replaceForNativeNormalize( $string ) {
+ private static function replaceForNativeNormalize( $string ) {
$string = preg_replace(
'/[\x00-\x08\x0b\x0c\x0e-\x1f]/',
UTF8_REPLACEMENT,
diff --git a/includes/normal/UtfNormalBench.php b/includes/normal/UtfNormalBench.php
index 944c4435..89de9290 100644
--- a/includes/normal/UtfNormalBench.php
+++ b/includes/normal/UtfNormalBench.php
@@ -19,11 +19,15 @@
* 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 UtfNormal
*/
+if( PHP_SAPI != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
if( isset( $_SERVER['argv'] ) && in_array( '--icu', $_SERVER['argv'] ) ) {
dl( 'php_utfnormal.so' );
}
@@ -34,10 +38,6 @@ require_once 'UtfNormal.php';
define( 'BENCH_CYCLES', 5 );
-if( php_sapi_name() != 'cli' ) {
- die( "Run me from the command line please.\n" );
-}
-
$testfiles = array(
'testdata/washington.txt' => 'English text',
'testdata/berlin.txt' => 'German text',
@@ -80,7 +80,7 @@ function benchmarkTest( &$u, $filename, $desc ) {
}
}
-function benchTime(){
+function benchTime() {
$st = explode( ' ', microtime() );
return (float)$st[0] + (float)$st[1];
}
diff --git a/includes/normal/UtfNormalDefines.php b/includes/normal/UtfNormalDefines.php
index 5142a414..b07e3399 100644
--- a/includes/normal/UtfNormalDefines.php
+++ b/includes/normal/UtfNormalDefines.php
@@ -2,7 +2,7 @@
/**
* Some constant definitions for the unicode normalization module.
*
- * Note: these constants must all be resolvable at compile time by HipHop,
+ * Note: these constants must all be resolvable at compile time by HipHop,
* since this file will not be executed during request startup for a compiled
* MediaWiki.
*
@@ -26,7 +26,7 @@
*/
define( 'UNICODE_HANGUL_FIRST', 0xac00 );
-define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
+define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
define( 'UNICODE_HANGUL_LBASE', 0x1100 );
define( 'UNICODE_HANGUL_VBASE', 0x1161 );
diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php
index e4c1138e..f392df52 100644
--- a/includes/normal/UtfNormalGenerate.php
+++ b/includes/normal/UtfNormalGenerate.php
@@ -25,7 +25,7 @@
* @ingroup UtfNormal
*/
-if( php_sapi_name() != 'cli' ) {
+if( PHP_SAPI != 'cli' ) {
die( "Run me from the command line please.\n" );
}
@@ -177,7 +177,7 @@ if( $out ) {
*
* @file
*/
-
+
UtfNormal::\$utfCombiningClass = unserialize( '$serCombining' );
UtfNormal::\$utfCanonicalComp = unserialize( '$serComp' );
UtfNormal::\$utfCanonicalDecomp = unserialize( '$serCanon' );
diff --git a/includes/normal/UtfNormalMemStress.php b/includes/normal/UtfNormalMemStress.php
index 1277dc20..9732d762 100644
--- a/includes/normal/UtfNormalMemStress.php
+++ b/includes/normal/UtfNormalMemStress.php
@@ -26,6 +26,10 @@
* @ingroup UtfNormal
*/
+if( PHP_SAPI != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
if( isset( $_SERVER['argv'] ) && in_array( '--icu', $_SERVER['argv'] ) ) {
dl( 'php_utfnormal.so' );
}
@@ -38,10 +42,6 @@ define( 'BENCH_CYCLES', 1 );
define( 'BIGSIZE', 1024 * 1024 * 10); // 10m
ini_set('memory_limit', BIGSIZE + 120 * 1024 * 1024);
-if( php_sapi_name() != 'cli' ) {
- die( "Run me from the command line please.\n" );
-}
-
$testfiles = array(
'testdata/washington.txt' => 'English text',
'testdata/berlin.txt' => 'German text',
@@ -82,7 +82,7 @@ function benchmarkTest( &$u, $filename, $desc ) {
}
}
-function benchTime(){
+function benchTime() {
$st = explode( ' ', microtime() );
return (float)$st[0] + (float)$st[1];
}
diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php
index 5872ec34..51183666 100644
--- a/includes/normal/UtfNormalTest.php
+++ b/includes/normal/UtfNormalTest.php
@@ -25,14 +25,16 @@
* @ingroup UtfNormal
*/
+if( PHP_SAPI != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
$verbose = true;
#define( 'PRETTY_UTF8', true );
if( defined( 'PRETTY_UTF8' ) ) {
function pretty( $string ) {
- return preg_replace( '/([\x00-\xff])/e',
- 'sprintf("%02X", ord("$1"))',
- $string );
+ return strtoupper( bin2hex( $string ) );
}
} else {
/**
@@ -40,9 +42,7 @@ if( defined( 'PRETTY_UTF8' ) ) {
* @return string
*/
function pretty( $string ) {
- return trim( preg_replace( '/(.)/use',
- 'sprintf("%04X ", utf8ToCodepoint("$1"))',
- $string ) );
+ return strtoupper( utf8ToHexSequence( $string ) );
}
}
@@ -54,10 +54,6 @@ require_once 'UtfNormalDefines.php';
require_once 'UtfNormalUtil.php';
require_once 'UtfNormal.php';
-if( php_sapi_name() != 'cli' ) {
- die( "Run me from the command line please.\n" );
-}
-
$in = fopen("NormalizationTest.txt", "rt");
if( !$in ) {
print "Couldn't open NormalizationTest.txt -- can't run tests.\n";
diff --git a/includes/normal/UtfNormalTest2.php b/includes/normal/UtfNormalTest2.php
index 691bfaa7..750c0099 100644
--- a/includes/normal/UtfNormalTest2.php
+++ b/includes/normal/UtfNormalTest2.php
@@ -1,4 +1,4 @@
-#!/usr/bin/php
+#!/usr/bin/env php
<?php
/**
* Other tests for the unicode normalization module.
@@ -22,7 +22,7 @@
* @ingroup UtfNormal
*/
-if( php_sapi_name() != 'cli' ) {
+if( PHP_SAPI != 'cli' ) {
die( "Run me from the command line please.\n" );
}
@@ -65,7 +65,7 @@ $f = fopen($file, "r");
later and slow down the runtime.
*/
-require_once("./UtfNormal.php");
+require_once './UtfNormal.php';
function normalize_form_c($c) { return UtfNormal::toNFC($c); }
function normalize_form_d($c) { return UtfNormal::toNFD($c); }
function normalize_form_kc($c) { return UtfNormal::toNFKC($c); }
diff --git a/includes/normal/UtfNormalUtil.php b/includes/normal/UtfNormalUtil.php
index bfad7095..e8fec936 100644
--- a/includes/normal/UtfNormalUtil.php
+++ b/includes/normal/UtfNormalUtil.php
@@ -71,14 +71,16 @@ function hexSequenceToUtf8( $sequence ) {
* Take a UTF-8 string and return a space-separated series of hex
* numbers representing Unicode code points. For debugging.
*
- * @param $str String: UTF-8 string.
+ * @param string $str UTF-8 string.
* @return string
* @private
*/
function utf8ToHexSequence( $str ) {
- return rtrim( preg_replace( '/(.)/uSe',
- 'sprintf("%04x ", utf8ToCodepoint("$1"))',
- $str ) );
+ $buf = '';
+ foreach ( preg_split( '//u', $str, -1, PREG_SPLIT_NO_EMPTY ) as $cp ) {
+ $buf .= sprintf( '%04x ', utf8ToCodepoint( $cp ) );
+ }
+ return rtrim( $buf );
}
/**
@@ -114,7 +116,7 @@ function utf8ToCodepoint( $char ) {
$z >>= $length;
# Add in the free bits from subsequent bytes
- for ( $i=1; $i<$length; $i++ ) {
+ for ( $i=1; $i < $length; $i++ ) {
$z <<= 6;
$z |= ord( $char[$i] ) & 0x3f;
}
@@ -125,7 +127,7 @@ function utf8ToCodepoint( $char ) {
/**
* Escape a string for inclusion in a PHP single-quoted string literal.
*
- * @param $string String: string to be escaped.
+ * @param string $string string to be escaped.
* @return String: escaped string.
* @public
*/
diff --git a/includes/objectcache/APCBagOStuff.php b/includes/objectcache/APCBagOStuff.php
index 1a0de218..3fb80835 100644
--- a/includes/objectcache/APCBagOStuff.php
+++ b/includes/objectcache/APCBagOStuff.php
@@ -29,11 +29,14 @@
class APCBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] int
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$val = apc_fetch( $key );
+ $casToken = $val;
+
if ( is_string( $val ) ) {
if ( $this->isInteger( $val ) ) {
$val = intval( $val );
@@ -62,6 +65,18 @@ class APCBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ // APC's CAS functions only work on integers
+ throw new MWException( "CAS is not implemented in " . __CLASS__ );
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
@@ -72,6 +87,17 @@ class APCBagOStuff extends BagOStuff {
return true;
}
+ /**
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
+ }
+
public function incr( $key, $value = 1 ) {
return apc_inc( $key, $value );
}
@@ -79,19 +105,4 @@ class APCBagOStuff extends BagOStuff {
public function decr( $key, $value = 1 ) {
return apc_dec( $key, $value );
}
-
- /**
- * @return Array
- */
- public function keys() {
- $info = apc_cache_info( 'user' );
- $list = $info['cache_list'];
- $keys = array();
-
- foreach ( $list as $entry ) {
- $keys[] = $entry['info'];
- }
-
- return $keys;
- }
}
diff --git a/includes/objectcache/BagOStuff.php b/includes/objectcache/BagOStuff.php
index 7bbaff93..857943ee 100644
--- a/includes/objectcache/BagOStuff.php
+++ b/includes/objectcache/BagOStuff.php
@@ -56,58 +56,153 @@ abstract class BagOStuff {
/**
* Get an item with the given key. Returns false if it does not exist.
* @param $key string
+ * @param $casToken[optional] mixed
* @return mixed Returns false on failure
*/
- abstract public function get( $key );
+ abstract public function get( $key, &$casToken = null );
/**
* Set an item.
* @param $key string
* @param $value mixed
- * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @return bool success
*/
abstract public function set( $key, $value, $exptime = 0 );
/**
+ * Check and set an item.
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @return bool success
+ */
+ abstract public function cas( $casToken, $key, $value, $exptime = 0 );
+
+ /**
* Delete an item.
* @param $key string
- * @param $time int Amount of time to delay the operation (mostly memcached-specific)
+ * @param int $time Amount of time to delay the operation (mostly memcached-specific)
* @return bool True if the item was deleted or not found, false on failure
*/
abstract public function delete( $key, $time = 0 );
/**
+ * Merge changes into the existing cache value (possibly creating a new one).
+ * The callback function returns the new value given the current value (possibly false),
+ * and takes the arguments: (this BagOStuff object, cache key, current value).
+ *
* @param $key string
- * @param $timeout integer
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
* @return bool success
*/
- public function lock( $key, $timeout = 0 ) {
- /* stub */
- return true;
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->mergeViaCas( $key, $callback, $exptime, $attempts );
}
/**
+ * @see BagOStuff::merge()
+ *
* @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
* @return bool success
*/
- public function unlock( $key ) {
- /* stub */
- return true;
+ protected function mergeViaCas( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ do {
+ $casToken = null; // passed by reference
+ $currentValue = $this->get( $key, $casToken ); // get the old value
+ $value = $callback( $this, $key, $currentValue ); // derive the new value
+
+ if ( $value === false ) {
+ $success = true; // do nothing
+ } elseif ( $currentValue === false ) {
+ // Try to create the key, failing if it gets created in the meantime
+ $success = $this->add( $key, $value, $exptime );
+ } else {
+ // Try to update the key, failing if it gets changed in the meantime
+ $success = $this->cas( $casToken, $key, $value, $exptime );
+ }
+ } while ( !$success && --$attempts );
+
+ return $success;
}
/**
- * @todo: what is this?
- * @return Array
+ * @see BagOStuff::merge()
+ *
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @return bool success
*/
- public function keys() {
- /* stub */
- return array();
+ protected function mergeViaLock( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ if ( !$this->lock( $key, 60 ) ) {
+ return false;
+ }
+
+ $currentValue = $this->get( $key ); // get the old value
+ $value = $callback( $this, $key, $currentValue ); // derive the new value
+
+ if ( $value === false ) {
+ $success = true; // do nothing
+ } else {
+ $success = $this->set( $key, $value, $exptime ); // set the new value
+ }
+
+ if ( !$this->unlock( $key ) ) {
+ // this should never happen
+ trigger_error( "Could not release lock for key '$key'." );
+ }
+
+ return $success;
+ }
+
+ /**
+ * @param $key string
+ * @param $timeout integer [optional]
+ * @return bool success
+ */
+ public function lock( $key, $timeout = 60 ) {
+ $timestamp = microtime( true ); // starting UNIX timestamp
+ if ( $this->add( "{$key}:lock", 1, $timeout ) ) {
+ return true;
+ }
+
+ $uRTT = ceil( 1e6 * ( microtime( true ) - $timestamp ) ); // estimate RTT (us)
+ $sleep = 2 * $uRTT; // rough time to do get()+set()
+
+ $locked = false; // lock acquired
+ $attempts = 0; // failed attempts
+ do {
+ if ( ++$attempts >= 3 && $sleep <= 1e6 ) {
+ // Exponentially back off after failed attempts to avoid network spam.
+ // About 2*$uRTT*(2^n-1) us of "sleep" happen for the next n attempts.
+ $sleep *= 2;
+ }
+ usleep( $sleep ); // back off
+ $locked = $this->add( "{$key}:lock", 1, $timeout );
+ } while ( !$locked );
+
+ return $locked;
+ }
+
+ /**
+ * @param $key string
+ * @return bool success
+ */
+ public function unlock( $key ) {
+ return $this->delete( "{$key}:lock" );
}
/**
* Delete all objects expiring before a certain date.
- * @param $date string The reference date in MW format
+ * @param string $date The reference date in MW format
* @param $progressCallback callback|bool Optional, a function which will be called
* regularly during long-running operations with the percentage progress
* as the first parameter.
@@ -123,7 +218,7 @@ abstract class BagOStuff {
/**
* Get an associative array containing the item for each of the keys that have items.
- * @param $keys Array List of strings
+ * @param array $keys List of strings
* @return Array
*/
public function getMulti( array $keys ) {
@@ -165,7 +260,7 @@ abstract class BagOStuff {
/**
* Increase stored value of $key by $value while preserving its TTL
- * @param $key String: Key to increase
+ * @param string $key Key to increase
* @param $value Integer: Value to add to $key (Default 1)
* @return integer|bool New value or false on failure
*/
diff --git a/includes/objectcache/DBABagOStuff.php b/includes/objectcache/DBABagOStuff.php
index 36ced496..c82b3aa4 100644
--- a/includes/objectcache/DBABagOStuff.php
+++ b/includes/objectcache/DBABagOStuff.php
@@ -111,9 +111,10 @@ class DBABagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__ . "($key)\n" );
@@ -138,7 +139,10 @@ class DBABagOStuff extends BagOStuff {
$val = false;
}
+ $casToken = $val;
+
wfProfileOut( __METHOD__ );
+
return $val;
}
@@ -168,6 +172,42 @@ class DBABagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__ . "($key)\n" );
+
+ $blob = $this->encode( $value, $exptime );
+
+ $handle = $this->getWriter();
+ if ( !$handle ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ // DBA is locked to any other write connection, so we can safely
+ // compare the current & previous value before saving new value
+ $val = dba_fetch( $key, $handle );
+ list( $val, $exptime ) = $this->decode( $val );
+ if ( $casToken !== $val ) {
+ dba_close( $handle );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $ret = dba_replace( $key, $blob, $handle );
+ dba_close( $handle );
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
@@ -211,7 +251,7 @@ class DBABagOStuff extends BagOStuff {
# Insert failed, check to see if it failed due to an expired key
if ( !$ret ) {
- list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) );
+ list( , $expiry ) = $this->decode( dba_fetch( $key, $handle ) );
if ( $expiry && $expiry < time() ) {
# Yes expired, delete and try again
@@ -264,23 +304,4 @@ class DBABagOStuff extends BagOStuff {
return ( $value === false ) ? false : (int)$value;
}
-
- function keys() {
- $reader = $this->getReader();
- $k1 = dba_firstkey( $reader );
-
- if ( !$k1 ) {
- return array();
- }
-
- $result[] = $k1;
-
- $key = dba_nextkey( $reader );
- while ( $key ) {
- $result[] = $key;
- $key = dba_nextkey( $reader );
- }
-
- return $result;
- }
}
diff --git a/includes/objectcache/EhcacheBagOStuff.php b/includes/objectcache/EhcacheBagOStuff.php
index f86cf157..960668f5 100644
--- a/includes/objectcache/EhcacheBagOStuff.php
+++ b/includes/objectcache/EhcacheBagOStuff.php
@@ -28,27 +28,28 @@
* @ingroup Cache
*/
class EhcacheBagOStuff extends BagOStuff {
- var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions,
+ var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions,
$requestData, $requestDataPos;
-
+
var $curls = array();
/**
* @param $params array
+ * @throws MWException
*/
function __construct( $params ) {
if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) {
- throw new MWException( __CLASS__.' requires curl version 7.16.2 or later.' );
+ throw new MWException( __CLASS__ . ' requires curl version 7.16.2 or later.' );
}
if ( !extension_loaded( 'zlib' ) ) {
- throw new MWException( __CLASS__.' requires the zlib extension' );
+ throw new MWException( __CLASS__ . ' requires the zlib extension' );
}
if ( !isset( $params['servers'] ) ) {
- throw new MWException( __METHOD__.': servers parameter is required' );
+ throw new MWException( __METHOD__ . ': servers parameter is required' );
}
$this->servers = $params['servers'];
$this->cacheName = isset( $params['cache'] ) ? $params['cache'] : 'mw';
- $this->connectTimeout = isset( $params['connectTimeout'] )
+ $this->connectTimeout = isset( $params['connectTimeout'] )
? $params['connectTimeout'] : 1;
$this->timeout = isset( $params['timeout'] ) ? $params['timeout'] : 1;
$this->curlOptions = array(
@@ -64,9 +65,10 @@ class EhcacheBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return bool|mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
wfProfileIn( __METHOD__ );
$response = $this->doItemRequest( $key );
if ( !$response || $response['http_code'] == 404 ) {
@@ -74,16 +76,16 @@ class EhcacheBagOStuff extends BagOStuff {
return false;
}
if ( $response['http_code'] >= 300 ) {
- wfDebug( __METHOD__.": GET failure, got HTTP {$response['http_code']}\n" );
+ wfDebug( __METHOD__ . ": GET failure, got HTTP {$response['http_code']}\n" );
wfProfileOut( __METHOD__ );
- return false;
+ return false;
}
$body = $response['body'];
$type = $response['content_type'];
if ( $type == 'application/vnd.php.serialized+deflate' ) {
$body = gzinflate( $body );
if ( !$body ) {
- wfDebug( __METHOD__.": error inflating $key\n" );
+ wfDebug( __METHOD__ . ": error inflating $key\n" );
wfProfileOut( __METHOD__ );
return false;
}
@@ -91,11 +93,13 @@ class EhcacheBagOStuff extends BagOStuff {
} elseif ( $type == 'application/vnd.php.serialized' ) {
$data = unserialize( $body );
} else {
- wfDebug( __METHOD__.": unknown content type \"$type\"\n" );
+ wfDebug( __METHOD__ . ": unknown content type \"$type\"\n" );
wfProfileOut( __METHOD__ );
return false;
}
+ $casToken = $body;
+
wfProfileOut( __METHOD__ );
return $data;
}
@@ -123,7 +127,7 @@ class EhcacheBagOStuff extends BagOStuff {
if ( $code == 404 ) {
// Maybe the cache does not exist yet, let's try creating it
if ( !$this->createCache( $key ) ) {
- wfDebug( __METHOD__.": cache creation failed\n" );
+ wfDebug( __METHOD__ . ": cache creation failed\n" );
wfProfileOut( __METHOD__ );
return false;
}
@@ -132,9 +136,9 @@ class EhcacheBagOStuff extends BagOStuff {
$result = false;
if ( !$code ) {
- wfDebug( __METHOD__.": PUT failure for key $key\n" );
+ wfDebug( __METHOD__ . ": PUT failure for key $key\n" );
} elseif ( $code >= 300 ) {
- wfDebug( __METHOD__.": PUT failure for key $key: HTTP $code\n" );
+ wfDebug( __METHOD__ . ": PUT failure for key $key: HTTP $code\n" );
} else {
$result = true;
}
@@ -144,6 +148,20 @@ class EhcacheBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ // Not sure if we can implement CAS for ehcache. There appears to be CAS-support per
+ // http://ehcache.org/documentation/get-started/consistency-options#cas-cache-operations,
+ // but I can't find any docs for our current implementation.
+ throw new MWException( "CAS is not implemented in " . __CLASS__ );
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
@@ -154,7 +172,7 @@ class EhcacheBagOStuff extends BagOStuff {
array( CURLOPT_CUSTOMREQUEST => 'DELETE' ) );
$code = isset( $response['http_code'] ) ? $response['http_code'] : 0;
if ( !$response || ( $code != 404 && $code >= 300 ) ) {
- wfDebug( __METHOD__.": DELETE failure for key $key\n" );
+ wfDebug( __METHOD__ . ": DELETE failure for key $key\n" );
$result = false;
} else {
$result = true;
@@ -164,6 +182,14 @@ class EhcacheBagOStuff extends BagOStuff {
}
/**
+ * @see BagOStuff::merge()
+ * @return bool success
+ */
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
+ }
+
+ /**
* @param $key string
* @return string
*/
@@ -202,9 +228,9 @@ class EhcacheBagOStuff extends BagOStuff {
* @return int
*/
protected function attemptPut( $key, $data, $type, $ttl ) {
- // In initial benchmarking, it was 30 times faster to use CURLOPT_POST
+ // In initial benchmarking, it was 30 times faster to use CURLOPT_POST
// than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because
- // CURLOPT_UPLOAD was pushing the request headers first, then waiting
+ // CURLOPT_UPLOAD was pushing the request headers first, then waiting
// for an ACK packet, then sending the data, whereas CURLOPT_POST just
// sends the headers and the data in a single send().
$response = $this->doItemRequest( $key,
@@ -230,15 +256,15 @@ class EhcacheBagOStuff extends BagOStuff {
* @return bool
*/
protected function createCache( $key ) {
- wfDebug( __METHOD__.": creating cache for $key\n" );
- $response = $this->doCacheRequest( $key,
+ wfDebug( __METHOD__ . ": creating cache for $key\n" );
+ $response = $this->doCacheRequest( $key,
array(
CURLOPT_POST => 1,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => '',
) );
if ( !$response ) {
- wfDebug( __CLASS__.": failed to create cache for $key\n" );
+ wfDebug( __CLASS__ . ": failed to create cache for $key\n" );
return false;
}
return ( $response['http_code'] == 201 /* created */
@@ -278,8 +304,8 @@ class EhcacheBagOStuff extends BagOStuff {
protected function doRequest( $curl, $url, $curlOptions = array() ) {
if ( array_diff_key( $curlOptions, $this->curlOptions ) ) {
// var_dump( array_diff_key( $curlOptions, $this->curlOptions ) );
- throw new MWException( __METHOD__.": to prevent options set in one doRequest() " .
- "call from affecting subsequent doRequest() calls, only options listed " .
+ throw new MWException( __METHOD__ . ": to prevent options set in one doRequest() " .
+ "call from affecting subsequent doRequest() calls, only options listed " .
"in \$this->curlOptions may be specified in the \$curlOptions parameter." );
}
$curlOptions += $this->curlOptions;
@@ -288,7 +314,7 @@ class EhcacheBagOStuff extends BagOStuff {
curl_setopt_array( $curl, $curlOptions );
$result = curl_exec( $curl );
if ( $result === false ) {
- wfDebug( __CLASS__.": curl error: " . curl_error( $curl ) . "\n" );
+ wfDebug( __CLASS__ . ": curl error: " . curl_error( $curl ) . "\n" );
return false;
}
$info = curl_getinfo( $curl );
diff --git a/includes/objectcache/EmptyBagOStuff.php b/includes/objectcache/EmptyBagOStuff.php
index bd28b241..62060579 100644
--- a/includes/objectcache/EmptyBagOStuff.php
+++ b/includes/objectcache/EmptyBagOStuff.php
@@ -30,9 +30,10 @@ class EmptyBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return bool
*/
- function get( $key ) {
+ function get( $key, &$casToken = null ) {
return false;
}
@@ -47,6 +48,17 @@ class EmptyBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exp int
+ * @return bool
+ */
+ function cas( $casToken, $key, $value, $exp = 0 ) {
+ return true;
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
@@ -54,6 +66,17 @@ class EmptyBagOStuff extends BagOStuff {
function delete( $key, $time = 0 ) {
return true;
}
+
+ /**
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return true;
+ }
}
/**
diff --git a/includes/objectcache/HashBagOStuff.php b/includes/objectcache/HashBagOStuff.php
index 799f26a3..d061eff0 100644
--- a/includes/objectcache/HashBagOStuff.php
+++ b/includes/objectcache/HashBagOStuff.php
@@ -52,9 +52,10 @@ class HashBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return bool|mixed
*/
- function get( $key ) {
+ function get( $key, &$casToken = null ) {
if ( !isset( $this->bag[$key] ) ) {
return false;
}
@@ -63,6 +64,8 @@ class HashBagOStuff extends BagOStuff {
return false;
}
+ $casToken = $this->bag[$key][0];
+
return $this->bag[$key][0];
}
@@ -78,6 +81,21 @@ class HashBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ function cas( $casToken, $key, $value, $exptime = 0 ) {
+ if ( $this->get( $key ) === $casToken ) {
+ return $this->set( $key, $value, $exptime );
+ }
+
+ return false;
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
@@ -91,12 +109,4 @@ class HashBagOStuff extends BagOStuff {
return true;
}
-
- /**
- * @return array
- */
- function keys() {
- return array_keys( $this->bag );
- }
}
-
diff --git a/includes/objectcache/MemcachedBagOStuff.php b/includes/objectcache/MemcachedBagOStuff.php
index 813c2727..f1644edb 100644
--- a/includes/objectcache/MemcachedBagOStuff.php
+++ b/includes/objectcache/MemcachedBagOStuff.php
@@ -43,7 +43,7 @@ class MemcachedBagOStuff extends BagOStuff {
if ( !isset( $params['persistent'] ) ) {
$params['persistent'] = $GLOBALS['wgMemCachedPersistent'];
}
- if ( !isset( $params['compress_threshold'] ) ) {
+ if ( !isset( $params['compress_threshold'] ) ) {
$params['compress_threshold'] = 1500;
}
if ( !isset( $params['timeout'] ) ) {
@@ -57,10 +57,11 @@ class MemcachedBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return Mixed
*/
- public function get( $key ) {
- return $this->client->get( $this->encodeKey( $key ) );
+ public function get( $key, &$casToken = null ) {
+ return $this->client->get( $this->encodeKey( $key ), $casToken );
}
/**
@@ -76,6 +77,18 @@ class MemcachedBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken mixed
+ * @param $value
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ return $this->client->cas( $casToken, $this->encodeKey( $key ),
+ $value, $this->fixExpiry( $exptime ) );
+ }
+
+ /**
+ * @param $key string
* @param $time int
* @return bool
*/
@@ -86,7 +99,7 @@ class MemcachedBagOStuff extends BagOStuff {
/**
* @param $key string
* @param $value int
- * @param $exptime int (default 0)
+ * @param int $exptime (default 0)
* @return Mixed
*/
public function add( $key, $value, $exptime = 0 ) {
@@ -101,7 +114,7 @@ class MemcachedBagOStuff extends BagOStuff {
* @return Mixed
*/
public function replace( $key, $value, $exptime = 0 ) {
- return $this->client->replace( $this->encodeKey( $key ), $value,
+ return $this->client->replace( $this->encodeKey( $key ), $value,
$this->fixExpiry( $exptime ) );
}
@@ -166,15 +179,9 @@ class MemcachedBagOStuff extends BagOStuff {
* Send a debug message to the log
*/
protected function debugLog( $text ) {
- global $wgDebugLogGroups;
- if( !isset( $wgDebugLogGroups['memcached'] ) ) {
- # Prefix message since it will end up in main debug log file
- $text = "memcached: $text";
- }
if ( substr( $text, -1 ) !== "\n" ) {
$text .= "\n";
}
wfDebugLog( 'memcached', $text );
}
}
-
diff --git a/includes/objectcache/MemcachedClient.php b/includes/objectcache/MemcachedClient.php
index 536ba6ea..e5f60b55 100644
--- a/includes/objectcache/MemcachedClient.php
+++ b/includes/objectcache/MemcachedClient.php
@@ -55,9 +55,9 @@
* 'compress_threshold' => 10240,
* 'persistent' => true));
*
- * $mc->add('key', array('some', 'array'));
- * $mc->replace('key', 'some random string');
- * $val = $mc->get('key');
+ * $mc->add( 'key', array( 'some', 'array' ) );
+ * $mc->replace( 'key', 'some random string' );
+ * $val = $mc->get( 'key' );
*
* @author Ryan T. Dean <rtdean@cytherianage.net>
* @version 0.1.2
@@ -99,7 +99,6 @@ class MWMemcached {
// }}}
-
/**
* Command statistics
*
@@ -242,7 +241,7 @@ class MWMemcached {
/**
* Memcache initializer
*
- * @param $args Array Associative array of settings
+ * @param array $args Associative array of settings
*
* @return mixed
*/
@@ -272,12 +271,12 @@ class MWMemcached {
* Adds a key/value to the memcache server if one isn't already set with
* that key
*
- * @param $key String: key to set with data
+ * @param string $key key to set with data
* @param $val Mixed: value to store
* @param $exp Integer: (optional) Expiration time. This can be a number of seconds
* to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
* longer must be the timestamp of the time at which the mapping should expire. It
- * is safe to use timestamps in all cases, regardless of exipration
+ * is safe to use timestamps in all cases, regardless of expiration
* eg: strtotime("+3 hour")
*
* @return Boolean
@@ -292,7 +291,7 @@ class MWMemcached {
/**
* Decrease a value stored on the memcache server
*
- * @param $key String: key to decrease
+ * @param string $key key to decrease
* @param $amt Integer: (optional) amount to decrease
*
* @return Mixed: FALSE on failure, value on success
@@ -307,7 +306,7 @@ class MWMemcached {
/**
* Deletes a key from the server, optionally after $time
*
- * @param $key String: key to delete
+ * @param string $key key to delete
* @param $time Integer: (optional) how long to wait before deleting
*
* @return Boolean: TRUE on success, FALSE on failure
@@ -330,7 +329,7 @@ class MWMemcached {
$this->stats['delete'] = 1;
}
$cmd = "delete $key $time\r\n";
- if( !$this->_fwrite( $sock, $cmd ) ) {
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
return false;
}
$res = $this->_fgets( $sock );
@@ -407,11 +406,12 @@ class MWMemcached {
/**
* Retrieves the value associated with the key from the memcache server
*
- * @param $key array|string key to retrieve
+ * @param array|string $key key to retrieve
+ * @param $casToken[optional] Float
*
* @return Mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
wfProfileIn( __METHOD__ );
if ( $this->_debug ) {
@@ -437,14 +437,14 @@ class MWMemcached {
$this->stats['get'] = 1;
}
- $cmd = "get $key\r\n";
+ $cmd = "gets $key\r\n";
if ( !$this->_fwrite( $sock, $cmd ) ) {
wfProfileOut( __METHOD__ );
return false;
}
$val = array();
- $this->_load_items( $sock, $val );
+ $this->_load_items( $sock, $val, $casToken );
if ( $this->_debug ) {
foreach ( $val as $k => $v ) {
@@ -466,7 +466,7 @@ class MWMemcached {
/**
* Get multiple keys from the server(s)
*
- * @param $keys Array: keys to retrieve
+ * @param array $keys keys to retrieve
*
* @return Array
*/
@@ -489,17 +489,17 @@ class MWMemcached {
}
$key = is_array( $key ) ? $key[1] : $key;
if ( !isset( $sock_keys[$sock] ) ) {
- $sock_keys[ intval( $sock ) ] = array();
+ $sock_keys[intval( $sock )] = array();
$socks[] = $sock;
}
- $sock_keys[ intval( $sock ) ][] = $key;
+ $sock_keys[intval( $sock )][] = $key;
}
$gather = array();
// Send out the requests
foreach ( $socks as $sock ) {
- $cmd = 'get';
- foreach ( $sock_keys[ intval( $sock ) ] as $key ) {
+ $cmd = 'gets';
+ foreach ( $sock_keys[intval( $sock )] as $key ) {
$cmd .= ' ' . $key;
}
$cmd .= "\r\n";
@@ -512,7 +512,7 @@ class MWMemcached {
// Parse responses
$val = array();
foreach ( $gather as $sock ) {
- $this->_load_items( $sock, $val );
+ $this->_load_items( $sock, $val, $casToken );
}
if ( $this->_debug ) {
@@ -530,7 +530,7 @@ class MWMemcached {
/**
* Increments $key (optionally) by $amt
*
- * @param $key String: key to increment
+ * @param string $key key to increment
* @param $amt Integer: (optional) amount to increment
*
* @return Integer: null if the key does not exist yet (this does NOT
@@ -547,7 +547,7 @@ class MWMemcached {
/**
* Overwrites an existing value for key; only works if key is already set
*
- * @param $key String: key to set value as
+ * @param string $key key to set value as
* @param $value Mixed: value to store
* @param $exp Integer: (optional) Expiration time. This can be a number of seconds
* to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
@@ -569,7 +569,7 @@ class MWMemcached {
* output as an array (null array if no output)
*
* @param $sock Resource: socket to send command on
- * @param $cmd String: command to run
+ * @param string $cmd command to run
*
* @return Array: output array
*/
@@ -603,7 +603,7 @@ class MWMemcached {
* Unconditionally sets a key to a given value in the memcache. Returns true
* if set successfully.
*
- * @param $key String: key to set value as
+ * @param string $key key to set value as
* @param $value Mixed: value to set
* @param $exp Integer: (optional) Expiration time. This can be a number of seconds
* to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
@@ -618,6 +618,28 @@ class MWMemcached {
}
// }}}
+ // {{{ cas()
+
+ /**
+ * Sets a key to a given value in the memcache if the current value still corresponds
+ * to a known, given value. Returns true if set successfully.
+ *
+ * @param $casToken Float: current known value
+ * @param string $key key to set value as
+ * @param $value Mixed: value to set
+ * @param $exp Integer: (optional) Expiration time. This can be a number of seconds
+ * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
+ * longer must be the timestamp of the time at which the mapping should expire. It
+ * is safe to use timestamps in all cases, regardless of exipration
+ * eg: strtotime("+3 hour")
+ *
+ * @return Boolean: TRUE on success
+ */
+ public function cas( $casToken, $key, $value, $exp = 0 ) {
+ return $this->_set( 'cas', $key, $value, $exp, $casToken );
+ }
+
+ // }}}
// {{{ set_compress_threshold()
/**
@@ -649,7 +671,7 @@ class MWMemcached {
/**
* Sets the server list to distribute key gets and puts between
*
- * @param $list Array of servers to connect to
+ * @param array $list of servers to connect to
*
* @see MWMemcached::__construct()
*/
@@ -684,7 +706,7 @@ class MWMemcached {
/**
* Close the specified socket
*
- * @param $sock String: socket to close
+ * @param string $sock socket to close
*
* @access private
*/
@@ -701,7 +723,7 @@ class MWMemcached {
* Connects $sock to $host, timing out after $timeout
*
* @param $sock Integer: socket to connect
- * @param $host String: Host:IP to connect to
+ * @param string $host Host:IP to connect to
*
* @return boolean
* @access private
@@ -711,7 +733,7 @@ class MWMemcached {
$sock = false;
$timeout = $this->_connect_timeout;
$errno = $errstr = null;
- for( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
+ for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
wfSuppressWarnings();
if ( $this->_persistent == 1 ) {
$sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
@@ -743,7 +765,7 @@ class MWMemcached {
/**
* Marks a host as dead until 30-40 seconds in the future
*
- * @param $sock String: socket to mark as dead
+ * @param string $sock socket to mark as dead
*
* @access private
*/
@@ -769,7 +791,7 @@ class MWMemcached {
/**
* get_sock
*
- * @param $key String: key to retrieve value for;
+ * @param string $key key to retrieve value for;
*
* @return Mixed: resource on success, false on failure
* @access private
@@ -788,7 +810,7 @@ class MWMemcached {
$bu = array();
foreach ( $this->_servers as $v ) {
if ( is_array( $v ) ) {
- for( $i = 0; $i < $v[1]; $i++ ) {
+ for ( $i = 0; $i < $v[1]; $i++ ) {
$bu[] = $v[0];
}
} else {
@@ -800,7 +822,7 @@ class MWMemcached {
}
$realkey = is_array( $key ) ? $key[1] : $key;
- for( $tries = 0; $tries < 20; $tries++ ) {
+ for ( $tries = 0; $tries < 20; $tries++ ) {
$host = $this->_buckets[$hv % $this->_bucketcount];
$sock = $this->sock_to_host( $host );
if ( is_resource( $sock ) ) {
@@ -818,7 +840,7 @@ class MWMemcached {
/**
* Creates a hash integer based on the $key
*
- * @param $key String: key to hash
+ * @param string $key key to hash
*
* @return Integer: hash value
* @access private
@@ -836,8 +858,8 @@ class MWMemcached {
/**
* Perform increment/decriment on $key
*
- * @param $cmd String command to perform
- * @param $key String|array key to perform it on
+ * @param string $cmd command to perform
+ * @param string|array $key key to perform it on
* @param $amt Integer amount to adjust
*
* @return Integer: new value of $key
@@ -878,40 +900,78 @@ class MWMemcached {
* Load items into $ret from $sock
*
* @param $sock Resource: socket to read from
- * @param $ret Array: returned values
+ * @param array $ret returned values
+ * @param $casToken[optional] Float
* @return boolean True for success, false for failure
*
* @access private
*/
- function _load_items( $sock, &$ret ) {
+ function _load_items( $sock, &$ret, &$casToken = null ) {
+ $results = array();
+
while ( 1 ) {
$decl = $this->_fgets( $sock );
- if( $decl === false ) {
+
+ if ( $decl === false ) {
+ /*
+ * If nothing can be read, something is wrong because we know exactly when
+ * to stop reading (right after "END") and we return right after that.
+ */
return false;
+ } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
+ /*
+ * Read all data returned. This can be either one or multiple values.
+ * Save all that data (in an array) to be processed later: we'll first
+ * want to continue reading until "END" before doing anything else,
+ * to make sure that we don't leave our client in a state where it's
+ * output is not yet fully read.
+ */
+ $results[] = array(
+ $match[1], // rkey
+ $match[2], // flags
+ $match[3], // len
+ $match[4], // casToken
+ $this->_fread( $sock, $match[3] + 2 ), // data
+ );
} elseif ( $decl == "END" ) {
- return true;
- } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)$/', $decl, $match ) ) {
- list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
- $data = $this->_fread( $sock, $len + 2 );
- if ( $data === false ) {
+ if ( count( $results ) == 0 ) {
return false;
}
- if ( substr( $data, -2 ) !== "\r\n" ) {
- $this->_handle_error( $sock,
- 'line ending missing from data block from $1' );
- return false;
- }
- $data = substr( $data, 0, -2 );
- $ret[$rkey] = $data;
- if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
- $ret[$rkey] = gzuncompress( $ret[$rkey] );
- }
+ /**
+ * All data has been read, time to process the data and build
+ * meaningful return values.
+ */
+ foreach ( $results as $vars ) {
+ list( $rkey, $flags, $len, $casToken, $data ) = $vars;
+
+ if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
+ $this->_handle_error( $sock,
+ 'line ending missing from data block from $1' );
+ return false;
+ }
+ $data = substr( $data, 0, -2 );
+ $ret[$rkey] = $data;
- if ( $flags & self::SERIALIZED ) {
- $ret[$rkey] = unserialize( $ret[$rkey] );
+ if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
+ $ret[$rkey] = gzuncompress( $ret[$rkey] );
+ }
+
+ /*
+ * This unserialize is the exact reason that we only want to
+ * process data after having read until "END" (instead of doing
+ * this right away): "unserialize" can trigger outside code:
+ * in the event that $ret[$rkey] is a serialized object,
+ * unserializing it will trigger __wakeup() if present. If that
+ * function attempted to read from memcached (while we did not
+ * yet read "END"), these 2 calls would collide.
+ */
+ if ( $flags & self::SERIALIZED ) {
+ $ret[$rkey] = unserialize( $ret[$rkey] );
+ }
}
+ return true;
} else {
$this->_handle_error( $sock, 'Error parsing response from $1' );
return false;
@@ -925,19 +985,20 @@ class MWMemcached {
/**
* Performs the requested storage operation to the memcache server
*
- * @param $cmd String: command to perform
- * @param $key String: key to act on
+ * @param string $cmd command to perform
+ * @param string $key key to act on
* @param $val Mixed: what we need to store
* @param $exp Integer: (optional) Expiration time. This can be a number of seconds
* to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
* longer must be the timestamp of the time at which the mapping should expire. It
* is safe to use timestamps in all cases, regardless of exipration
* eg: strtotime("+3 hour")
+ * @param $casToken[optional] Float
*
* @return Boolean
* @access private
*/
- function _set( $cmd, $key, $val, $exp ) {
+ function _set( $cmd, $key, $val, $exp, $casToken = null ) {
if ( !$this->_active ) {
return false;
}
@@ -966,7 +1027,7 @@ class MWMemcached {
$len = strlen( $val );
if ( $this->_have_zlib && $this->_compress_enable &&
- $this->_compress_threshold && $len >= $this->_compress_threshold )
+ $this->_compress_threshold && $len >= $this->_compress_threshold )
{
$c_val = gzcompress( $val, 9 );
$c_len = strlen( $c_val );
@@ -980,7 +1041,13 @@ class MWMemcached {
$flags |= self::COMPRESSED;
}
}
- if ( !$this->_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) {
+
+ $command = "$cmd $key $flags $exp $len";
+ if ( $casToken ) {
+ $command .= " $casToken";
+ }
+
+ if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
return false;
}
@@ -1001,7 +1068,7 @@ class MWMemcached {
/**
* Returns the socket for the host
*
- * @param $host String: Host:IP to get socket for
+ * @param string $host Host:IP to get socket for
*
* @return Mixed: IO Stream or false
* @access private
@@ -1036,11 +1103,6 @@ class MWMemcached {
* @param $text string
*/
function _debugprint( $text ) {
- global $wgDebugLogGroups;
- if( !isset( $wgDebugLogGroups['memcached'] ) ) {
- # Prefix message since it will end up in main debug log file
- $text = "memcached: $text";
- }
wfDebugLog( 'memcached', $text );
}
@@ -1061,7 +1123,7 @@ class MWMemcached {
function _fwrite( $sock, $buf ) {
$bytesWritten = 0;
$bufSize = strlen( $buf );
- while ( $bytesWritten < $bufSize ) {
+ while ( $bytesWritten < $bufSize ) {
$result = fwrite( $sock, $buf );
$data = stream_get_meta_data( $sock );
if ( $data['timed_out'] ) {
@@ -1096,7 +1158,7 @@ class MWMemcached {
}
/**
- * Read the specified number of bytes from a stream. If there is an error,
+ * Read the specified number of bytes from a stream. If there is an error,
* mark the socket dead.
*
* @param $sock The socket
@@ -1137,7 +1199,7 @@ class MWMemcached {
function _fgets( $sock ) {
$result = fgets( $sock );
// fgets() may return a partial line if there is a select timeout after
- // a successful recv(), so we have to check for a timeout even if we
+ // a successful recv(), so we have to check for a timeout even if we
// got a string response.
$data = stream_get_meta_data( $sock );
if ( $data['timed_out'] ) {
@@ -1167,10 +1229,16 @@ class MWMemcached {
if ( !is_resource( $f ) ) {
return;
}
- $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
+ $r = array( $f );
+ $w = null;
+ $e = null;
+ $n = stream_select( $r, $w, $e, 0, 0 );
while ( $n == 1 && !feof( $f ) ) {
fread( $f, 1024 );
- $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
+ $r = array( $f );
+ $w = null;
+ $e = null;
+ $n = stream_select( $r, $w, $e, 0, 0 );
}
}
@@ -1179,7 +1247,6 @@ class MWMemcached {
// }}}
}
-
// }}}
class MemCachedClientforWiki extends MWMemcached {
diff --git a/includes/objectcache/MemcachedPeclBagOStuff.php b/includes/objectcache/MemcachedPeclBagOStuff.php
index 76886ebb..0c3b228f 100644
--- a/includes/objectcache/MemcachedPeclBagOStuff.php
+++ b/includes/objectcache/MemcachedPeclBagOStuff.php
@@ -37,6 +37,8 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
* - compress_threshold: The minimum size an object must be before it is compressed
* - timeout: The read timeout in microseconds
* - connect_timeout: The connect timeout in seconds
+ * - retry_timeout: Time in seconds to wait before retrying a failed connect attempt
+ * - server_failure_limit: Limit for server connect failures before it is removed
* - serializer: May be either "php" or "igbinary". Igbinary produces more compact
* values, but serialization is much slower unless the php.ini option
* igbinary.compact_strings is off.
@@ -47,7 +49,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
if ( $params['persistent'] ) {
// The pool ID must be unique to the server/option combination.
// The Memcached object is essentially shared for each pool ID.
- // We can only resuse a pool ID if we keep the config consistent.
+ // We can only reuse a pool ID if we keep the config consistent.
$this->client = new Memcached( md5( serialize( $params ) ) );
if ( count( $this->client->getServerList() ) ) {
wfDebug( __METHOD__ . ": persistent Memcached object already loaded.\n" );
@@ -61,6 +63,14 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
$params['serializer'] = 'php';
}
+ if ( isset( $params['retry_timeout'] ) ) {
+ $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
+ }
+
+ if ( isset( $params['server_failure_limit'] ) ) {
+ $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
+ }
+
// The compression threshold is an undocumented php.ini option for some
// reason. There's probably not much harm in setting it globally, for
// compatibility with the settings for the PHP client.
@@ -87,13 +97,13 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
break;
case 'igbinary':
if ( !Memcached::HAVE_IGBINARY ) {
- throw new MWException( __CLASS__.': the igbinary extension is not available ' .
+ throw new MWException( __CLASS__ . ': the igbinary extension is not available ' .
'but igbinary serialization was requested.' );
}
$this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
break;
default:
- throw new MWException( __CLASS__.': invalid value for serializer parameter' );
+ throw new MWException( __CLASS__ . ': invalid value for serializer parameter' );
}
$servers = array();
foreach ( $params['servers'] as $host ) {
@@ -104,11 +114,16 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] float
* @return Mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
+ wfProfileIn( __METHOD__ );
$this->debugLog( "get($key)" );
- return $this->checkResult( $key, parent::get( $key ) );
+ $result = $this->client->get( $this->encodeKey( $key ), null, $casToken );
+ $result = $this->checkResult( $key, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
}
/**
@@ -123,6 +138,18 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
}
/**
+ * @param $casToken float
+ * @param $key string
+ * @param $value
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ $this->debugLog( "cas($key)" );
+ return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
@@ -189,7 +216,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
* the client, but some day we might find a case where it should be
* different.
*
- * @param $key string The key used by the caller, or false if there wasn't one.
+ * @param string $key The key used by the caller, or false if there wasn't one.
* @param $result Mixed The return value
* @return Mixed
*/
@@ -224,9 +251,11 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
* @return Array
*/
public function getMulti( array $keys ) {
+ wfProfileIn( __METHOD__ );
$this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
$callback = array( $this, 'encodeKey' );
$result = $this->client->getMulti( array_map( $callback, $keys ) );
+ wfProfileOut( __METHOD__ );
return $this->checkResult( false, $result );
}
diff --git a/includes/objectcache/MemcachedPhpBagOStuff.php b/includes/objectcache/MemcachedPhpBagOStuff.php
index a46dc716..33a134c7 100644
--- a/includes/objectcache/MemcachedPhpBagOStuff.php
+++ b/includes/objectcache/MemcachedPhpBagOStuff.php
@@ -81,7 +81,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
public function unlock( $key ) {
return $this->client->unlock( $this->encodeKey( $key ) );
}
-
+
/**
* @param $key string
* @param $value int
@@ -100,4 +100,3 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
return $this->client->decr( $this->encodeKey( $key ), $value );
}
}
-
diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php
index e496ddd8..e550c0d0 100644
--- a/includes/objectcache/MultiWriteBagOStuff.php
+++ b/includes/objectcache/MultiWriteBagOStuff.php
@@ -22,8 +22,8 @@
*/
/**
- * A cache class that replicates all writes to multiple child caches. Reads
- * are implemented by reading from the caches in the order they are given in
+ * A cache class that replicates all writes to multiple child caches. Reads
+ * are implemented by reading from the caches in the order they are given in
* the configuration until a cache gives a positive result.
*
* @ingroup Cache
@@ -43,7 +43,7 @@ class MultiWriteBagOStuff extends BagOStuff {
*/
public function __construct( $params ) {
if ( !isset( $params['caches'] ) ) {
- throw new MWException( __METHOD__.': the caches parameter is required' );
+ throw new MWException( __METHOD__ . ': the caches parameter is required' );
}
$this->caches = array();
@@ -61,9 +61,10 @@ class MultiWriteBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return bool|mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
foreach ( $this->caches as $cache ) {
$value = $cache->get( $key );
if ( $value !== false ) {
@@ -74,6 +75,17 @@ class MultiWriteBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ throw new MWException( "CAS is not implemented in " . __CLASS__ );
+ }
+
+ /**
* @param $key string
* @param $value mixed
* @param $exptime int
@@ -157,6 +169,17 @@ class MultiWriteBagOStuff extends BagOStuff {
}
/**
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->doWrite( 'merge', $key, $callback, $exptime );
+ }
+
+ /**
* @param $method string
* @return bool
*/
diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php
index 9b360f32..6c1433a9 100644
--- a/includes/objectcache/ObjectCache.php
+++ b/includes/objectcache/ObjectCache.php
@@ -34,7 +34,7 @@ class ObjectCache {
*
* @param $id string
*
- * @return ObjectCache
+ * @return BagOStuff
*/
static function getInstance( $id ) {
if ( isset( self::$instances[$id] ) ) {
@@ -59,7 +59,7 @@ class ObjectCache {
* @param $id string
*
* @throws MWException
- * @return ObjectCache
+ * @return BagOStuff
*/
static function newFromId( $id ) {
global $wgObjectCaches;
@@ -78,7 +78,7 @@ class ObjectCache {
* @param $params array
*
* @throws MWException
- * @return ObjectCache
+ * @return BagOStuff
*/
static function newFromParams( $params ) {
if ( isset( $params['factory'] ) ) {
@@ -102,7 +102,7 @@ class ObjectCache {
* If no cache choice is configured (by default $wgMainCacheType is CACHE_NONE),
* then CACHE_ANYTHING will forward to CACHE_DB.
* @param $params array
- * @return ObjectCache
+ * @return BagOStuff
*/
static function newAnything( $params ) {
global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType;
@@ -120,14 +120,14 @@ class ObjectCache {
*
* @param $params array
* @throws MWException
- * @return ObjectCache
+ * @return BagOStuff
*/
static function newAccelerator( $params ) {
- if ( function_exists( 'apc_fetch') ) {
+ if ( function_exists( 'apc_fetch' ) ) {
$id = 'apc';
- } elseif( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
+ } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
$id = 'xcache';
- } elseif( function_exists( 'wincache_ucache_get' ) ) {
+ } elseif ( function_exists( 'wincache_ucache_get' ) ) {
$id = 'wincache';
} else {
throw new MWException( "CACHE_ACCEL requested but no suitable object " .
@@ -139,9 +139,9 @@ class ObjectCache {
/**
* Factory function that creates a memcached client object.
*
- * This always uses the PHP client, since the PECL client has a different
- * hashing scheme and a different interpretation of the flags bitfield, so
- * switching between the two clients randomly would be disasterous.
+ * This always uses the PHP client, since the PECL client has a different
+ * hashing scheme and a different interpretation of the flags bitfield, so
+ * switching between the two clients randomly would be disastrous.
*
* @param $params array
*
diff --git a/includes/objectcache/ObjectCacheSessionHandler.php b/includes/objectcache/ObjectCacheSessionHandler.php
index f55da94d..7cf960e7 100644
--- a/includes/objectcache/ObjectCacheSessionHandler.php
+++ b/includes/objectcache/ObjectCacheSessionHandler.php
@@ -58,7 +58,7 @@ class ObjectCacheSessionHandler {
/**
* Get a cache key for the given session id.
*
- * @param $id String: session id
+ * @param string $id session id
* @return String: cache key
*/
static function getKey( $id ) {
@@ -89,12 +89,12 @@ class ObjectCacheSessionHandler {
/**
* Callback when reading session data.
*
- * @param $id String: session id
+ * @param string $id session id
* @return Mixed: session data
*/
static function read( $id ) {
$data = self::getCache()->get( self::getKey( $id ) );
- if( $data === false ) {
+ if ( $data === false ) {
return '';
}
return $data;
@@ -103,7 +103,7 @@ class ObjectCacheSessionHandler {
/**
* Callback when writing session data.
*
- * @param $id String: session id
+ * @param string $id session id
* @param $data Mixed: session data
* @return Boolean: success
*/
@@ -116,7 +116,7 @@ class ObjectCacheSessionHandler {
/**
* Callback to destroy a session when calling session_destroy().
*
- * @param $id String: session id
+ * @param string $id session id
* @return Boolean: success
*/
static function destroy( $id ) {
diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php
index c5966cdb..135e0e83 100644
--- a/includes/objectcache/RedisBagOStuff.php
+++ b/includes/objectcache/RedisBagOStuff.php
@@ -20,29 +20,13 @@
* @file
*/
-
class RedisBagOStuff extends BagOStuff {
- protected $connectTimeout, $persistent, $password, $automaticFailover;
-
- /**
- * A list of server names, from $params['servers']
- */
+ /** @var RedisConnectionPool */
+ protected $redisPool;
+ /** @var Array List of server names */
protected $servers;
-
- /**
- * A cache of Redis objects, representing connections to Redis servers.
- * The key is the server name.
- */
- protected $conns = array();
-
- /**
- * An array listing "dead" servers which have had a connection error in
- * the past. Servers are marked dead for a limited period of time, to
- * avoid excessive overhead from repeated connection timeouts. The key in
- * the array is the server name, the value is the UNIX timestamp at which
- * the server is resurrected.
- */
- protected $deadServers = array();
+ /** @var bool */
+ protected $automaticFailover;
/**
* Construct a RedisBagOStuff object. Parameters are:
@@ -71,18 +55,15 @@ class RedisBagOStuff extends BagOStuff {
* flap, for example if it is in swap death.
*/
function __construct( $params ) {
- if ( !extension_loaded( 'redis' ) ) {
- throw new MWException( __CLASS__. ' requires the phpredis extension: ' .
- 'https://github.com/nicolasff/phpredis' );
+ $redisConf = array( 'serializer' => 'php' );
+ foreach ( array( 'connectTimeout', 'persistent', 'password' ) as $opt ) {
+ if ( isset( $params[$opt] ) ) {
+ $redisConf[$opt] = $params[$opt];
+ }
}
+ $this->redisPool = RedisConnectionPool::singleton( $redisConf );
$this->servers = $params['servers'];
- $this->connectTimeout = isset( $params['connectTimeout'] )
- ? $params['connectTimeout'] : 1;
- $this->persistent = !empty( $params['persistent'] );
- if ( isset( $params['password'] ) ) {
- $this->password = $params['password'];
- }
if ( isset( $params['automaticFailover'] ) ) {
$this->automaticFailover = $params['automaticFailover'];
} else {
@@ -90,7 +71,7 @@ class RedisBagOStuff extends BagOStuff {
}
}
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
wfProfileIn( __METHOD__ );
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
@@ -101,8 +82,9 @@ class RedisBagOStuff extends BagOStuff {
$result = $conn->get( $key );
} catch ( RedisException $e ) {
$result = false;
- $this->handleException( $server, $e );
+ $this->handleException( $server, $conn, $e );
}
+ $casToken = $result;
$this->logRequest( 'get', $key, $server, $result );
wfProfileOut( __METHOD__ );
return $result;
@@ -125,7 +107,7 @@ class RedisBagOStuff extends BagOStuff {
}
} catch ( RedisException $e ) {
$result = false;
- $this->handleException( $server, $e );
+ $this->handleException( $server, $conn, $e );
}
$this->logRequest( 'set', $key, $server, $result );
@@ -133,6 +115,48 @@ class RedisBagOStuff extends BagOStuff {
return $result;
}
+ public function cas( $casToken, $key, $value, $expiry = 0 ) {
+ wfProfileIn( __METHOD__ );
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $expiry = $this->convertToRelative( $expiry );
+ try {
+ $conn->watch( $key );
+
+ if ( $this->get( $key ) !== $casToken ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $conn->multi();
+
+ if ( !$expiry ) {
+ // No expiry, that is very different from zero expiry in Redis
+ $conn->set( $key, $value );
+ } else {
+ $conn->setex( $key, $expiry, $value );
+ }
+
+ /*
+ * multi()/exec() (transactional mode) allows multiple values to
+ * be set/get at once and will return an array of results, in
+ * the order they were set/get. In this case, we only set 1
+ * value, which should (in case of success) result in true.
+ */
+ $result = ( $conn->exec() == array( true ) );
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $server, $conn, $e );
+ }
+
+ $this->logRequest( 'cas', $key, $server, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
public function delete( $key, $time = 0 ) {
wfProfileIn( __METHOD__ );
list( $server, $conn ) = $this->getConnection( $key );
@@ -146,7 +170,7 @@ class RedisBagOStuff extends BagOStuff {
$result = true;
} catch ( RedisException $e ) {
$result = false;
- $this->handleException( $server, $e );
+ $this->handleException( $server, $conn, $e );
}
$this->logRequest( 'delete', $key, $server, $result );
wfProfileOut( __METHOD__ );
@@ -184,7 +208,7 @@ class RedisBagOStuff extends BagOStuff {
}
}
} catch ( RedisException $e ) {
- $this->handleException( $server, $e );
+ $this->handleException( $server, $conn, $e );
}
}
@@ -209,7 +233,7 @@ class RedisBagOStuff extends BagOStuff {
}
} catch ( RedisException $e ) {
$result = false;
- $this->handleException( $server, $e );
+ $this->handleException( $server, $conn, $e );
}
$this->logRequest( 'add', $key, $server, $result );
wfProfileOut( __METHOD__ );
@@ -241,7 +265,7 @@ class RedisBagOStuff extends BagOStuff {
}
} catch ( RedisException $e ) {
$result = false;
- $this->handleException( $server, $e );
+ $this->handleException( $server, $conn, $e );
}
$this->logRequest( 'replace', $key, $server, $result );
@@ -250,60 +274,22 @@ class RedisBagOStuff extends BagOStuff {
}
/**
- * Non-atomic implementation of incr().
- *
- * Probably all callers actually want incr() to atomically initialise
- * values to zero if they don't exist, as provided by the Redis INCR
- * command. But we are constrained by the memcached-like interface to
- * return null in that case. Once the key exists, further increments are
- * atomic.
- */
- public function incr( $key, $value = 1 ) {
- wfProfileIn( __METHOD__ );
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
- if ( !$conn->exists( $key ) ) {
- wfProfileOut( __METHOD__ );
- return null;
- }
- try {
- $result = $conn->incrBy( $key, $value );
- } catch ( RedisException $e ) {
- $result = false;
- $this->handleException( $server, $e );
- }
-
- $this->logRequest( 'incr', $key, $server, $result );
- wfProfileOut( __METHOD__ );
- return $result;
- }
-
- /**
* Get a Redis object with a connection suitable for fetching the specified key
+ * @return Array (server, RedisConnRef) or (false, false)
*/
protected function getConnection( $key ) {
if ( count( $this->servers ) === 1 ) {
$candidates = $this->servers;
} else {
- // Use consistent hashing
- $hashes = array();
- foreach ( $this->servers as $server ) {
- $hashes[$server] = md5( $server . '/' . $key );
- }
- asort( $hashes );
+ $candidates = $this->servers;
+ ArrayUtils::consistentHashSort( $candidates, $key, '/' );
if ( !$this->automaticFailover ) {
- reset( $hashes );
- $candidates = array( key( $hashes ) );
- } else {
- $candidates = array_keys( $hashes );
+ $candidates = array_slice( $candidates, 0, 1 );
}
}
foreach ( $candidates as $server ) {
- $conn = $this->getConnectionToServer( $server );
+ $conn = $this->redisPool->getConnection( $server );
if ( $conn ) {
return array( $server, $conn );
}
@@ -312,79 +298,6 @@ class RedisBagOStuff extends BagOStuff {
}
/**
- * Get a connection to the server with the specified name. Connections
- * are cached, and failures are persistent to avoid multiple timeouts.
- *
- * @return Redis object, or false on failure
- */
- protected function getConnectionToServer( $server ) {
- if ( isset( $this->deadServers[$server] ) ) {
- $now = time();
- if ( $now > $this->deadServers[$server] ) {
- // Dead time expired
- unset( $this->deadServers[$server] );
- } else {
- // Server is dead
- $this->debug( "server $server is marked down for another " .
- ($this->deadServers[$server] - $now ) .
- " seconds, can't get connection" );
- return false;
- }
- }
-
- if ( isset( $this->conns[$server] ) ) {
- return $this->conns[$server];
- }
-
- if ( substr( $server, 0, 1 ) === '/' ) {
- // UNIX domain socket
- // These are required by the redis extension to start with a slash, but
- // we still need to set the port to a special value to make it work.
- $host = $server;
- $port = 0;
- } else {
- // TCP connection
- $hostPort = IP::splitHostAndPort( $server );
- if ( !$hostPort ) {
- throw new MWException( __CLASS__.": invalid configured server \"$server\"" );
- }
- list( $host, $port ) = $hostPort;
- if ( $port === false ) {
- $port = 6379;
- }
- }
- $conn = new Redis;
- try {
- if ( $this->persistent ) {
- $this->debug( "opening persistent connection to $host:$port" );
- $result = $conn->pconnect( $host, $port, $this->connectTimeout );
- } else {
- $this->debug( "opening non-persistent connection to $host:$port" );
- $result = $conn->connect( $host, $port, $this->connectTimeout );
- }
- if ( !$result ) {
- $this->logError( "could not connect to server $server" );
- // Mark server down for 30s to avoid further timeouts
- $this->deadServers[$server] = time() + 30;
- return false;
- }
- if ( $this->password !== null ) {
- if ( !$conn->auth( $this->password ) ) {
- $this->logError( "authentication error connecting to $server" );
- }
- }
- } catch ( RedisException $e ) {
- $this->deadServers[$server] = time() + 30;
- wfDebugLog( 'redis', "Redis exception: " . $e->getMessage() . "\n" );
- return false;
- }
-
- $conn->setOption( Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP );
- $this->conns[$server] = $conn;
- return $conn;
- }
-
- /**
* Log a fatal error
*/
protected function logError( $msg ) {
@@ -397,9 +310,8 @@ class RedisBagOStuff extends BagOStuff {
* not. The safest response for us is to explicitly destroy the connection
* object and let it be reopened during the next request.
*/
- protected function handleException( $server, $e ) {
- wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
- unset( $this->conns[$server] );
+ protected function handleException( $server, RedisConnRef $conn, $e ) {
+ $this->redisPool->handleException( $server, $conn, $e );
}
/**
@@ -410,4 +322,3 @@ class RedisBagOStuff extends BagOStuff {
( $result === false ? "failure" : "success" ) );
}
}
-
diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php
index 54051dc1..acf27036 100644
--- a/includes/objectcache/SqlBagOStuff.php
+++ b/includes/objectcache/SqlBagOStuff.php
@@ -32,23 +32,26 @@ class SqlBagOStuff extends BagOStuff {
*/
var $lb;
- /**
- * @var DatabaseBase
- */
- var $db;
- var $serverInfo;
+ var $serverInfos;
+ var $serverNames;
+ var $numServers;
+ var $conns;
var $lastExpireAll = 0;
var $purgePeriod = 100;
var $shards = 1;
var $tableName = 'objectcache';
- protected $connFailureTime = 0; // UNIX timestamp
- protected $connFailureError; // exception
+ protected $connFailureTimes = array(); // UNIX timestamps
+ protected $connFailureErrors = array(); // exceptions
/**
* Constructor. Parameters are:
- * - server: A server info structure in the format required by each
- * element in $wgDBServers.
+ * - server: A server info structure in the format required by each
+ * element in $wgDBServers.
+ *
+ * - servers: An array of server info structures describing a set of
+ * database servers to distribute keys to. If this is
+ * specified, the "server" option will be ignored.
*
* - purgePeriod: The average number of object cache requests in between
* garbage collection operations, where expired entries
@@ -59,8 +62,8 @@ class SqlBagOStuff extends BagOStuff {
*
* - tableName: The table name to use, default is "objectcache".
*
- * - shards: The number of tables to use for data storage. If this is
- * more than 1, table names will be formed in the style
+ * - shards: The number of tables to use for data storage on each server.
+ * If this is more than 1, table names will be formed in the style
* objectcacheNNN where NNN is the shard index, between 0 and
* shards-1. The number of digits will be the minimum number
* required to hold the largest shard index. Data will be
@@ -70,9 +73,19 @@ class SqlBagOStuff extends BagOStuff {
* @param $params array
*/
public function __construct( $params ) {
- if ( isset( $params['server'] ) ) {
- $this->serverInfo = $params['server'];
- $this->serverInfo['load'] = 1;
+ if ( isset( $params['servers'] ) ) {
+ $this->serverInfos = $params['servers'];
+ $this->numServers = count( $this->serverInfos );
+ $this->serverNames = array();
+ foreach ( $this->serverInfos as $i => $info ) {
+ $this->serverNames[$i] = isset( $info['host'] ) ? $info['host'] : "#$i";
+ }
+ } elseif ( isset( $params['server'] ) ) {
+ $this->serverInfos = array( $params['server'] );
+ $this->numServers = count( $this->serverInfos );
+ } else {
+ $this->serverInfos = false;
+ $this->numServers = 1;
}
if ( isset( $params['purgePeriod'] ) ) {
$this->purgePeriod = intval( $params['purgePeriod'] );
@@ -86,60 +99,81 @@ class SqlBagOStuff extends BagOStuff {
}
/**
+ * Get a connection to the specified database
+ *
+ * @param $serverIndex integer
* @return DatabaseBase
*/
- protected function getDB() {
+ protected function getDB( $serverIndex ) {
global $wgDebugDBTransactions;
- # Don't keep timing out trying to connect for each call if the DB is down
- if ( $this->connFailureError && ( time() - $this->connFailureTime ) < 60 ) {
- throw $this->connFailureError;
- }
+ if ( !isset( $this->conns[$serverIndex] ) ) {
+ if ( $serverIndex >= $this->numServers ) {
+ throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
+ }
+
+ # Don't keep timing out trying to connect for each call if the DB is down
+ if ( isset( $this->connFailureErrors[$serverIndex] )
+ && ( time() - $this->connFailureTimes[$serverIndex] ) < 60 )
+ {
+ throw $this->connFailureErrors[$serverIndex];
+ }
- if ( !isset( $this->db ) ) {
# If server connection info was given, use that
- if ( $this->serverInfo ) {
+ if ( $this->serverInfos ) {
if ( $wgDebugDBTransactions ) {
- wfDebug( sprintf( "Using provided serverInfo for SqlBagOStuff\n" ) );
+ wfDebug( "Using provided serverInfo for SqlBagOStuff\n" );
}
- $this->lb = new LoadBalancer( array(
- 'servers' => array( $this->serverInfo ) ) );
- $this->db = $this->lb->getConnection( DB_MASTER );
- $this->db->clearFlag( DBO_TRX );
+ $info = $this->serverInfos[$serverIndex];
+ $type = isset( $info['type'] ) ? $info['type'] : 'mysql';
+ $host = isset( $info['host'] ) ? $info['host'] : '[unknown]';
+ wfDebug( __CLASS__ . ": connecting to $host\n" );
+ $db = DatabaseBase::factory( $type, $info );
+ $db->clearFlag( DBO_TRX );
} else {
/*
* We must keep a separate connection to MySQL in order to avoid deadlocks
- * However, SQLite has an opposite behaviour. And PostgreSQL needs to know
+ * However, SQLite has an opposite behavior. And PostgreSQL needs to know
* if we are in transaction or no
*/
if ( wfGetDB( DB_MASTER )->getType() == 'mysql' ) {
$this->lb = wfGetLBFactory()->newMainLB();
- $this->db = $this->lb->getConnection( DB_MASTER );
- $this->db->clearFlag( DBO_TRX ); // auto-commit mode
+ $db = $this->lb->getConnection( DB_MASTER );
+ $db->clearFlag( DBO_TRX ); // auto-commit mode
} else {
- $this->db = wfGetDB( DB_MASTER );
+ $db = wfGetDB( DB_MASTER );
}
}
if ( $wgDebugDBTransactions ) {
- wfDebug( sprintf( "Connection %s will be used for SqlBagOStuff\n", $this->db ) );
+ wfDebug( sprintf( "Connection %s will be used for SqlBagOStuff\n", $db ) );
}
+ $this->conns[$serverIndex] = $db;
}
- return $this->db;
+ return $this->conns[$serverIndex];
}
/**
- * Get the table name for a given key
+ * Get the server index and table name for a given key
* @param $key string
- * @return string
+ * @return Array: server index and table name
*/
protected function getTableByKey( $key ) {
if ( $this->shards > 1 ) {
$hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
- return $this->getTableByShard( $hash % $this->shards );
+ $tableIndex = $hash % $this->shards;
} else {
- return $this->tableName;
+ $tableIndex = 0;
+ }
+ if ( $this->numServers > 1 ) {
+ $sortedServers = $this->serverNames;
+ ArrayUtils::consistentHashSort( $sortedServers, $key );
+ reset( $sortedServers );
+ $serverIndex = key( $sortedServers );
+ } else {
+ $serverIndex = 0;
}
+ return array( $serverIndex, $this->getTableNameByShard( $tableIndex ) );
}
/**
@@ -147,7 +181,7 @@ class SqlBagOStuff extends BagOStuff {
* @param $index int
* @return string
*/
- protected function getTableByShard( $index ) {
+ protected function getTableNameByShard( $index ) {
if ( $this->shards > 1 ) {
$decimals = strlen( $this->shards - 1 );
return $this->tableName .
@@ -159,11 +193,16 @@ class SqlBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$values = $this->getMulti( array( $key ) );
- return array_key_exists( $key, $values ) ? $values[$key] : false;
+ if ( array_key_exists( $key, $values ) ) {
+ $casToken = $values[$key];
+ return $values[$key];
+ }
+ return false;
}
/**
@@ -173,59 +212,61 @@ class SqlBagOStuff extends BagOStuff {
public function getMulti( array $keys ) {
$values = array(); // array of (key => value)
- try {
- $db = $this->getDB();
- $keysByTableName = array();
- foreach ( $keys as $key ) {
- $tableName = $this->getTableByKey( $key );
- if ( !isset( $keysByTableName[$tableName] ) ) {
- $keysByTableName[$tableName] = array();
- }
- $keysByTableName[$tableName][] = $key;
- }
+ $keysByTable = array();
+ foreach ( $keys as $key ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ $keysByTable[$serverIndex][$tableName][] = $key;
+ }
- $this->garbageCollect(); // expire old entries if any
+ $this->garbageCollect(); // expire old entries if any
- $dataRows = array();
- foreach ( $keysByTableName as $tableName => $tableKeys ) {
- $res = $db->select( $tableName,
- array( 'keyname', 'value', 'exptime' ),
- array( 'keyname' => $tableKeys ),
- __METHOD__ );
- foreach ( $res as $row ) {
- $dataRows[$row->keyname] = $row;
+ $dataRows = array();
+ foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+ try {
+ $db = $this->getDB( $serverIndex );
+ foreach ( $serverKeys as $tableName => $tableKeys ) {
+ $res = $db->select( $tableName,
+ array( 'keyname', 'value', 'exptime' ),
+ array( 'keyname' => $tableKeys ),
+ __METHOD__ );
+ foreach ( $res as $row ) {
+ $row->serverIndex = $serverIndex;
+ $row->tableName = $tableName;
+ $dataRows[$row->keyname] = $row;
+ }
}
+ } catch ( DBError $e ) {
+ $this->handleReadError( $e, $serverIndex );
}
+ }
- foreach ( $keys as $key ) {
- if ( isset( $dataRows[$key] ) ) { // HIT?
- $row = $dataRows[$key];
- $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
- if ( $this->isExpired( $row->exptime ) ) { // MISS
+ foreach ( $keys as $key ) {
+ if ( isset( $dataRows[$key] ) ) { // HIT?
+ $row = $dataRows[$key];
+ $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
+ try {
+ $db = $this->getDB( $row->serverIndex );
+ if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
$this->debug( "get: key has expired, deleting" );
- try {
- $db->begin( __METHOD__ );
- # Put the expiry time in the WHERE condition to avoid deleting a
- # newly-inserted value
- $db->delete( $this->getTableByKey( $key ),
- array( 'keyname' => $key, 'exptime' => $row->exptime ),
- __METHOD__ );
- $db->commit( __METHOD__ );
- } catch ( DBQueryError $e ) {
- $this->handleWriteError( $e );
- }
+ $db->begin( __METHOD__ );
+ # Put the expiry time in the WHERE condition to avoid deleting a
+ # newly-inserted value
+ $db->delete( $row->tableName,
+ array( 'keyname' => $key, 'exptime' => $row->exptime ),
+ __METHOD__ );
+ $db->commit( __METHOD__ );
$values[$key] = false;
} else { // HIT
$values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) );
}
- } else { // MISS
- $values[$key] = false;
- $this->debug( 'get: no matching rows' );
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e, $row->serverIndex );
}
+ } else { // MISS
+ $values[$key] = false;
+ $this->debug( 'get: no matching rows' );
}
- } catch ( DBError $e ) {
- $this->handleReadError( $e );
- };
+ }
return $values;
}
@@ -237,8 +278,9 @@ class SqlBagOStuff extends BagOStuff {
* @return bool
*/
public function set( $key, $value, $exptime = 0 ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
try {
- $db = $this->getDB();
+ $db = $this->getDB( $serverIndex );
$exptime = intval( $exptime );
if ( $exptime < 0 ) {
@@ -246,7 +288,7 @@ class SqlBagOStuff extends BagOStuff {
}
if ( $exptime == 0 ) {
- $encExpiry = $this->getMaxDateTime();
+ $encExpiry = $this->getMaxDateTime( $db );
} else {
if ( $exptime < 3.16e8 ) { # ~10 years
$exptime += time();
@@ -258,7 +300,7 @@ class SqlBagOStuff extends BagOStuff {
// (bug 24425) use a replace if the db supports it instead of
// delete/insert to avoid clashes with conflicting keynames
$db->replace(
- $this->getTableByKey( $key ),
+ $tableName,
array( 'keyname' ),
array(
'keyname' => $key,
@@ -267,7 +309,7 @@ class SqlBagOStuff extends BagOStuff {
), __METHOD__ );
$db->commit( __METHOD__ );
} catch ( DBError $e ) {
- $this->handleWriteError( $e );
+ $this->handleWriteError( $e, $serverIndex );
return false;
}
@@ -275,21 +317,73 @@ class SqlBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ try {
+ $db = $this->getDB( $serverIndex );
+ $exptime = intval( $exptime );
+
+ if ( $exptime < 0 ) {
+ $exptime = 0;
+ }
+
+ if ( $exptime == 0 ) {
+ $encExpiry = $this->getMaxDateTime( $db );
+ } else {
+ if ( $exptime < 3.16e8 ) { # ~10 years
+ $exptime += time();
+ }
+ $encExpiry = $db->timestamp( $exptime );
+ }
+ $db->begin( __METHOD__ );
+ // (bug 24425) use a replace if the db supports it instead of
+ // delete/insert to avoid clashes with conflicting keynames
+ $db->update(
+ $tableName,
+ array(
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $value ) ),
+ 'exptime' => $encExpiry
+ ),
+ array(
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $casToken ) )
+ ),
+ __METHOD__
+ );
+ $db->commit( __METHOD__ );
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e, $serverIndex );
+
+ return false;
+ }
+
+ return (bool)$db->affectedRows();
+ }
+
+ /**
* @param $key string
* @param $time int
* @return bool
*/
public function delete( $key, $time = 0 ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
try {
- $db = $this->getDB();
+ $db = $this->getDB( $serverIndex );
$db->begin( __METHOD__ );
$db->delete(
- $this->getTableByKey( $key ),
+ $tableName,
array( 'keyname' => $key ),
__METHOD__ );
$db->commit( __METHOD__ );
} catch ( DBError $e ) {
- $this->handleWriteError( $e );
+ $this->handleWriteError( $e, $serverIndex );
return false;
}
@@ -302,9 +396,9 @@ class SqlBagOStuff extends BagOStuff {
* @return int|null
*/
public function incr( $key, $step = 1 ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
try {
- $db = $this->getDB();
- $tableName = $this->getTableByKey( $key );
+ $db = $this->getDB( $serverIndex );
$step = intval( $step );
$db->begin( __METHOD__ );
$row = $db->selectRow(
@@ -320,7 +414,7 @@ class SqlBagOStuff extends BagOStuff {
return null;
}
$db->delete( $tableName, array( 'keyname' => $key ), __METHOD__ );
- if ( $this->isExpired( $row->exptime ) ) {
+ if ( $this->isExpired( $db, $row->exptime ) ) {
// Expired, do not reinsert
$db->commit( __METHOD__ );
@@ -342,7 +436,7 @@ class SqlBagOStuff extends BagOStuff {
}
$db->commit( __METHOD__ );
} catch ( DBError $e ) {
- $this->handleWriteError( $e );
+ $this->handleWriteError( $e, $serverIndex );
return null;
}
@@ -350,43 +444,21 @@ class SqlBagOStuff extends BagOStuff {
}
/**
- * @return Array
- */
- public function keys() {
- $result = array();
-
- try {
- $db = $this->getDB();
- for ( $i = 0; $i < $this->shards; $i++ ) {
- $res = $db->select( $this->getTableByShard( $i ),
- array( 'keyname' ), false, __METHOD__ );
- foreach ( $res as $row ) {
- $result[] = $row->keyname;
- }
- }
- } catch ( DBError $e ) {
- $this->handleReadError( $e );
- }
-
- return $result;
- }
-
- /**
* @param $exptime string
* @return bool
*/
- protected function isExpired( $exptime ) {
- return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time();
+ protected function isExpired( $db, $exptime ) {
+ return $exptime != $this->getMaxDateTime( $db ) && wfTimestamp( TS_UNIX, $exptime ) < time();
}
/**
* @return string
*/
- protected function getMaxDateTime() {
+ protected function getMaxDateTime( $db ) {
if ( time() > 0x7fffffff ) {
- return $this->getDB()->timestamp( 1 << 62 );
+ return $db->timestamp( 1 << 62 );
} else {
- return $this->getDB()->timestamp( 0x7fffffff );
+ return $db->timestamp( 0x7fffffff );
}
}
@@ -418,87 +490,91 @@ class SqlBagOStuff extends BagOStuff {
* @return bool
*/
public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
- try {
- $db = $this->getDB();
- $dbTimestamp = $db->timestamp( $timestamp );
- $totalSeconds = false;
- $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) );
- for ( $i = 0; $i < $this->shards; $i++ ) {
- $maxExpTime = false;
- while ( true ) {
- $conds = $baseConds;
- if ( $maxExpTime !== false ) {
- $conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime );
- }
- $rows = $db->select(
- $this->getTableByShard( $i ),
- array( 'keyname', 'exptime' ),
- $conds,
- __METHOD__,
- array( 'LIMIT' => 100, 'ORDER BY' => 'exptime' ) );
- if ( !$rows->numRows() ) {
- break;
- }
- $keys = array();
- $row = $rows->current();
- $minExpTime = $row->exptime;
- if ( $totalSeconds === false ) {
- $totalSeconds = wfTimestamp( TS_UNIX, $timestamp )
- - wfTimestamp( TS_UNIX, $minExpTime );
- }
- foreach ( $rows as $row ) {
- $keys[] = $row->keyname;
- $maxExpTime = $row->exptime;
- }
-
- $db->begin( __METHOD__ );
- $db->delete(
- $this->getTableByShard( $i ),
- array(
- 'exptime >= ' . $db->addQuotes( $minExpTime ),
- 'exptime < ' . $db->addQuotes( $dbTimestamp ),
- 'keyname' => $keys
- ),
- __METHOD__ );
- $db->commit( __METHOD__ );
+ for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+ try {
+ $db = $this->getDB( $serverIndex );
+ $dbTimestamp = $db->timestamp( $timestamp );
+ $totalSeconds = false;
+ $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) );
+ for ( $i = 0; $i < $this->shards; $i++ ) {
+ $maxExpTime = false;
+ while ( true ) {
+ $conds = $baseConds;
+ if ( $maxExpTime !== false ) {
+ $conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime );
+ }
+ $rows = $db->select(
+ $this->getTableNameByShard( $i ),
+ array( 'keyname', 'exptime' ),
+ $conds,
+ __METHOD__,
+ array( 'LIMIT' => 100, 'ORDER BY' => 'exptime' ) );
+ if ( !$rows->numRows() ) {
+ break;
+ }
+ $keys = array();
+ $row = $rows->current();
+ $minExpTime = $row->exptime;
+ if ( $totalSeconds === false ) {
+ $totalSeconds = wfTimestamp( TS_UNIX, $timestamp )
+ - wfTimestamp( TS_UNIX, $minExpTime );
+ }
+ foreach ( $rows as $row ) {
+ $keys[] = $row->keyname;
+ $maxExpTime = $row->exptime;
+ }
- if ( $progressCallback ) {
- if ( intval( $totalSeconds ) === 0 ) {
- $percent = 0;
- } else {
- $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp )
- - wfTimestamp( TS_UNIX, $maxExpTime );
- if ( $remainingSeconds > $totalSeconds ) {
- $totalSeconds = $remainingSeconds;
+ $db->begin( __METHOD__ );
+ $db->delete(
+ $this->getTableNameByShard( $i ),
+ array(
+ 'exptime >= ' . $db->addQuotes( $minExpTime ),
+ 'exptime < ' . $db->addQuotes( $dbTimestamp ),
+ 'keyname' => $keys
+ ),
+ __METHOD__ );
+ $db->commit( __METHOD__ );
+
+ if ( $progressCallback ) {
+ if ( intval( $totalSeconds ) === 0 ) {
+ $percent = 0;
+ } else {
+ $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp )
+ - wfTimestamp( TS_UNIX, $maxExpTime );
+ if ( $remainingSeconds > $totalSeconds ) {
+ $totalSeconds = $remainingSeconds;
+ }
+ $percent = ( $i + $remainingSeconds / $totalSeconds )
+ / $this->shards * 100;
}
- $percent = ( $i + $remainingSeconds / $totalSeconds )
- / $this->shards * 100;
+ $percent = ( $percent / $this->numServers )
+ + ( $serverIndex / $this->numServers * 100 );
+ call_user_func( $progressCallback, $percent );
}
- call_user_func( $progressCallback, $percent );
}
}
+ } catch ( DBError $e ) {
+ $this->handleWriteError( $e, $serverIndex );
+ return false;
}
- } catch ( DBError $e ) {
- $this->handleWriteError( $e );
- return false;
}
-
return true;
}
public function deleteAll() {
- try {
- $db = $this->getDB();
- for ( $i = 0; $i < $this->shards; $i++ ) {
- $db->begin( __METHOD__ );
- $db->delete( $this->getTableByShard( $i ), '*', __METHOD__ );
- $db->commit( __METHOD__ );
+ for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+ try {
+ $db = $this->getDB( $serverIndex );
+ for ( $i = 0; $i < $this->shards; $i++ ) {
+ $db->begin( __METHOD__ );
+ $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
+ $db->commit( __METHOD__ );
+ }
+ } catch ( DBError $e ) {
+ $this->handleWriteError( $e, $serverIndex );
+ return false;
}
- } catch ( DBError $e ) {
- $this->handleWriteError( $e );
- return false;
}
-
return true;
}
@@ -544,58 +620,77 @@ class SqlBagOStuff extends BagOStuff {
/**
* Handle a DBError which occurred during a read operation.
*/
- protected function handleReadError( DBError $exception ) {
+ protected function handleReadError( DBError $exception, $serverIndex ) {
if ( $exception instanceof DBConnectionError ) {
- $this->connFailureTime = time();
- $this->connFailureError = $exception;
+ $this->markServerDown( $exception, $serverIndex );
}
wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" );
- if ( $this->db ) {
- wfDebug( __METHOD__ . ": ignoring query error\n" );
- } else {
+ if ( $exception instanceof DBConnectionError ) {
wfDebug( __METHOD__ . ": ignoring connection error\n" );
+ } else {
+ wfDebug( __METHOD__ . ": ignoring query error\n" );
}
}
/**
* Handle a DBQueryError which occurred during a write operation.
*/
- protected function handleWriteError( DBError $exception ) {
+ protected function handleWriteError( DBError $exception, $serverIndex ) {
if ( $exception instanceof DBConnectionError ) {
- $this->connFailureTime = time();
- $this->connFailureError = $exception;
+ $this->markServerDown( $exception, $serverIndex );
}
- if ( $this->db && $this->db->wasReadOnlyError() ) {
+ if ( $exception->db && $exception->db->wasReadOnlyError() ) {
try {
- $this->db->rollback( __METHOD__ );
+ $exception->db->rollback( __METHOD__ );
} catch ( DBError $e ) {}
}
wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" );
- if ( $this->db ) {
- wfDebug( __METHOD__ . ": ignoring query error\n" );
- } else {
+ if ( $exception instanceof DBConnectionError ) {
wfDebug( __METHOD__ . ": ignoring connection error\n" );
+ } else {
+ wfDebug( __METHOD__ . ": ignoring query error\n" );
+ }
+ }
+
+ /**
+ * Mark a server down due to a DBConnectionError exception
+ */
+ protected function markServerDown( $exception, $serverIndex ) {
+ if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
+ if ( time() - $this->connFailureTimes[$serverIndex] >= 60 ) {
+ unset( $this->connFailureTimes[$serverIndex] );
+ unset( $this->connFailureErrors[$serverIndex] );
+ } else {
+ wfDebug( __METHOD__ . ": Server #$serverIndex already down\n" );
+ return;
+ }
}
+ $now = time();
+ wfDebug( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) . "\n" );
+ $this->connFailureTimes[$serverIndex] = $now;
+ $this->connFailureErrors[$serverIndex] = $exception;
}
/**
* Create shard tables. For use from eval.php.
*/
public function createTables() {
- $db = $this->getDB();
- if ( $db->getType() !== 'mysql'
- || version_compare( $db->getServerVersion(), '4.1.0', '<' ) )
- {
- throw new MWException( __METHOD__ . ' is not supported on this DB server' );
- }
+ for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+ $db = $this->getDB( $serverIndex );
+ if ( $db->getType() !== 'mysql'
+ || version_compare( $db->getServerVersion(), '4.1.0', '<' ) )
+ {
+ throw new MWException( __METHOD__ . ' is not supported on this DB server' );
+ }
- for ( $i = 0; $i < $this->shards; $i++ ) {
- $db->begin( __METHOD__ );
- $db->query(
- 'CREATE TABLE ' . $db->tableName( $this->getTableByShard( $i ) ) .
- ' LIKE ' . $db->tableName( 'objectcache' ),
- __METHOD__ );
- $db->commit( __METHOD__ );
+ for ( $i = 0; $i < $this->shards; $i++ ) {
+ $db->begin( __METHOD__ );
+ $db->query(
+ 'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
+ ' LIKE ' . $db->tableName( 'objectcache' ),
+ __METHOD__ );
+ $db->commit( __METHOD__ );
+ }
}
}
}
@@ -604,4 +699,3 @@ class SqlBagOStuff extends BagOStuff {
* Backwards compatibility alias
*/
class MediaWikiBagOStuff extends SqlBagOStuff { }
-
diff --git a/includes/objectcache/WinCacheBagOStuff.php b/includes/objectcache/WinCacheBagOStuff.php
index 21aa39e7..6d9b47ad 100644
--- a/includes/objectcache/WinCacheBagOStuff.php
+++ b/includes/objectcache/WinCacheBagOStuff.php
@@ -32,12 +32,15 @@ class WinCacheBagOStuff extends BagOStuff {
/**
* Get a value from the WinCache object cache
*
- * @param $key String: cache key
+ * @param string $key cache key
+ * @param $casToken[optional] int: cas token
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$val = wincache_ucache_get( $key );
+ $casToken = $val;
+
if ( is_string( $val ) ) {
$val = unserialize( $val );
}
@@ -48,9 +51,9 @@ class WinCacheBagOStuff extends BagOStuff {
/**
* Store a value in the WinCache object cache
*
- * @param $key String: cache key
+ * @param string $key cache key
* @param $value Mixed: object to store
- * @param $expire Int: expiration time
+ * @param int $expire expiration time
* @return bool
*/
public function set( $key, $value, $expire = 0 ) {
@@ -62,34 +65,28 @@ class WinCacheBagOStuff extends BagOStuff {
}
/**
- * Remove a value from the WinCache object cache
+ * Store a value in the WinCache object cache, race condition-safe
*
- * @param $key String: cache key
- * @param $time Int: not used in this implementation
+ * @param int $casToken cas token
+ * @param string $key cache key
+ * @param int $value object to store
+ * @param int $exptime expiration time
* @return bool
*/
- public function delete( $key, $time = 0 ) {
- wincache_ucache_delete( $key );
-
- return true;
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ return wincache_ucache_cas( $key, $casToken, serialize( $value ) );
}
/**
- * @return Array
+ * Remove a value from the WinCache object cache
+ *
+ * @param string $key cache key
+ * @param int $time not used in this implementation
+ * @return bool
*/
- public function keys() {
- $info = wincache_ucache_info();
- $list = $info['ucache_entries'];
- $keys = array();
-
- if ( is_null( $list ) ) {
- return array();
- }
-
- foreach ( $list as $entry ) {
- $keys[] = $entry['key_name'];
- }
+ public function delete( $key, $time = 0 ) {
+ wincache_ucache_delete( $key );
- return $keys;
+ return true;
}
}
diff --git a/includes/objectcache/XCacheBagOStuff.php b/includes/objectcache/XCacheBagOStuff.php
index bc68b596..0f45db73 100644
--- a/includes/objectcache/XCacheBagOStuff.php
+++ b/includes/objectcache/XCacheBagOStuff.php
@@ -31,10 +31,11 @@ class XCacheBagOStuff extends BagOStuff {
/**
* Get a value from the XCache object cache
*
- * @param $key String: cache key
+ * @param string $key cache key
+ * @param $casToken mixed: cas token
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$val = xcache_get( $key );
if ( is_string( $val ) ) {
@@ -53,9 +54,9 @@ class XCacheBagOStuff extends BagOStuff {
/**
* Store a value in the XCache object cache
*
- * @param $key String: cache key
+ * @param string $key cache key
* @param $value Mixed: object to store
- * @param $expire Int: expiration time
+ * @param int $expire expiration time
* @return bool
*/
public function set( $key, $value, $expire = 0 ) {
@@ -68,10 +69,22 @@ class XCacheBagOStuff extends BagOStuff {
}
/**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ // Can't find any documentation on xcache cas
+ throw new MWException( "CAS is not implemented in " . __CLASS__ );
+ }
+
+ /**
* Remove a value from the XCache object cache
*
- * @param $key String: cache key
- * @param $time Int: not used in this implementation
+ * @param string $key cache key
+ * @param int $time not used in this implementation
* @return bool
*/
public function delete( $key, $time = 0 ) {
@@ -79,6 +92,21 @@ class XCacheBagOStuff extends BagOStuff {
return true;
}
+ /**
+ * Merge an item.
+ * XCache does not seem to support any way of performing CAS - this however will
+ * provide a way to perform CAS-like functionality.
+ *
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
+ }
+
public function incr( $key, $value = 1 ) {
return xcache_inc( $key, $value );
}
diff --git a/includes/parser/CacheTime.php b/includes/parser/CacheTime.php
index 881dded7..8190a8a0 100644
--- a/includes/parser/CacheTime.php
+++ b/includes/parser/CacheTime.php
@@ -93,7 +93,7 @@ class CacheTime {
$expire = min( $expire, $wgParserCacheExpireTime );
}
- if( $this->containsOldMagic() ) { //compatibility hack
+ if ( $this->containsOldMagic() ) { //compatibility hack
$expire = min( $expire, 3600 ); # 1 hour
}
@@ -116,7 +116,7 @@ class CacheTime {
* per-article cache invalidation timestamps, or if it comes from
* an incompatible older version.
*
- * @param $touched String: the affected article's last touched timestamp
+ * @param string $touched the affected article's last touched timestamp
* @return Boolean
*/
public function expired( $touched ) {
diff --git a/includes/parser/CoreLinkFunctions.php b/includes/parser/CoreLinkFunctions.php
deleted file mode 100644
index 4bfa9d35..00000000
--- a/includes/parser/CoreLinkFunctions.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-/**
- * Link functions provided by MediaWiki core; experimental
- *
- * 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 Parser
- */
-
-/**
- * Various core link functions, registered in Parser::firstCallInit()
- * @ingroup Parser
- */
-class CoreLinkFunctions {
- /**
- * @param $parser Parser_LinkHooks
- * @return bool
- */
- static function register( $parser ) {
- $parser->setLinkHook( NS_CATEGORY, array( __CLASS__, 'categoryLinkHook' ) );
- return true;
- }
-
- /**
- * @param $parser Parser
- * @param $holders LinkHolderArray
- * @param $markers LinkMarkerReplacer
- * @param Title $title
- * @param $titleText
- * @param null $displayText
- * @param bool $leadingColon
- * @return bool
- */
- static function defaultLinkHook( $parser, $holders, $markers,
- Title $title, $titleText, &$displayText = null, &$leadingColon = false ) {
- if( isset($displayText) && $markers->findMarker( $displayText ) ) {
- # There are links inside of the displayText
- # For backwards compatibility the deepest links are dominant so this
- # link should not be handled
- $displayText = $markers->expand($displayText);
- # Return false so that this link is reverted back to WikiText
- return false;
- }
- return $holders->makeHolder( $title, isset($displayText) ? $displayText : $titleText, array(), '', '' );
- }
-
- /**
- * @param $parser Parser
- * @param $holders LinkHolderArray
- * @param $markers LinkMarkerReplacer
- * @param Title $title
- * @param $titleText
- * @param null $sortText
- * @param bool $leadingColon
- * @return bool|string
- */
- static function categoryLinkHook( $parser, $holders, $markers,
- Title $title, $titleText, &$sortText = null, &$leadingColon = false ) {
- global $wgContLang;
- # When a category link starts with a : treat it as a normal link
- if( $leadingColon ) return true;
- if( isset($sortText) && $markers->findMarker( $sortText ) ) {
- # There are links inside of the sortText
- # For backwards compatibility the deepest links are dominant so this
- # link should not be handled
- $sortText = $markers->expand($sortText);
- # Return false so that this link is reverted back to WikiText
- return false;
- }
- if( !isset($sortText) ) $sortText = $parser->getDefaultSort();
- $sortText = Sanitizer::decodeCharReferences( $sortText );
- $sortText = str_replace( "\n", '', $sortText );
- $sortText = $wgContLang->convertCategoryKey( $sortText );
- $parser->mOutput->addCategory( $title->getDBkey(), $sortText );
- return '';
- }
-
-}
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
index 8917b6d0..4b6eeca2 100644
--- a/includes/parser/CoreParserFunctions.php
+++ b/includes/parser/CoreParserFunctions.php
@@ -88,6 +88,8 @@ class CoreParserFunctions {
$parser->setFunctionHook( 'pagenamee', array( __CLASS__, 'pagenamee' ), SFH_NO_HASH );
$parser->setFunctionHook( 'fullpagename', array( __CLASS__, 'fullpagename' ), SFH_NO_HASH );
$parser->setFunctionHook( 'fullpagenamee', array( __CLASS__, 'fullpagenamee' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'rootpagename', array( __CLASS__, 'rootpagename' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'rootpagenamee', array( __CLASS__, 'rootpagenamee' ), SFH_NO_HASH );
$parser->setFunctionHook( 'basepagename', array( __CLASS__, 'basepagename' ), SFH_NO_HASH );
$parser->setFunctionHook( 'basepagenamee', array( __CLASS__, 'basepagenamee' ), SFH_NO_HASH );
$parser->setFunctionHook( 'subpagename', array( __CLASS__, 'subpagename' ), SFH_NO_HASH );
@@ -137,9 +139,10 @@ class CoreParserFunctions {
$pref = $parser->getOptions()->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 )
+ // if the user has 'default' for their setting
+ if ( $pref == 'default' && $defaultPref ) {
$pref = $defaultPref;
+ }
$date = $df->reformat( $pref, $date, array( 'match-whole' ) );
return $date;
@@ -175,8 +178,8 @@ class CoreParserFunctions {
* For links to "wiki"s, or similar software, spaces are encoded as '_',
*
* @param $parser Parser object
- * @param $s String: The text to encode.
- * @param $arg String (optional): The type of encoding.
+ * @param string $s The text to encode.
+ * @param string $arg (optional): The type of encoding.
* @return string
*/
static function urlencode( $parser, $s = '', $arg = null ) {
@@ -184,7 +187,7 @@ class CoreParserFunctions {
if ( is_null( $magicWords ) ) {
$magicWords = new MagicWordArray( array( 'url_path', 'url_query', 'url_wiki' ) );
}
- switch( $magicWords->matchStartToEnd( $arg ) ) {
+ switch ( $magicWords->matchStartToEnd( $arg ) ) {
// Encode as though it's a wiki page, '_' for ' '.
case 'url_wiki':
@@ -248,14 +251,15 @@ class CoreParserFunctions {
# before arriving here; if that's true, then the title can't be created
# and the variable will fail. If we can't get a decent title from the first
# attempt, url-decode and try for a second.
- if( is_null( $title ) )
+ if ( is_null( $title ) ) {
$title = Title::newFromURL( urldecode( $s ) );
- if( !is_null( $title ) ) {
+ }
+ if ( !is_null( $title ) ) {
# Convert NS_MEDIA -> NS_FILE
- if( $title->getNamespace() == NS_MEDIA ) {
+ if ( $title->getNamespace() == NS_MEDIA ) {
$title = Title::makeTitle( NS_FILE, $title->getDBkey() );
}
- if( !is_null( $arg ) ) {
+ if ( !is_null( $arg ) ) {
$text = $title->$func( $arg );
} else {
$text = $title->$func();
@@ -269,12 +273,14 @@ class CoreParserFunctions {
/**
* @param $parser Parser
* @param string $num
- * @param null $raw
- * @return
+ * @param string $arg
+ * @return string
*/
- static function formatnum( $parser, $num = '', $raw = null) {
- if ( self::isRaw( $raw ) ) {
+ static function formatnum( $parser, $num = '', $arg = null ) {
+ if ( self::matchAgainstMagicword( 'rawsuffix', $arg ) ) {
$func = array( $parser->getFunctionLang(), 'parseFormattedNumber' );
+ } elseif ( self::matchAgainstMagicword( 'nocommafysuffix', $arg ) ) {
+ $func = array( $parser->getFunctionLang(), 'formatNumNoSeparators' );
} else {
$func = array( $parser->getFunctionLang(), 'formatNum' );
}
@@ -351,55 +357,77 @@ class CoreParserFunctions {
* title which will normalise to the canonical title
*
* @param $parser Parser: parent parser
- * @param $text String: desired title text
+ * @param string $text desired title text
* @return String
*/
static function displaytitle( $parser, $text = '' ) {
global $wgRestrictDisplayTitle;
- #parse a limited subset of wiki markup (just the single quote items)
+ // 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
+ // 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
+ // 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 ) );
+ // disallow some styles that could be used to bypass $wgRestrictDisplayTitle
+ if ( $wgRestrictDisplayTitle ) {
+ $htmlTagsCallback = function ( &$params ) {
+ $decoded = Sanitizer::decodeTagAttributes( $params );
+
+ if ( isset( $decoded['style'] ) ) {
+ // this is called later anyway, but we need it right now for the regexes below to be safe
+ // calling it twice doesn't hurt
+ $decoded['style'] = Sanitizer::checkCss( $decoded['style'] );
+
+ if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) {
+ $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */';
+ }
+ }
+
+ $params = Sanitizer::safeEncodeTagAttributes( $decoded );
+ };
+ } else {
+ $htmlTagsCallback = null;
+ }
+
+ // 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, $htmlTagsCallback, array(), array(), $bad ) );
$title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
- if( !$wgRestrictDisplayTitle ) {
+ if ( !$wgRestrictDisplayTitle ) {
+ $parser->mOutput->setDisplayTitle( $text );
+ } elseif ( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) ) {
$parser->mOutput->setDisplayTitle( $text );
- } else {
- if ( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) ) {
- $parser->mOutput->setDisplayTitle( $text );
- }
}
return '';
}
- static function isRaw( $param ) {
- static $mwRaw;
- if ( !$mwRaw ) {
- $mwRaw =& MagicWord::get( 'rawsuffix' );
- }
- if ( is_null( $param ) ) {
+ /**
+ * Matches the given value against the value of given magic word
+ *
+ * @param string $magicword magic word key
+ * @param mixed $value value to match
+ * @return boolean true on successful match
+ */
+ static private function matchAgainstMagicword( $magicword, $value ) {
+ if ( strval( $value ) === '' ) {
return false;
- } else {
- return $mwRaw->match( $param );
}
+ $mwObject = MagicWord::get( $magicword );
+ return $mwObject->match( $value );
}
static function formatRaw( $num, $raw ) {
- if( self::isRaw( $raw ) ) {
+ if ( self::matchAgainstMagicword( 'rawsuffix', $raw ) ) {
return $num;
} else {
global $wgContLang;
@@ -422,22 +450,22 @@ class CoreParserFunctions {
return self::formatRaw( SiteStats::images(), $raw );
}
static function numberofadmins( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::numberingroup('sysop'), $raw );
+ return self::formatRaw( SiteStats::numberingroup( 'sysop' ), $raw );
}
static function numberofedits( $parser, $raw = null ) {
return self::formatRaw( SiteStats::edits(), $raw );
}
static function numberofviews( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::views(), $raw );
+ global $wgDisableCounters;
+ return !$wgDisableCounters ? self::formatRaw( SiteStats::views(), $raw ) : '';
}
static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
}
- static function numberingroup( $parser, $name = '', $raw = null) {
+ 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
@@ -447,44 +475,51 @@ 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 namespacenumber( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null( $t ) )
+ if ( is_null( $t ) ) {
return '';
+ }
return $t->getNamespace();
}
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() );
}
@@ -495,75 +530,101 @@ 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 wfEscapeWikiText( $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 wfEscapeWikiText( $t->getPrefixedURL() );
}
static function subpagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null( $t ) )
+ if ( is_null( $t ) ) {
return '';
+ }
return wfEscapeWikiText( $t->getSubpageText() );
}
static function subpagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null( $t ) )
+ if ( is_null( $t ) ) {
return '';
+ }
return wfEscapeWikiText( $t->getSubpageUrlForm() );
}
+ static function rootpagename( $parser, $title = null ) {
+ $t = Title::newFromText( $title );
+ if ( is_null( $t ) ) {
+ return '';
+ }
+ return wfEscapeWikiText( $t->getRootText() );
+ }
+ static function rootpagenamee( $parser, $title = null ) {
+ $t = Title::newFromText( $title );
+ if ( is_null( $t ) ) {
+ return '';
+ }
+ return wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $t->getRootText() ) ) );
+ }
static function basepagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null( $t ) )
+ if ( is_null( $t ) ) {
return '';
+ }
return wfEscapeWikiText( $t->getBaseText() );
}
static function basepagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null( $t ) )
+ if ( is_null( $t ) ) {
return '';
+ }
return wfEscapeWikiText( 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 wfEscapeWikiText( $t->getTalkPage()->getPrefixedUrl() );
+ }
+ return wfEscapeWikiText( $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 wfEscapeWikiText( $t->getSubjectPage()->getPrefixedUrl() );
+ }
+ return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedURL() );
}
/**
@@ -573,6 +634,7 @@ class CoreParserFunctions {
* @return string
*/
static function pagesincategory( $parser, $name = '', $arg1 = null, $arg2 = null ) {
+ global $wgContLang;
static $magicWords = null;
if ( is_null( $magicWords ) ) {
$magicWords = new MagicWordArray( array(
@@ -585,7 +647,7 @@ class CoreParserFunctions {
static $cache = array();
// split the given option to its variable
- if( self::isRaw( $arg1 ) ) {
+ if ( self::matchAgainstMagicword( 'rawsuffix', $arg1 ) ) {
//{{pagesincategory:|raw[|type]}}
$raw = $arg1;
$type = $magicWords->matchStartToEnd( $arg2 );
@@ -594,23 +656,24 @@ class CoreParserFunctions {
$type = $magicWords->matchStartToEnd( $arg1 );
$raw = $arg2;
}
- if( !$type ) { //backward compatibility
+ if ( !$type ) { //backward compatibility
$type = 'pagesincategory_all';
}
$title = Title::makeTitleSafe( NS_CATEGORY, $name );
- if( !$title ) { # invalid title
+ if ( !$title ) { # invalid title
return self::formatRaw( 0, $raw );
}
+ $wgContLang->findVariantLink( $name, $title, true );
// Normalize name for cache
$name = $title->getDBkey();
- if( !isset( $cache[$name] ) ) {
+ if ( !isset( $cache[$name] ) ) {
$category = Category::newFromTitle( $title );
$allCount = $subcatCount = $fileCount = $pagesCount = 0;
- if( $parser->incrementExpensiveFunctionCount() ) {
+ if ( $parser->incrementExpensiveFunctionCount() ) {
// $allCount is the total number of cat members,
// not the count of how many members are normal pages.
$allCount = (int)$category->getPageCount();
@@ -632,8 +695,6 @@ 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.
*
- * @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
@@ -641,15 +702,15 @@ class CoreParserFunctions {
* @todo Document parameters
*
* @param $parser Parser
- * @param $page String TODO DOCUMENT (Default: empty string)
- * @param $raw TODO DOCUMENT (Default: null)
+ * @param $page String Name of page to check (Default: empty string)
+ * @param $raw String Should number be human readable with commas or just number
* @return string
*/
static function pagesize( $parser, $page = '', $raw = null ) {
static $cache = array();
$title = Title::newFromText( $page );
- if( !is_object( $title ) ) {
+ if ( !is_object( $title ) ) {
$cache[$page] = 0;
return self::formatRaw( 0, $raw );
}
@@ -658,25 +719,35 @@ class CoreParserFunctions {
$page = $title->getPrefixedText();
$length = 0;
- if( isset( $cache[$page] ) ) {
+ if ( isset( $cache[$page] ) ) {
$length = $cache[$page];
- } elseif( $parser->incrementExpensiveFunctionCount() ) {
+ } elseif ( $parser->incrementExpensiveFunctionCount() ) {
$rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
- $id = $rev ? $rev->getPage() : 0;
+ $pageID = $rev ? $rev->getPage() : 0;
+ $revID = $rev ? $rev->getId() : 0;
$length = $cache[$page] = $rev ? $rev->getSize() : 0;
// Register dependency in templatelinks
- $parser->mOutput->addTemplate( $title, $id, $rev ? $rev->getId() : 0 );
+ $parser->mOutput->addTemplate( $title, $pageID, $revID );
}
return self::formatRaw( $length, $raw );
}
/**
- * Returns the requested protection level for the current page
+ * Returns the requested protection level for the current page
+ *
+ * @param Parser $parser
+ * @param string $type
+ * @param string $title
+ *
* @return string
*/
- static function protectionlevel( $parser, $type = '' ) {
- $restrictions = $parser->mTitle->getRestrictions( strtolower( $type ) );
+ static function protectionlevel( $parser, $type = '', $title = '' ) {
+ $titleObject = Title::newFromText( $title );
+ if ( !( $titleObject instanceof Title ) ) {
+ $titleObject = $parser->mTitle;
+ }
+ $restrictions = $titleObject->getRestrictions( strtolower( $type ) );
# Title::getRestrictions returns an array, its possible it may have
# multiple values in the future
return implode( $restrictions, ',' );
@@ -685,8 +756,8 @@ class CoreParserFunctions {
/**
* Gives language names.
* @param $parser Parser
- * @param $code String Language code (of which to get name)
- * @param $inLanguage String Language code (in which to get name)
+ * @param string $code Language code (of which to get name)
+ * @param string $inLanguage Language code (in which to get name)
* @return String
*/
static function language( $parser, $code = '', $inLanguage = '' ) {
@@ -703,7 +774,9 @@ class CoreParserFunctions {
static function pad( $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT ) {
$padding = $parser->killMarkers( $padding );
$lengthOfPadding = mb_strlen( $padding );
- if ( $lengthOfPadding == 0 ) return $string;
+ 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 );
@@ -739,7 +812,7 @@ class CoreParserFunctions {
*/
static function anchorencode( $parser, $text ) {
$text = $parser->killMarkers( $text );
- return substr( $parser->guessSectionNameFromWikiText( $text ), 1);
+ return (string)substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
}
static function special( $parser, $text ) {
@@ -748,7 +821,9 @@ class CoreParserFunctions {
$title = SpecialPage::getTitleFor( $page, $subpage );
return $title->getPrefixedText();
} else {
- return wfMessage( 'nosuchspecialpage' )->inContentLanguage()->text();
+ // unknown special page, just use the given text as its title, if at all possible
+ $title = Title::makeTitleSafe( NS_SPECIAL, $text );
+ return $title ? $title->getPrefixedText() : self::special( $parser, 'Badtitle' );
}
}
@@ -758,8 +833,8 @@ class CoreParserFunctions {
/**
* @param $parser Parser
- * @param $text String The sortkey to use
- * @param $uarg String Either "noreplace" or "noerror" (in en)
+ * @param string $text The sortkey to use
+ * @param string $uarg Either "noreplace" or "noerror" (in en)
* both suppress errors, and noreplace does nothing if
* a default sortkey already exists.
* @return string
@@ -772,35 +847,41 @@ class CoreParserFunctions {
$arg = $magicWords->matchStartToEnd( $uarg );
$text = trim( $text );
- if( strlen( $text ) == 0 )
+ if ( strlen( $text ) == 0 ) {
return '';
+ }
$old = $parser->getCustomDefaultSort();
if ( $old === false || $arg !== 'defaultsort_noreplace' ) {
$parser->setDefaultSort( $text );
}
- if( $old === false || $old == $text || $arg ) {
+ if ( $old === false || $old == $text || $arg ) {
return '';
} else {
- return( '<span class="error">' .
- wfMessage( 'duplicate-defaultsort', $old, $text )->inContentLanguage()->escaped() .
- '</span>' );
+ $converter = $parser->getConverterLanguage()->getConverter();
+ return '<span class="error">' .
+ wfMessage( 'duplicate-defaultsort',
+ // Message should be parsed, but these params should only be escaped.
+ $converter->markNoConversion( wfEscapeWikiText( $old ) ),
+ $converter->markNoConversion( wfEscapeWikiText( $text ) )
+ )->inContentLanguage()->text() .
+ '</span>';
}
}
// Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}} or {{filepath|300|nowiki}}
// or {{filepath|300px}}, {{filepath|200x300px}}, {{filepath|nowiki|200x300px}}, {{filepath|200x300px|nowiki}}
- public static function filepath( $parser, $name='', $argA='', $argB='' ) {
+ public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
$file = wfFindFile( $name );
- if( $argA == 'nowiki' ) {
+ if ( $argA == 'nowiki' ) {
// {{filepath: | option [| size] }}
$isNowiki = true;
$parsedWidthParam = $parser->parseWidthParam( $argB );
} else {
// {{filepath: [| size [|option]] }}
$parsedWidthParam = $parser->parseWidthParam( $argA );
- $isNowiki = ($argB == 'nowiki');
+ $isNowiki = ( $argB == 'nowiki' );
}
if ( $file ) {
diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php
index 296be66f..a2eb6987 100644
--- a/includes/parser/CoreTagHooks.php
+++ b/includes/parser/CoreTagHooks.php
@@ -72,11 +72,12 @@ class CoreTagHooks {
* @param $content string
* @param $attributes array
* @param $parser Parser
+ * @throws MWException
* @return array
*/
static function html( $content, $attributes, $parser ) {
global $wgRawHtml;
- if( $wgRawHtml ) {
+ if ( $wgRawHtml ) {
return array( $content, 'markerType' => 'nowiki' );
} else {
throw new MWException( '<html> extension tag encountered unexpectedly' );
diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php
index 2917b4a7..0a69b045 100644
--- a/includes/parser/DateFormatter.php
+++ b/includes/parser/DateFormatter.php
@@ -22,7 +22,7 @@
*/
/**
- * Date formatter, recognises dates in plain text and formats them accoding to user preferences.
+ * Date formatter, recognises dates in plain text and formats them according to user preferences.
* @todo preferences, OutputPage
* @ingroup Parser
*/
@@ -55,7 +55,7 @@ class DateFormatter {
$this->lang = $lang;
$this->monthNames = $this->getMonthRegex();
- for ( $i=1; $i<=12; $i++ ) {
+ for ( $i = 1; $i <= 12; $i++ ) {
$this->xMonths[$this->lang->lc( $this->lang->getMonthName( $i ) )] = $i;
$this->xMonths[$this->lang->lc( $this->lang->getMonthAbbreviation( $i ) )] = $i;
}
@@ -102,11 +102,11 @@ class DateFormatter {
# Rules
# pref source target
- $this->rules[self::DMY][self::MD] = self::DM;
- $this->rules[self::ALL][self::MD] = self::MD;
- $this->rules[self::MDY][self::DM] = self::MD;
- $this->rules[self::ALL][self::DM] = self::DM;
- $this->rules[self::NONE][self::ISO2] = self::ISO1;
+ $this->rules[self::DMY][self::MD] = self::DM;
+ $this->rules[self::ALL][self::MD] = self::MD;
+ $this->rules[self::MDY][self::DM] = self::MD;
+ $this->rules[self::ALL][self::DM] = self::DM;
+ $this->rules[self::NONE][self::ISO2] = self::ISO1;
$this->preferences = array(
'default' => self::NONE,
@@ -140,12 +140,12 @@ class DateFormatter {
}
/**
- * @param $preference String: User preference
- * @param $text String: Text to reformat
- * @param $options Array: can contain 'linked' and/or 'match-whole'
+ * @param string $preference User preference
+ * @param string $text Text to reformat
+ * @param array $options can contain 'linked' and/or 'match-whole'
* @return mixed|String
*/
- function reformat( $preference, $text, $options = array('linked') ) {
+ function reformat( $preference, $text, $options = array( 'linked' ) ) {
$linked = in_array( 'linked', $options );
$match_whole = in_array( 'match-whole', $options );
@@ -154,12 +154,12 @@ class DateFormatter {
} else {
$preference = self::NONE;
}
- for ( $i=1; $i<=self::LAST; $i++ ) {
+ for ( $i = 1; $i <= self::LAST; $i++ ) {
$this->mSource = $i;
- if ( isset ( $this->rules[$preference][$i] ) ) {
+ if ( isset( $this->rules[$preference][$i] ) ) {
# Specific rules
$this->mTarget = $this->rules[$preference][$i];
- } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
+ } elseif ( isset( $this->rules[self::ALL][$i] ) ) {
# General rules
$this->mTarget = $this->rules[self::ALL][$i];
} elseif ( $preference ) {
@@ -172,21 +172,21 @@ class DateFormatter {
$regex = $this->regexes[$i];
// Horrible hack
- if (!$linked) {
+ if ( !$linked ) {
$regex = str_replace( array( '\[\[', '\]\]' ), '', $regex );
}
- if ($match_whole) {
+ if ( $match_whole ) {
// Let's hope this works
$regex = preg_replace( '!^/!', '/^', $regex );
$regex = str_replace( $this->regexTrail,
- '$'.$this->regexTrail, $regex );
+ '$' . $this->regexTrail, $regex );
}
// Another horrible hack
$this->mLinked = $linked;
$text = preg_replace_callback( $regex, array( &$this, 'replace' ), $text );
- unset($this->mLinked);
+ unset( $this->mLinked );
}
return $text;
}
@@ -198,14 +198,15 @@ class DateFormatter {
function replace( $matches ) {
# Extract information from $matches
$linked = true;
- if ( isset( $this->mLinked ) )
+ if ( isset( $this->mLinked ) ) {
$linked = $this->mLinked;
-
+ }
+
$bits = array();
$key = $this->keys[$this->mSource];
- for ( $p=0; $p < strlen($key); $p++ ) {
+ for ( $p = 0; $p < strlen( $key ); $p++ ) {
if ( $key[$p] != ' ' ) {
- $bits[$key[$p]] = $matches[$p+1];
+ $bits[$key[$p]] = $matches[$p + 1];
}
}
@@ -219,8 +220,8 @@ class DateFormatter {
*/
function formatDate( $bits, $link = true ) {
$format = $this->targets[$this->mTarget];
-
- if (!$link) {
+
+ if ( !$link ) {
// strip piped links
$format = preg_replace( '/\[\[[^|]+\|([^\]]+)\]\]/', '$1', $format );
// strip remaining links
@@ -232,10 +233,12 @@ class DateFormatter {
$fail = false;
// Pre-generate y/Y stuff because we need the year for the <span> title.
- if ( !isset( $bits['y'] ) && isset( $bits['Y'] ) )
+ if ( !isset( $bits['y'] ) && isset( $bits['Y'] ) ) {
$bits['y'] = $this->makeIsoYear( $bits['Y'] );
- if ( !isset( $bits['Y'] ) && isset( $bits['y'] ) )
+ }
+ if ( !isset( $bits['Y'] ) && isset( $bits['y'] ) ) {
$bits['Y'] = $this->makeNormalYear( $bits['y'] );
+ }
if ( !isset( $bits['m'] ) ) {
$m = $this->makeIsoMonth( $bits['F'] );
@@ -246,11 +249,11 @@ class DateFormatter {
}
}
- if ( !isset($bits['d']) ) {
+ if ( !isset( $bits['d'] ) ) {
$bits['d'] = sprintf( '%02d', $bits['j'] );
}
- for ( $p=0; $p < strlen( $format ); $p++ ) {
+ for ( $p = 0; $p < strlen( $format ); $p++ ) {
$char = $format[$p];
switch ( $char ) {
case 'd': # ISO day of month
@@ -263,7 +266,7 @@ class DateFormatter {
$text .= $bits['y'];
break;
case 'j': # ordinary day of month
- if ( !isset($bits['j']) ) {
+ if ( !isset( $bits['j'] ) ) {
$text .= intval( $bits['d'] );
} else {
$text .= $bits['j'];
@@ -271,7 +274,7 @@ class DateFormatter {
break;
case 'F': # long month
if ( !isset( $bits['F'] ) ) {
- $m = intval($bits['m']);
+ $m = intval( $bits['m'] );
if ( $m > 12 || $m < 1 ) {
$fail = true;
} else {
@@ -293,8 +296,9 @@ class DateFormatter {
}
$isoBits = array();
- if ( isset($bits['y']) )
+ if ( isset( $bits['y'] ) ) {
$isoBits[] = $bits['y'];
+ }
$isoBits[] = $bits['m'];
$isoBits[] = $bits['d'];
$isoDate = implode( '-', $isoBits );
@@ -312,7 +316,7 @@ class DateFormatter {
*/
function getMonthRegex() {
$names = array();
- for( $i = 1; $i <= 12; $i++ ) {
+ for ( $i = 1; $i <= 12; $i++ ) {
$names[] = $this->lang->getMonthName( $i );
$names[] = $this->lang->getMonthAbbreviation( $i );
}
@@ -321,7 +325,7 @@ class DateFormatter {
/**
* Makes an ISO month, e.g. 02, from a month name
- * @param $monthName String: month name
+ * @param string $monthName month name
* @return string ISO month name
*/
function makeIsoMonth( $monthName ) {
@@ -331,13 +335,13 @@ class DateFormatter {
/**
* @todo document
- * @param $year String: Year name
+ * @param string $year Year name
* @return string ISO year name
*/
function makeIsoYear( $year ) {
# Assumes the year is in a nice format, as enforced by the regex
if ( substr( $year, -2 ) == 'BC' ) {
- $num = intval(substr( $year, 0, -3 )) - 1;
+ $num = intval( substr( $year, 0, -3 ) ) - 1;
# PHP bug note: sprintf( "%04d", -1 ) fails poorly
$text = sprintf( '-%04d', $num );
@@ -353,7 +357,7 @@ class DateFormatter {
*/
function makeNormalYear( $iso ) {
if ( $iso[0] == '-' ) {
- $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
+ $text = ( intval( substr( $iso, 1 ) ) + 1 ) . ' BC';
} else {
$text = intval( $iso );
}
diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php
index d9356b48..f1a0b258 100644
--- a/includes/parser/LinkHolderArray.php
+++ b/includes/parser/LinkHolderArray.php
@@ -43,9 +43,9 @@ class LinkHolderArray {
}
}
- /**
+ /**
* Don't serialize the parent object, it is big, and not needed when it is
- * a parameter to mergeForeign(), which is the only application of
+ * a parameter to mergeForeign(), which is the only application of
* serializing at present.
*
* Compact the titles, only serialize the text form.
@@ -103,15 +103,15 @@ class LinkHolderArray {
}
/**
- * Merge a LinkHolderArray from another parser instance into this one. The
- * keys will not be preserved. Any text which went with the old
- * LinkHolderArray and needs to work with the new one should be passed in
+ * Merge a LinkHolderArray from another parser instance into this one. The
+ * keys will not be preserved. Any text which went with the old
+ * LinkHolderArray and needs to work with the new one should be passed in
* the $texts array. The strings in this array will have their link holders
* converted for use in the destination link holder. The resulting array of
* strings will be returned.
*
* @param $other LinkHolderArray
- * @param $texts Array of strings
+ * @param array $texts of strings
* @return Array
*/
function mergeForeign( $other, $texts ) {
@@ -126,7 +126,7 @@ class LinkHolderArray {
$maxId = $newKey > $maxId ? $newKey : $maxId;
}
}
- $texts = preg_replace_callback( '/(<!--LINK \d+:)(\d+)(-->)/',
+ $texts = preg_replace_callback( '/(<!--LINK \d+:)(\d+)(-->)/',
array( $this, 'mergeForeignCallback' ), $texts );
# Renumber interwiki links
@@ -135,7 +135,7 @@ class LinkHolderArray {
$this->interwikis[$newKey] = $entry;
$maxId = $newKey > $maxId ? $newKey : $maxId;
}
- $texts = preg_replace_callback( '/(<!--IWLINK )(\d+)(-->)/',
+ $texts = preg_replace_callback( '/(<!--IWLINK )(\d+)(-->)/',
array( $this, 'mergeForeignCallback' ), $texts );
# Set the parent link ID to be beyond the highest used ID
@@ -159,8 +159,8 @@ class LinkHolderArray {
# Internal links
$pos = 0;
while ( $pos < strlen( $text ) ) {
- if ( !preg_match( '/<!--LINK (\d+):(\d+)-->/',
- $text, $m, PREG_OFFSET_CAPTURE, $pos ) )
+ if ( !preg_match( '/<!--LINK (\d+):(\d+)-->/',
+ $text, $m, PREG_OFFSET_CAPTURE, $pos ) )
{
break;
}
@@ -210,14 +210,14 @@ class LinkHolderArray {
*
* @param $nt Title
* @param $text String
- * @param $query Array [optional]
- * @param $trail String [optional]
- * @param $prefix String [optional]
+ * @param array $query [optional]
+ * @param string $trail [optional]
+ * @param string $prefix [optional]
* @return string
*/
- function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
+ function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
wfProfileIn( __METHOD__ );
- if ( ! is_object($nt) ) {
+ if ( !is_object( $nt ) ) {
# Fail gracefully
$retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
} else {
@@ -226,7 +226,7 @@ class LinkHolderArray {
$entry = array(
'title' => $nt,
- 'text' => $prefix.$text.$inside,
+ 'text' => $prefix . $text . $inside,
'pdbk' => $nt->getPrefixedDBkey(),
);
if ( $query !== array() ) {
@@ -254,12 +254,12 @@ class LinkHolderArray {
* @todo FIXME: Update documentation. makeLinkObj() is deprecated.
* Replace <!--LINK--> link placeholders with actual links, in the buffer
* Placeholders created in Skin::makeLinkObj()
- * Returns an array of link CSS classes, indexed by PDBK.
+ * @return array of link CSS classes, indexed by PDBK.
*/
function replace( &$text ) {
wfProfileIn( __METHOD__ );
- $colours = $this->replaceInternal( $text );
+ $colours = $this->replaceInternal( $text ); // FIXME: replaceInternal doesn't return a value
$this->replaceInterwiki( $text );
wfProfileOut( __METHOD__ );
@@ -281,93 +281,95 @@ class LinkHolderArray {
$linkCache = LinkCache::singleton();
$output = $this->parent->getOutput();
- wfProfileIn( __METHOD__.'-check' );
- $dbr = wfGetDB( DB_SLAVE );
- $threshold = $this->parent->getOptions()->getStubThreshold();
+ if ( $linkCache->useDatabase() ) {
+ wfProfileIn( __METHOD__ . '-check' );
+ $dbr = wfGetDB( DB_SLAVE );
+ $threshold = $this->parent->getOptions()->getStubThreshold();
- # Sort by namespace
- ksort( $this->internals );
+ # Sort by namespace
+ ksort( $this->internals );
- $linkcolour_ids = array();
+ $linkcolour_ids = array();
- # Generate query
- $queries = array();
- foreach ( $this->internals as $ns => $entries ) {
- foreach ( $entries as $entry ) {
- $title = $entry['title'];
- $pdbk = $entry['pdbk'];
+ # Generate query
+ $queries = array();
+ foreach ( $this->internals as $ns => $entries ) {
+ foreach ( $entries as $entry ) {
+ $title = $entry['title'];
+ $pdbk = $entry['pdbk'];
- # Skip invalid entries.
- # Result will be ugly, but prevents crash.
- if ( is_null( $title ) ) {
- continue;
- }
+ # Skip invalid entries.
+ # Result will be ugly, but prevents crash.
+ if ( is_null( $title ) ) {
+ continue;
+ }
- # Check if it's a static known link, e.g. interwiki
- if ( $title->isAlwaysKnown() ) {
- $colours[$pdbk] = '';
- } elseif ( $ns == NS_SPECIAL ) {
- $colours[$pdbk] = 'new';
- } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
- $colours[$pdbk] = Linker::getLinkColour( $title, $threshold );
- $output->addLink( $title, $id );
- $linkcolour_ids[$id] = $pdbk;
- } elseif ( $linkCache->isBadLink( $pdbk ) ) {
- $colours[$pdbk] = 'new';
- } else {
- # Not in the link cache, add it to the query
- $queries[$ns][] = $title->getDBkey();
+ # Check if it's a static known link, e.g. interwiki
+ if ( $title->isAlwaysKnown() ) {
+ $colours[$pdbk] = '';
+ } elseif ( $ns == NS_SPECIAL ) {
+ $colours[$pdbk] = 'new';
+ } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
+ $colours[$pdbk] = Linker::getLinkColour( $title, $threshold );
+ $output->addLink( $title, $id );
+ $linkcolour_ids[$id] = $pdbk;
+ } elseif ( $linkCache->isBadLink( $pdbk ) ) {
+ $colours[$pdbk] = 'new';
+ } else {
+ # Not in the link cache, add it to the query
+ $queries[$ns][] = $title->getDBkey();
+ }
}
}
- }
- if ( $queries ) {
- $where = array();
- foreach( $queries as $ns => $pages ){
- $where[] = $dbr->makeList(
- array(
- 'page_namespace' => $ns,
- 'page_title' => $pages,
- ),
- LIST_AND
- );
- }
+ if ( $queries ) {
+ $where = array();
+ foreach ( $queries as $ns => $pages ) {
+ $where[] = $dbr->makeList(
+ array(
+ 'page_namespace' => $ns,
+ 'page_title' => $pages,
+ ),
+ LIST_AND
+ );
+ }
- $res = $dbr->select(
- 'page',
- array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest' ),
- $dbr->makeList( $where, LIST_OR ),
- __METHOD__
- );
+ $res = $dbr->select(
+ 'page',
+ array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest' ),
+ $dbr->makeList( $where, LIST_OR ),
+ __METHOD__
+ );
- # Fetch data and form into an associative array
- # non-existent = broken
- foreach ( $res as $s ) {
- $title = Title::makeTitle( $s->page_namespace, $s->page_title );
- $pdbk = $title->getPrefixedDBkey();
- $linkCache->addGoodLinkObjFromRow( $title, $s );
- $output->addLink( $title, $s->page_id );
- # @todo FIXME: Convoluted data flow
- # The redirect status and length is passed to getLinkColour via the LinkCache
- # Use formal parameters instead
- $colours[$pdbk] = Linker::getLinkColour( $title, $threshold );
- //add id to the extension todolist
- $linkcolour_ids[$s->page_id] = $pdbk;
+ # Fetch data and form into an associative array
+ # non-existent = broken
+ foreach ( $res as $s ) {
+ $title = Title::makeTitle( $s->page_namespace, $s->page_title );
+ $pdbk = $title->getPrefixedDBkey();
+ $linkCache->addGoodLinkObjFromRow( $title, $s );
+ $output->addLink( $title, $s->page_id );
+ # @todo FIXME: Convoluted data flow
+ # The redirect status and length is passed to getLinkColour via the LinkCache
+ # Use formal parameters instead
+ $colours[$pdbk] = Linker::getLinkColour( $title, $threshold );
+ //add id to the extension todolist
+ $linkcolour_ids[$s->page_id] = $pdbk;
+ }
+ unset( $res );
}
- unset( $res );
- }
- if ( count($linkcolour_ids) ) {
- //pass an array of page_ids to an extension
- wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
+ if ( count( $linkcolour_ids ) ) {
+ //pass an array of page_ids to an extension
+ wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
+ }
+ wfProfileOut( __METHOD__ . '-check' );
}
- wfProfileOut( __METHOD__.'-check' );
# Do a second query for different language variants of links and categories
- if($wgContLang->hasVariants()) {
+ if ( $wgContLang->hasVariants() ) {
$this->doVariants( $colours );
}
# Construct search and replace arrays
- wfProfileIn( __METHOD__.'-construct' );
+ wfProfileIn( __METHOD__ . '-construct' );
$replacePairs = array();
foreach ( $this->internals as $ns => $entries ) {
foreach ( $entries as $index => $entry ) {
@@ -377,6 +379,10 @@ class LinkHolderArray {
$key = "$ns:$index";
$searchkey = "<!--LINK $key-->";
$displayText = $entry['text'];
+ if ( isset( $entry['selflink'] ) ) {
+ $replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayText, $query );
+ continue;
+ }
if ( $displayText === '' ) {
$displayText = null;
}
@@ -399,16 +405,17 @@ class LinkHolderArray {
}
}
$replacer = new HashtableReplacer( $replacePairs, 1 );
- wfProfileOut( __METHOD__.'-construct' );
+ wfProfileOut( __METHOD__ . '-construct' );
# Do the thing
- wfProfileIn( __METHOD__.'-replace' );
+ wfProfileIn( __METHOD__ . '-replace' );
$text = preg_replace_callback(
'/(<!--LINK .*?-->)/',
$replacer->cb(),
- $text);
+ $text
+ );
- wfProfileOut( __METHOD__.'-replace' );
+ wfProfileOut( __METHOD__ . '-replace' );
wfProfileOut( __METHOD__ );
}
@@ -424,7 +431,7 @@ class LinkHolderArray {
# Make interwiki link HTML
$output = $this->parent->getOutput();
$replacePairs = array();
- foreach( $this->interwikis as $key => $link ) {
+ foreach ( $this->interwikis as $key => $link ) {
$replacePairs[$key] = Linker::link( $link['title'], $link['text'] );
$output->addInterwikiLink( $link['title'] );
}
@@ -454,20 +461,17 @@ class LinkHolderArray {
// single string to all variants. This would improve parser's performance
// significantly.
foreach ( $this->internals as $ns => $entries ) {
+ if ( $ns == NS_SPECIAL ) {
+ continue;
+ }
foreach ( $entries as $index => $entry ) {
$pdbk = $entry['pdbk'];
// we only deal with new links (in its first query)
if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
- $title = $entry['title'];
- $titleText = $title->getText();
- $titlesAttrs[] = array(
- 'ns' => $ns,
- 'key' => "$ns:$index",
- 'titleText' => $titleText,
- );
+ $titlesAttrs[] = array( $index, $entry['title'] );
// separate titles with \0 because it would never appears
// in a valid title
- $titlesToBeConverted .= $titleText . "\0";
+ $titlesToBeConverted .= $entry['title']->getText() . "\0";
}
}
}
@@ -478,39 +482,58 @@ class LinkHolderArray {
foreach ( $titlesAllVariants as &$titlesVariant ) {
$titlesVariant = explode( "\0", $titlesVariant );
}
- $l = count( $titlesAttrs );
+
// Then add variants of links to link batch
- for ( $i = 0; $i < $l; $i ++ ) {
+ $parentTitle = $this->parent->getTitle();
+ foreach ( $titlesAttrs as $i => $attrs ) {
+ list( $index, $title ) = $attrs;
+ $ns = $title->getNamespace();
+ $text = $title->getText();
+
foreach ( $allVariantsName as $variantName ) {
$textVariant = $titlesAllVariants[$variantName][$i];
- if ( $textVariant != $titlesAttrs[$i]['titleText'] ) {
- $variantTitle = Title::makeTitle( $titlesAttrs[$i]['ns'], $textVariant );
- if( is_null( $variantTitle ) ) {
- continue;
- }
- $linkBatch->addObj( $variantTitle );
- $variantMap[$variantTitle->getPrefixedDBkey()][] = $titlesAttrs[$i]['key'];
+ if ( $textVariant === $text ) {
+ continue;
+ }
+
+ $variantTitle = Title::makeTitle( $ns, $textVariant );
+ if ( is_null( $variantTitle ) ) {
+ continue;
+ }
+
+ // Self-link checking for mixed/different variant titles. At this point, we
+ // already know the exact title does not exist, so the link cannot be to a
+ // variant of the current title that exists as a separate page.
+ if ( $variantTitle->equals( $parentTitle ) && $title->getFragment() === '' ) {
+ $this->internals[$ns][$index]['selflink'] = true;
+ continue 2;
}
+
+ $linkBatch->addObj( $variantTitle );
+ $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
}
}
// process categories, check if a category exists in some variant
$categoryMap = array(); // maps $category_variant => $category (dbkeys)
$varCategories = array(); // category replacements oldDBkey => newDBkey
- foreach( $output->getCategoryLinks() as $category ){
+ foreach ( $output->getCategoryLinks() as $category ) {
+ $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
+ $linkBatch->addObj( $categoryTitle );
$variants = $wgContLang->autoConvertToAllVariants( $category );
- foreach($variants as $variant){
- if($variant != $category){
- $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
- if(is_null($variantTitle)) continue;
+ foreach ( $variants as $variant ) {
+ if ( $variant !== $category ) {
+ $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
+ if ( is_null( $variantTitle ) ) {
+ continue;
+ }
$linkBatch->addObj( $variantTitle );
- $categoryMap[$variant] = $category;
+ $categoryMap[$variant] = array( $category, $categoryTitle );
}
}
}
-
- if(!$linkBatch->isEmpty()){
+ if ( !$linkBatch->isEmpty() ) {
// construct query
$dbr = wfGetDB( DB_SLAVE );
$varRes = $dbr->select( 'page',
@@ -529,14 +552,14 @@ class LinkHolderArray {
$vardbk = $variantTitle->getDBkey();
$holderKeys = array();
- if( isset( $variantMap[$varPdbk] ) ) {
+ if ( isset( $variantMap[$varPdbk] ) ) {
$holderKeys = $variantMap[$varPdbk];
$linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
$output->addLink( $variantTitle, $s->page_id );
}
// loop over link holders
- foreach( $holderKeys as $key ) {
+ foreach ( $holderKeys as $key ) {
list( $ns, $index ) = explode( ':', $key, 2 );
$entry =& $this->internals[$ns][$index];
$pdbk = $entry['pdbk'];
@@ -556,25 +579,28 @@ class LinkHolderArray {
}
// check if the object is a variant of a category
- if(isset($categoryMap[$vardbk])){
- $oldkey = $categoryMap[$vardbk];
- if($oldkey != $vardbk)
- $varCategories[$oldkey]=$vardbk;
+ if ( isset( $categoryMap[$vardbk] ) ) {
+ list( $oldkey, $oldtitle ) = $categoryMap[$vardbk];
+ if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
+ $varCategories[$oldkey] = $vardbk;
+ }
}
}
wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
// rebuild the categories in original order (if there are replacements)
- if(count($varCategories)>0){
+ if ( count( $varCategories ) > 0 ) {
$newCats = array();
$originalCats = $output->getCategories();
- foreach($originalCats as $cat => $sortkey){
+ foreach ( $originalCats as $cat => $sortkey ) {
// make the replacement
- if( array_key_exists($cat,$varCategories) )
+ if ( array_key_exists( $cat, $varCategories ) ) {
$newCats[$varCategories[$cat]] = $sortkey;
- else $newCats[$cat] = $sortkey;
+ } else {
+ $newCats[$cat] = $sortkey;
+ }
}
- $output->setCategoryLinks($newCats);
+ $output->setCategoryLinks( $newCats );
}
}
}
@@ -607,14 +633,14 @@ class LinkHolderArray {
*/
function replaceTextCallback( $matches ) {
$type = $matches[1];
- $key = $matches[2];
- if( $type == 'LINK' ) {
+ $key = $matches[2];
+ if ( $type == 'LINK' ) {
list( $ns, $index ) = explode( ':', $key, 2 );
- if( isset( $this->internals[$ns][$index]['text'] ) ) {
+ if ( isset( $this->internals[$ns][$index]['text'] ) ) {
return $this->internals[$ns][$index]['text'];
}
- } elseif( $type == 'IWLINK' ) {
- if( isset( $this->interwikis[$key]['text'] ) ) {
+ } elseif ( $type == 'IWLINK' ) {
+ if ( isset( $this->interwikis[$key]['text'] ) ) {
return $this->interwikis[$key]['text'];
}
}
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
index 10765de2..1f14223d 100644
--- a/includes/parser/Parser.php
+++ b/includes/parser/Parser.php
@@ -28,7 +28,7 @@
/**
* PHP Parser - Processes wiki markup (which uses a more user-friendly
* syntax, such as "[[link]]" for making links), and provides a one-way
- * transformation of that wiki markup it into XHTML output / markup
+ * transformation of that wiki markup it into (X)HTML output / markup
* (which in turn the browser understands, and can display).
*
* There are seven main entry points into the Parser class:
@@ -54,7 +54,6 @@
* @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
*
* @par Settings:
- * $wgLocaltimezone
* $wgNamespacesWithSubpages
*
* @par Settings only within ParserOptions:
@@ -62,7 +61,6 @@
* $wgAllowSpecialInclusion
* $wgInterwikiMagic
* $wgMaxArticleSize
- * $wgUseDynamicDates
*
* @ingroup Parser
*/
@@ -117,14 +115,18 @@ class Parser {
# Marker Suffix needs to be accessible staticly.
const MARKER_SUFFIX = "-QINU\x7f";
+ # Markers used for wrapping the table of contents
+ const TOC_START = '<mw:toc>';
+ const TOC_END = '</mw:toc>';
+
# Persistent:
var $mTagHooks = array();
var $mTransparentTagHooks = array();
var $mFunctionHooks = array();
var $mFunctionSynonyms = array( 0 => array(), 1 => array() );
var $mFunctionTagHooks = array();
- var $mStripList = array();
- var $mDefaultStripList = array();
+ var $mStripList = array();
+ var $mDefaultStripList = array();
var $mVarCache = array();
var $mImageParams = array();
var $mImageParamsMagicArray = array();
@@ -193,7 +195,9 @@ class Parser {
var $mRevisionId; # ID to display in {{REVISIONID}} tags
var $mRevisionTimestamp; # The timestamp of the specified revision ID
var $mRevisionUser; # User to display in {{REVISIONUSER}} tag
+ var $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
var $mRevIdForTs; # The revision ID which was used to fetch the timestamp
+ var $mInputSize = false; # For {{PAGESIZE}} on current page.
/**
* @var string
@@ -201,6 +205,13 @@ class Parser {
var $mUniqPrefix;
/**
+ * @var Array with the language name of each language link (i.e. the
+ * interwiki prefix) in the key, value arbitrary. Used to avoid sending
+ * duplicate language links to the ParserOutput.
+ */
+ var $mLangLinkLanguages;
+
+ /**
* Constructor
*
* @param $conf array
@@ -208,12 +219,12 @@ class Parser {
public function __construct( $conf = array() ) {
$this->mConf = $conf;
$this->mUrlProtocols = wfUrlProtocols();
- $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')'.
- self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
+ $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
+ self::EXT_LINK_URL_CLASS . '+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
if ( isset( $conf['preprocessorClass'] ) ) {
$this->mPreprocessorClass = $conf['preprocessorClass'];
- } elseif ( defined( 'MW_COMPILED' ) ) {
- # Preprocessor_Hash is much faster than Preprocessor_DOM in compiled mode
+ } elseif ( defined( 'HPHP_VERSION' ) ) {
+ # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
$this->mPreprocessorClass = 'Preprocessor_Hash';
} elseif ( extension_loaded( 'domxml' ) ) {
# PECL extension that conflicts with the core DOM extension (bug 13770)
@@ -240,6 +251,13 @@ class Parser {
}
/**
+ * Allow extensions to clean up when the parser is cloned
+ */
+ function __clone() {
+ wfRunHooks( 'ParserCloned', array( $this ) );
+ }
+
+ /**
* Do various kinds of initialisation on the first call of the parser
*/
function firstCallInit() {
@@ -279,9 +297,10 @@ class Parser {
$this->mLinkHolders = new LinkHolderArray( $this );
$this->mLinkID = 0;
$this->mRevisionObject = $this->mRevisionTimestamp =
- $this->mRevisionId = $this->mRevisionUser = null;
+ $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
$this->mVarCache = array();
$this->mUser = null;
+ $this->mLangLinkLanguages = array();
/**
* Prefix for temporary replacement strings for the multipass parser.
@@ -291,12 +310,11 @@ class Parser {
* string constructs.
*
* Must not consist of all title characters, or else it will change
- * the behaviour of <nowiki> in a link.
+ * the behavior of <nowiki> in a link.
*/
$this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
$this->mStripState = new StripState( $this->mUniqPrefix );
-
# Clear these on every parse, bug 4549
$this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
@@ -327,12 +345,12 @@ class Parser {
* Convert wikitext to HTML
* Do not call this function recursively.
*
- * @param $text String: text we want to parse
+ * @param string $text text we want to parse
* @param $title Title object
* @param $options ParserOptions
* @param $linestart boolean
* @param $clearState boolean
- * @param $revid Int: number to pass in {{REVISIONID}}
+ * @param int $revid number to pass in {{REVISIONID}}
* @return ParserOutput a ParserOutput
*/
public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
@@ -341,13 +359,18 @@ class Parser {
* to internalParse() which does all the real work.
*/
- global $wgUseTidy, $wgAlwaysUseTidy;
- $fname = __METHOD__.'-' . wfGetCaller();
+ global $wgUseTidy, $wgAlwaysUseTidy, $wgShowHostnames;
+ $fname = __METHOD__ . '-' . wfGetCaller();
wfProfileIn( __METHOD__ );
wfProfileIn( $fname );
$this->startParse( $title, $options, self::OT_HTML, $clearState );
+ $this->mInputSize = strlen( $text );
+ if ( $this->mOptions->getEnableLimitReport() ) {
+ $this->mOutput->resetParseStartTime();
+ }
+
# Remove the strip marker tag prefix from the input, if present.
if ( $clearState ) {
$text = str_replace( $this->mUniqPrefix, '', $text );
@@ -357,11 +380,13 @@ class Parser {
$oldRevisionObject = $this->mRevisionObject;
$oldRevisionTimestamp = $this->mRevisionTimestamp;
$oldRevisionUser = $this->mRevisionUser;
+ $oldRevisionSize = $this->mRevisionSize;
if ( $revid !== null ) {
$this->mRevisionId = $revid;
$this->mRevisionObject = null;
$this->mRevisionTimestamp = null;
$this->mRevisionUser = null;
+ $this->mRevisionSize = null;
}
wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
@@ -397,9 +422,7 @@ class Parser {
if ( !( $options->getDisableContentConversion()
|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) )
{
- # Run convert unconditionally in 1.18-compatible mode
- global $wgBug34832TransitionalRollback;
- if ( $wgBug34832TransitionalRollback || !$this->mOptions->getInterfaceMessage() ) {
+ if ( !$this->mOptions->getInterfaceMessage() ) {
# The position of the convert() call should not be changed. it
# assumes that the links are all replaced and the only thing left
# is the <nowiki> mark.
@@ -479,23 +502,73 @@ class Parser {
# Information on include size limits, for the benefit of users who try to skirt them
if ( $this->mOptions->getEnableLimitReport() ) {
$max = $this->mOptions->getMaxIncludeSize();
- $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n";
- $limitReport =
- "NewPP limit report\n" .
- "Preprocessor visited node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
- "Preprocessor generated node count: " .
- "{$this->mGeneratedPPNodeCount}/{$this->mOptions->getMaxGeneratedPPNodeCount()}\n" .
- "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
- "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
- "Highest expansion depth: {$this->mHighestExpansionDepth}/{$this->mOptions->getMaxPPExpandDepth()}\n".
- $PFreport;
+
+ $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
+ if ( $cpuTime !== null ) {
+ $this->mOutput->setLimitReportData( 'limitreport-cputime',
+ sprintf( "%.3f", $cpuTime )
+ );
+ }
+
+ $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
+ $this->mOutput->setLimitReportData( 'limitreport-walltime',
+ sprintf( "%.3f", $wallTime )
+ );
+
+ $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
+ array( $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() )
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
+ array( $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() )
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
+ array( $this->mIncludeSizes['post-expand'], $max )
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
+ array( $this->mIncludeSizes['arg'], $max )
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
+ array( $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() )
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
+ array( $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() )
+ );
+ wfRunHooks( 'ParserLimitReportPrepare', array( $this, $this->mOutput ) );
+
+ $limitReport = "NewPP limit report\n";
+ if ( $wgShowHostnames ) {
+ $limitReport .= 'Parsed by ' . wfHostname() . "\n";
+ }
+ foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
+ if ( wfRunHooks( 'ParserLimitReportFormat',
+ array( $key, $value, &$limitReport, false, false )
+ ) ) {
+ $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
+ $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) )
+ ->inLanguage( 'en' )->useDatabase( false );
+ if ( !$valueMsg->exists() ) {
+ $valueMsg = new RawMessage( '$1' );
+ }
+ if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
+ $valueMsg->params( $value );
+ $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
+ }
+ }
+ }
+ // Since we're not really outputting HTML, decode the entities and
+ // then re-encode the things that need hiding inside HTML comments.
+ $limitReport = htmlspecialchars_decode( $limitReport );
wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
// Sanitize for comment. Note '‐' in the replacement is U+2010,
// which looks much like the problematic '-'.
$limitReport = str_replace( array( '-', '&' ), array( '‐', '&amp;' ), $limitReport );
-
$text .= "\n<!-- \n$limitReport-->\n";
+
+ if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
+ wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
+ $this->mTitle->getPrefixedDBkey() );
+ }
}
$this->mOutput->setText( $text );
@@ -503,6 +576,8 @@ class Parser {
$this->mRevisionObject = $oldRevisionObject;
$this->mRevisionTimestamp = $oldRevisionTimestamp;
$this->mRevisionUser = $oldRevisionUser;
+ $this->mRevisionSize = $oldRevisionSize;
+ $this->mInputSize = false;
wfProfileOut( $fname );
wfProfileOut( __METHOD__ );
@@ -515,12 +590,12 @@ class Parser {
*
* 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 string $text text extension wants to have parsed
* @param $frame PPFrame: The frame to use for expanding any template variables
*
* @return string
*/
- function recursiveTagParse( $text, $frame=false ) {
+ function recursiveTagParse( $text, $frame = false ) {
wfProfileIn( __METHOD__ );
wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
@@ -534,7 +609,7 @@ class Parser {
* Also removes comments.
* @return mixed|string
*/
- function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) {
+ function preprocess( $text, Title $title = null, ParserOptions $options, $revid = null ) {
wfProfileIn( __METHOD__ );
$this->startParse( $title, $options, self::OT_PREPROCESS, true );
if ( $revid !== null ) {
@@ -552,7 +627,7 @@ class Parser {
* Recursive parser entry point that can be called from an extension tag
* hook.
*
- * @param $text String: text to be expanded
+ * @param string $text text to be expanded
* @param $frame PPFrame: The frame to use for expanding any template variables
* @return String
* @since 1.19
@@ -593,7 +668,7 @@ class Parser {
*
* @return string
*/
- static public function getRandomString() {
+ public static function getRandomString() {
return wfRandomString( 16 );
}
@@ -682,7 +757,7 @@ class Parser {
/**
* Accessor/mutator for the output type
*
- * @param $x int|null New value or null to just get the current one
+ * @param int|null $x New value or null to just get the current one
* @return Integer
*/
function OutputType( $x = null ) {
@@ -745,6 +820,7 @@ class Parser {
*
* @since 1.19
*
+ * @throws MWException
* @return Language|null
*/
public function getTargetLanguage() {
@@ -752,9 +828,9 @@ class Parser {
if ( $target !== null ) {
return $target;
- } elseif( $this->mOptions->getInterfaceMessage() ) {
+ } elseif ( $this->mOptions->getInterfaceMessage() ) {
return $this->mOptions->getUserLangObj();
- } elseif( is_null( $this->mTitle ) ) {
+ } elseif ( is_null( $this->mTitle ) ) {
throw new MWException( __METHOD__ . ': $this->mTitle is null' );
}
@@ -765,12 +841,7 @@ class Parser {
* Get the language object for language conversion
*/
function getConverterLanguage() {
- global $wgBug34832TransitionalRollback, $wgContLang;
- if ( $wgBug34832TransitionalRollback ) {
- return $wgContLang;
- } else {
- return $this->getTargetLanguage();
- }
+ return $this->getTargetLanguage();
}
/**
@@ -813,9 +884,9 @@ class Parser {
* '<element param="x">tag content</element>' ) )
* @endcode
*
- * @param $elements array list of element names. Comments are always extracted.
- * @param $text string Source text string.
- * @param $matches array Out parameter, Array: extracted tags
+ * @param array $elements list of element names. Comments are always extracted.
+ * @param string $text Source text string.
+ * @param array $matches Out parameter, Array: extracted tags
* @param $uniq_prefix string
* @return String: stripped text
*/
@@ -835,16 +906,16 @@ class Parser {
}
if ( count( $p ) > 5 ) {
# comment
- $element = $p[4];
+ $element = $p[4];
$attributes = '';
- $close = '';
- $inside = $p[5];
+ $close = '';
+ $inside = $p[5];
} else {
# tag
- $element = $p[1];
+ $element = $p[1];
$attributes = $p[2];
- $close = $p[3];
- $inside = $p[4];
+ $close = $p[3];
+ $inside = $p[4];
}
$marker = "$uniq_prefix-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
@@ -928,33 +999,33 @@ class Parser {
$line = trim( $outLine );
if ( $line === '' ) { # empty line, go to next line
- $out .= $outLine."\n";
+ $out .= $outLine . "\n";
continue;
}
$first_character = $line[0];
$matches = array();
- if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
+ if ( preg_match( '/^(:*)\{\|(.*)$/', $line, $matches ) ) {
# First check if we are starting a new table
$indent_level = strlen( $matches[1] );
$attributes = $this->mStripState->unstripBoth( $matches[2] );
- $attributes = Sanitizer::fixTagAttributes( $attributes , 'table' );
-
- $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
- array_push( $td_history , false );
- array_push( $last_tag_history , '' );
- array_push( $tr_history , false );
- array_push( $tr_attributes , '' );
- array_push( $has_opened_tr , false );
+ $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
+
+ $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
+ array_push( $td_history, false );
+ array_push( $last_tag_history, '' );
+ array_push( $tr_history, false );
+ array_push( $tr_attributes, '' );
+ array_push( $has_opened_tr, false );
} elseif ( count( $td_history ) == 0 ) {
# Don't do any of the following
- $out .= $outLine."\n";
+ $out .= $outLine . "\n";
continue;
- } elseif ( substr( $line , 0 , 2 ) === '|}' ) {
+ } elseif ( substr( $line, 0, 2 ) === '|}' ) {
# We are ending a table
- $line = '</table>' . substr( $line , 2 );
+ $line = '</table>' . substr( $line, 2 );
$last_tag = array_pop( $last_tag_history );
if ( !array_pop( $has_opened_tr ) ) {
@@ -969,8 +1040,8 @@ class Parser {
$line = "</{$last_tag}>{$line}";
}
array_pop( $tr_attributes );
- $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
- } elseif ( substr( $line , 0 , 2 ) === '|-' ) {
+ $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
+ } elseif ( substr( $line, 0, 2 ) === '|-' ) {
# Now we have a table row
$line = preg_replace( '#^\|-+#', '', $line );
@@ -983,7 +1054,7 @@ class Parser {
$line = '';
$last_tag = array_pop( $last_tag_history );
array_pop( $has_opened_tr );
- array_push( $has_opened_tr , true );
+ array_push( $has_opened_tr, true );
if ( array_pop( $tr_history ) ) {
$line = '</tr>';
@@ -994,27 +1065,27 @@ class Parser {
}
$outLine = $line;
- array_push( $tr_history , false );
- array_push( $td_history , false );
- array_push( $last_tag_history , '' );
- } elseif ( $first_character === '|' || $first_character === '!' || substr( $line , 0 , 2 ) === '|+' ) {
+ array_push( $tr_history, false );
+ array_push( $td_history, false );
+ array_push( $last_tag_history, '' );
+ } elseif ( $first_character === '|' || $first_character === '!' || substr( $line, 0, 2 ) === '|+' ) {
# This might be cell elements, td, th or captions
- if ( substr( $line , 0 , 2 ) === '|+' ) {
+ if ( substr( $line, 0, 2 ) === '|+' ) {
$first_character = '+';
- $line = substr( $line , 1 );
+ $line = substr( $line, 1 );
}
- $line = substr( $line , 1 );
+ $line = substr( $line, 1 );
if ( $first_character === '!' ) {
- $line = str_replace( '!!' , '||' , $line );
+ $line = str_replace( '!!', '||', $line );
}
# Split up multiple cells on the same line.
# FIXME : This can result in improper nesting of tags processed
# by earlier parser steps, but should avoid splitting up eg
# attribute values containing literal "||".
- $cells = StringUtils::explodeMarkup( '||' , $line );
+ $cells = StringUtils::explodeMarkup( '||', $line );
$outLine = '';
@@ -1026,10 +1097,10 @@ class Parser {
if ( !array_pop( $tr_history ) ) {
$previous = "<tr{$tr_after}>\n";
}
- array_push( $tr_history , true );
- array_push( $tr_attributes , '' );
+ array_push( $tr_history, true );
+ array_push( $tr_attributes, '' );
array_pop( $has_opened_tr );
- array_push( $has_opened_tr , true );
+ array_push( $has_opened_tr, true );
}
$last_tag = array_pop( $last_tag_history );
@@ -1048,10 +1119,10 @@ class Parser {
$last_tag = '';
}
- array_push( $last_tag_history , $last_tag );
+ array_push( $last_tag_history, $last_tag );
# A cell could contain both parameters and data
- $cell_data = explode( '|' , $cell , 2 );
+ $cell_data = explode( '|', $cell, 2 );
# Bug 553: Note that a '|' inside an invalid link should not
# be mistaken as delimiting cell parameters
@@ -1061,12 +1132,12 @@ class Parser {
$cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
} else {
$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
- $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
+ $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
}
$outLine .= $cell;
- array_push( $td_history , true );
+ array_push( $td_history, true );
}
}
$out .= $outLine . "\n";
@@ -1081,7 +1152,7 @@ class Parser {
$out .= "</tr>\n";
}
if ( !array_pop( $has_opened_tr ) ) {
- $out .= "<tr><td></td></tr>\n" ;
+ $out .= "<tr><td></td></tr>\n";
}
$out .= "</table>\n";
@@ -1122,7 +1193,7 @@ class Parser {
# Hook to suspend the parser in this state
if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
wfProfileOut( __METHOD__ );
- return $text ;
+ return $text;
}
# if $frame is provided, then use $frame for replacing any variables
@@ -1156,17 +1227,13 @@ class Parser {
$text = $this->doDoubleUnderscore( $text );
$text = $this->doHeadings( $text );
- if ( $this->mOptions->getUseDynamicDates() ) {
- $df = DateFormatter::getInstance();
- $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
- }
$text = $this->replaceInternalLinks( $text );
$text = $this->doAllQuotes( $text );
$text = $this->replaceExternalLinks( $text );
# replaceInternalLinks may sometimes leave behind
# absolute URLs, which have to be masked to hide them from replaceExternalLinks
- $text = str_replace( $this->mUniqPrefix.'NOPARSE', '', $text );
+ $text = str_replace( $this->mUniqPrefix . 'NOPARSE', '', $text );
$text = $this->doMagicLinks( $text );
$text = $this->formatHeadings( $text, $origText, $isMain );
@@ -1234,7 +1301,7 @@ class Parser {
$CssClass = 'mw-magiclink-pmid';
$id = $m[4];
} else {
- throw new MWException( __METHOD__.': unrecognised match type "' .
+ throw new MWException( __METHOD__ . ': unrecognised match type "' .
substr( $m[0], 0, 20 ) . '"' );
}
$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
@@ -1248,8 +1315,8 @@ class Parser {
'x' => 'X',
));
$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
- return'<a href="' .
- htmlspecialchars( $titleObj->getLocalUrl() ) .
+ return '<a href="' .
+ htmlspecialchars( $titleObj->getLocalURL() ) .
"\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
} else {
return $m[0];
@@ -1298,7 +1365,8 @@ class Parser {
if ( $text === false ) {
# Not an image, make a link
$text = Linker::makeExternalLink( $url,
- $this->getConverterLanguage()->markNoConversion($url), true, 'free',
+ $this->getConverterLanguage()->markNoConversion( $url, true ),
+ true, 'free',
$this->getExternalLinkAttribs( $url ) );
# Register it in the output object...
# Replace unnecessary URL escape codes with their equivalent characters
@@ -1309,7 +1377,6 @@ class Parser {
return $text . $trail;
}
-
/**
* Parse headers and return html
*
@@ -1323,8 +1390,7 @@ class Parser {
wfProfileIn( __METHOD__ );
for ( $i = 6; $i >= 1; --$i ) {
$h = str_repeat( '=', $i );
- $text = preg_replace( "/^$h(.+)$h\\s*$/m",
- "<h$i>\\1</h$i>", $text );
+ $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
}
wfProfileOut( __METHOD__ );
return $text;
@@ -1345,7 +1411,7 @@ class Parser {
foreach ( $lines as $line ) {
$outtext .= $this->doQuotes( $line ) . "\n";
}
- $outtext = substr( $outtext, 0,-1 );
+ $outtext = substr( $outtext, 0, -1 );
wfProfileOut( __METHOD__ );
return $outtext;
}
@@ -1359,170 +1425,192 @@ class Parser {
*/
public function doQuotes( $text ) {
$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
- if ( count( $arr ) == 1 ) {
+ $countarr = count( $arr );
+ if ( $countarr == 1 ) {
return $text;
- } else {
- # First, do some preliminary work. This may shift some apostrophes from
- # being mark-up to being text. It also counts the number of occurrences
- # of bold and italics mark-ups.
- $numbold = 0;
- $numitalics = 0;
- for ( $i = 0; $i < count( $arr ); $i++ ) {
- if ( ( $i % 2 ) == 1 ) {
- # If there are ever four apostrophes, assume the first is supposed to
- # be text, and the remaining three constitute mark-up for bold text.
- if ( strlen( $arr[$i] ) == 4 ) {
- $arr[$i-1] .= "'";
- $arr[$i] = "'''";
- } elseif ( strlen( $arr[$i] ) > 5 ) {
- # If there are more than 5 apostrophes in a row, assume they're all
- # text except for the last 5.
- $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
- $arr[$i] = "'''''";
- }
- # Count the number of occurrences of bold and italics mark-ups.
- # We are not counting sequences of five apostrophes.
- if ( strlen( $arr[$i] ) == 2 ) {
- $numitalics++;
- } elseif ( strlen( $arr[$i] ) == 3 ) {
- $numbold++;
- } elseif ( strlen( $arr[$i] ) == 5 ) {
- $numitalics++;
- $numbold++;
- }
- }
+ }
+
+ // First, do some preliminary work. This may shift some apostrophes from
+ // being mark-up to being text. It also counts the number of occurrences
+ // of bold and italics mark-ups.
+ $numbold = 0;
+ $numitalics = 0;
+ for ( $i = 1; $i < $countarr; $i += 2 ) {
+ $thislen = strlen( $arr[$i] );
+ // If there are ever four apostrophes, assume the first is supposed to
+ // be text, and the remaining three constitute mark-up for bold text.
+ // (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
+ if ( $thislen == 4 ) {
+ $arr[$i - 1] .= "'";
+ $arr[$i] = "'''";
+ $thislen = 3;
+ } elseif ( $thislen > 5 ) {
+ // If there are more than 5 apostrophes in a row, assume they're all
+ // text except for the last 5.
+ // (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
+ $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
+ $arr[$i] = "'''''";
+ $thislen = 5;
+ }
+ // Count the number of occurrences of bold and italics mark-ups.
+ if ( $thislen == 2 ) {
+ $numitalics++;
+ } elseif ( $thislen == 3 ) {
+ $numbold++;
+ } elseif ( $thislen == 5 ) {
+ $numitalics++;
+ $numbold++;
}
+ }
- # If there is an odd number of both bold and italics, it is likely
- # that one of the bold ones was meant to be an apostrophe followed
- # by italics. Which one we cannot know for certain, but it is more
- # likely to be one that has a single-letter word before it.
- if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
- $i = 0;
- $firstsingleletterword = -1;
- $firstmultiletterword = -1;
- $firstspace = -1;
- foreach ( $arr as $r ) {
- if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) ) {
- $x1 = substr( $arr[$i-1], -1 );
- $x2 = substr( $arr[$i-1], -2, 1 );
- if ( $x1 === ' ' ) {
- if ( $firstspace == -1 ) {
- $firstspace = $i;
- }
- } elseif ( $x2 === ' ') {
- if ( $firstsingleletterword == -1 ) {
- $firstsingleletterword = $i;
- }
- } else {
- if ( $firstmultiletterword == -1 ) {
- $firstmultiletterword = $i;
- }
+ // If there is an odd number of both bold and italics, it is likely
+ // that one of the bold ones was meant to be an apostrophe followed
+ // by italics. Which one we cannot know for certain, but it is more
+ // likely to be one that has a single-letter word before it.
+ if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
+ $firstsingleletterword = -1;
+ $firstmultiletterword = -1;
+ $firstspace = -1;
+ for ( $i = 1; $i < $countarr; $i += 2 ) {
+ if ( strlen( $arr[$i] ) == 3 ) {
+ $x1 = substr( $arr[$i - 1], -1 );
+ $x2 = substr( $arr[$i - 1], -2, 1 );
+ if ( $x1 === ' ' ) {
+ if ( $firstspace == -1 ) {
+ $firstspace = $i;
+ }
+ } elseif ( $x2 === ' ' ) {
+ if ( $firstsingleletterword == -1 ) {
+ $firstsingleletterword = $i;
+ // if $firstsingleletterword is set, we don't
+ // look at the other options, so we can bail early.
+ break;
+ }
+ } else {
+ if ( $firstmultiletterword == -1 ) {
+ $firstmultiletterword = $i;
}
}
- $i++;
}
+ }
- # If there is a single-letter word, use it!
- if ( $firstsingleletterword > -1 ) {
- $arr[$firstsingleletterword] = "''";
- $arr[$firstsingleletterword-1] .= "'";
- } elseif ( $firstmultiletterword > -1 ) {
- # If not, but there's a multi-letter word, use that one.
- $arr[$firstmultiletterword] = "''";
- $arr[$firstmultiletterword-1] .= "'";
- } elseif ( $firstspace > -1 ) {
- # ... otherwise use the first one that has neither.
- # (notice that it is possible for all three to be -1 if, for example,
- # there is only one pentuple-apostrophe in the line)
- $arr[$firstspace] = "''";
- $arr[$firstspace-1] .= "'";
- }
+ // If there is a single-letter word, use it!
+ if ( $firstsingleletterword > -1 ) {
+ $arr[$firstsingleletterword] = "''";
+ $arr[$firstsingleletterword - 1] .= "'";
+ } elseif ( $firstmultiletterword > -1 ) {
+ // If not, but there's a multi-letter word, use that one.
+ $arr[$firstmultiletterword] = "''";
+ $arr[$firstmultiletterword - 1] .= "'";
+ } elseif ( $firstspace > -1 ) {
+ // ... otherwise use the first one that has neither.
+ // (notice that it is possible for all three to be -1 if, for example,
+ // there is only one pentuple-apostrophe in the line)
+ $arr[$firstspace] = "''";
+ $arr[$firstspace - 1] .= "'";
}
+ }
- # Now let's actually convert our apostrophic mush to HTML!
- $output = '';
- $buffer = '';
- $state = '';
- $i = 0;
- foreach ( $arr as $r ) {
- if ( ( $i % 2 ) == 0 ) {
- if ( $state === 'both' ) {
- $buffer .= $r;
- } else {
- $output .= $r;
- }
+ // Now let's actually convert our apostrophic mush to HTML!
+ $output = '';
+ $buffer = '';
+ $state = '';
+ $i = 0;
+ foreach ( $arr as $r ) {
+ if ( ( $i % 2 ) == 0 ) {
+ if ( $state === 'both' ) {
+ $buffer .= $r;
} else {
- if ( strlen( $r ) == 2 ) {
- if ( $state === 'i' ) {
- $output .= '</i>'; $state = '';
- } elseif ( $state === 'bi' ) {
- $output .= '</i>'; $state = 'b';
- } elseif ( $state === 'ib' ) {
- $output .= '</b></i><b>'; $state = 'b';
- } elseif ( $state === 'both' ) {
- $output .= '<b><i>'.$buffer.'</i>'; $state = 'b';
- } else { # $state can be 'b' or ''
- $output .= '<i>'; $state .= 'i';
- }
- } elseif ( strlen( $r ) == 3 ) {
- if ( $state === 'b' ) {
- $output .= '</b>'; $state = '';
- } elseif ( $state === 'bi' ) {
- $output .= '</i></b><i>'; $state = 'i';
- } elseif ( $state === 'ib' ) {
- $output .= '</b>'; $state = 'i';
- } elseif ( $state === 'both' ) {
- $output .= '<i><b>'.$buffer.'</b>'; $state = 'i';
- } else { # $state can be 'i' or ''
- $output .= '<b>'; $state .= 'b';
- }
- } elseif ( strlen( $r ) == 5 ) {
- if ( $state === 'b' ) {
- $output .= '</b><i>'; $state = 'i';
- } elseif ( $state === 'i' ) {
- $output .= '</i><b>'; $state = 'b';
- } elseif ( $state === 'bi' ) {
- $output .= '</i></b>'; $state = '';
- } elseif ( $state === 'ib' ) {
- $output .= '</b></i>'; $state = '';
- } elseif ( $state === 'both' ) {
- $output .= '<i><b>'.$buffer.'</b></i>'; $state = '';
- } else { # ($state == '')
- $buffer = ''; $state = 'both';
- }
+ $output .= $r;
+ }
+ } else {
+ $thislen = strlen( $r );
+ if ( $thislen == 2 ) {
+ if ( $state === 'i' ) {
+ $output .= '</i>';
+ $state = '';
+ } elseif ( $state === 'bi' ) {
+ $output .= '</i>';
+ $state = 'b';
+ } elseif ( $state === 'ib' ) {
+ $output .= '</b></i><b>';
+ $state = 'b';
+ } elseif ( $state === 'both' ) {
+ $output .= '<b><i>' . $buffer . '</i>';
+ $state = 'b';
+ } else { // $state can be 'b' or ''
+ $output .= '<i>';
+ $state .= 'i';
+ }
+ } elseif ( $thislen == 3 ) {
+ if ( $state === 'b' ) {
+ $output .= '</b>';
+ $state = '';
+ } elseif ( $state === 'bi' ) {
+ $output .= '</i></b><i>';
+ $state = 'i';
+ } elseif ( $state === 'ib' ) {
+ $output .= '</b>';
+ $state = 'i';
+ } elseif ( $state === 'both' ) {
+ $output .= '<i><b>' . $buffer . '</b>';
+ $state = 'i';
+ } else { // $state can be 'i' or ''
+ $output .= '<b>';
+ $state .= 'b';
+ }
+ } elseif ( $thislen == 5 ) {
+ if ( $state === 'b' ) {
+ $output .= '</b><i>';
+ $state = 'i';
+ } elseif ( $state === 'i' ) {
+ $output .= '</i><b>';
+ $state = 'b';
+ } elseif ( $state === 'bi' ) {
+ $output .= '</i></b>';
+ $state = '';
+ } elseif ( $state === 'ib' ) {
+ $output .= '</b></i>';
+ $state = '';
+ } elseif ( $state === 'both' ) {
+ $output .= '<i><b>' . $buffer . '</b></i>';
+ $state = '';
+ } else { // ($state == '')
+ $buffer = '';
+ $state = 'both';
}
}
- $i++;
- }
- # Now close all remaining tags. Notice that the order is important.
- if ( $state === 'b' || $state === 'ib' ) {
- $output .= '</b>';
}
- if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
- $output .= '</i>';
- }
- if ( $state === 'bi' ) {
- $output .= '</b>';
- }
- # There might be lonely ''''', so make sure we have a buffer
- if ( $state === 'both' && $buffer ) {
- $output .= '<b><i>'.$buffer.'</i></b>';
- }
- return $output;
+ $i++;
+ }
+ // Now close all remaining tags. Notice that the order is important.
+ if ( $state === 'b' || $state === 'ib' ) {
+ $output .= '</b>';
+ }
+ if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
+ $output .= '</i>';
+ }
+ if ( $state === 'bi' ) {
+ $output .= '</b>';
+ }
+ // There might be lonely ''''', so make sure we have a buffer
+ if ( $state === 'both' && $buffer ) {
+ $output .= '<b><i>' . $buffer . '</i></b>';
}
+ return $output;
}
/**
* Replace external links (REL)
*
* Note: this is all very hackish and the order of execution matters a lot.
- * Make sure to run maintenance/parserTests.php if you change this code.
+ * Make sure to run tests/parserTests.php if you change this code.
*
* @private
*
* @param $text string
*
+ * @throws MWException
* @return string
*/
function replaceExternalLinks( $text ) {
@@ -1530,14 +1618,15 @@ class Parser {
$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
if ( $bits === false ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" );
}
$s = array_shift( $bits );
$i = 0;
- while ( $i<count( $bits ) ) {
+ while ( $i < count( $bits ) ) {
$url = $bits[$i++];
- $protocol = $bits[$i++];
+ $i++; // protocol
$text = $bits[$i++];
$trail = $bits[$i++];
@@ -1595,26 +1684,39 @@ class Parser {
wfProfileOut( __METHOD__ );
return $s;
}
-
+ /**
+ * Get the rel attribute for a particular external link.
+ *
+ * @since 1.21
+ * @param string|bool $url optional URL, to extract the domain from for rel =>
+ * nofollow if appropriate
+ * @param $title Title optional Title, for wgNoFollowNsExceptions lookups
+ * @return string|null rel attribute for $url
+ */
+ public static function getExternalLinkRel( $url = false, $title = null ) {
+ global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
+ $ns = $title ? $title->getNamespace() : false;
+ if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) &&
+ !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) )
+ {
+ return 'nofollow';
+ }
+ return null;
+ }
/**
* Get an associative array of additional HTML attributes appropriate for a
* particular external link. This currently may include rel => nofollow
* (depending on configuration, namespace, and the URL's domain) and/or a
* target attribute (depending on configuration).
*
- * @param $url String|bool optional URL, to extract the domain from for rel =>
+ * @param string|bool $url optional URL, to extract the domain from for rel =>
* nofollow if appropriate
* @return Array associative array of HTML attributes
*/
function getExternalLinkAttribs( $url = false ) {
$attribs = array();
- global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
- $ns = $this->mTitle->getNamespace();
- if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) &&
- !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) )
- {
- $attribs['rel'] = 'nofollow';
- }
+ $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle );
+
if ( $this->mOptions->getExternalLinkTarget() ) {
$attribs['target'] = $this->mOptions->getExternalLinkTarget();
}
@@ -1726,6 +1828,8 @@ class Parser {
/**
* Process [[ ]] wikilinks (RIL)
+ * @param $s
+ * @throws MWException
* @return LinkHolderArray
*
* @private
@@ -1733,8 +1837,8 @@ class Parser {
function replaceInternalLinks2( &$s ) {
wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__.'-setup' );
- static $tc = FALSE, $e1, $e1_img;
+ wfProfileIn( __METHOD__ . '-setup' );
+ static $tc = false, $e1, $e1_img;
# the % is needed to support urlencoded titles as well
if ( !$tc ) {
$tc = Title::legalChars() . '#%';
@@ -1763,9 +1867,9 @@ class Parser {
}
if ( is_null( $this->mTitle ) ) {
- wfProfileOut( __METHOD__.'-setup' );
+ wfProfileOut( __METHOD__ . '-setup' );
wfProfileOut( __METHOD__ );
- throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
+ throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
}
$nottalk = !$this->mTitle->isTalkPage();
@@ -1780,17 +1884,11 @@ class Parser {
$prefix = '';
}
- if ( $this->getConverterLanguage()->hasVariants() ) {
- $selflink = $this->getConverterLanguage()->autoConvertToAllVariants(
- $this->mTitle->getPrefixedText() );
- } else {
- $selflink = array( $this->mTitle->getPrefixedText() );
- }
$useSubpages = $this->areSubpagesAllowed();
- wfProfileOut( __METHOD__.'-setup' );
+ wfProfileOut( __METHOD__ . '-setup' );
# Loop for each link
- for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
+ for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
# Check for excessive memory usage
if ( $holders->isBig() ) {
# Too big
@@ -1800,24 +1898,24 @@ class Parser {
}
if ( $useLinkPrefixExtension ) {
- wfProfileIn( __METHOD__.'-prefixhandling' );
+ wfProfileIn( __METHOD__ . '-prefixhandling' );
if ( preg_match( $e2, $s, $m ) ) {
$prefix = $m[2];
$s = $m[1];
} else {
- $prefix='';
+ $prefix = '';
}
# first link
if ( $first_prefix ) {
$prefix = $first_prefix;
$first_prefix = false;
}
- wfProfileOut( __METHOD__.'-prefixhandling' );
+ wfProfileOut( __METHOD__ . '-prefixhandling' );
}
$might_be_img = false;
- wfProfileIn( __METHOD__."-e1" );
+ wfProfileIn( __METHOD__ . "-e1" );
if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
$text = $m[2];
# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
@@ -1839,7 +1937,7 @@ class Parser {
# fix up urlencoded title texts
if ( strpos( $m[1], '%' ) !== false ) {
# Should anchors '#' also be rejected?
- $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), rawurldecode( $m[1] ) );
+ $m[1] = str_replace( array( '<', '>' ), array( '&lt;', '&gt;' ), rawurldecode( $m[1] ) );
}
$trail = $m[3];
} elseif ( preg_match( $e1_img, $line, $m ) ) { # Invalid, but might be an image with a link in its caption
@@ -1850,19 +1948,19 @@ class Parser {
}
$trail = "";
} else { # Invalid form; output directly
- $s .= $prefix . '[[' . $line ;
- wfProfileOut( __METHOD__."-e1" );
+ $s .= $prefix . '[[' . $line;
+ wfProfileOut( __METHOD__ . "-e1" );
continue;
}
- wfProfileOut( __METHOD__."-e1" );
- wfProfileIn( __METHOD__."-misc" );
+ wfProfileOut( __METHOD__ . "-e1" );
+ wfProfileIn( __METHOD__ . "-misc" );
# Don't allow internal links to pages containing
# PROTO: where PROTO is a valid URL protocol; these
# should be external links.
if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $m[1] ) ) {
- $s .= $prefix . '[[' . $line ;
- wfProfileOut( __METHOD__."-misc" );
+ $s .= $prefix . '[[' . $line;
+ wfProfileOut( __METHOD__ . "-misc" );
continue;
}
@@ -1879,21 +1977,21 @@ class Parser {
$link = substr( $link, 1 );
}
- wfProfileOut( __METHOD__."-misc" );
- wfProfileIn( __METHOD__."-title" );
+ wfProfileOut( __METHOD__ . "-misc" );
+ wfProfileIn( __METHOD__ . "-title" );
$nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
if ( $nt === null ) {
$s .= $prefix . '[[' . $line;
- wfProfileOut( __METHOD__."-title" );
+ wfProfileOut( __METHOD__ . "-title" );
continue;
}
$ns = $nt->getNamespace();
$iw = $nt->getInterWiki();
- wfProfileOut( __METHOD__."-title" );
+ wfProfileOut( __METHOD__ . "-title" );
if ( $might_be_img ) { # if this is actually an invalid link
- wfProfileIn( __METHOD__."-might_be_img" );
+ wfProfileIn( __METHOD__ . "-might_be_img" );
if ( $ns == NS_FILE && $noforce ) { # but might be an image
$found = false;
while ( true ) {
@@ -1925,19 +2023,19 @@ class Parser {
$holders->merge( $this->replaceInternalLinks2( $text ) );
$s .= "{$prefix}[[$link|$text";
# note: no $trail, because without an end, there *is* no trail
- wfProfileOut( __METHOD__."-might_be_img" );
+ wfProfileOut( __METHOD__ . "-might_be_img" );
continue;
}
} else { # it's not an image, so output it raw
$s .= "{$prefix}[[$link|$text";
# note: no $trail, because without an end, there *is* no trail
- wfProfileOut( __METHOD__."-might_be_img" );
+ wfProfileOut( __METHOD__ . "-might_be_img" );
continue;
}
- wfProfileOut( __METHOD__."-might_be_img" );
+ wfProfileOut( __METHOD__ . "-might_be_img" );
}
- $wasblank = ( $text == '' );
+ $wasblank = ( $text == '' );
if ( $wasblank ) {
$text = $link;
} else {
@@ -1951,18 +2049,25 @@ class Parser {
# Link not escaped by : , create the various objects
if ( $noforce ) {
# Interwikis
- wfProfileIn( __METHOD__."-interwiki" );
+ wfProfileIn( __METHOD__ . "-interwiki" );
if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
- $this->mOutput->addLanguageLink( $nt->getFullText() );
+ // XXX: the above check prevents links to sites with identifiers that are not language codes
+
+ # Bug 24502: filter duplicates
+ if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
+ $this->mLangLinkLanguages[$iw] = true;
+ $this->mOutput->addLanguageLink( $nt->getFullText() );
+ }
+
$s = rtrim( $s . $prefix );
$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
- wfProfileOut( __METHOD__."-interwiki" );
+ wfProfileOut( __METHOD__ . "-interwiki" );
continue;
}
- wfProfileOut( __METHOD__."-interwiki" );
+ wfProfileOut( __METHOD__ . "-interwiki" );
if ( $ns == NS_FILE ) {
- wfProfileIn( __METHOD__."-image" );
+ wfProfileIn( __METHOD__ . "-image" );
if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
if ( $wasblank ) {
# if no parameters were passed, $text
@@ -1983,12 +2088,12 @@ class Parser {
} else {
$s .= $prefix . $trail;
}
- wfProfileOut( __METHOD__."-image" );
+ wfProfileOut( __METHOD__ . "-image" );
continue;
}
if ( $ns == NS_CATEGORY ) {
- wfProfileIn( __METHOD__."-category" );
+ wfProfileIn( __METHOD__ . "-category" );
$s = rtrim( $s . "\n" ); # bug 87
if ( $wasblank ) {
@@ -2007,23 +2112,23 @@ class Parser {
*/
$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
- wfProfileOut( __METHOD__."-category" );
+ wfProfileOut( __METHOD__ . "-category" );
continue;
}
}
- # Self-link checking
- if ( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
- if ( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
- $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
- continue;
- }
+ # Self-link checking. For some languages, variants of the title are checked in
+ # LinkHolderArray::doVariants() to allow batching the existence checks necessary
+ # for linking to a different variant.
+ if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && $nt->getFragment() === '' ) {
+ $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
+ continue;
}
# NS_MEDIA is a pseudo-namespace for linking directly to a file
# @todo FIXME: Should do batch file existence checks, see comment below
if ( $ns == NS_MEDIA ) {
- wfProfileIn( __METHOD__."-media" );
+ wfProfileIn( __METHOD__ . "-media" );
# Give extensions a chance to select the file revision for us
$options = array();
$descQuery = false;
@@ -2034,11 +2139,11 @@ class Parser {
# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
$s .= $prefix . $this->armorLinks(
Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
- wfProfileOut( __METHOD__."-media" );
+ wfProfileOut( __METHOD__ . "-media" );
continue;
}
- wfProfileIn( __METHOD__."-always_known" );
+ wfProfileIn( __METHOD__ . "-always_known" );
# Some titles, such as valid special pages or files in foreign repos, should
# be shown as bluelinks even though they're not included in the page table
#
@@ -2051,7 +2156,7 @@ class Parser {
# Links will be added to the output link list after checking
$s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix );
}
- wfProfileOut( __METHOD__."-always_known" );
+ wfProfileOut( __METHOD__ . "-always_known" );
}
wfProfileOut( __METHOD__ );
return $holders;
@@ -2066,7 +2171,7 @@ class Parser {
*
* @param $nt Title
* @param $text String
- * @param $query Array or String
+ * @param array $query or String
* @param $trail String
* @param $prefix String
* @return String: HTML-wikitext mix oh yuck
@@ -2093,7 +2198,7 @@ class Parser {
* Not needed quite as much as it used to be since free links are a bit
* more sensible these days. But bracketed links are still an issue.
*
- * @param $text String: more-or-less HTML
+ * @param string $text more-or-less HTML
* @return String: less-or-more HTML with NOPARSE bits
*/
function armorLinks( $text ) {
@@ -2113,7 +2218,7 @@ class Parser {
/**
* Handle link to subpage if necessary
*
- * @param $target String: the source of the link
+ * @param string $target the source of the link
* @param &$text String: the link text, modified as necessary
* @return string the full name of the link
* @private
@@ -2131,7 +2236,7 @@ class Parser {
function closeParagraph() {
$result = '';
if ( $this->mLastSection != '' ) {
- $result = '</' . $this->mLastSection . ">\n";
+ $result = '</' . $this->mLastSection . ">\n";
}
$this->mInPre = false;
$this->mLastSection = '';
@@ -2176,13 +2281,13 @@ class Parser {
$result = $this->closeParagraph();
if ( '*' === $char ) {
- $result .= '<ul><li>';
+ $result .= "<ul>\n<li>";
} elseif ( '#' === $char ) {
- $result .= '<ol><li>';
+ $result .= "<ol>\n<li>";
} elseif ( ':' === $char ) {
- $result .= '<dl><dd>';
+ $result .= "<dl>\n<dd>";
} elseif ( ';' === $char ) {
- $result .= '<dl><dt>';
+ $result .= "<dl>\n<dt>";
$this->mDTopen = true;
} else {
$result = '<!-- ERR 1 -->';
@@ -2200,11 +2305,11 @@ class Parser {
*/
function nextItem( $char ) {
if ( '*' === $char || '#' === $char ) {
- return '</li><li>';
+ return "</li>\n<li>";
} elseif ( ':' === $char || ';' === $char ) {
- $close = '</dd>';
+ $close = "</dd>\n";
if ( $this->mDTopen ) {
- $close = '</dt>';
+ $close = "</dt>\n";
}
if ( ';' === $char ) {
$this->mDTopen = true;
@@ -2226,20 +2331,20 @@ class Parser {
*/
function closeList( $char ) {
if ( '*' === $char ) {
- $text = '</li></ul>';
+ $text = "</li>\n</ul>";
} elseif ( '#' === $char ) {
- $text = '</li></ol>';
+ $text = "</li>\n</ol>";
} elseif ( ':' === $char ) {
if ( $this->mDTopen ) {
$this->mDTopen = false;
- $text = '</dt></dl>';
+ $text = "</dt>\n</dl>";
} else {
- $text = '</dd></dl>';
+ $text = "</dd>\n</dl>";
}
} else {
return '<!-- ERR 3 -->';
}
- return $text."\n";
+ return $text . "\n";
}
/**#@-*/
@@ -2264,6 +2369,7 @@ class Parser {
$this->mDTopen = $inBlockElem = false;
$prefixLength = 0;
$paragraphStack = false;
+ $inBlockquote = false;
foreach ( $textLines as $oLine ) {
# Fix up $linestart
@@ -2306,7 +2412,7 @@ class Parser {
$output .= $this->nextItem( substr( $prefix, -1 ) );
$paragraphStack = false;
- if ( substr( $prefix, -1 ) === ';') {
+ if ( substr( $prefix, -1 ) === ';' ) {
# The one nasty exception: definition lists work like this:
# ; title : definition text
# So we check for : in the remainder text to split up the
@@ -2326,13 +2432,13 @@ class Parser {
# Close all the prefixes which aren't shared.
while ( $commonPrefixLength < $lastPrefixLength ) {
- $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
+ $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
--$lastPrefixLength;
}
# Continue the current prefix if appropriate.
if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
- $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
+ $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
}
# Open prefixes where appropriate.
@@ -2354,13 +2460,13 @@ class Parser {
# If we have no prefixes, go to paragraph mode.
if ( 0 == $prefixLength ) {
- wfProfileIn( __METHOD__."-paragraph" );
+ wfProfileIn( __METHOD__ . "-paragraph" );
# No prefix (not in list)--go to paragraph mode
# XXX: use a stack for nestable elements like span, table and div
- $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
+ $openmatch = preg_match( '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
$closematch = preg_match(
- '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
- '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
+ '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|' .
+ '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|' . $this->mUniqPrefix . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS', $t );
if ( $openmatch or $closematch ) {
$paragraphStack = false;
# TODO bug 5718: paragraph closed
@@ -2368,13 +2474,18 @@ class Parser {
if ( $preOpenMatch and !$preCloseMatch ) {
$this->mInPre = true;
}
+ $bqOffset = 0;
+ while ( preg_match( '/<(\\/?)blockquote[\s>]/i', $t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) {
+ $inBlockquote = !$bqMatch[1][0]; // is this a close tag?
+ $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
+ }
$inBlockElem = !$closematch;
} elseif ( !$inBlockElem && !$this->mInPre ) {
- if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) ) {
+ if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) and !$inBlockquote ) {
# pre
if ( $this->mLastSection !== 'pre' ) {
$paragraphStack = false;
- $output .= $this->closeParagraph().'<pre>';
+ $output .= $this->closeParagraph() . '<pre>';
$this->mLastSection = 'pre';
}
$t = substr( $t, 1 );
@@ -2382,7 +2493,7 @@ class Parser {
# paragraph
if ( trim( $t ) === '' ) {
if ( $paragraphStack ) {
- $output .= $paragraphStack.'<br />';
+ $output .= $paragraphStack . '<br />';
$paragraphStack = false;
$this->mLastSection = 'p';
} else {
@@ -2400,24 +2511,24 @@ class Parser {
$paragraphStack = false;
$this->mLastSection = 'p';
} elseif ( $this->mLastSection !== 'p' ) {
- $output .= $this->closeParagraph().'<p>';
+ $output .= $this->closeParagraph() . '<p>';
$this->mLastSection = 'p';
}
}
}
}
- wfProfileOut( __METHOD__."-paragraph" );
+ wfProfileOut( __METHOD__ . "-paragraph" );
}
# somewhere above we forget to get out of pre block (bug 785)
if ( $preCloseMatch && $this->mInPre ) {
$this->mInPre = false;
}
if ( $paragraphStack === false ) {
- $output .= $t."\n";
+ $output .= $t . "\n";
}
}
while ( $prefixLength ) {
- $output .= $this->closeList( $prefix2[$prefixLength-1] );
+ $output .= $this->closeList( $prefix2[$prefixLength - 1] );
--$prefixLength;
}
if ( $this->mLastSection != '' ) {
@@ -2433,9 +2544,10 @@ class Parser {
* Split up a string on ':', ignoring any occurrences inside tags
* to prevent illegal overlapping.
*
- * @param $str String the string to split
+ * @param string $str the string to split
* @param &$before String set to everything before the ':'
* @param &$after String set to everything after the ':'
+ * @throws MWException
* @return String the position of the ':', or false if none found
*/
function findColonNoLinks( $str, &$before, &$after ) {
@@ -2452,7 +2564,7 @@ class Parser {
if ( $lt === false || $lt > $pos ) {
# Easy; no tag nesting to worry about
$before = substr( $str, 0, $pos );
- $after = substr( $str, $pos+1 );
+ $after = substr( $str, $pos + 1 );
wfProfileOut( __METHOD__ );
return $pos;
}
@@ -2461,13 +2573,13 @@ class Parser {
$state = self::COLON_STATE_TEXT;
$stack = 0;
$len = strlen( $str );
- for( $i = 0; $i < $len; $i++ ) {
+ for ( $i = 0; $i < $len; $i++ ) {
$c = $str[$i];
- switch( $state ) {
+ switch ( $state ) {
# (Using the number is a performance hack for common cases)
case 0: # self::COLON_STATE_TEXT:
- switch( $c ) {
+ switch ( $c ) {
case "<":
# Could be either a <start> tag or an </end> tag
$state = self::COLON_STATE_TAGSTART;
@@ -2512,7 +2624,7 @@ class Parser {
break;
case 1: # self::COLON_STATE_TAG:
# In a <tag>
- switch( $c ) {
+ switch ( $c ) {
case ">":
$stack++;
$state = self::COLON_STATE_TEXT;
@@ -2526,7 +2638,7 @@ class Parser {
}
break;
case 2: # self::COLON_STATE_TAGSTART:
- switch( $c ) {
+ switch ( $c ) {
case "/":
$state = self::COLON_STATE_CLOSETAG;
break;
@@ -2546,7 +2658,7 @@ class Parser {
if ( $c === ">" ) {
$stack--;
if ( $stack < 0 ) {
- wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
+ wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
wfProfileOut( __METHOD__ );
return false;
}
@@ -2582,11 +2694,12 @@ class Parser {
}
break;
default:
+ wfProfileOut( __METHOD__ );
throw new MWException( "State machine error in " . __METHOD__ );
}
}
if ( $stack > 0 ) {
- wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
+ wfDebug( __METHOD__ . ": Invalid input; not enough close tags (stack $stack, state $state)\n" );
wfProfileOut( __METHOD__ );
return false;
}
@@ -2600,8 +2713,9 @@ class Parser {
* @private
*
* @param $index integer
- * @param $frame PPFrame
+ * @param bool|\PPFrame $frame
*
+ * @throws MWException
* @return string
*/
function getVariableValue( $index, $frame = false ) {
@@ -2630,71 +2744,50 @@ class Parser {
$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
- # Use the time zone
- global $wgLocaltimezone;
- if ( isset( $wgLocaltimezone ) ) {
- $oldtz = date_default_timezone_get();
- date_default_timezone_set( $wgLocaltimezone );
- }
-
- $localTimestamp = date( 'YmdHis', $ts );
- $localMonth = date( 'm', $ts );
- $localMonth1 = date( 'n', $ts );
- $localMonthName = date( 'n', $ts );
- $localDay = date( 'j', $ts );
- $localDay2 = date( 'd', $ts );
- $localDayOfWeek = date( 'w', $ts );
- $localWeek = date( 'W', $ts );
- $localYear = date( 'Y', $ts );
- $localHour = date( 'H', $ts );
- if ( isset( $wgLocaltimezone ) ) {
- date_default_timezone_set( $oldtz );
- }
-
$pageLang = $this->getFunctionLang();
switch ( $index ) {
case 'currentmonth':
- $value = $pageLang->formatNum( gmdate( 'm', $ts ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
break;
case 'currentmonth1':
- $value = $pageLang->formatNum( gmdate( 'n', $ts ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
break;
case 'currentmonthname':
- $value = $pageLang->getMonthName( gmdate( 'n', $ts ) );
+ $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
break;
case 'currentmonthnamegen':
- $value = $pageLang->getMonthNameGen( gmdate( 'n', $ts ) );
+ $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
break;
case 'currentmonthabbrev':
- $value = $pageLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+ $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
break;
case 'currentday':
- $value = $pageLang->formatNum( gmdate( 'j', $ts ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
break;
case 'currentday2':
- $value = $pageLang->formatNum( gmdate( 'd', $ts ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
break;
case 'localmonth':
- $value = $pageLang->formatNum( $localMonth );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
break;
case 'localmonth1':
- $value = $pageLang->formatNum( $localMonth1 );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
break;
case 'localmonthname':
- $value = $pageLang->getMonthName( $localMonthName );
+ $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
break;
case 'localmonthnamegen':
- $value = $pageLang->getMonthNameGen( $localMonthName );
+ $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
break;
case 'localmonthabbrev':
- $value = $pageLang->getMonthAbbreviation( $localMonthName );
+ $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
break;
case 'localday':
- $value = $pageLang->formatNum( $localDay );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
break;
case 'localday2':
- $value = $pageLang->formatNum( $localDay2 );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
break;
case 'pagename':
$value = wfEscapeWikiText( $this->mTitle->getText() );
@@ -2714,6 +2807,12 @@ class Parser {
case 'subpagenamee':
$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
break;
+ case 'rootpagename':
+ $value = wfEscapeWikiText( $this->mTitle->getRootText() );
+ break;
+ case 'rootpagenamee':
+ $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getRootText() ) ) );
+ break;
case 'basepagename':
$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
break;
@@ -2731,7 +2830,7 @@ class Parser {
case 'talkpagenamee':
if ( $this->mTitle->canTalk() ) {
$talkPage = $this->mTitle->getTalkPage();
- $value = wfEscapeWikiText( $talkPage->getPrefixedUrl() );
+ $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
} else {
$value = '';
}
@@ -2742,11 +2841,11 @@ class Parser {
break;
case 'subjectpagenamee':
$subjPage = $this->mTitle->getSubjectPage();
- $value = wfEscapeWikiText( $subjPage->getPrefixedUrl() );
+ $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
break;
case 'pageid': // requested in bug 23427
- $pageid = $this->getTitle()->getArticleId();
- if( $pageid == 0 ) {
+ $pageid = $this->getTitle()->getArticleID();
+ if ( $pageid == 0 ) {
# 0 means the page doesn't exist in the database,
# which means the user is previewing a new page.
# The vary-revision flag must be set, because the magic word
@@ -2812,8 +2911,15 @@ class Parser {
wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
$value = $this->getRevisionUser();
break;
+ case 'revisionsize':
+ # 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__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
+ $value = $this->getRevisionSize();
+ break;
case 'namespace':
- $value = str_replace( '_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
break;
case 'namespacee':
$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
@@ -2822,56 +2928,56 @@ class Parser {
$value = $this->mTitle->getNamespace();
break;
case 'talkspace':
- $value = $this->mTitle->canTalk() ? str_replace( '_',' ',$this->mTitle->getTalkNsText() ) : '';
+ $value = $this->mTitle->canTalk() ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() ) : '';
break;
case 'talkspacee':
$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
break;
case 'subjectspace':
- $value = $this->mTitle->getSubjectNsText();
+ $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
break;
case 'subjectspacee':
$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
break;
case 'currentdayname':
- $value = $pageLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+ $value = $pageLang->getWeekdayName( MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
break;
case 'currentyear':
- $value = $pageLang->formatNum( gmdate( 'Y', $ts ), true );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
break;
case 'currenttime':
$value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
break;
case 'currenthour':
- $value = $pageLang->formatNum( gmdate( 'H', $ts ), true );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
break;
case 'currentweek':
# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
# int to remove the padding
- $value = $pageLang->formatNum( (int)gmdate( 'W', $ts ) );
+ $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
break;
case 'currentdow':
- $value = $pageLang->formatNum( gmdate( 'w', $ts ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
break;
case 'localdayname':
- $value = $pageLang->getWeekdayName( $localDayOfWeek + 1 );
+ $value = $pageLang->getWeekdayName( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1 );
break;
case 'localyear':
- $value = $pageLang->formatNum( $localYear, true );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
break;
case 'localtime':
- $value = $pageLang->time( $localTimestamp, false, false );
+ $value = $pageLang->time( MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ), false, false );
break;
case 'localhour':
- $value = $pageLang->formatNum( $localHour, true );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
break;
case 'localweek':
# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
# int to remove the padding
- $value = $pageLang->formatNum( (int)$localWeek );
+ $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
break;
case 'localdow':
- $value = $pageLang->formatNum( $localDayOfWeek );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
break;
case 'numberofarticles':
$value = $pageLang->formatNum( SiteStats::articles() );
@@ -2902,7 +3008,7 @@ class Parser {
$value = wfTimestamp( TS_MW, $ts );
break;
case 'localtimestamp':
- $value = $localTimestamp;
+ $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
break;
case 'currentversion':
$value = SpecialVersion::getVersion();
@@ -2960,7 +3066,7 @@ class Parser {
* Preprocess some wikitext and return the document tree.
* This is the ghost of replace_variables().
*
- * @param $text String: The text to parse
+ * @param string $text The text to parse
* @param $flags Integer: bitwise combination of:
* self::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being
* included. Default is to assume a direct page view.
@@ -3015,7 +3121,7 @@ class Parser {
* self::OT_PREPROCESS: templates but not extension tags
* self::OT_HTML: all templates and extension tags
*
- * @param $text String the text to transform
+ * @param string $text the text to transform
* @param $frame PPFrame 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.
@@ -3034,7 +3140,7 @@ class Parser {
if ( $frame === false ) {
$frame = $this->getPreprocessor()->newFrame();
} elseif ( !( $frame instanceof PPFrame ) ) {
- wfDebug( __METHOD__." called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
+ wfDebug( __METHOD__ . " called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
$frame = $this->getPreprocessor()->newCustomFrame( $frame );
}
@@ -3062,7 +3168,7 @@ class Parser {
$assocArgs[$index++] = $arg;
} else {
$name = trim( substr( $arg, 0, $eqpos ) );
- $value = trim( substr( $arg, $eqpos+1 ) );
+ $value = trim( substr( $arg, $eqpos + 1 ) );
if ( $value === false ) {
$value = '';
}
@@ -3079,7 +3185,7 @@ class Parser {
* Warn the user when a parser limitation is reached
* Will warn at most once the user per limitation type
*
- * @param $limitationType String: should be one of:
+ * @param string $limitationType should be one of:
* 'expensive-parserfunction' (corresponding messages:
* 'expensive-parserfunction-warning',
* 'expensive-parserfunction-category')
@@ -3089,14 +3195,20 @@ class Parser {
* 'post-expand-template-inclusion' (corresponding messages:
* 'post-expand-template-inclusion-warning',
* 'post-expand-template-inclusion-category')
- * @param $current int|null Current value
- * @param $max int|null Maximum allowed, when an explicit limit has been
+ * 'node-count-exceeded' (corresponding messages:
+ * 'node-count-exceeded-warning',
+ * 'node-count-exceeded-category')
+ * 'expansion-depth-exceeded' (corresponding messages:
+ * 'expansion-depth-exceeded-warning',
+ * 'expansion-depth-exceeded-category')
+ * @param int|null $current Current value
+ * @param int|null $max Maximum allowed, when an explicit limit has been
* exceeded, provide the values (optional)
*/
function limitationWarn( $limitationType, $current = '', $max = '' ) {
# does no harm if $current and $max are present but are unnecessary for the message
$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
- ->inContentLanguage()->escaped();
+ ->inLanguage( $this->mOptions->getUserLangObj() )->text();
$this->mOutput->addWarning( $warning );
$this->addTrackingCategory( "$limitationType-category" );
}
@@ -3105,18 +3217,18 @@ class Parser {
* Return the text of a template, after recursively
* replacing any variables or templates within the template.
*
- * @param $piece Array: the parts of the template
+ * @param array $piece the parts of the template
* $piece['title']: the title, i.e. the part before the |
* $piece['parts']: the parameter array
* $piece['lineStart']: whether the brace was at the start of a line
* @param $frame PPFrame The current frame, contains template arguments
+ * @throws MWException
* @return String: the text of the template
* @private
*/
function braceSubstitution( $piece, $frame ) {
- global $wgContLang;
wfProfileIn( __METHOD__ );
- wfProfileIn( __METHOD__.'-setup' );
+ wfProfileIn( __METHOD__ . '-setup' );
# Flags
$found = false; # $text has been filled
@@ -3141,12 +3253,12 @@ class Parser {
# $args is a list of argument nodes, starting from index 0, not including $part1
# @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object
$args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
- wfProfileOut( __METHOD__.'-setup' );
+ wfProfileOut( __METHOD__ . '-setup' );
$titleProfileIn = null; // profile templates
# SUBST
- wfProfileIn( __METHOD__.'-modifiers' );
+ wfProfileIn( __METHOD__ . '-modifiers' );
if ( !$found ) {
$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
@@ -3203,7 +3315,7 @@ class Parser {
$forceRawInterwiki = true;
}
}
- wfProfileOut( __METHOD__.'-modifiers' );
+ wfProfileOut( __METHOD__ . '-modifiers' );
# Parser functions
if ( !$found ) {
@@ -3211,70 +3323,23 @@ class Parser {
$colonPos = strpos( $part1, ':' );
if ( $colonPos !== false ) {
- # Case sensitive functions
- $function = substr( $part1, 0, $colonPos );
- if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
- $function = $this->mFunctionSynonyms[1][$function];
- } else {
- # Case insensitive functions
- $function = $wgContLang->lc( $function );
- if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
- $function = $this->mFunctionSynonyms[0][$function];
- } else {
- $function = false;
- }
+ $func = substr( $part1, 0, $colonPos );
+ $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
+ for ( $i = 0; $i < $args->getLength(); $i++ ) {
+ $funcArgs[] = $args->item( $i );
}
- if ( $function ) {
- wfProfileIn( __METHOD__ . '-pfunc-' . $function );
- list( $callback, $flags ) = $this->mFunctionHooks[$function];
- $initialArgs = array( &$this );
- $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
- if ( $flags & SFH_OBJECT_ARGS ) {
- # Add a frame parameter, and pass the arguments as an array
- $allArgs = $initialArgs;
- $allArgs[] = $frame;
- for ( $i = 0; $i < $args->getLength(); $i++ ) {
- $funcArgs[] = $args->item( $i );
- }
- $allArgs[] = $funcArgs;
- } else {
- # Convert arguments to plain text
- for ( $i = 0; $i < $args->getLength(); $i++ ) {
- $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
- }
- $allArgs = array_merge( $initialArgs, $funcArgs );
- }
-
- # Workaround for PHP bug 35229 and similar
- if ( !is_callable( $callback ) ) {
- wfProfileOut( __METHOD__ . '-pfunc-' . $function );
- 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];
- unset( $result[0] );
- }
-
- # Extract flags into the local scope
- # This allows callers to set flags such as nowiki, found, etc.
- extract( $result );
- } else {
- $text = $result;
- }
- if ( !$noparse ) {
- $text = $this->preprocessToDom( $text, $preprocessFlags );
- $isChildObj = true;
- }
- wfProfileOut( __METHOD__ . '-pfunc-' . $function );
+ try {
+ $result = $this->callParserFunction( $frame, $func, $funcArgs );
+ } catch ( Exception $ex ) {
+ wfProfileOut( __METHOD__ . '-pfunc' );
+ wfProfileOut( __METHOD__ );
+ throw $ex;
}
+
+ # The interface for parser functions allows for extracting
+ # flags into the local scope. Extract any forwarded flags
+ # here.
+ extract( $result );
}
wfProfileOut( __METHOD__ . '-pfunc' );
}
@@ -3285,8 +3350,9 @@ class Parser {
$ns = NS_TEMPLATE;
# Split the title into page and subpage
$subpage = '';
- $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
- if ( $subpage !== '' ) {
+ $relative = $this->maybeDoSubpageLink( $part1, $subpage );
+ if ( $part1 !== $relative ) {
+ $part1 = $relative;
$ns = $this->mTitle->getNamespace();
}
$title = Title::newFromText( $part1, $ns );
@@ -3312,7 +3378,7 @@ class Parser {
if ( !$found && $title ) {
if ( !Profiler::instance()->isPersistent() ) {
# Too many unique items can kill profiling DBs/collectors
- $titleProfileIn = __METHOD__ . "-title-" . $title->getDBKey();
+ $titleProfileIn = __METHOD__ . "-title-" . $title->getPrefixedDBkey();
wfProfileIn( $titleProfileIn ); // template in
}
wfProfileIn( __METHOD__ . '-loadtpl' );
@@ -3350,7 +3416,7 @@ class Parser {
}
} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
$found = false; # access denied
- wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
+ wfDebug( __METHOD__ . ": template inclusion denied for " . $title->getPrefixedDBkey() );
} else {
list( $text, $title ) = $this->getTemplateDom( $title );
if ( $text !== false ) {
@@ -3385,7 +3451,7 @@ class Parser {
$text = '<span class="error">'
. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
. '</span>';
- wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
+ wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
}
wfProfileOut( __METHOD__ . '-loadtpl' );
}
@@ -3442,7 +3508,7 @@ class Parser {
{
# Bug 529: if the template begins with a table or block-level
# element, it should be treated as beginning a new line.
- # This behaviour is somewhat controversial.
+ # This behavior is somewhat controversial.
$text = "\n" . $text;
}
@@ -3472,6 +3538,120 @@ class Parser {
}
/**
+ * Call a parser function and return an array with text and flags.
+ *
+ * The returned array will always contain a boolean 'found', indicating
+ * whether the parser function was found or not. It may also contain the
+ * following:
+ * text: string|object, resulting wikitext or PP DOM object
+ * isHTML: bool, $text is HTML, armour it against wikitext transformation
+ * isChildObj: bool, $text is a DOM node needing expansion in a child frame
+ * isLocalObj: bool, $text is a DOM node needing expansion in the current frame
+ * nowiki: bool, wiki markup in $text should be escaped
+ *
+ * @since 1.21
+ * @param $frame PPFrame The current frame, contains template arguments
+ * @param $function string Function name
+ * @param $args array Arguments to the function
+ * @return array
+ */
+ public function callParserFunction( $frame, $function, array $args = array() ) {
+ global $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ # Case sensitive functions
+ if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
+ $function = $this->mFunctionSynonyms[1][$function];
+ } else {
+ # Case insensitive functions
+ $function = $wgContLang->lc( $function );
+ if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
+ $function = $this->mFunctionSynonyms[0][$function];
+ } else {
+ wfProfileOut( __METHOD__ );
+ return array( 'found' => false );
+ }
+ }
+
+ wfProfileIn( __METHOD__ . '-pfunc-' . $function );
+ list( $callback, $flags ) = $this->mFunctionHooks[$function];
+
+ # Workaround for PHP bug 35229 and similar
+ if ( !is_callable( $callback ) ) {
+ wfProfileOut( __METHOD__ . '-pfunc-' . $function );
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Tag hook for $function is not callable\n" );
+ }
+
+ $allArgs = array( &$this );
+ if ( $flags & SFH_OBJECT_ARGS ) {
+ # Convert arguments to PPNodes and collect for appending to $allArgs
+ $funcArgs = array();
+ foreach ( $args as $k => $v ) {
+ if ( $v instanceof PPNode || $k === 0 ) {
+ $funcArgs[] = $v;
+ } else {
+ $funcArgs[] = $this->mPreprocessor->newPartNodeArray( array( $k => $v ) )->item( 0 );
+ }
+ }
+
+ # Add a frame parameter, and pass the arguments as an array
+ $allArgs[] = $frame;
+ $allArgs[] = $funcArgs;
+ } else {
+ # Convert arguments to plain text and append to $allArgs
+ foreach ( $args as $k => $v ) {
+ if ( $v instanceof PPNode ) {
+ $allArgs[] = trim( $frame->expand( $v ) );
+ } elseif ( is_int( $k ) && $k >= 0 ) {
+ $allArgs[] = trim( $v );
+ } else {
+ $allArgs[] = trim( "$k=$v" );
+ }
+ }
+ }
+
+ $result = call_user_func_array( $callback, $allArgs );
+
+ # The interface for function hooks allows them to return a wikitext
+ # string or an array containing the string and any flags. This mungs
+ # things around to match what this method should return.
+ if ( !is_array( $result ) ) {
+ $result = array(
+ 'found' => true,
+ 'text' => $result,
+ );
+ } else {
+ if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
+ $result['text'] = $result[0];
+ }
+ unset( $result[0] );
+ $result += array(
+ 'found' => true,
+ );
+ }
+
+ $noparse = true;
+ $preprocessFlags = 0;
+ if ( isset( $result['noparse'] ) ) {
+ $noparse = $result['noparse'];
+ }
+ if ( isset( $result['preprocessFlags'] ) ) {
+ $preprocessFlags = $result['preprocessFlags'];
+ }
+
+ if ( !$noparse ) {
+ $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
+ $result['isChildObj'] = true;
+ }
+ wfProfileOut( __METHOD__ . '-pfunc-' . $function );
+ wfProfileOut( __METHOD__ );
+
+ return $result;
+ }
+
+ /**
* Get the semi-parsed DOM representation of a template with a given title,
* and its redirect destination title. Cached.
*
@@ -3501,7 +3681,7 @@ class Parser {
}
$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
- $this->mTplDomCache[ $titleText ] = $dom;
+ $this->mTplDomCache[$titleText] = $dom;
if ( !$title->equals( $cacheTitle ) ) {
$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
@@ -3524,6 +3704,11 @@ class Parser {
if ( isset( $stuff['deps'] ) ) {
foreach ( $stuff['deps'] as $dep ) {
$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
+ if ( $dep['title']->equals( $this->getTitle() ) ) {
+ // If we transclude ourselves, the final result
+ // will change based on the new version of the page
+ $this->mOutput->setFlag( 'vary-revision' );
+ }
}
}
return array( $text, $finalTitle );
@@ -3563,9 +3748,9 @@ class Parser {
if ( $skip ) {
$text = false;
$deps[] = array(
- 'title' => $title,
- 'page_id' => $title->getArticleID(),
- 'rev_id' => null
+ 'title' => $title,
+ 'page_id' => $title->getArticleID(),
+ 'rev_id' => null
);
break;
}
@@ -3581,19 +3766,25 @@ class Parser {
}
$deps[] = array(
- 'title' => $title,
- 'page_id' => $title->getArticleID(),
- 'rev_id' => $rev_id );
+ 'title' => $title,
+ 'page_id' => $title->getArticleID(),
+ 'rev_id' => $rev_id );
if ( $rev && !$title->equals( $rev->getTitle() ) ) {
# We fetched a rev from a different title; register it too...
$deps[] = array(
- 'title' => $rev->getTitle(),
- 'page_id' => $rev->getPage(),
- 'rev_id' => $rev_id );
+ 'title' => $rev->getTitle(),
+ 'page_id' => $rev->getPage(),
+ 'rev_id' => $rev_id );
}
if ( $rev ) {
- $text = $rev->getText();
+ $content = $rev->getContent();
+ $text = $content ? $content->getWikitextForTransclusion() : null;
+
+ if ( $text === false || $text === null ) {
+ $text = false;
+ break;
+ }
} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
global $wgContLang;
$message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
@@ -3601,16 +3792,17 @@ class Parser {
$text = false;
break;
}
+ $content = $message->content();
$text = $message->plain();
} else {
break;
}
- if ( $text === false ) {
+ if ( !$content ) {
break;
}
# Redirect?
$finalTitle = $title;
- $title = Title::newFromRedirect( $text );
+ $title = $content->getRedirectTarget();
}
return array(
'text' => $text,
@@ -3622,7 +3814,7 @@ class Parser {
* Fetch a file and its title and register a reference to it.
* If 'broken' is a key in $options then the file will appear as a broken thumbnail.
* @param Title $title
- * @param Array $options Array of options to RepoGroup::findFile
+ * @param array $options Array of options to RepoGroup::findFile
* @return File|bool
*/
function fetchFile( $title, $options = array() ) {
@@ -3634,17 +3826,12 @@ class Parser {
* Fetch a file and its title and register a reference to it.
* If 'broken' is a key in $options then the file will appear as a broken thumbnail.
* @param Title $title
- * @param Array $options Array of options to RepoGroup::findFile
+ * @param array $options Array of options to RepoGroup::findFile
* @return Array ( File or false, Title of file )
*/
function fetchFileAndTitle( $title, $options = array() ) {
- if ( isset( $options['broken'] ) ) {
- $file = false; // broken thumbnail forced by hook
- } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
- $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
- } else { // get by (name,timestamp)
- $file = wfFindFile( $title, $options );
- }
+ $file = $this->fetchFileNoRegister( $title, $options );
+
$time = $file ? $file->getTimestamp() : false;
$sha1 = $file ? $file->getSha1() : false;
# Register the file as a dependency...
@@ -3663,6 +3850,27 @@ class Parser {
}
/**
+ * Helper function for fetchFileAndTitle.
+ *
+ * Also useful if you need to fetch a file but not use it yet,
+ * for example to get the file's handler.
+ *
+ * @param Title $title
+ * @param array $options Array of options to RepoGroup::findFile
+ * @return File or false
+ */
+ protected function fetchFileNoRegister( $title, $options = array() ) {
+ if ( isset( $options['broken'] ) ) {
+ $file = false; // broken thumbnail forced by hook
+ } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
+ $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
+ } else { // get by (name,timestamp)
+ $file = wfFindFile( $title, $options );
+ }
+ return $file;
+ }
+
+ /**
* Transclude an interwiki link.
*
* @param $title Title
@@ -3674,10 +3882,10 @@ class Parser {
global $wgEnableScaryTranscluding;
if ( !$wgEnableScaryTranscluding ) {
- return wfMessage('scarytranscludedisabled')->inContentLanguage()->text();
+ return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
}
- $url = $title->getFullUrl( "action=$action" );
+ $url = $title->getFullURL( array( 'action' => $action ) );
if ( strlen( $url ) > 255 ) {
return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
@@ -3693,23 +3901,28 @@ class Parser {
global $wgTranscludeCacheExpiry;
$dbr = wfGetDB( DB_SLAVE );
$tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
- $obj = $dbr->selectRow( 'transcache', array('tc_time', 'tc_contents' ),
+ $obj = $dbr->selectRow( 'transcache', array( 'tc_time', 'tc_contents' ),
array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
if ( $obj ) {
return $obj->tc_contents;
}
- $text = Http::get( $url );
- if ( !$text ) {
+ $req = MWHttpRequest::factory( $url );
+ $status = $req->execute(); // Status object
+ if ( $status->isOK() ) {
+ $text = $req->getContent();
+ } elseif ( $req->getStatus() != 200 ) { // Though we failed to fetch the content, this status is useless.
+ return wfMessage( 'scarytranscludefailed-httpstatus', $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
+ } else {
return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
}
$dbw = wfGetDB( DB_MASTER );
- $dbw->replace( 'transcache', array('tc_url'), array(
+ $dbw->replace( 'transcache', array( 'tc_url' ), array(
'tc_url' => $url,
'tc_time' => $dbw->timestamp( time() ),
- 'tc_contents' => $text)
- );
+ 'tc_contents' => $text
+ ) );
return $text;
}
@@ -3731,7 +3944,7 @@ class Parser {
$argName = trim( $nameWithSpaces );
$object = false;
$text = $frame->getArgument( $argName );
- if ( $text === false && $parts->getLength() > 0
+ if ( $text === false && $parts->getLength() > 0
&& (
$this->ot['html']
|| $this->ot['pre']
@@ -3767,7 +3980,7 @@ class Parser {
* Return the text to be used for a given extension tag.
* This is the ghost of strip().
*
- * @param $params array Associative array of parameters:
+ * @param array $params Associative array of parameters:
* name PPNode for the tag name
* attr PPNode for unparsed text where tag attributes are thought to be
* attributes Optional associative array of parsed attributes
@@ -3775,6 +3988,7 @@ class Parser {
* noClose Original text did not have a close tag
* @param $frame PPFrame
*
+ * @throws MWException
* @return string
*/
function extensionSubstitution( $params, $frame ) {
@@ -3783,7 +3997,7 @@ class Parser {
$content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
$marker = "{$this->mUniqPrefix}-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
- $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower($name)] ) &&
+ $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
( $this->ot['html'] || $this->ot['pre'] );
if ( $isFunctionTag ) {
$markerType = 'none';
@@ -3805,7 +4019,7 @@ class Parser {
$output = call_user_func_array( $this->mTagHooks[$name],
array( $content, $attributes, $this, $frame ) );
} elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
- list( $callback, $flags ) = $this->mFunctionTagHooks[$name];
+ list( $callback, ) = $this->mFunctionTagHooks[$name];
if ( !is_callable( $callback ) ) {
throw new MWException( "Tag hook for $name is not callable\n" );
}
@@ -3848,7 +4062,7 @@ class Parser {
} elseif ( $markerType === 'general' ) {
$this->mStripState->addGeneral( $marker, $output );
} else {
- throw new MWException( __METHOD__.': invalid marker type' );
+ throw new MWException( __METHOD__ . ': invalid marker type' );
}
return $marker;
}
@@ -3856,7 +4070,7 @@ class Parser {
/**
* Increment an include size counter
*
- * @param $type String: the type of expansion
+ * @param string $type the type of expansion
* @param $size Integer: the size of the text
* @return Boolean: false if this inclusion would take it over the maximum, true otherwise
*/
@@ -3942,12 +4156,12 @@ class Parser {
* 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
+ * @param string $msg message key
* @return Boolean: whether the addition was successful
*/
public function addTrackingCategory( $msg ) {
if ( $this->mTitle->getNamespace() === NS_SPECIAL ) {
- wfDebug( __METHOD__.": Not adding tracking category $msg to special page!\n" );
+ wfDebug( __METHOD__ . ": Not adding tracking category $msg to special page!\n" );
return false;
}
// Important to parse with correct title (bug 31469)
@@ -3966,7 +4180,7 @@ class Parser {
$this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
return true;
} else {
- wfDebug( __METHOD__.": [[MediaWiki:$msg]] is not a valid title!\n" );
+ wfDebug( __METHOD__ . ": [[MediaWiki:$msg]] is not a valid title!\n" );
return false;
}
}
@@ -3982,13 +4196,13 @@ class Parser {
* string and re-inserts the newly formatted headlines.
*
* @param $text String
- * @param $origText String: original, untouched wikitext
+ * @param string $origText original, untouched wikitext
* @param $isMain Boolean
* @return mixed|string
* @private
*/
- function formatHeadings( $text, $origText, $isMain=true ) {
- global $wgMaxTocLevel, $wgHtml5, $wgExperimentalHtmlIds;
+ function formatHeadings( $text, $origText, $isMain = true ) {
+ global $wgMaxTocLevel, $wgExperimentalHtmlIds;
# Inhibit editsection links if requested in the page
if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
@@ -4004,7 +4218,7 @@ class Parser {
# Get all headlines for numbering them and adding funky stuff like [edit]
# links - this is for later, but we need the number of headlines right now
$matches = array();
- $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
+ $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?' . '>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i', $text, $matches );
# if there are fewer than 4 headlines in the article, do not show TOC
# unless it's been explicitly enabled.
@@ -4062,11 +4276,11 @@ class Parser {
$sectionIndex = false;
$numbering = '';
$markerMatches = array();
- if ( preg_match("/^$markerRegex/", $headline, $markerMatches ) ) {
+ if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
$serial = $markerMatches[1];
list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
$isTemplate = ( $titleText != $baseTitleText );
- $headline = preg_replace( "/^$markerRegex/", "", $headline );
+ $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
}
if ( $toclevel ) {
@@ -4078,7 +4292,7 @@ class Parser {
# Increase TOC level
$toclevel++;
$sublevelCount[$toclevel] = 0;
- if ( $toclevel<$wgMaxTocLevel ) {
+ if ( $toclevel < $wgMaxTocLevel ) {
$prevtoclevel = $toclevel;
$toc .= Linker::tocIndent();
$numVisible++;
@@ -4100,7 +4314,7 @@ class Parser {
if ( $i == 0 ) {
$toclevel = 1;
}
- if ( $toclevel<$wgMaxTocLevel ) {
+ if ( $toclevel < $wgMaxTocLevel ) {
if ( $prevtoclevel < $wgMaxTocLevel ) {
# Unindent only if the previous toc level was shown :p
$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
@@ -4111,7 +4325,7 @@ class Parser {
}
} else {
# No change in level, end TOC line
- if ( $toclevel<$wgMaxTocLevel ) {
+ if ( $toclevel < $wgMaxTocLevel ) {
$toc .= Linker::tocLineEnd();
}
}
@@ -4119,9 +4333,9 @@ class Parser {
$levelCount[$toclevel] = $level;
# count number of headlines for each level
- @$sublevelCount[$toclevel]++;
+ $sublevelCount[$toclevel]++;
$dot = 0;
- for( $i = 1; $i <= $toclevel; $i++ ) {
+ for ( $i = 1; $i <= $toclevel; $i++ ) {
if ( !empty( $sublevelCount[$i] ) ) {
if ( $dot ) {
$numbering .= '.';
@@ -4144,23 +4358,29 @@ class Parser {
$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
# Strip out HTML (first regex removes any tag not allowed)
- # Allowed tags are <sup> and <sub> (bug 8393), <i> (bug 26375) and <b> (r105284)
- # We strip any parameter from accepted tags (second regex)
+ # Allowed tags are:
+ # * <sup> and <sub> (bug 8393)
+ # * <i> (bug 26375)
+ # * <b> (r105284)
+ # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
+ #
+ # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
+ # to allow setting directionality in toc items.
$tocline = preg_replace(
- array( '#<(?!/?(sup|sub|i|b)(?: [^>]*)?>).*?'.'>#', '#<(/?(sup|sub|i|b))(?: .*?)?'.'>#' ),
- array( '', '<$1>' ),
+ array( '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' . '>#', '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' . '>#' ),
+ array( '', '<$1>' ),
$safeHeadline
);
$tocline = trim( $tocline );
# For the anchor, strip out HTML-y stuff period
- $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
+ $safeHeadline = preg_replace( '/<.*?' . '>/', '', $safeHeadline );
$safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
# Save headline for section edit hint before it's escaped
$headlineHint = $safeHeadline;
- if ( $wgHtml5 && $wgExperimentalHtmlIds ) {
+ if ( $wgExperimentalHtmlIds ) {
# For reverse compatibility, provide an id that's
# HTML4-compatible, like we used to.
#
@@ -4230,7 +4450,8 @@ class Parser {
# Add the section to the section tree
# Find the DOM node for this header
- while ( $node && !$isTemplate ) {
+ $noOffset = ( $isTemplate || $sectionIndex === false );
+ while ( $node && !$noOffset ) {
if ( $node->getName() === 'h' ) {
$bits = $node->splitHeading();
if ( $bits['i'] == $sectionIndex ) {
@@ -4248,7 +4469,7 @@ class Parser {
'number' => $numbering,
'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
'fromtitle' => $titleText,
- 'byteoffset' => ( $isTemplate ? null : $byteOffset ),
+ 'byteoffset' => ( $noOffset ? null : $byteOffset ),
'anchor' => $anchor,
);
@@ -4269,9 +4490,9 @@ class Parser {
// We use a page and section attribute to stop the language converter from converting these important bits
// of data, but put the headline hint inside a content block because the language converter is supposed to
// be able to convert that piece of data.
- $editlink = '<mw:editsection page="' . htmlspecialchars($editlinkArgs[0]);
- $editlink .= '" section="' . htmlspecialchars($editlinkArgs[1]) .'"';
- if ( isset($editlinkArgs[2]) ) {
+ $editlink = '<mw:editsection page="' . htmlspecialchars( $editlinkArgs[0] );
+ $editlink .= '" section="' . htmlspecialchars( $editlinkArgs[1] ) . '"';
+ if ( isset( $editlinkArgs[2] ) ) {
$editlink .= '>' . $editlinkArgs[2] . '</mw:editsection>';
} else {
$editlink .= '/>';
@@ -4299,6 +4520,7 @@ class Parser {
}
$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
$this->mOutput->setTOCHTML( $toc );
+ $toc = self::TOC_START . $toc . self::TOC_END;
}
if ( $isMain ) {
@@ -4306,7 +4528,7 @@ class Parser {
}
# split up and insert constructed headlines
- $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
+ $blocks = preg_split( '/<H[1-6].*?' . '>[\s\S]*?<\/H[1-6]>/i', $text );
$i = 0;
// build an array of document sections
@@ -4353,7 +4575,7 @@ class Parser {
* Transform wiki markup when saving a page by doing "\r\n" -> "\n"
* conversion, substitting signatures, {{subst:}} templates, etc.
*
- * @param $text String: the text to transform
+ * @param string $text the text to transform
* @param $title Title: the Title object for the current article
* @param $user User: the User object describing the current user
* @param $options ParserOptions: parsing options
@@ -4368,7 +4590,7 @@ class Parser {
"\r\n" => "\n",
);
$text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
- if( $options->getPreSaveTransform() ) {
+ if ( $options->getPreSaveTransform() ) {
$text = $this->pstPass2( $text, $user );
}
$text = $this->mStripState->unstripBoth( $text );
@@ -4388,7 +4610,7 @@ class Parser {
* @return string
*/
function pstPass2( $text, $user ) {
- global $wgContLang, $wgLocaltimezone;
+ global $wgContLang;
# Note: This is the timestamp saved as hardcoded wikitext to
# the database, we use $wgContLang here in order to give
@@ -4396,19 +4618,11 @@ class Parser {
# than the one selected in each user's preferences.
# (see also bug 12815)
$ts = $this->mOptions->getTimestamp();
- if ( isset( $wgLocaltimezone ) ) {
- $tz = $wgLocaltimezone;
- } else {
- $tz = date_default_timezone_get();
- }
+ $timestamp = MWTimestamp::getLocalInstance( $ts );
+ $ts = $timestamp->format( 'YmdHis' );
+ $tzMsg = $timestamp->format( 'T' ); # might vary on DST changeover!
- $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!
-
- # Allow translation of timezones through wiki. date() can return
+ # Allow translation of timezones through wiki. format() can return
# whatever crap the system uses, localised or not, so we cannot
# ship premade translations.
$key = 'timezone-' . strtolower( trim( $tzMsg ) );
@@ -4417,8 +4631,6 @@ class Parser {
$tzMsg = $msg->text();
}
- date_default_timezone_set( $oldtz );
-
$d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
# Variable replacement
@@ -4436,14 +4648,14 @@ class Parser {
'~~~' => $sigText
) );
- # Context links: [[|name]] and [[name (context)|]]
+ # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
$tc = '[' . Title::legalChars() . ']';
$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
- $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
- $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]]
- $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]]
- $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
+ $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
+ $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]] (double-width brackets, added in r40257)
+ $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]] (using either single or double-width comma)
+ $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] (reverse pipe trick: add context from page title)
# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
@@ -4476,7 +4688,7 @@ class Parser {
* as it may have changed if it's the $wgParser.
*
* @param $user User
- * @param $nickname String|bool nickname to use or false to use user's default nickname
+ * @param string|bool $nickname nickname to use or false to use user's default nickname
* @param $fancySig Boolean|null whether the nicknname is the complete signature
* or null to use default value
* @return string
@@ -4487,8 +4699,9 @@ class Parser {
$username = $user->getName();
# If not given, retrieve from the user object.
- if ( $nickname === false )
+ if ( $nickname === false ) {
$nickname = $user->getOption( 'nickname' );
+ }
if ( is_null( $fancySig ) ) {
$fancySig = $user->getBoolOption( 'fancysig' );
@@ -4507,7 +4720,7 @@ class Parser {
} else {
# Failed to validate; fall back to the default
$nickname = $username;
- wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
+ wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
}
}
@@ -4529,7 +4742,7 @@ class Parser {
* @return mixed An expanded string, or false if invalid.
*/
function validateSig( $text ) {
- return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
+ return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
}
/**
@@ -4539,7 +4752,7 @@ class Parser {
* 2) Substitute all transclusions
*
* @param $text String
- * @param $parsing bool Whether we're cleaning (preferences save) or parsing
+ * @param bool $parsing Whether we're cleaning (preferences save) or parsing
* @return String: signature text
*/
public function cleanSig( $text, $parsing = false ) {
@@ -4614,7 +4827,7 @@ class Parser {
/**
* Wrapper for preprocess()
*
- * @param $text String: the text to preprocess
+ * @param string $text the text to preprocess
* @param $options ParserOptions: options
* @param $title Title object or null to use $wgTitle
* @return String
@@ -4633,11 +4846,7 @@ class Parser {
global $wgTitle;
$title = $wgTitle;
}
- if ( !$title ) {
- # It's not uncommon having a null $wgTitle in scripts. See r80898
- # Create a ghost title in such case
- $title = Title::newFromText( 'Dwimmerlaik' );
- }
+
$text = $this->preprocess( $text, $title, $options );
$executing = false;
@@ -4666,6 +4875,7 @@ class Parser {
*
* @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>"
* @param $callback Mixed: the callback function (and object) to use for the tag
+ * @throws MWException
* @return Mixed|null The old value of the mTagHooks array associated with the hook
*/
public function setHook( $tag, $callback ) {
@@ -4696,6 +4906,7 @@ class Parser {
*
* @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>"
* @param $callback Mixed: the callback function (and object) to use for the tag
+ * @throws MWException
* @return Mixed|null The old value of the mTagHooks array associated with the hook
*/
function setTransparentTagHook( $tag, $callback ) {
@@ -4734,7 +4945,7 @@ class Parser {
* nowiki Wiki markup in the return value should be escaped
* isHTML The returned text is HTML, armour it against wikitext transformation
*
- * @param $id String: The magic word ID
+ * @param string $id The magic word ID
* @param $callback Mixed: the callback function (and object) to use
* @param $flags Integer: a combination of the following flags:
* SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
@@ -4758,6 +4969,7 @@ class Parser {
* Please read the documentation in includes/parser/Preprocessor.php for more information
* about the methods available in PPFrame and PPNode.
*
+ * @throws MWException
* @return string|callback The old callback function for this name, if any
*/
public function setFunctionHook( $id, $callback, $flags = 0 ) {
@@ -4768,8 +4980,9 @@ class Parser {
# Add to function cache
$mw = MagicWord::get( $id );
- if ( !$mw )
- throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
+ if ( !$mw ) {
+ throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
+ }
$synonyms = $mw->getSynonyms();
$sensitive = intval( $mw->isCaseSensitive() );
@@ -4805,11 +5018,17 @@ class Parser {
* Create a tag function, e.g. "<test>some stuff</test>".
* Unlike tag hooks, tag functions are parsed at preprocessor level.
* Unlike parser functions, their content is not preprocessed.
+ * @param $tag
+ * @param $callback
+ * @param $flags
+ * @throws MWException
* @return null
*/
function setFunctionTagHook( $tag, $callback, $flags ) {
$tag = strtolower( $tag );
- if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
+ if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
+ throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
+ }
$old = isset( $this->mFunctionTagHooks[$tag] ) ?
$this->mFunctionTagHooks[$tag] : null;
$this->mFunctionTagHooks[$tag] = array( $callback, $flags );
@@ -4860,7 +5079,20 @@ class Parser {
* @return string HTML
*/
function renderImageGallery( $text, $params ) {
- $ig = new ImageGallery();
+ wfProfileIn( __METHOD__ );
+
+ $mode = false;
+ if ( isset( $params['mode'] ) ) {
+ $mode = $params['mode'];
+ }
+
+ try {
+ $ig = ImageGalleryBase::factory( $mode );
+ } catch ( MWException $e ) {
+ // If invalid type set, fallback to default.
+ $ig = ImageGalleryBase::factory( false );
+ }
+
$ig->setContextTitle( $this->mTitle );
$ig->setShowBytes( false );
$ig->setShowFilename( false );
@@ -4888,6 +5120,7 @@ class Parser {
if ( isset( $params['heights'] ) ) {
$ig->setHeights( $params['heights'] );
}
+ $ig->setAdditionalOptions( $params );
wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
@@ -4911,34 +5144,81 @@ class Parser {
continue;
}
+ # We need to get what handler the file uses, to figure out parameters.
+ # Note, a hook can overide the file name, and chose an entirely different
+ # file (which potentially could be of a different type and have different handler).
+ $options = array();
+ $descQuery = false;
+ wfRunHooks( 'BeforeParserFetchFileAndTitle',
+ array( $this, $title, &$options, &$descQuery ) );
+ # Don't register it now, as ImageGallery does that later.
+ $file = $this->fetchFileNoRegister( $title, $options );
+ $handler = $file ? $file->getHandler() : false;
+
+ wfProfileIn( __METHOD__ . '-getMagicWord' );
+ $paramMap = array(
+ 'img_alt' => 'gallery-internal-alt',
+ 'img_link' => 'gallery-internal-link',
+ );
+ if ( $handler ) {
+ $paramMap = $paramMap + $handler->getParamMap();
+ // We don't want people to specify per-image widths.
+ // Additionally the width parameter would need special casing anyhow.
+ unset( $paramMap['img_width'] );
+ }
+
+ $mwArray = new MagicWordArray( array_keys( $paramMap ) );
+ wfProfileOut( __METHOD__ . '-getMagicWord' );
+
$label = '';
$alt = '';
$link = '';
+ $handlerOptions = array();
if ( isset( $matches[3] ) ) {
// look for an |alt= definition while trying not to break existing
// captions with multiple pipes (|) in it, until a more sensible grammar
// is defined for images in galleries
+ // FIXME: Doing recursiveTagParse at this stage, and the trim before
+ // splitting on '|' is a bit odd, and different from makeImage.
$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
- $parameterMatches = StringUtils::explode('|', $matches[3]);
- $magicWordAlt = MagicWord::get( 'img_alt' );
- $magicWordLink = MagicWord::get( 'img_link' );
+ $parameterMatches = StringUtils::explode( '|', $matches[3] );
foreach ( $parameterMatches as $parameterMatch ) {
- if ( $match = $magicWordAlt->matchVariableStartToEnd( $parameterMatch ) ) {
- $alt = $this->stripAltText( $match, false );
- }
- elseif( $match = $magicWordLink->matchVariableStartToEnd( $parameterMatch ) ){
- $link = strip_tags($this->replaceLinkHoldersText($match));
- $chars = self::EXT_LINK_URL_CLASS;
- $prots = $this->mUrlProtocols;
- //check to see if link matches an absolute url, if not then it must be a wiki link.
- if(!preg_match( "/^($prots)$chars+$/u", $link)){
- $localLinkTitle = Title::newFromText($link);
- $link = $localLinkTitle->getLocalURL();
+ list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
+ if ( $magicName ) {
+ $paramName = $paramMap[$magicName];
+
+ switch ( $paramName ) {
+ case 'gallery-internal-alt':
+ $alt = $this->stripAltText( $match, false );
+ break;
+ case 'gallery-internal-link':
+ $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
+ $chars = self::EXT_LINK_URL_CLASS;
+ $prots = $this->mUrlProtocols;
+ //check to see if link matches an absolute url, if not then it must be a wiki link.
+ if ( preg_match( "/^($prots)$chars+$/u", $linkValue ) ) {
+ $link = $linkValue;
+ } else {
+ $localLinkTitle = Title::newFromText( $linkValue );
+ if ( $localLinkTitle !== null ) {
+ $link = $localLinkTitle->getLocalURL();
+ }
+ }
+ break;
+ default:
+ // Must be a handler specific parameter.
+ if ( $handler->validateParam( $paramName, $match ) ) {
+ $handlerOptions[$paramName] = $match;
+ } else {
+ // Guess not. Append it to the caption.
+ wfDebug( "$parameterMatch failed parameter validation" );
+ $label .= '|' . $parameterMatch;
+ }
}
- }
- else {
+
+ } else {
// concatenate all other pipes
$label .= '|' . $parameterMatch;
}
@@ -4947,9 +5227,11 @@ class Parser {
$label = substr( $label, 1 );
}
- $ig->add( $title, $label, $alt ,$link);
+ $ig->add( $title, $label, $alt, $link, $handlerOptions );
}
- return $ig->toHTML();
+ $html = $ig->toHTML();
+ wfProfileOut( __METHOD__ );
+ return $html;
}
/**
@@ -4962,7 +5244,7 @@ class Parser {
} else {
$handlerClass = '';
}
- if ( !isset( $this->mImageParams[$handlerClass] ) ) {
+ if ( !isset( $this->mImageParams[$handlerClass] ) ) {
# Initialise static lists
static $internalParamNames = array(
'horizAlign' => array( 'left', 'right', 'center', 'none' ),
@@ -5064,14 +5346,14 @@ class Parser {
# Special case; width and height come in one variable together
if ( $type === 'handler' && $paramName === 'width' ) {
$parsedWidthParam = $this->parseWidthParam( $value );
- if( isset( $parsedWidthParam['width'] ) ) {
+ if ( isset( $parsedWidthParam['width'] ) ) {
$width = $parsedWidthParam['width'];
if ( $handler->validateParam( 'width', $width ) ) {
$params[$type]['width'] = $width;
$validated = true;
}
}
- if( isset( $parsedWidthParam['height'] ) ) {
+ if ( isset( $parsedWidthParam['height'] ) ) {
$height = $parsedWidthParam['height'];
if ( $handler->validateParam( 'height', $height ) ) {
$params[$type]['height'] = $height;
@@ -5085,7 +5367,7 @@ class Parser {
$validated = $handler->validateParam( $paramName, $value );
} else {
# Validate internal parameters
- switch( $paramName ) {
+ switch ( $paramName ) {
case 'manualthumb':
case 'alt':
case 'class':
@@ -5180,7 +5462,7 @@ class Parser {
} else { # Inline image
if ( !isset( $params['frame']['alt'] ) ) {
# No alt text, use the "caption" for the alt text
- if ( $caption !== '') {
+ if ( $caption !== '' ) {
$params['frame']['alt'] = $this->stripAltText( $caption, $holders );
} else {
# No caption, fall back to using the filename for the
@@ -5303,8 +5585,8 @@ class Parser {
*
* External callers should use the getSection and replaceSection methods.
*
- * @param $text String: Page wikitext
- * @param $section String: a section identifier string of the form:
+ * @param string $text Page wikitext
+ * @param string $section a section identifier string of the form:
* "<flag1> - <flag2> - ... - <section number>"
*
* Currently the only recognised flag is "T", which means the target section number
@@ -5321,12 +5603,12 @@ class Parser {
* string. If $text is the empty string and section 0 is replaced, $newText is
* returned.
*
- * @param $mode String: one of "get" or "replace"
- * @param $newText String: replacement text for section data.
+ * @param string $mode one of "get" or "replace"
+ * @param string $newText replacement text for section data.
* @return String: for "get", the extracted section text.
* for "replace", the whole page with the section replaced.
*/
- private function extractSections( $text, $section, $mode, $newText='' ) {
+ private function extractSections( $text, $section, $mode, $newText = '' ) {
global $wgTitle; # not generally used but removes an ugly failure mode
$this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
$outText = '';
@@ -5442,12 +5724,12 @@ class Parser {
*
* If a section contains subsections, these are also returned.
*
- * @param $text String: text to look in
- * @param $section String: section identifier
- * @param $deftext String: default to return if section is not found
+ * @param string $text text to look in
+ * @param string $section section identifier
+ * @param string $deftext default to return if section is not found
* @return string text of the requested section
*/
- public function getSection( $text, $section, $deftext='' ) {
+ public function getSection( $text, $section, $deftext = '' ) {
return $this->extractSections( $text, $section, "get", $deftext );
}
@@ -5456,9 +5738,9 @@ class Parser {
* specified by $section has been replaced with $text. If the target
* section does not exist, $oldtext is returned unchanged.
*
- * @param $oldtext String: former text of the article
- * @param $section int section identifier
- * @param $text String: replacing text
+ * @param string $oldtext former text of the article
+ * @param int $section section identifier
+ * @param string $text replacing text
* @return String: modified text
*/
public function replaceSection( $oldtext, $section, $text ) {
@@ -5523,14 +5805,14 @@ class Parser {
* @return String: user name
*/
function getRevisionUser() {
- if( is_null( $this->mRevisionUser ) ) {
+ if ( is_null( $this->mRevisionUser ) ) {
$revObject = $this->getRevisionObject();
# if this template is subst: the revision id will be blank,
# so just use the current user's name
- if( $revObject ) {
+ if ( $revObject ) {
$this->mRevisionUser = $revObject->getUserText();
- } elseif( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
+ } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
$this->mRevisionUser = $this->getUser()->getName();
}
}
@@ -5538,9 +5820,30 @@ class Parser {
}
/**
+ * Get the size of the revision
+ *
+ * @return int|null revision size
+ */
+ function getRevisionSize() {
+ if ( is_null( $this->mRevisionSize ) ) {
+ $revObject = $this->getRevisionObject();
+
+ # if this variable is subst: the revision id will be blank,
+ # so just use the parser input size, because the own substituation
+ # will change the size.
+ if ( $revObject ) {
+ $this->mRevisionSize = $revObject->getSize();
+ } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
+ $this->mRevisionSize = $this->mInputSize;
+ }
+ }
+ return $this->mRevisionSize;
+ }
+
+ /**
* Mutator for $mDefaultSort
*
- * @param $sort string New value
+ * @param string $sort New value
*/
public function setDefaultSort( $sort ) {
$this->mDefaultSort = $sort;
@@ -5596,7 +5899,7 @@ class Parser {
* instead. For use in redirects, since IE6 interprets Redirect: headers
* as something other than UTF-8 (apparently?), resulting in breakage.
*
- * @param $text String: The section name
+ * @param string $text The section name
* @return string An anchor
*/
public function guessLegacySectionNameFromWikiText( $text ) {
@@ -5616,7 +5919,7 @@ class Parser {
* to create valid section anchors by mimicing the output of the
* parser when headings are parsed.
*
- * @param $text String: text string to be stripped of wikitext
+ * @param string $text text string to be stripped of wikitext
* for use in a Section anchor
* @return string Filtered text string
*/
@@ -5767,12 +6070,13 @@ class Parser {
* If the $data array has been stored persistently, the caller should first
* check whether it is still valid, by calling isValidHalfParsedText().
*
- * @param $data array Serialized data
+ * @param array $data Serialized data
+ * @throws MWException
* @return String
*/
function unserializeHalfParsedText( $data ) {
if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
- throw new MWException( __METHOD__.': invalid version' );
+ throw new MWException( __METHOD__ . ': invalid version' );
}
# First, extract the strip state.
@@ -5809,7 +6113,7 @@ class Parser {
*/
public function parseWidthParam( $value ) {
$parsedWidthParam = array();
- if( $value === '' ) {
+ if ( $value === '' ) {
return $parsedWidthParam;
}
$m = array();
diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php
index 6a4ef0c5..7053f134 100644
--- a/includes/parser/ParserCache.php
+++ b/includes/parser/ParserCache.php
@@ -48,6 +48,7 @@ class ParserCache {
* May be a memcached client or a BagOStuff derivative.
*
* @param $memCached Object
+ * @throws MWException
*/
protected function __construct( $memCached ) {
if ( !$memCached ) {
@@ -66,7 +67,7 @@ class ParserCache {
// idhash seem to mean 'page id' + 'rendering hash' (r3710)
$pageid = $article->getID();
- $renderkey = (int)($wgRequest->getVal('action') == 'render');
+ $renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' );
$key = wfMemcKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
return $key;
@@ -127,7 +128,7 @@ class ParserCache {
public function getKey( $article, $popts, $useOutdated = true ) {
global $wgCacheEpoch;
- if( $popts instanceof User ) {
+ if ( $popts instanceof User ) {
wfWarn( "Use of outdated prototype ParserCache::getKey( &\$article, &\$user )\n" );
$popts = ParserOptions::newFromUser( $popts );
}
@@ -200,8 +201,8 @@ class ParserCache {
wfDebug( "ParserOutput cache found.\n" );
- // The edit section preference may not be the appropiate one in
- // the ParserOutput, as we are not storing it in the parsercache
+ // The edit section preference may not be the appropiate one in
+ // the ParserOutput, as we are not storing it in the parsercache
// key. Force it here. See bug 31445.
$value->setEditSectionTokens( $popts->getEditSection() );
@@ -222,19 +223,19 @@ class ParserCache {
* @param $parserOutput ParserOutput
* @param $article Article
* @param $popts ParserOptions
+ * @param $cacheTime Time when the cache was generated
*/
- public function save( $parserOutput, $article, $popts ) {
+ public function save( $parserOutput, $article, $popts, $cacheTime = null ) {
$expire = $parserOutput->getCacheExpiry();
-
- if( $expire > 0 ) {
- $now = wfTimestampNow();
+ if ( $expire > 0 ) {
+ $cacheTime = $cacheTime ?: wfTimestampNow();
$optionsKey = new CacheTime;
$optionsKey->mUsedOptions = $parserOutput->getUsedOptions();
$optionsKey->updateCacheExpiry( $expire );
- $optionsKey->setCacheTime( $now );
- $parserOutput->setCacheTime( $now );
+ $optionsKey->setCacheTime( $cacheTime );
+ $parserOutput->setCacheTime( $cacheTime );
$optionsKey->setContainsOldMagic( $parserOutput->containsOldMagic() );
@@ -244,8 +245,8 @@ class ParserCache {
// Save the timestamp so that we don't have to load the revision row on view
$parserOutput->setTimestamp( $article->getTimestamp() );
- $parserOutput->mText .= "\n<!-- Saved in parser cache with key $parserOutputKey and timestamp $now -->\n";
- wfDebug( "Saved in parser cache with key $parserOutputKey and timestamp $now\n" );
+ $parserOutput->mText .= "\n<!-- Saved in parser cache with key $parserOutputKey and timestamp $cacheTime\n -->\n";
+ wfDebug( "Saved in parser cache with key $parserOutputKey and timestamp $cacheTime\n" );
// Save the parser output
$this->mMemc->set( $parserOutputKey, $parserOutput, $expire );
diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php
index 009b18a1..e12f32d8 100644
--- a/includes/parser/ParserOptions.php
+++ b/includes/parser/ParserOptions.php
@@ -29,67 +29,62 @@
* @ingroup Parser
*/
class ParserOptions {
-
- /**
- * Use DateFormatter to format dates
- */
- var $mUseDynamicDates;
-
+
/**
* Interlanguage links are removed and returned in an array
*/
var $mInterwikiMagic;
-
+
/**
* Allow external images inline?
*/
var $mAllowExternalImages;
-
+
/**
* If not, any exception?
*/
var $mAllowExternalImagesFrom;
-
+
/**
* If not or it doesn't match, should we check an on-wiki whitelist?
*/
var $mEnableImageWhitelist;
-
+
/**
* Date format index
*/
var $mDateFormat = null;
-
+
/**
* Create "edit section" links?
*/
var $mEditSection = true;
-
+
/**
* Allow inclusion of special pages?
*/
var $mAllowSpecialInclusion;
-
+
/**
* Use tidy to cleanup output HTML?
*/
var $mTidy = false;
-
+
/**
* Which lang to call for PLURAL and GRAMMAR
*/
var $mInterfaceMessage = false;
-
+
/**
* Overrides $mInterfaceMessage with arbitrary language
*/
var $mTargetLanguage = null;
-
+
/**
* Maximum size of template expansions, in bytes
*/
var $mMaxIncludeSize;
-
+
/**
* Maximum number of nodes touched by PPFrame::expand()
*/
@@ -99,56 +94,56 @@ class ParserOptions {
* Maximum number of nodes generated by Preprocessor::preprocessToObj()
*/
var $mMaxGeneratedPPNodeCount;
-
+
/**
* Maximum recursion depth in PPFrame::expand()
*/
var $mMaxPPExpandDepth;
-
+
/**
* Maximum recursion depth for templates within templates
*/
var $mMaxTemplateDepth;
-
+
/**
* Maximum number of calls per parse to expensive parser functions
*/
var $mExpensiveParserFunctionLimit;
-
+
/**
* Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
*/
var $mRemoveComments = true;
-
+
/**
* Callback for template fetching. Used as first argument to call_user_func().
*/
var $mTemplateCallback =
array( 'Parser', 'statelessFetchTemplate' );
-
+
/**
* Enable limit report in an HTML comment on output
*/
var $mEnableLimitReport = false;
-
+
/**
* Timestamp used for {{CURRENTDAY}} etc.
*/
var $mTimestamp;
-
+
/**
* Target attribute for external links
*/
var $mExternalLinkTarget;
-
+
/**
- * Clean up signature texts?
+ * Clean up signature texts?
*
* 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures
* 2) Substitute all transclusions
*/
var $mCleanSignatures;
-
+
/**
* Transform wiki markup when saving the page?
*/
@@ -168,43 +163,43 @@ class ParserOptions {
* Automatically number headings?
*/
var $mNumberHeadings;
-
+
/**
* User math preference (as integer). Not used (1.19)
*/
var $mMath;
-
+
/**
* Thumb size preferred by the user.
*/
var $mThumbSize;
-
+
/**
* Maximum article size of an article to be marked as "stub"
*/
private $mStubThreshold;
-
+
/**
* Language object of the User language.
*/
var $mUserLang;
/**
- * @var User
+ * @var User
* Stored user object
*/
var $mUser;
-
+
/**
* Parsing the page for a "preview" operation?
*/
var $mIsPreview = false;
-
+
/**
* Parsing the page for a "preview" operation on a single section?
*/
var $mIsSectionPreview = false;
-
+
/**
* Parsing the printable version of the page?
*/
@@ -220,7 +215,6 @@ class ParserOptions {
*/
protected $onAccessCallback = null;
- function getUseDynamicDates() { return $this->mUseDynamicDates; }
function getInterwikiMagic() { return $this->mInterwikiMagic; }
function getAllowExternalImages() { return $this->mAllowExternalImages; }
function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; }
@@ -246,6 +240,7 @@ class ParserOptions {
function getExternalLinkTarget() { return $this->mExternalLinkTarget; }
function getDisableContentConversion() { return $this->mDisableContentConversion; }
function getDisableTitleConversion() { return $this->mDisableTitleConversion; }
+ /** @deprecated since 1.22 use User::getOption('math') instead */
function getMath() { $this->optionUsed( 'math' );
return $this->mMath; }
function getThumbSize() { $this->optionUsed( 'thumbsize' );
@@ -286,9 +281,17 @@ class ParserOptions {
}
/**
+ * Get the user language used by the parser for this page.
+ *
* You shouldn't use this. Really. $parser->getFunctionLang() is all you need.
- * Using this fragments the cache and is discouraged. Yes, {{int: }} uses this,
- * producing inconsistent tables (Bug 14404).
+ *
+ * To avoid side-effects where the page will be rendered based on the language
+ * of the user who last saved, this function will triger a cache fragmentation.
+ * Usage of this method is discouraged for that reason.
+ *
+ * When saving, this will return the default language instead of the user's.
+ *
+ * {{int: }} uses this which used to produce inconsistent link tables (bug 14404).
*
* @return Language object
* @since 1.19
@@ -308,7 +311,6 @@ class ParserOptions {
return $this->getUserLangObj()->getCode();
}
- function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); }
function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
@@ -319,7 +321,7 @@ class ParserOptions {
function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); }
function setTidy( $x ) { return wfSetVar( $this->mTidy, $x ); }
- /** @deprecated in 1.19; will be removed in 1.20 */
+ /** @deprecated in 1.19 */
function setSkin( $x ) { wfDeprecated( __METHOD__, '1.19' ); }
function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x ); }
function setTargetLanguage( $x ) { return wfSetVar( $this->mTargetLanguage, $x, true ); }
@@ -337,6 +339,7 @@ class ParserOptions {
function setExternalLinkTarget( $x ) { return wfSetVar( $this->mExternalLinkTarget, $x ); }
function disableContentConversion( $x = true ) { return wfSetVar( $this->mDisableContentConversion, $x ); }
function disableTitleConversion( $x = true ) { return wfSetVar( $this->mDisableTitleConversion, $x ); }
+ /** @deprecated since 1.22 */
function setMath( $x ) { return wfSetVar( $this->mMath, $x ); }
function setUserLang( $x ) {
if ( is_string( $x ) ) {
@@ -415,14 +418,14 @@ class ParserOptions {
return new ParserOptions( $context->getUser(), $context->getLanguage() );
}
- /**
- * Get user options
+ /**
+ * Get user options
*
* @param $user User object
* @param $lang Language object
*/
private function initialiseFromUser( $user, $lang ) {
- global $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages,
+ global $wgInterwikiMagic, $wgAllowExternalImages,
$wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion,
$wgMaxArticleSize, $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth,
$wgCleanSignatures, $wgExternalLinkTarget, $wgExpensiveParserFunctionLimit,
@@ -430,7 +433,6 @@ class ParserOptions {
wfProfileIn( __METHOD__ );
- $this->mUseDynamicDates = $wgUseDynamicDates;
$this->mInterwikiMagic = $wgInterwikiMagic;
$this->mAllowExternalImages = $wgAllowExternalImages;
$this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
@@ -481,12 +483,7 @@ class ParserOptions {
* @return array
*/
public static function legacyOptions() {
- global $wgUseDynamicDates;
- $legacyOpts = array( 'math', 'stubthreshold', 'numberheadings', 'userlang', 'thumbsize', 'editsection', 'printable' );
- if ( $wgUseDynamicDates ) {
- $legacyOpts[] = 'dateformat';
- }
- return $legacyOpts;
+ return array( 'math', 'stubthreshold', 'numberheadings', 'userlang', 'thumbsize', 'editsection', 'printable' );
}
/**
@@ -518,14 +515,13 @@ class ParserOptions {
$confstr .= '*';
}
-
// Space assigned for the stubthreshold but unused
// since it disables the parser cache, its value will always
// be 0 when this function is called by parsercache.
if ( in_array( 'stubthreshold', $forOptions ) ) {
$confstr .= '!' . $this->mStubThreshold;
} else {
- $confstr .= '!*' ;
+ $confstr .= '!*';
}
if ( in_array( 'dateformat', $forOptions ) ) {
@@ -552,7 +548,7 @@ class ParserOptions {
// add in language specific options, if any
// @todo FIXME: This is just a way of retrieving the url/user preferred variant
- if( !is_null( $title ) ) {
+ if ( !is_null( $title ) ) {
$confstr .= $title->getPageLanguage()->getExtraHashOptions();
} else {
global $wgContLang;
@@ -571,8 +567,9 @@ class ParserOptions {
$confstr .= '!printable=1';
}
- if ( $this->mExtraKey != '' )
+ if ( $this->mExtraKey != '' ) {
$confstr .= $this->mExtraKey;
+ }
// Give a chance for extensions to modify the hash, if they have
// extra options or other effects on the parser cache.
diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php
index 41b4a385..502f0fd1 100644
--- a/includes/parser/ParserOutput.php
+++ b/includes/parser/ParserOutput.php
@@ -47,10 +47,14 @@ class ParserOutput extends CacheTime {
$mEditSectionTokens = false, # prefix/suffix markers if edit sections were output as tokens
$mProperties = array(), # Name/value pairs to be cached in the DB
$mTOCHTML = '', # HTML of the TOC
- $mTimestamp; # Timestamp of the revision
+ $mTimestamp, # Timestamp of the revision
+ $mTOCEnabled = true; # Whether TOC should be shown, can't override __NOTOC__
private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change.
private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
- private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
+ private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else.
+ private $mExtensionData = array(); # extra data used by extensions
+ private $mLimitReportData = array(); # Parser limit report data
+ private $mParseStartTime = array(); # Timestamps for getTimeSinceStart()
const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
@@ -65,28 +69,46 @@ class ParserOutput extends CacheTime {
}
function getText() {
+ wfProfileIn( __METHOD__ );
+ $text = $this->mText;
if ( $this->mEditSectionTokens ) {
- return preg_replace_callback( ParserOutput::EDITSECTION_REGEX,
- array( &$this, 'replaceEditSectionLinksCallback' ), $this->mText );
+ $text = preg_replace_callback( ParserOutput::EDITSECTION_REGEX,
+ array( &$this, 'replaceEditSectionLinksCallback' ), $text );
+ } else {
+ $text = preg_replace( ParserOutput::EDITSECTION_REGEX, '', $text );
}
- return preg_replace( ParserOutput::EDITSECTION_REGEX, '', $this->mText );
+
+ // If you have an old cached version of this class - sorry, you can't disable the TOC
+ if ( isset( $this->mTOCEnabled ) && $this->mTOCEnabled ) {
+ $text = str_replace( array( Parser::TOC_START, Parser::TOC_END ), '', $text );
+ } else {
+ $text = preg_replace(
+ '#'. preg_quote( Parser::TOC_START ) . '.*?' . preg_quote( Parser::TOC_END ) . '#s',
+ '',
+ $text
+ );
+ }
+ wfProfileOut( __METHOD__ );
+ return $text;
}
/**
* callback used by getText to replace editsection tokens
* @private
+ * @param $m
+ * @throws MWException
* @return mixed
*/
function replaceEditSectionLinksCallback( $m ) {
global $wgOut, $wgLang;
$args = array(
- htmlspecialchars_decode($m[1]),
- htmlspecialchars_decode($m[2]),
- isset($m[4]) ? $m[3] : null,
+ htmlspecialchars_decode( $m[1] ),
+ htmlspecialchars_decode( $m[2] ),
+ isset( $m[4] ) ? $m[3] : null,
);
$args[0] = Title::newFromText( $args[0] );
- if ( !is_object($args[0]) ) {
- throw new MWException("Bad parser output text.");
+ if ( !is_object( $args[0] ) ) {
+ throw new MWException( "Bad parser output text." );
}
$args[] = $wgLang->getCode();
$skin = $wgOut->getSkin();
@@ -117,6 +139,8 @@ class ParserOutput extends CacheTime {
function getIndexPolicy() { return $this->mIndexPolicy; }
function getTOCHTML() { return $this->mTOCHTML; }
function getTimestamp() { return $this->mTimestamp; }
+ function getLimitReportData() { return $this->mLimitReportData; }
+ function getTOCEnabled() { return $this->mTOCEnabled; }
function setText( $text ) { return wfSetVar( $this->mText, $text ); }
function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); }
@@ -128,6 +152,7 @@ class ParserOutput extends CacheTime {
function setIndexPolicy( $policy ) { return wfSetVar( $this->mIndexPolicy, $policy ); }
function setTOCHTML( $tochtml ) { return wfSetVar( $this->mTOCHTML, $tochtml ); }
function setTimestamp( $timestamp ) { return wfSetVar( $this->mTimestamp, $timestamp ); }
+ function setTOCEnabled( $flag ) { return wfSetVar( $this->mTOCEnabled, $flag ); }
function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; }
function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; }
@@ -140,21 +165,45 @@ class ParserOutput extends CacheTime {
function setNewSection( $value ) {
$this->mNewSection = (bool)$value;
}
- function hideNewSection ( $value ) {
+ function hideNewSection( $value ) {
$this->mHideNewSection = (bool)$value;
}
- function getHideNewSection () {
+ function getHideNewSection() {
return (bool)$this->mHideNewSection;
}
function getNewSection() {
return (bool)$this->mNewSection;
}
+ /**
+ * Checks, if a url is pointing to the own server
+ *
+ * @param string $internal the server to check against
+ * @param string $url the url to check
+ * @return bool
+ */
+ static function isLinkInternal( $internal, $url ) {
+ return (bool)preg_match( '/^' .
+ # If server is proto relative, check also for http/https links
+ ( substr( $internal, 0, 2 ) === '//' ? '(?:https?:)?' : '' ) .
+ preg_quote( $internal, '/' ) .
+ # check for query/path/anchor or end of link in each case
+ '(?:[\?\/\#]|$)/i',
+ $url
+ );
+ }
+
function addExternalLink( $url ) {
# We don't register links pointing to our own server, unless... :-)
global $wgServer, $wgRegisterInternalExternals;
- if( $wgRegisterInternalExternals or stripos($url,$wgServer.'/')!==0)
+
+ $registerExternalLink = true;
+ if ( !$wgRegisterInternalExternals ) {
+ $registerExternalLink = !self::isLinkInternal( $wgServer, $url );
+ }
+ if ( $registerExternalLink ) {
$this->mExternalLinks[$url] = 1;
+ }
}
/**
@@ -163,7 +212,7 @@ class ParserOutput extends CacheTime {
* @param $title Title object
* @param $id Mixed: optional known page_id so we can skip the lookup
*/
- function addLink( $title, $id = null ) {
+ function addLink( Title $title, $id = null ) {
if ( $title->isExternal() ) {
// Don't record interwikis in pagelinks
$this->addInterwikiLink( $title );
@@ -174,11 +223,11 @@ class ParserOutput extends CacheTime {
if ( $ns == NS_MEDIA ) {
// Normalize this pseudo-alias if it makes it down here...
$ns = NS_FILE;
- } elseif( $ns == NS_SPECIAL ) {
+ } elseif ( $ns == NS_SPECIAL ) {
// We don't record Special: links currently
// It might actually be wise to, but we'd need to do some normalization.
return;
- } elseif( $dbk === '' ) {
+ } elseif ( $dbk === '' ) {
// Don't record self links - [[#Foo]]
return;
}
@@ -193,9 +242,9 @@ class ParserOutput extends CacheTime {
/**
* Register a file dependency for this output
- * @param $name string Title dbKey
- * @param $timestamp string MW timestamp of file creation (or false if non-existing)
- * @param $sha1 string base 36 SHA-1 of file (or false if non-existing)
+ * @param string $name Title dbKey
+ * @param string $timestamp MW timestamp of file creation (or false if non-existing)
+ * @param string $sha1 base 36 SHA-1 of file (or false if non-existing)
* @return void
*/
function addImage( $name, $timestamp = null, $sha1 = null ) {
@@ -231,10 +280,10 @@ class ParserOutput extends CacheTime {
*/
function addInterwikiLink( $title ) {
$prefix = $title->getInterwiki();
- if( $prefix == '' ) {
+ if ( $prefix == '' ) {
throw new MWException( 'Non-interwiki link passed, internal parser error.' );
}
- if (!isset($this->mInterwikiLinks[$prefix])) {
+ if ( !isset( $this->mInterwikiLinks[$prefix] ) ) {
$this->mInterwikiLinks[$prefix] = array();
}
$this->mInterwikiLinks[$prefix][$title->getDBkey()] = 1;
@@ -254,7 +303,7 @@ class ParserOutput extends CacheTime {
}
public function addModules( $modules ) {
- $this->mModules = array_merge( $this->mModules, (array) $modules );
+ $this->mModules = array_merge( $this->mModules, (array)$modules );
}
public function addModuleScripts( $modules ) {
@@ -288,7 +337,7 @@ class ParserOutput extends CacheTime {
* -- this is assumed to have been validated
* (check equal normalisation, etc.)
*
- * @param $text String: desired title text
+ * @param string $text desired title text
*/
public function setDisplayTitle( $text ) {
$this->setTitleText( $text );
@@ -302,7 +351,7 @@ class ParserOutput extends CacheTime {
*/
public function getDisplayTitle() {
$t = $this->getTitleText();
- if( $t === '' ) {
+ if ( $t === '' ) {
return false;
}
return $t;
@@ -320,13 +369,67 @@ class ParserOutput extends CacheTime {
}
/**
- * Set a property to be cached in the DB
+ * Set a property to be stored in the page_props database table.
+ *
+ * page_props is a key value store indexed by the page ID. This allows
+ * the parser to set a property on a page which can then be quickly
+ * retrieved given the page ID or via a DB join when given the page
+ * title.
+ *
+ * setProperty() is thus used to propagate properties from the parsed
+ * page to request contexts other than a page view of the currently parsed
+ * article.
+ *
+ * Some applications examples:
+ *
+ * * To implement hidden categories, hiding pages from category listings
+ * by storing a property.
+ *
+ * * Overriding the displayed article title.
+ * @see ParserOutput::setDisplayTitle()
+ *
+ * * To implement image tagging, for example displaying an icon on an
+ * image thumbnail to indicate that it is listed for deletion on
+ * Wikimedia Commons.
+ * This is not actually implemented, yet but would be pretty cool.
+ *
+ * @note: Do not use setProperty() to set a property which is only used
+ * in a context where the ParserOutput object itself is already available,
+ * for example a normal page view. There is no need to save such a property
+ * in the database since it the text is already parsed. You can just hook
+ * OutputPageParserOutput and get your data out of the ParserOutput object.
+ *
+ * If you are writing an extension where you want to set a property in the
+ * parser which is used by an OutputPageParserOutput hook, you have to
+ * associate the extension data directly with the ParserOutput object.
+ * Since MediaWiki 1.21, you can use setExtensionData() to do this:
+ *
+ * @par Example:
+ * @code
+ * $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
+ * @endcode
+ *
+ * And then later, in OutputPageParserOutput or similar:
+ *
+ * @par Example:
+ * @code
+ * $output->getExtensionData( 'my_ext_foo' );
+ * @endcode
+ *
+ * In MediaWiki 1.20 and older, you have to use a custom member variable
+ * within the ParserOutput object:
+ *
+ * @par Example:
+ * @code
+ * $parser->getOutput()->my_ext_foo = '...';
+ * @endcode
+ *
*/
public function setProperty( $name, $value ) {
$this->mProperties[$name] = $value;
}
- public function getProperty( $name ){
+ public function getProperty( $name ) {
return isset( $this->mProperties[$name] ) ? $this->mProperties[$name] : false;
}
@@ -337,26 +440,25 @@ class ParserOutput extends CacheTime {
return $this->mProperties;
}
-
/**
* Returns the options from its ParserOptions which have been taken
* into account to produce this output or false if not available.
* @return mixed Array
*/
- public function getUsedOptions() {
+ public function getUsedOptions() {
if ( !isset( $this->mAccessedOptions ) ) {
return array();
}
return array_keys( $this->mAccessedOptions );
- }
+ }
- /**
- * Callback passed by the Parser to the ParserOptions to keep track of which options are used.
- * @access private
- */
- function recordOption( $option ) {
- $this->mAccessedOptions[$option] = true;
- }
+ /**
+ * Callback passed by the Parser to the ParserOptions to keep track of which options are used.
+ * @access private
+ */
+ function recordOption( $option ) {
+ $this->mAccessedOptions[$option] = true;
+ }
/**
* Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to
@@ -375,9 +477,13 @@ class ParserOutput extends CacheTime {
* extracted from the page's content, including a LinksUpdate object for all links stored in
* this ParserOutput object.
*
+ * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content
+ * handler may provide additional update objects.
+ *
* @since 1.20
*
- * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
+ * @param $title Title The title of the page we're updating. If not given, a title object will be created
+ * based on $this->getTitleText()
* @param $recursive Boolean: queue jobs for recursive updates?
*
* @return Array. An array of instances of DataUpdate
@@ -389,13 +495,138 @@ class ParserOutput extends CacheTime {
$linksUpdate = new LinksUpdate( $title, $this, $recursive );
- if ( $this->mSecondaryDataUpdates === array() ) {
- return array( $linksUpdate );
+ return array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) );
+ }
+
+ /**
+ * Attaches arbitrary data to this ParserObject. This can be used to store some information in
+ * the ParserOutput object for later use during page output. The data will be cached along with
+ * the ParserOutput object, but unlike data set using setProperty(), it is not recorded in the
+ * database.
+ *
+ * This method is provided to overcome the unsafe practice of attaching extra information to a
+ * ParserObject by directly assigning member variables.
+ *
+ * To use setExtensionData() to pass extension information from a hook inside the parser to a
+ * hook in the page output, use this in the parser hook:
+ *
+ * @par Example:
+ * @code
+ * $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
+ * @endcode
+ *
+ * And then later, in OutputPageParserOutput or similar:
+ *
+ * @par Example:
+ * @code
+ * $output->getExtensionData( 'my_ext_foo' );
+ * @endcode
+ *
+ * In MediaWiki 1.20 and older, you have to use a custom member variable
+ * within the ParserOutput object:
+ *
+ * @par Example:
+ * @code
+ * $parser->getOutput()->my_ext_foo = '...';
+ * @endcode
+ *
+ * @since 1.21
+ *
+ * @param string $key The key for accessing the data. Extensions should take care to avoid
+ * conflicts in naming keys. It is suggested to use the extension's name as a
+ * prefix.
+ *
+ * @param mixed $value The value to set. Setting a value to null is equivalent to removing
+ * the value.
+ */
+ public function setExtensionData( $key, $value ) {
+ if ( $value === null ) {
+ unset( $this->mExtensionData[$key] );
} else {
- $updates = array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) );
+ $this->mExtensionData[$key] = $value;
+ }
+ }
+
+ /**
+ * Gets extensions data previously attached to this ParserOutput using setExtensionData().
+ * Typically, such data would be set while parsing the page, e.g. by a parser function.
+ *
+ * @since 1.21
+ *
+ * @param string $key The key to look up.
+ *
+ * @return mixed The value previously set for the given key using setExtensionData( $key ),
+ * or null if no value was set for this key.
+ */
+ public function getExtensionData( $key ) {
+ if ( isset( $this->mExtensionData[$key] ) ) {
+ return $this->mExtensionData[$key];
}
- return $updates;
- }
+ return null;
+ }
+ private static function getTimes( $clock = null ) {
+ $ret = array();
+ if ( !$clock || $clock === 'wall' ) {
+ $ret['wall'] = microtime( true );
+ }
+ if ( ( !$clock || $clock === 'cpu' ) && function_exists( 'getrusage' ) ) {
+ $ru = getrusage();
+ $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
+ $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
+ }
+ return $ret;
+ }
+
+ /**
+ * Resets the parse start timestamps for future calls to getTimeSinceStart()
+ * @since 1.22
+ */
+ function resetParseStartTime() {
+ $this->mParseStartTime = self::getTimes();
+ }
+
+ /**
+ * Returns the time since resetParseStartTime() was last called
+ *
+ * Clocks available are:
+ * - wall: Wall clock time
+ * - cpu: CPU time (requires getrusage)
+ *
+ * @since 1.22
+ * @param string $clock
+ * @return float|null
+ */
+ function getTimeSinceStart( $clock ) {
+ if ( !isset( $this->mParseStartTime[$clock] ) ) {
+ return null;
+ }
+
+ $end = self::getTimes( $clock );
+ return $end[$clock] - $this->mParseStartTime[$clock];
+ }
+
+ /**
+ * Sets parser limit report data for a key
+ *
+ * The key is used as the prefix for various messages used for formatting:
+ * - $key: The label for the field in the limit report
+ * - $key-value-text: Message used to format the value in the "NewPP limit
+ * report" HTML comment. If missing, uses $key-format.
+ * - $key-value-html: Message used to format the value in the preview
+ * limit report table. If missing, uses $key-format.
+ * - $key-value: Message used to format the value. If missing, uses "$1".
+ *
+ * Note that all values are interpreted as wikitext, and so should be
+ * encoded with htmlspecialchars() as necessary, but should avoid complex
+ * HTML for sanity of display in the "NewPP limit report" comment.
+ *
+ * @since 1.22
+ * @param string $key Message key
+ * @param mixed $value Appropriate for Message::params()
+ */
+ function setLimitReportData( $key, $value ) {
+ $this->mLimitReportData[$key] = $value;
+ }
}
diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php
index f25340fa..aeae234a 100644
--- a/includes/parser/Parser_DiffTest.php
+++ b/includes/parser/Parser_DiffTest.php
@@ -122,7 +122,7 @@ class Parser_DiffTest
function setFunctionHook( $id, $callback, $flags = 0 ) {
$this->init();
- foreach ( $this->parsers as $parser ) {
+ foreach ( $this->parsers as $parser ) {
$parser->setFunctionHook( $id, $callback, $flags );
}
}
diff --git a/includes/parser/Parser_LinkHooks.php b/includes/parser/Parser_LinkHooks.php
deleted file mode 100644
index 6bcc324d..00000000
--- a/includes/parser/Parser_LinkHooks.php
+++ /dev/null
@@ -1,324 +0,0 @@
-<?php
-/**
- * Modified version of the PHP parser with hooks for wiki links; experimental
- *
- * 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 Parser
- */
-
-/**
- * Parser with LinkHooks experiment
- * @ingroup Parser
- */
-class Parser_LinkHooks extends Parser {
- /**
- * Update this version number when the ParserOutput format
- * changes in an incompatible way, so the parser cache
- * can automatically discard old data.
- */
- const VERSION = '1.6.4';
-
- # Flags for Parser::setLinkHook
- # Also available as global constants from Defines.php
- const SLH_PATTERN = 1;
-
- # Constants needed for external link processing
- # Everything except bracket, space, or control characters
- const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
- const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
- \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
-
- /**#@+
- * @private
- */
- # Persistent:
- var $mLinkHooks;
-
- /**#@-*/
-
- /**
- * Constructor
- */
- public function __construct( $conf = array() ) {
- parent::__construct( $conf );
- $this->mLinkHooks = array();
- }
-
- /**
- * Do various kinds of initialisation on the first call of the parser
- */
- function firstCallInit() {
- parent::__construct();
- if ( !$this->mFirstCall ) {
- return;
- }
- $this->mFirstCall = false;
-
- wfProfileIn( __METHOD__ );
-
- $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
- CoreParserFunctions::register( $this );
- CoreLinkFunctions::register( $this );
- $this->initialiseVariables();
-
- wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Create a link hook, e.g. [[Namepsace:...|display}}
- * The callback function should have the form:
- * function myLinkCallback( $parser, $holders, $markers,
- * Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... }
- *
- * Or with SLH_PATTERN:
- * function myLinkCallback( $parser, $holders, $markers, )
- * &$titleText, &$sortText = null, &$leadingColon = false ) { ... }
- *
- * The callback may either return a number of different possible values:
- * String) Text result of the link
- * True) (Treat as link) Parse the link according to normal link rules
- * False) (Bad link) Just output the raw wikitext (You may modify the text first)
- *
- * @param $ns Integer or String: the Namespace ID or regex pattern if SLH_PATTERN is set
- * @param $callback Mixed: the callback function (and object) to use
- * @param $flags Integer: a combination of the following flags:
- * SLH_PATTERN Use a regex link pattern rather than a namespace
- *
- * @return callback|null The old callback function for this name, if any
- */
- public function setLinkHook( $ns, $callback, $flags = 0 ) {
- if( $flags & SLH_PATTERN && !is_string($ns) )
- throw new MWException( __METHOD__.'() expecting a regex string pattern.' );
- elseif( $flags | ~SLH_PATTERN && !is_int($ns) )
- throw new MWException( __METHOD__.'() expecting a namespace index.' );
- $oldVal = isset( $this->mLinkHooks[$ns] ) ? $this->mLinkHooks[$ns][0] : null;
- $this->mLinkHooks[$ns] = array( $callback, $flags );
- return $oldVal;
- }
-
- /**
- * Get all registered link hook identifiers
- *
- * @return array
- */
- function getLinkHooks() {
- return array_keys( $this->mLinkHooks );
- }
-
- /**
- * Process [[ ]] wikilinks
- * @return LinkHolderArray
- *
- * @private
- */
- function replaceInternalLinks2( &$s ) {
- wfProfileIn( __METHOD__ );
-
- wfProfileIn( __METHOD__.'-setup' );
- static $tc = FALSE, $titleRegex;//$e1, $e1_img;
- if( !$tc ) {
- # the % is needed to support urlencoded titles as well
- $tc = Title::legalChars() . '#%';
- # Match a link having the form [[namespace:link|alternate]]trail
- //$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
- # Match cases where there is no "]]", which might still be images
- //$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
- # Match a valid plain title
- $titleRegex = "/^([{$tc}]+)$/sD";
- }
-
- $holders = new LinkHolderArray( $this );
-
- if( is_null( $this->mTitle ) ) {
- wfProfileOut( __METHOD__ );
- wfProfileOut( __METHOD__.'-setup' );
- throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
- }
-
- wfProfileOut( __METHOD__.'-setup' );
-
- $offset = 0;
- $offsetStack = array();
- $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) );
- while( true ) {
- $startBracketOffset = strpos( $s, '[[', $offset );
- $endBracketOffset = strpos( $s, ']]', $offset );
- # Finish when there are no more brackets
- if( $startBracketOffset === false && $endBracketOffset === false ) break;
- # Determine if the bracket is a starting or ending bracket
- # When we find both, use the first one
- elseif( $startBracketOffset !== false && $endBracketOffset !== false )
- $isStart = $startBracketOffset <= $endBracketOffset;
- # When we only found one, check which it is
- else $isStart = $startBracketOffset !== false;
- $bracketOffset = $isStart ? $startBracketOffset : $endBracketOffset;
- if( $isStart ) {
- /** Opening bracket **/
- # Just push our current offset in the string onto the stack
- $offsetStack[] = $startBracketOffset;
- } else {
- /** Closing bracket **/
- # Pop the start pos for our current link zone off the stack
- $startBracketOffset = array_pop($offsetStack);
- # Just to clean up the code, lets place offsets on the outer ends
- $endBracketOffset += 2;
-
- # Only do logic if we actually have a opening bracket for this
- if( isset($startBracketOffset) ) {
- # Extract text inside the link
- @list( $titleText, $paramText ) = explode('|',
- substr($s, $startBracketOffset+2, $endBracketOffset-$startBracketOffset-4), 2);
- # Create markers only for valid links
- if( preg_match( $titleRegex, $titleText ) ) {
- # Store the text for the marker
- $marker = $markers->addMarker($titleText, $paramText);
- # Replace the current link with the marker
- $s = substr($s,0,$startBracketOffset).
- $marker.
- substr($s, $endBracketOffset);
- # We have modified $s, because of this we need to set the
- # offset manually since the end position is different now
- $offset = $startBracketOffset+strlen($marker);
- continue;
- }
- # ToDo: Some LinkHooks may allow recursive links inside of
- # the link text, create a regex that also matches our
- # <!-- LINKMARKER ### --> sequence in titles
- # ToDO: Some LinkHooks use patterns rather than namespaces
- # these need to be tested at this point here
- }
-
- }
- # Bump our offset to after our current bracket
- $offset = $bracketOffset+2;
- }
-
-
- # Now expand our tree
- wfProfileIn( __METHOD__.'-expand' );
- $s = $markers->expand( $s );
- wfProfileOut( __METHOD__.'-expand' );
-
- wfProfileOut( __METHOD__ );
- return $holders;
- }
-
- function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) {
- wfProfileIn( __METHOD__ );
- $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
- wfProfileIn( __METHOD__."-misc" );
- # Don't allow internal links to pages containing
- # PROTO: where PROTO is a valid URL protocol; these
- # should be external links.
- if( preg_match('/^\b(?i:' . wfUrlProtocols() . ')/', $titleText) ) {
- wfProfileOut( __METHOD__ );
- return $wt;
- }
-
- # Make subpage if necessary
- if( $this->areSubpagesAllowed() ) {
- $titleText = $this->maybeDoSubpageLink( $titleText, $paramText );
- }
-
- # Check for a leading colon and strip it if it is there
- $leadingColon = $titleText[0] == ':';
- if( $leadingColon ) $titleText = substr( $titleText, 1 );
-
- wfProfileOut( __METHOD__."-misc" );
- # Make title object
- wfProfileIn( __METHOD__."-title" );
- $title = Title::newFromText( $this->mStripState->unstripNoWiki( $titleText ) );
- if( !$title ) {
- wfProfileOut( __METHOD__."-title" );
- wfProfileOut( __METHOD__ );
- return $wt;
- }
- $ns = $title->getNamespace();
- wfProfileOut( __METHOD__."-title" );
-
- # Default for Namespaces is a default link
- # ToDo: Default for patterns is plain wikitext
- $return = true;
- if( isset( $this->mLinkHooks[$ns] ) ) {
- list( $callback, $flags ) = $this->mLinkHooks[$ns];
- if( $flags & SLH_PATTERN ) {
- $args = array( $parser, $holders, $markers, $titleText, &$paramText, &$leadingColon );
- } else {
- $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
- }
- # Workaround for PHP bug 35229 and similar
- if ( !is_callable( $callback ) ) {
- throw new MWException( "Tag hook for namespace $ns is not callable\n" );
- }
- $return = call_user_func_array( $callback, $args );
- }
- if( $return === true ) {
- # True (treat as plain link) was returned, call the defaultLinkHook
- $return = CoreLinkFunctions::defaultLinkHook( $parser, $holders, $markers, $title,
- $titleText, $paramText, $leadingColon );
- }
- if( $return === false ) {
- # False (no link) was returned, output plain wikitext
- # Build it again as the hook is allowed to modify $paramText
- $return = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
- }
- # Content was returned, return it
- wfProfileOut( __METHOD__ );
- return $return;
- }
-
-}
-
-class LinkMarkerReplacer {
-
- protected $markers, $nextId, $parser, $holders, $callback;
-
- function __construct( $parser, $holders, $callback ) {
- $this->nextId = 0;
- $this->markers = array();
- $this->parser = $parser;
- $this->holders = $holders;
- $this->callback = $callback;
- }
-
- function addMarker($titleText, $paramText) {
- $id = $this->nextId++;
- $this->markers[$id] = array( $titleText, $paramText );
- return "<!-- LINKMARKER $id -->";
- }
-
- function findMarker( $string ) {
- return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string );
- }
-
- function expand( $string ) {
- return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string );
- }
-
- function callback( $m ) {
- $id = intval($m[1]);
- if( !array_key_exists($id, $this->markers) ) return $m[0];
- $args = $this->markers[$id];
- array_unshift( $args, $this );
- array_unshift( $args, $this->holders );
- array_unshift( $args, $this->parser );
- return call_user_func_array( $this->callback, $args );
- }
-
-}
diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php
index bd13f9ae..aeacd2e1 100644
--- a/includes/parser/Preprocessor.php
+++ b/includes/parser/Preprocessor.php
@@ -84,9 +84,9 @@ interface PPFrame {
/**
* Create a child frame
*
- * @param $args array
- * @param $title Title
- * @param $indexOffset A number subtracted from the index attributes of the arguments
+ * @param array $args
+ * @param Title $title
+ * @param int $indexOffset A number subtracted from the index attributes of the arguments
*
* @return PPFrame
*/
@@ -205,7 +205,6 @@ interface PPNode {
*/
function getChildrenOfType( $type );
-
/**
* Returns the length of the array, or false if this is not an array-type node
*/
diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php
index 34de0ba5..3138f483 100644
--- a/includes/parser/Preprocessor_DOM.php
+++ b/includes/parser/Preprocessor_DOM.php
@@ -72,9 +72,8 @@ class Preprocessor_DOM implements Preprocessor {
$xml = "<list>";
foreach ( $values as $k => $val ) {
-
if ( is_int( $k ) ) {
- $xml .= "<part><name index=\"$k\"/><value>" . htmlspecialchars( $val ) ."</value></part>";
+ $xml .= "<part><name index=\"$k\"/><value>" . htmlspecialchars( $val ) . "</value></part>";
} else {
$xml .= "<part><name>" . htmlspecialchars( $k ) . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>";
}
@@ -110,7 +109,7 @@ class Preprocessor_DOM implements Preprocessor {
* Preprocess some wikitext and return the document tree.
* This is the ghost of Parser::replace_variables().
*
- * @param $text String: the text to parse
+ * @param string $text the text to parse
* @param $flags Integer: bitwise combination of:
* Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being
* included. Default is to assume a direct page view.
@@ -126,6 +125,7 @@ class Preprocessor_DOM implements Preprocessor {
* cache may be implemented at a later date which takes further advantage of these strict
* dependency requirements.
*
+ * @throws MWException
* @return PPNode_DOM
*/
function preprocessToObj( $text, $flags = 0 ) {
@@ -136,9 +136,9 @@ class Preprocessor_DOM implements Preprocessor {
$cacheable = ( $wgPreprocessorCacheThreshold !== false
&& strlen( $text ) > $wgPreprocessorCacheThreshold );
if ( $cacheable ) {
- wfProfileIn( __METHOD__.'-cacheable' );
+ wfProfileIn( __METHOD__ . '-cacheable' );
- $cacheKey = wfMemcKey( 'preprocess-xml', md5($text), $flags );
+ $cacheKey = wfMemcKey( 'preprocess-xml', md5( $text ), $flags );
$cacheValue = $wgMemc->get( $cacheKey );
if ( $cacheValue ) {
$version = substr( $cacheValue, 0, 8 );
@@ -148,30 +148,32 @@ class Preprocessor_DOM implements Preprocessor {
wfDebugLog( "Preprocessor", "Loaded preprocessor XML from memcached (key $cacheKey)" );
}
}
- }
- if ( $xml === false ) {
- if ( $cacheable ) {
- wfProfileIn( __METHOD__.'-cache-miss' );
+ if ( $xml === false ) {
+ wfProfileIn( __METHOD__ . '-cache-miss' );
$xml = $this->preprocessToXml( $text, $flags );
$cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . $xml;
$wgMemc->set( $cacheKey, $cacheValue, 86400 );
- wfProfileOut( __METHOD__.'-cache-miss' );
+ wfProfileOut( __METHOD__ . '-cache-miss' );
wfDebugLog( "Preprocessor", "Saved preprocessor XML to memcached (key $cacheKey)" );
- } else {
- $xml = $this->preprocessToXml( $text, $flags );
}
-
+ } else {
+ $xml = $this->preprocessToXml( $text, $flags );
}
+
// Fail if the number of elements exceeds acceptable limits
- // Do not attempt to generate the DOM
+ // Do not attempt to generate the DOM
$this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' );
$max = $this->parser->mOptions->getMaxGeneratedPPNodeCount();
if ( $this->parser->mGeneratedPPNodeCount > $max ) {
- throw new MWException( __METHOD__.': generated node count limit exceeded' );
+ if ( $cacheable ) {
+ wfProfileOut( __METHOD__ . '-cacheable' );
+ }
+ wfProfileOut( __METHOD__ );
+ throw new MWException( __METHOD__ . ': generated node count limit exceeded' );
}
- wfProfileIn( __METHOD__.'-loadXML' );
+ wfProfileIn( __METHOD__ . '-loadXML' );
$dom = new DOMDocument;
wfSuppressWarnings();
$result = $dom->loadXML( $xml );
@@ -181,16 +183,21 @@ class Preprocessor_DOM implements Preprocessor {
$xml = UtfNormal::cleanUp( $xml );
// 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 don't barf when the XML is >256 levels deep
$result = $dom->loadXML( $xml, 1 << 19 );
- if ( !$result ) {
- throw new MWException( __METHOD__.' generated invalid XML' );
- }
}
- $obj = new PPNode_DOM( $dom->documentElement );
- wfProfileOut( __METHOD__.'-loadXML' );
+ if ( $result ) {
+ $obj = new PPNode_DOM( $dom->documentElement );
+ }
+ wfProfileOut( __METHOD__ . '-loadXML' );
+
if ( $cacheable ) {
- wfProfileOut( __METHOD__.'-cacheable' );
+ wfProfileOut( __METHOD__ . '-cacheable' );
}
+
wfProfileOut( __METHOD__ );
+
+ if ( !$result ) {
+ throw new MWException( __METHOD__ . ' generated invalid XML' );
+ }
return $obj;
}
@@ -354,9 +361,11 @@ class Preprocessor_DOM implements Preprocessor {
}
// Handle comments
if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
- // To avoid leaving blank lines, when a comment is both preceded
- // and followed by a newline (ignoring spaces), trim leading and
- // trailing spaces and one of the newlines.
+
+ // To avoid leaving blank lines, when a sequence of
+ // space-separated comments is both preceded and followed by
+ // a newline (ignoring spaces), then
+ // trim leading and trailing spaces and the trailing newline.
// Find the end
$endPos = strpos( $text, '-->', $i + 4 );
@@ -367,10 +376,25 @@ class Preprocessor_DOM implements Preprocessor {
$i = $lengthText;
} else {
// Search backwards for leading whitespace
- $wsStart = $i ? ( $i - strspn( $revText, ' ', $lengthText - $i ) ) : 0;
+ $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
+
// Search forwards for trailing whitespace
// $wsEnd will be the position of the last space (or the '>' if there's none)
- $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+ $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
+
+ // Keep looking forward as long as we're finding more
+ // comments.
+ $comments = array( array( $wsStart, $wsEnd ) );
+ while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
+ $c = strpos( $text, '-->', $wsEnd + 4 );
+ if ( $c === false ) {
+ break;
+ }
+ $c = $c + 2 + strspn( $text, " \t", $c + 3 );
+ $comments[] = array( $wsEnd + 1, $c );
+ $wsEnd = $c;
+ }
+
// Eat the line if possible
// TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
// the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
@@ -378,14 +402,26 @@ class Preprocessor_DOM implements Preprocessor {
if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
&& substr( $text, $wsEnd + 1, 1 ) == "\n" )
{
- $startPos = $wsStart;
- $endPos = $wsEnd + 1;
// Remove leading whitespace from the end of the accumulator
// Sanity check first though
$wsLength = $i - $wsStart;
- if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) {
+ if ( $wsLength > 0
+ && strspn( $accum, " \t", -$wsLength ) === $wsLength )
+ {
$accum = substr( $accum, 0, -$wsLength );
}
+
+ // Dump all but the last comment to the accumulator
+ foreach ( $comments as $j => $com ) {
+ $startPos = $com[0];
+ $endPos = $com[1] + 1;
+ if ( $j == ( count( $comments ) - 1 ) ) {
+ break;
+ }
+ $inner = substr( $text, $startPos, $endPos - $startPos );
+ $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+ }
+
// Do a line-start run next time to look for headings after the comment
$fakeLineStart = true;
} else {
@@ -396,7 +432,7 @@ class Preprocessor_DOM implements Preprocessor {
if ( $stack->top ) {
$part = $stack->top->getCurrentPart();
- if ( ! (isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 )) {
+ if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
$part->visualEnd = $wsStart;
}
// Else comments abutting, no change in visual end
@@ -431,7 +467,7 @@ class Preprocessor_DOM implements Preprocessor {
}
$tagStartPos = $i;
- if ( $text[$tagEndPos-1] == '/' ) {
+ if ( $text[$tagEndPos - 1] == '/' ) {
$attrEnd = $tagEndPos - 1;
$inner = null;
$i = $tagEndPos + 1;
@@ -521,7 +557,7 @@ class Preprocessor_DOM implements Preprocessor {
if ( $equalsLength > 0 ) {
if ( $searchStart - $equalsLength == $piece->startPos ) {
// This is just a single string of equals signs on its own line
- // Replicate the doHeadings behaviour /={count}(.+)={count}/
+ // Replicate the doHeadings behavior /={count}(.+)={count}/
// First find out how many equals signs there really are (don't stop at 6)
$count = $equalsLength;
if ( $count < 3 ) {
@@ -568,7 +604,7 @@ class Preprocessor_DOM implements Preprocessor {
'open' => $curChar,
'close' => $rule['end'],
'count' => $count,
- 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+ 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
);
$stack->push( $piece );
@@ -657,19 +693,13 @@ class Preprocessor_DOM implements Preprocessor {
$piece->parts = array( new PPDPart );
$piece->count -= $matchingCount;
# do we still qualify for any callback with remaining count?
- $names = $rules[$piece->open]['names'];
- $skippedBraces = 0;
- $enclosingAccum =& $accum;
- while ( $piece->count ) {
- if ( array_key_exists( $piece->count, $names ) ) {
- $stack->push( $piece );
- $accum =& $stack->getAccum();
- break;
- }
- --$piece->count;
- $skippedBraces ++;
+ $min = $rules[$piece->open]['min'];
+ if ( $piece->count >= $min ) {
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ } else {
+ $accum .= str_repeat( $piece->open, $piece->count );
}
- $enclosingAccum .= str_repeat( $piece->open, $skippedBraces );
}
$flags = $stack->getFlags();
extract( $flags );
@@ -751,18 +781,18 @@ class PPDStack {
$class = $this->elementClass;
$this->stack[] = new $class( $data );
}
- $this->top = $this->stack[ count( $this->stack ) - 1 ];
+ $this->top = $this->stack[count( $this->stack ) - 1];
$this->accum =& $this->top->getAccum();
}
function pop() {
if ( !count( $this->stack ) ) {
- throw new MWException( __METHOD__.': no elements remaining' );
+ throw new MWException( __METHOD__ . ': no elements remaining' );
}
$temp = array_pop( $this->stack );
if ( count( $this->stack ) ) {
- $this->top = $this->stack[ count( $this->stack ) - 1 ];
+ $this->top = $this->stack[count( $this->stack ) - 1];
$this->accum =& $this->top->getAccum();
} else {
$this->top = self::$false;
@@ -796,8 +826,8 @@ class PPDStack {
* @ingroup Parser
*/
class PPDStackElement {
- var $open, // Opening character (\n for heading)
- $close, // Matching closing character
+ var $open, // Opening character (\n for heading)
+ $close, // Matching closing character
$count, // Number of opening characters found (number of "=" for heading)
$parts, // Array of PPDPart objects describing pipe-separated parts.
$lineStart; // True if the open char appeared at the start of the input line. Not set for headings.
@@ -814,7 +844,7 @@ class PPDStackElement {
}
function &getAccum() {
- return $this->parts[count($this->parts) - 1]->out;
+ return $this->parts[count( $this->parts ) - 1]->out;
}
function addPart( $s = '' ) {
@@ -823,7 +853,7 @@ class PPDStackElement {
}
function getCurrentPart() {
- return $this->parts[count($this->parts) - 1];
+ return $this->parts[count( $this->parts ) - 1];
}
/**
@@ -916,7 +946,6 @@ class PPFrame_DOM implements PPFrame {
*/
var $depth;
-
/**
* Construct a new preprocessor frame.
* @param $preprocessor Preprocessor The parent preprocessor
@@ -1020,11 +1049,13 @@ class PPFrame_DOM implements PPFrame {
while ( count( $iteratorStack ) > 1 ) {
$level = count( $outStack ) - 1;
- $iteratorNode =& $iteratorStack[ $level ];
+ $iteratorNode =& $iteratorStack[$level];
$out =& $outStack[$level];
$index =& $indexStack[$level];
- if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node;
+ if ( $iteratorNode instanceof PPNode_DOM ) {
+ $iteratorNode = $iteratorNode->node;
+ }
if ( is_array( $iteratorNode ) ) {
if ( $index >= count( $iteratorNode ) ) {
@@ -1117,7 +1148,7 @@ class PPFrame_DOM implements PPFrame {
}
# Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
# Not in RECOVER_COMMENTS mode (extractSections) though
- elseif ( $this->parser->ot['wiki'] && ! ( $flags & PPFrame::RECOVER_COMMENTS ) ) {
+ elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
$out .= $this->parser->insertStripItem( $contextNode->textContent );
}
# Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
@@ -1154,9 +1185,7 @@ class PPFrame_DOM implements PPFrame {
# Insert a heading marker only for <h> children of <root>
# This is to stop extractSections from going over multiple tree levels
- if ( $contextNode->parentNode->nodeName == 'root'
- && $this->parser->ot['html'] )
- {
+ if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
# Insert heading index marker
$headingIndex = $contextNode->getAttribute( 'i' );
$titleText = $this->title->getPrefixedDBkey();
@@ -1174,7 +1203,7 @@ class PPFrame_DOM implements PPFrame {
}
} else {
wfProfileOut( __METHOD__ );
- throw new MWException( __METHOD__.': Invalid parameter type' );
+ throw new MWException( __METHOD__ . ': Invalid parameter type' );
}
if ( $newIterator !== false ) {
@@ -1212,7 +1241,9 @@ class PPFrame_DOM implements PPFrame {
$first = true;
$s = '';
foreach ( $args as $root ) {
- if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( $root instanceof PPNode_DOM ) {
+ $root = $root->node;
+ }
if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
$root = array( $root );
}
@@ -1458,25 +1489,25 @@ class PPTemplateFrame_DOM extends PPFrame_DOM {
function getArguments() {
$arguments = array();
foreach ( array_merge(
- array_keys($this->numberedArgs),
- array_keys($this->namedArgs)) as $key ) {
- $arguments[$key] = $this->getArgument($key);
+ array_keys( $this->numberedArgs ),
+ array_keys( $this->namedArgs ) ) as $key ) {
+ $arguments[$key] = $this->getArgument( $key );
}
return $arguments;
}
function getNumberedArguments() {
$arguments = array();
- foreach ( array_keys($this->numberedArgs) as $key ) {
- $arguments[$key] = $this->getArgument($key);
+ foreach ( array_keys( $this->numberedArgs ) as $key ) {
+ $arguments[$key] = $this->getArgument( $key );
}
return $arguments;
}
function getNamedArguments() {
$arguments = array();
- foreach ( array_keys($this->namedArgs) as $key ) {
- $arguments[$key] = $this->getArgument($key);
+ foreach ( array_keys( $this->namedArgs ) as $key ) {
+ $arguments[$key] = $this->getArgument( $key );
}
return $arguments;
}
@@ -1673,6 +1704,7 @@ class PPNode_DOM implements PPNode {
* - index String index
* - value PPNode value
*
+ * @throws MWException
* @return array
*/
function splitArg() {
@@ -1694,6 +1726,7 @@ class PPNode_DOM implements PPNode {
* Split an "<ext>" node into an associative array containing name, attr, inner and close
* All values in the resulting array are PPNodes. Inner and close are optional.
*
+ * @throws MWException
* @return array
*/
function splitExt() {
@@ -1719,6 +1752,7 @@ class PPNode_DOM implements PPNode {
/**
* Split a "<h>" node
+ * @throws MWException
* @return array
*/
function splitHeading() {
diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php
index 4f04c865..2fc5e118 100644
--- a/includes/parser/Preprocessor_Hash.php
+++ b/includes/parser/Preprocessor_Hash.php
@@ -89,7 +89,7 @@ class Preprocessor_Hash implements Preprocessor {
* Preprocess some wikitext and return the document tree.
* This is the ghost of Parser::replace_variables().
*
- * @param $text String: the text to parse
+ * @param string $text the text to parse
* @param $flags Integer: bitwise combination of:
* Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being
* included. Default is to assume a direct page view.
@@ -105,6 +105,7 @@ class Preprocessor_Hash implements Preprocessor {
* cache may be implemented at a later date which takes further advantage of these strict
* dependency requirements.
*
+ * @throws MWException
* @return PPNode_Hash_Tree
*/
function preprocessToObj( $text, $flags = 0 ) {
@@ -115,9 +116,9 @@ class Preprocessor_Hash implements Preprocessor {
$cacheable = $wgPreprocessorCacheThreshold !== false && strlen( $text ) > $wgPreprocessorCacheThreshold;
if ( $cacheable ) {
- wfProfileIn( __METHOD__.'-cacheable' );
+ wfProfileIn( __METHOD__ . '-cacheable' );
- $cacheKey = wfMemcKey( 'preprocess-hash', md5($text), $flags );
+ $cacheKey = wfMemcKey( 'preprocess-hash', md5( $text ), $flags );
$cacheValue = $wgMemc->get( $cacheKey );
if ( $cacheValue ) {
$version = substr( $cacheValue, 0, 8 );
@@ -126,12 +127,12 @@ class Preprocessor_Hash implements Preprocessor {
// From the cache
wfDebugLog( "Preprocessor",
"Loaded preprocessor hash from memcached (key $cacheKey)" );
- wfProfileOut( __METHOD__.'-cacheable' );
+ wfProfileOut( __METHOD__ . '-cacheable' );
wfProfileOut( __METHOD__ );
return $hash;
}
}
- wfProfileIn( __METHOD__.'-cache-miss' );
+ wfProfileIn( __METHOD__ . '-cache-miss' );
}
$rules = array(
@@ -286,9 +287,11 @@ class Preprocessor_Hash implements Preprocessor {
}
// Handle comments
if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
- // To avoid leaving blank lines, when a comment is both preceded
- // and followed by a newline (ignoring spaces), trim leading and
- // trailing spaces and one of the newlines.
+
+ // To avoid leaving blank lines, when a sequence of
+ // space-separated comments is both preceded and followed by
+ // a newline (ignoring spaces), then
+ // trim leading and trailing spaces and the trailing newline.
// Find the end
$endPos = strpos( $text, '-->', $i + 4 );
@@ -299,10 +302,25 @@ class Preprocessor_Hash implements Preprocessor {
$i = $lengthText;
} else {
// Search backwards for leading whitespace
- $wsStart = $i ? ( $i - strspn( $revText, ' ', $lengthText - $i ) ) : 0;
+ $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
+
// Search forwards for trailing whitespace
// $wsEnd will be the position of the last space (or the '>' if there's none)
- $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+ $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
+
+ // Keep looking forward as long as we're finding more
+ // comments.
+ $comments = array( array( $wsStart, $wsEnd ) );
+ while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
+ $c = strpos( $text, '-->', $wsEnd + 4 );
+ if ( $c === false ) {
+ break;
+ }
+ $c = $c + 2 + strspn( $text, " \t", $c + 3 );
+ $comments[] = array( $wsEnd + 1, $c );
+ $wsEnd = $c;
+ }
+
// Eat the line if possible
// TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
// the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
@@ -310,17 +328,27 @@ class Preprocessor_Hash implements Preprocessor {
if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
&& substr( $text, $wsEnd + 1, 1 ) == "\n" )
{
- $startPos = $wsStart;
- $endPos = $wsEnd + 1;
// Remove leading whitespace from the end of the accumulator
// Sanity check first though
$wsLength = $i - $wsStart;
if ( $wsLength > 0
&& $accum->lastNode instanceof PPNode_Hash_Text
- && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
+ && strspn( $accum->lastNode->value, " \t", -$wsLength ) === $wsLength )
{
$accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
}
+
+ // Dump all but the last comment to the accumulator
+ foreach ( $comments as $j => $com ) {
+ $startPos = $com[0];
+ $endPos = $com[1] + 1;
+ if ( $j == ( count( $comments ) - 1 ) ) {
+ break;
+ }
+ $inner = substr( $text, $startPos, $endPos - $startPos );
+ $accum->addNodeWithText( 'comment', $inner );
+ }
+
// Do a line-start run next time to look for headings after the comment
$fakeLineStart = true;
} else {
@@ -331,7 +359,7 @@ class Preprocessor_Hash implements Preprocessor {
if ( $stack->top ) {
$part = $stack->top->getCurrentPart();
- if ( ! (isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 )) {
+ if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
$part->visualEnd = $wsStart;
}
// Else comments abutting, no change in visual end
@@ -366,7 +394,7 @@ class Preprocessor_Hash implements Preprocessor {
}
$tagStartPos = $i;
- if ( $text[$tagEndPos-1] == '/' ) {
+ if ( $text[$tagEndPos - 1] == '/' ) {
// Short end tag
$attrEnd = $tagEndPos - 1;
$inner = null;
@@ -390,7 +418,7 @@ class Preprocessor_Hash implements Preprocessor {
}
// <includeonly> and <noinclude> just become <ignore> tags
if ( in_array( $lowerName, $ignoredElements ) ) {
- $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
+ $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
continue;
}
@@ -461,7 +489,7 @@ class Preprocessor_Hash implements Preprocessor {
if ( $equalsLength > 0 ) {
if ( $searchStart - $equalsLength == $piece->startPos ) {
// This is just a single string of equals signs on its own line
- // Replicate the doHeadings behaviour /={count}(.+)={count}/
+ // Replicate the doHeadings behavior /={count}(.+)={count}/
// First find out how many equals signs there really are (don't stop at 6)
$count = $equalsLength;
if ( $count < 3 ) {
@@ -514,7 +542,7 @@ class Preprocessor_Hash implements Preprocessor {
'open' => $curChar,
'close' => $rule['end'],
'count' => $count,
- 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+ 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
);
$stack->push( $piece );
@@ -548,7 +576,7 @@ class Preprocessor_Hash implements Preprocessor {
}
}
- if ($matchingCount <= 0) {
+ if ( $matchingCount <= 0 ) {
# No matching element found in callback array
# Output a literal closing brace and continue
$accum->addLiteral( str_repeat( $curChar, $count ) );
@@ -590,10 +618,20 @@ class Preprocessor_Hash implements Preprocessor {
$lastNode = $node;
}
if ( !$node ) {
- throw new MWException( __METHOD__. ': eqpos not found' );
+ if ( $cacheable ) {
+ wfProfileOut( __METHOD__ . '-cache-miss' );
+ wfProfileOut( __METHOD__ . '-cacheable' );
+ }
+ wfProfileOut( __METHOD__ );
+ throw new MWException( __METHOD__ . ': eqpos not found' );
}
if ( $node->name !== 'equals' ) {
- throw new MWException( __METHOD__ .': eqpos is not equals' );
+ if ( $cacheable ) {
+ wfProfileOut( __METHOD__ . '-cache-miss' );
+ wfProfileOut( __METHOD__ . '-cacheable' );
+ }
+ wfProfileOut( __METHOD__ );
+ throw new MWException( __METHOD__ . ': eqpos is not equals' );
}
$equalsNode = $node;
@@ -638,23 +676,17 @@ class Preprocessor_Hash implements Preprocessor {
$accum =& $stack->getAccum();
# Re-add the old stack element if it still has unmatched opening characters remaining
- if ($matchingCount < $piece->count) {
+ if ( $matchingCount < $piece->count ) {
$piece->parts = array( new PPDPart_Hash );
$piece->count -= $matchingCount;
# do we still qualify for any callback with remaining count?
- $names = $rules[$piece->open]['names'];
- $skippedBraces = 0;
- $enclosingAccum =& $accum;
- while ( $piece->count ) {
- if ( array_key_exists( $piece->count, $names ) ) {
- $stack->push( $piece );
- $accum =& $stack->getAccum();
- break;
- }
- --$piece->count;
- $skippedBraces ++;
+ $min = $rules[$piece->open]['min'];
+ if ( $piece->count >= $min ) {
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ } else {
+ $accum->addLiteral( str_repeat( $piece->open, $piece->count ) );
}
- $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
}
extract( $stack->getFlags() );
@@ -695,11 +727,11 @@ class Preprocessor_Hash implements Preprocessor {
$rootNode->lastChild = $stack->rootAccum->lastNode;
// Cache
- if ($cacheable) {
+ if ( $cacheable ) {
$cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . serialize( $rootNode );
$wgMemc->set( $cacheKey, $cacheValue, 86400 );
- wfProfileOut( __METHOD__.'-cache-miss' );
- wfProfileOut( __METHOD__.'-cacheable' );
+ wfProfileOut( __METHOD__ . '-cache-miss' );
+ wfProfileOut( __METHOD__ . '-cacheable' );
wfDebugLog( "Preprocessor", "Saved preprocessor Hash to memcached (key $cacheKey)" );
}
@@ -866,7 +898,6 @@ class PPFrame_Hash implements PPFrame {
*/
var $depth;
-
/**
* Construct a new preprocessor frame.
* @param $preprocessor Preprocessor: the parent preprocessor
@@ -884,9 +915,11 @@ class PPFrame_Hash implements PPFrame {
* Create a new child frame
* $args is optionally a multi-root PPNode or array containing the template arguments
*
- * @param $args PPNode_Hash_Array|array
+ * @param array|bool|\PPNode_Hash_Array $args PPNode_Hash_Array|array
* @param $title Title|bool
*
+ * @param int $indexOffset
+ * @throws MWException
* @return PPTemplateFrame_Hash
*/
function newChild( $args = false, $title = false, $indexOffset = 0 ) {
@@ -956,7 +989,7 @@ class PPFrame_Hash implements PPFrame {
while ( count( $iteratorStack ) > 1 ) {
$level = count( $outStack ) - 1;
- $iteratorNode =& $iteratorStack[ $level ];
+ $iteratorNode =& $iteratorStack[$level];
$out =& $outStack[$level];
$index =& $indexStack[$level];
@@ -1035,7 +1068,7 @@ class PPFrame_Hash implements PPFrame {
}
# Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
# Not in RECOVER_COMMENTS mode (extractSections) though
- elseif ( $this->parser->ot['wiki'] && ! ( $flags & PPFrame::RECOVER_COMMENTS ) ) {
+ elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
$out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
}
# Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
@@ -1082,7 +1115,7 @@ class PPFrame_Hash implements PPFrame {
$newIterator = $contextNode->getChildren();
}
} else {
- throw new MWException( __METHOD__.': Invalid parameter type' );
+ throw new MWException( __METHOD__ . ': Invalid parameter type' );
}
if ( $newIterator !== false ) {
@@ -1371,9 +1404,9 @@ class PPTemplateFrame_Hash extends PPFrame_Hash {
function getArguments() {
$arguments = array();
foreach ( array_merge(
- array_keys($this->numberedArgs),
- array_keys($this->namedArgs)) as $key ) {
- $arguments[$key] = $this->getArgument($key);
+ array_keys( $this->numberedArgs ),
+ array_keys( $this->namedArgs ) ) as $key ) {
+ $arguments[$key] = $this->getArgument( $key );
}
return $arguments;
}
@@ -1383,8 +1416,8 @@ class PPTemplateFrame_Hash extends PPFrame_Hash {
*/
function getNumberedArguments() {
$arguments = array();
- foreach ( array_keys($this->numberedArgs) as $key ) {
- $arguments[$key] = $this->getArgument($key);
+ foreach ( array_keys( $this->numberedArgs ) as $key ) {
+ $arguments[$key] = $this->getArgument( $key );
}
return $arguments;
}
@@ -1394,8 +1427,8 @@ class PPTemplateFrame_Hash extends PPFrame_Hash {
*/
function getNamedArguments() {
$arguments = array();
- foreach ( array_keys($this->namedArgs) as $key ) {
- $arguments[$key] = $this->getArgument($key);
+ foreach ( array_keys( $this->namedArgs ) as $key ) {
+ $arguments[$key] = $this->getArgument( $key );
}
return $arguments;
}
@@ -1609,6 +1642,7 @@ class PPNode_Hash_Tree implements PPNode {
* - index String index
* - value PPNode value
*
+ * @throws MWException
* @return array
*/
function splitArg() {
@@ -1642,6 +1676,7 @@ class PPNode_Hash_Tree implements PPNode {
* Split an "<ext>" node into an associative array containing name, attr, inner and close
* All values in the resulting array are PPNodes. Inner and close are optional.
*
+ * @throws MWException
* @return array
*/
function splitExt() {
@@ -1669,6 +1704,7 @@ class PPNode_Hash_Tree implements PPNode {
/**
* Split an "<h>" node
*
+ * @throws MWException
* @return array
*/
function splitHeading() {
@@ -1695,6 +1731,7 @@ class PPNode_Hash_Tree implements PPNode {
/**
* Split a "<template>" or "<tplarg>" node
*
+ * @throws MWException
* @return array
*/
function splitTemplate() {
diff --git a/includes/parser/Preprocessor_HipHop.hphp b/includes/parser/Preprocessor_HipHop.hphp
deleted file mode 100644
index 8b71a1b5..00000000
--- a/includes/parser/Preprocessor_HipHop.hphp
+++ /dev/null
@@ -1,2013 +0,0 @@
-<?php
-/**
- * A preprocessor optimised for HipHop, using HipHop-specific syntax.
- * vim: ft=php
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Parser
- */
-
-/**
- * @ingroup Parser
- */
-class Preprocessor_HipHop implements Preprocessor {
- /**
- * @var Parser
- */
- var $parser;
-
- const CACHE_VERSION = 1;
-
- /**
- * @param $parser Parser
- */
- function __construct( $parser ) {
- $this->parser = $parser;
- }
-
- /**
- * @return PPFrame_HipHop
- */
- function newFrame() {
- return new PPFrame_HipHop( $this );
- }
-
- /**
- * @param $args array
- * @return PPCustomFrame_HipHop
- */
- function newCustomFrame( $args ) {
- return new PPCustomFrame_HipHop( $this, $args );
- }
-
- /**
- * @param $values array
- * @return PPNode_HipHop_Array
- */
- function newPartNodeArray( $values ) {
- $list = array();
-
- foreach ( $values as $k => $val ) {
- $partNode = new PPNode_HipHop_Tree( 'part' );
- $nameNode = new PPNode_HipHop_Tree( 'name' );
-
- if ( is_int( $k ) ) {
- $nameNode->addChild( new PPNode_HipHop_Attr( 'index', $k ) );
- $partNode->addChild( $nameNode );
- } else {
- $nameNode->addChild( new PPNode_HipHop_Text( $k ) );
- $partNode->addChild( $nameNode );
- $partNode->addChild( new PPNode_HipHop_Text( '=' ) );
- }
-
- $valueNode = new PPNode_HipHop_Tree( 'value' );
- $valueNode->addChild( new PPNode_HipHop_Text( $val ) );
- $partNode->addChild( $valueNode );
-
- $list[] = $partNode;
- }
-
- $node = new PPNode_HipHop_Array( $list );
- return $node;
- }
-
- /**
- * Preprocess some wikitext and return the document tree.
- * This is the ghost of Parser::replace_variables().
- *
- * @param $text String: the text to parse
- * @param $flags Integer: bitwise combination of:
- * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
- * included. Default is to assume a direct page view.
- *
- * The generated DOM tree must depend only on the input text and the flags.
- * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
- *
- * Any flag added to the $flags parameter here, or any other parameter liable to cause a
- * change in the DOM tree for a given text, must be passed through the section identifier
- * in the section edit link and thus back to extractSections().
- *
- * The output of this function is currently only cached in process memory, but a persistent
- * cache may be implemented at a later date which takes further advantage of these strict
- * dependency requirements.
- *
- * @throws MWException
- * @return PPNode_HipHop_Tree
- */
- function preprocessToObj( $text, $flags = 0 ) {
- wfProfileIn( __METHOD__ );
-
- // Check cache.
- global $wgMemc, $wgPreprocessorCacheThreshold;
-
- $lengthText = strlen( $text );
-
- $cacheable = ($wgPreprocessorCacheThreshold !== false && $lengthText > $wgPreprocessorCacheThreshold);
- if ( $cacheable ) {
- wfProfileIn( __METHOD__.'-cacheable' );
-
- $cacheKey = strval( wfMemcKey( 'preprocess-hash', md5($text), $flags ) );
- $cacheValue = strval( $wgMemc->get( $cacheKey ) );
- if ( $cacheValue !== '' ) {
- $version = substr( $cacheValue, 0, 8 );
- if ( intval( $version ) == self::CACHE_VERSION ) {
- $hash = unserialize( substr( $cacheValue, 8 ) );
- // From the cache
- wfDebugLog( "Preprocessor",
- "Loaded preprocessor hash from memcached (key $cacheKey)" );
- wfProfileOut( __METHOD__.'-cacheable' );
- wfProfileOut( __METHOD__ );
- return $hash;
- }
- }
- wfProfileIn( __METHOD__.'-cache-miss' );
- }
-
- $rules = array(
- '{' => array(
- 'end' => '}',
- 'names' => array(
- 2 => 'template',
- 3 => 'tplarg',
- ),
- 'min' => 2,
- 'max' => 3,
- ),
- '[' => array(
- 'end' => ']',
- 'names' => array( 2 => 'LITERAL' ),
- 'min' => 2,
- 'max' => 2,
- )
- );
-
- $forInclusion = (bool)( $flags & Parser::PTD_FOR_INCLUSION );
-
- $xmlishElements = (array)$this->parser->getStripList();
- $enableOnlyinclude = false;
- if ( $forInclusion ) {
- $ignoredTags = array( 'includeonly', '/includeonly' );
- $ignoredElements = array( 'noinclude' );
- $xmlishElements[] = 'noinclude';
- if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
- $enableOnlyinclude = true;
- }
- } else if ( $this->parser->ot['wiki'] ) {
- $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude', 'includeonly', '/includeonly' );
- $ignoredElements = array();
- } else {
- $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
- $ignoredElements = array( 'includeonly' );
- $xmlishElements[] = 'includeonly';
- }
- $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
-
- // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
- $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
-
- $stack = new PPDStack_HipHop;
-
- $searchBase = "[{<\n";
- $revText = strrev( $text ); // For fast reverse searches
-
- $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
- $accum = $stack->getAccum(); # Current accumulator
- $headingIndex = 1;
- $stackFlags = array(
- 'findPipe' => false, # True to take notice of pipe characters
- 'findEquals' => false, # True to find equals signs in arguments
- 'inHeading' => false, # True if $i is inside a possible heading
- );
- $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
- $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
- $fakeLineStart = true; # Do a line-start run without outputting an LF character
-
- while ( true ) {
- //$this->memCheck();
-
- if ( $findOnlyinclude ) {
- // Ignore all input up to the next <onlyinclude>
- $variantStartPos = strpos( $text, '<onlyinclude>', $i );
- if ( $variantStartPos === false ) {
- // Ignored section runs to the end
- $accum->addNodeWithText( 'ignore', strval( substr( $text, $i ) ) );
- break;
- }
- $startPos1 = intval( $variantStartPos );
- $tagEndPos = $startPos1 + strlen( '<onlyinclude>' ); // past-the-end
- $accum->addNodeWithText( 'ignore', strval( substr( $text, $i, $tagEndPos - $i ) ) );
- $i = $tagEndPos;
- $findOnlyinclude = false;
- }
-
- if ( $fakeLineStart ) {
- $found = 'line-start';
- $curChar = '';
- } else {
- # Find next opening brace, closing brace or pipe
- $search = $searchBase;
- if ( $stack->top === false ) {
- $currentClosing = '';
- } else {
- $currentClosing = strval( $stack->getTop()->close );
- $search .= $currentClosing;
- }
- if ( $stackFlags['findPipe'] ) {
- $search .= '|';
- }
- if ( $stackFlags['findEquals'] ) {
- // First equals will be for the template
- $search .= '=';
- }
- $rule = null;
- # Output literal section, advance input counter
- $literalLength = intval( strcspn( $text, $search, $i ) );
- if ( $literalLength > 0 ) {
- $accum->addLiteral( strval( substr( $text, $i, $literalLength ) ) );
- $i += $literalLength;
- }
- if ( $i >= $lengthText ) {
- if ( $currentClosing === "\n" ) {
- // Do a past-the-end run to finish off the heading
- $curChar = '';
- $found = 'line-end';
- } else {
- # All done
- break;
- }
- } else {
- $curChar = $text[$i];
- if ( $curChar === '|' ) {
- $found = 'pipe';
- } elseif ( $curChar === '=' ) {
- $found = 'equals';
- } elseif ( $curChar === '<' ) {
- $found = 'angle';
- } elseif ( $curChar === "\n" ) {
- if ( $stackFlags['inHeading'] ) {
- $found = 'line-end';
- } else {
- $found = 'line-start';
- }
- } elseif ( $curChar === $currentClosing ) {
- $found = 'close';
- } elseif ( isset( $rules[$curChar] ) ) {
- $found = 'open';
- $rule = $rules[$curChar];
- } else {
- # Some versions of PHP have a strcspn which stops on null characters
- # Ignore and continue
- ++$i;
- continue;
- }
- }
- }
-
- if ( $found === 'angle' ) {
- $matches = false;
- // Handle </onlyinclude>
- if ( $enableOnlyinclude
- && substr( $text, $i, strlen( '</onlyinclude>' ) ) === '</onlyinclude>' )
- {
- $findOnlyinclude = true;
- continue;
- }
-
- // Determine element name
- if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
- // Element name missing or not listed
- $accum->addLiteral( '<' );
- ++$i;
- continue;
- }
- // Handle comments
- if ( isset( $matches[2] ) && $matches[2] === '!--' ) {
- // To avoid leaving blank lines, when a comment is both preceded
- // and followed by a newline (ignoring spaces), trim leading and
- // trailing spaces and one of the newlines.
-
- // Find the end
- $variantEndPos = strpos( $text, '-->', $i + 4 );
- if ( $variantEndPos === false ) {
- // Unclosed comment in input, runs to end
- $inner = strval( substr( $text, $i ) );
- $accum->addNodeWithText( 'comment', $inner );
- $i = $lengthText;
- } else {
- $endPos = intval( $variantEndPos );
- // Search backwards for leading whitespace
- if ( $i ) {
- $wsStart = $i - intval( strspn( $revText, ' ', $lengthText - $i ) );
- } else {
- $wsStart = 0;
- }
- // Search forwards for trailing whitespace
- // $wsEnd will be the position of the last space (or the '>' if there's none)
- $wsEnd = $endPos + 2 + intval( strspn( $text, ' ', $endPos + 3 ) );
- // Eat the line if possible
- // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
- // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
- // it's a possible beneficial b/c break.
- if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) === "\n"
- && substr( $text, $wsEnd + 1, 1 ) === "\n" )
- {
- $startPos2 = $wsStart;
- $endPos = $wsEnd + 1;
- // Remove leading whitespace from the end of the accumulator
- // Sanity check first though
- $wsLength = $i - $wsStart;
- if ( $wsLength > 0
- && $accum->lastNode instanceof PPNode_HipHop_Text
- && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
- {
- $accum->lastNode->value = strval( substr( $accum->lastNode->value, 0, -$wsLength ) );
- }
- // Do a line-start run next time to look for headings after the comment
- $fakeLineStart = true;
- } else {
- // No line to eat, just take the comment itself
- $startPos2 = $i;
- $endPos += 2;
- }
-
- if ( $stack->top ) {
- $part = $stack->getTop()->getCurrentPart();
- if ( ! (isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 )) {
- $part->visualEnd = $wsStart;
- }
- // Else comments abutting, no change in visual end
- $part->commentEnd = $endPos;
- }
- $i = $endPos + 1;
- $inner = strval( substr( $text, $startPos2, $endPos - $startPos2 + 1 ) );
- $accum->addNodeWithText( 'comment', $inner );
- }
- continue;
- }
- $name = strval( $matches[1] );
- $lowerName = strtolower( $name );
- $attrStart = $i + strlen( $name ) + 1;
-
- // Find end of tag
- $variantTagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
- if ( $variantTagEndPos === false ) {
- // Infinite backtrack
- // Disable tag search to prevent worst-case O(N^2) performance
- $noMoreGT = true;
- $accum->addLiteral( '<' );
- ++$i;
- continue;
- }
- $tagEndPos = intval( $variantTagEndPos );
-
- // Handle ignored tags
- if ( in_array( $lowerName, $ignoredTags ) ) {
- $accum->addNodeWithText( 'ignore', strval( substr( $text, $i, $tagEndPos - $i + 1 ) ) );
- $i = $tagEndPos + 1;
- continue;
- }
-
- $tagStartPos = $i;
- $close = '';
- if ( $text[$tagEndPos-1] === '/' ) {
- // Short end tag
- $attrEnd = $tagEndPos - 1;
- $shortEnd = true;
- $inner = '';
- $i = $tagEndPos + 1;
- $haveClose = false;
- } else {
- $attrEnd = $tagEndPos;
- $shortEnd = false;
- // Find closing tag
- if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
- $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) )
- {
- $inner = strval( substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ) );
- $i = intval( $matches[0][1] ) + strlen( $matches[0][0] );
- $close = strval( $matches[0][0] );
- $haveClose = true;
- } else {
- // No end tag -- let it run out to the end of the text.
- $inner = strval( substr( $text, $tagEndPos + 1 ) );
- $i = $lengthText;
- $haveClose = false;
- }
- }
- // <includeonly> and <noinclude> just become <ignore> tags
- if ( in_array( $lowerName, $ignoredElements ) ) {
- $accum->addNodeWithText( 'ignore', strval( substr( $text, $tagStartPos, $i - $tagStartPos ) ) );
- continue;
- }
-
- if ( $attrEnd <= $attrStart ) {
- $attr = '';
- } else {
- // Note that the attr element contains the whitespace between name and attribute,
- // this is necessary for precise reconstruction during pre-save transform.
- $attr = strval( substr( $text, $attrStart, $attrEnd - $attrStart ) );
- }
-
- $extNode = new PPNode_HipHop_Tree( 'ext' );
- $extNode->addChild( PPNode_HipHop_Tree::newWithText( 'name', $name ) );
- $extNode->addChild( PPNode_HipHop_Tree::newWithText( 'attr', $attr ) );
- if ( !$shortEnd ) {
- $extNode->addChild( PPNode_HipHop_Tree::newWithText( 'inner', $inner ) );
- }
- if ( $haveClose ) {
- $extNode->addChild( PPNode_HipHop_Tree::newWithText( 'close', $close ) );
- }
- $accum->addNode( $extNode );
- }
-
- elseif ( $found === 'line-start' ) {
- // Is this the start of a heading?
- // Line break belongs before the heading element in any case
- if ( $fakeLineStart ) {
- $fakeLineStart = false;
- } else {
- $accum->addLiteral( $curChar );
- $i++;
- }
-
- $count = intval( strspn( $text, '=', $i, 6 ) );
- if ( $count == 1 && $stackFlags['findEquals'] ) {
- // DWIM: This looks kind of like a name/value separator
- // Let's let the equals handler have it and break the potential heading
- // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
- } elseif ( $count > 0 ) {
- $partData = array(
- 'open' => "\n",
- 'close' => "\n",
- 'parts' => array( new PPDPart_HipHop( str_repeat( '=', $count ) ) ),
- 'startPos' => $i,
- 'count' => $count );
- $stack->push( $partData );
- $accum = $stack->getAccum();
- $stackFlags = $stack->getFlags();
- $i += $count;
- }
- } elseif ( $found === 'line-end' ) {
- $piece = $stack->getTop();
- // A heading must be open, otherwise \n wouldn't have been in the search list
- assert( $piece->open === "\n" ); // Passing the assert condition directly instead of string, as
- // HPHP /compiler/ chokes on strings when ASSERT_ACTIVE != 0.
- $part = $piece->getCurrentPart();
- // Search back through the input to see if it has a proper close
- // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
- $wsLength = intval( strspn( $revText, " \t", $lengthText - $i ) );
- $searchStart = $i - $wsLength;
- if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
- // Comment found at line end
- // Search for equals signs before the comment
- $searchStart = intval( $part->visualEnd );
- $searchStart -= intval( strspn( $revText, " \t", $lengthText - $searchStart ) );
- }
- $count = intval( $piece->count );
- $equalsLength = intval( strspn( $revText, '=', $lengthText - $searchStart ) );
- $isTreeNode = false;
- $resultAccum = $accum;
- if ( $equalsLength > 0 ) {
- if ( $searchStart - $equalsLength == $piece->startPos ) {
- // This is just a single string of equals signs on its own line
- // Replicate the doHeadings behaviour /={count}(.+)={count}/
- // First find out how many equals signs there really are (don't stop at 6)
- $count = $equalsLength;
- if ( $count < 3 ) {
- $count = 0;
- } else {
- $count = intval( ( $count - 1 ) / 2 );
- if ( $count > 6 ) {
- $count = 6;
- }
- }
- } else {
- if ( $count > $equalsLength ) {
- $count = $equalsLength;
- }
- }
- if ( $count > 0 ) {
- // Normal match, output <h>
- $tree = new PPNode_HipHop_Tree( 'possible-h' );
- $tree->addChild( new PPNode_HipHop_Attr( 'level', $count ) );
- $tree->addChild( new PPNode_HipHop_Attr( 'i', $headingIndex++ ) );
- $tree->lastChild->nextSibling = $accum->firstNode;
- $tree->lastChild = $accum->lastNode;
- $isTreeNode = true;
- } else {
- // Single equals sign on its own line, count=0
- // Output $resultAccum
- }
- } else {
- // No match, no <h>, just pass down the inner text
- // Output $resultAccum
- }
- // Unwind the stack
- $stack->pop();
- $accum = $stack->getAccum();
- $stackFlags = $stack->getFlags();
-
- // Append the result to the enclosing accumulator
- if ( $isTreeNode ) {
- $accum->addNode( $tree );
- } else {
- $accum->addAccum( $resultAccum );
- }
- // Note that we do NOT increment the input pointer.
- // This is because the closing linebreak could be the opening linebreak of
- // another heading. Infinite loops are avoided because the next iteration MUST
- // hit the heading open case above, which unconditionally increments the
- // input pointer.
- } elseif ( $found === 'open' ) {
- # count opening brace characters
- $count = intval( strspn( $text, $curChar, $i ) );
-
- # we need to add to stack only if opening brace count is enough for one of the rules
- if ( $count >= $rule['min'] ) {
- # Add it to the stack
- $partData = array(
- 'open' => $curChar,
- 'close' => $rule['end'],
- 'count' => $count,
- 'lineStart' => ($i == 0 || $text[$i-1] === "\n"),
- );
-
- $stack->push( $partData );
- $accum = $stack->getAccum();
- $stackFlags = $stack->getFlags();
- } else {
- # Add literal brace(s)
- $accum->addLiteral( str_repeat( $curChar, $count ) );
- }
- $i += $count;
- } elseif ( $found === 'close' ) {
- $piece = $stack->getTop();
- # lets check if there are enough characters for closing brace
- $maxCount = intval( $piece->count );
- $count = intval( strspn( $text, $curChar, $i, $maxCount ) );
-
- # check for maximum matching characters (if there are 5 closing
- # characters, we will probably need only 3 - depending on the rules)
- $rule = $rules[$piece->open];
- if ( $count > $rule['max'] ) {
- # The specified maximum exists in the callback array, unless the caller
- # has made an error
- $matchingCount = intval( $rule['max'] );
- } else {
- # Count is less than the maximum
- # Skip any gaps in the callback array to find the true largest match
- # Need to use array_key_exists not isset because the callback can be null
- $matchingCount = $count;
- while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
- --$matchingCount;
- }
- }
-
- if ($matchingCount <= 0) {
- # No matching element found in callback array
- # Output a literal closing brace and continue
- $accum->addLiteral( str_repeat( $curChar, $count ) );
- $i += $count;
- continue;
- }
- $name = strval( $rule['names'][$matchingCount] );
- $isTreeNode = false;
- if ( $name === 'LITERAL' ) {
- // No element, just literal text
- $resultAccum = $piece->breakSyntax( $matchingCount );
- $resultAccum->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
- } else {
- # Create XML element
- # Note: $parts is already XML, does not need to be encoded further
- $isTreeNode = true;
- $parts = $piece->parts;
- $titleAccum = PPDAccum_HipHop::cast( $parts[0]->out );
- unset( $parts[0] );
-
- $tree = new PPNode_HipHop_Tree( $name );
-
- # The invocation is at the start of the line if lineStart is set in
- # the stack, and all opening brackets are used up.
- if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
- $tree->addChild( new PPNode_HipHop_Attr( 'lineStart', 1 ) );
- }
- $titleNode = new PPNode_HipHop_Tree( 'title' );
- $titleNode->firstChild = $titleAccum->firstNode;
- $titleNode->lastChild = $titleAccum->lastNode;
- $tree->addChild( $titleNode );
- $argIndex = 1;
- foreach ( $parts as $variantPart ) {
- $part = PPDPart_HipHop::cast( $variantPart );
- if ( isset( $part->eqpos ) ) {
- // Find equals
- $lastNode = false;
- for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
- if ( $node === $part->eqpos ) {
- break;
- }
- $lastNode = $node;
- }
- if ( !$node ) {
- throw new MWException( __METHOD__. ': eqpos not found' );
- }
- if ( $node->name !== 'equals' ) {
- throw new MWException( __METHOD__ .': eqpos is not equals' );
- }
- $equalsNode = $node;
-
- // Construct name node
- $nameNode = new PPNode_HipHop_Tree( 'name' );
- if ( $lastNode !== false ) {
- $lastNode->nextSibling = false;
- $nameNode->firstChild = $part->out->firstNode;
- $nameNode->lastChild = $lastNode;
- }
-
- // Construct value node
- $valueNode = new PPNode_HipHop_Tree( 'value' );
- if ( $equalsNode->nextSibling !== false ) {
- $valueNode->firstChild = $equalsNode->nextSibling;
- $valueNode->lastChild = $part->out->lastNode;
- }
- $partNode = new PPNode_HipHop_Tree( 'part' );
- $partNode->addChild( $nameNode );
- $partNode->addChild( $equalsNode->firstChild );
- $partNode->addChild( $valueNode );
- $tree->addChild( $partNode );
- } else {
- $partNode = new PPNode_HipHop_Tree( 'part' );
- $nameNode = new PPNode_HipHop_Tree( 'name' );
- $nameNode->addChild( new PPNode_HipHop_Attr( 'index', $argIndex++ ) );
- $valueNode = new PPNode_HipHop_Tree( 'value' );
- $valueNode->firstChild = $part->out->firstNode;
- $valueNode->lastChild = $part->out->lastNode;
- $partNode->addChild( $nameNode );
- $partNode->addChild( $valueNode );
- $tree->addChild( $partNode );
- }
- }
- }
-
- # Advance input pointer
- $i += $matchingCount;
-
- # Unwind the stack
- $stack->pop();
- $accum = $stack->getAccum();
-
- # Re-add the old stack element if it still has unmatched opening characters remaining
- if ($matchingCount < $piece->count) {
- $piece->parts = array( new PPDPart_HipHop );
- $piece->count -= $matchingCount;
- # do we still qualify for any callback with remaining count?
- $names = $rules[$piece->open]['names'];
- $skippedBraces = 0;
- $enclosingAccum = $accum;
- while ( $piece->count ) {
- if ( array_key_exists( $piece->count, $names ) ) {
- $stack->push( $piece );
- $accum = $stack->getAccum();
- break;
- }
- --$piece->count;
- $skippedBraces ++;
- }
- $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
- }
-
- $stackFlags = $stack->getFlags();
-
- # Add XML element to the enclosing accumulator
- if ( $isTreeNode ) {
- $accum->addNode( $tree );
- } else {
- $accum->addAccum( $resultAccum );
- }
- } elseif ( $found === 'pipe' ) {
- $stackFlags['findEquals'] = true; // shortcut for getFlags()
- $stack->addPart();
- $accum = $stack->getAccum();
- ++$i;
- } elseif ( $found === 'equals' ) {
- $stackFlags['findEquals'] = false; // shortcut for getFlags()
- $accum->addNodeWithText( 'equals', '=' );
- $stack->getCurrentPart()->eqpos = $accum->lastNode;
- ++$i;
- }
- }
-
- # Output any remaining unclosed brackets
- foreach ( $stack->stack as $variantPiece ) {
- $piece = PPDStackElement_HipHop::cast( $variantPiece );
- $stack->rootAccum->addAccum( $piece->breakSyntax() );
- }
-
- # Enable top-level headings
- for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
- if ( isset( $node->name ) && $node->name === 'possible-h' ) {
- $node->name = 'h';
- }
- }
-
- $rootNode = new PPNode_HipHop_Tree( 'root' );
- $rootNode->firstChild = $stack->rootAccum->firstNode;
- $rootNode->lastChild = $stack->rootAccum->lastNode;
-
- // Cache
- if ($cacheable) {
- $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . serialize( $rootNode );
- $wgMemc->set( $cacheKey, $cacheValue, 86400 );
- wfProfileOut( __METHOD__.'-cache-miss' );
- wfProfileOut( __METHOD__.'-cacheable' );
- wfDebugLog( "Preprocessor", "Saved preprocessor Hash to memcached (key $cacheKey)" );
- }
-
- wfProfileOut( __METHOD__ );
- return $rootNode;
- }
-}
-
-
-
-/**
- * Stack class to help Preprocessor::preprocessToObj()
- * @ingroup Parser
- */
-class PPDStack_HipHop {
- var $stack, $rootAccum;
-
- /**
- * @var PPDStack
- */
- var $top;
- var $out;
-
- static $false = false;
-
- function __construct() {
- $this->stack = array();
- $this->top = false;
- $this->rootAccum = new PPDAccum_HipHop;
- $this->accum = $this->rootAccum;
- }
-
- /**
- * @return int
- */
- function count() {
- return count( $this->stack );
- }
-
- function getAccum() {
- return PPDAccum_HipHop::cast( $this->accum );
- }
-
- function getCurrentPart() {
- return $this->getTop()->getCurrentPart();
- }
-
- function getTop() {
- return PPDStackElement_HipHop::cast( $this->top );
- }
-
- function push( $data ) {
- if ( $data instanceof PPDStackElement_HipHop ) {
- $this->stack[] = $data;
- } else {
- $this->stack[] = new PPDStackElement_HipHop( $data );
- }
- $this->top = $this->stack[ count( $this->stack ) - 1 ];
- $this->accum = $this->top->getAccum();
- }
-
- function pop() {
- if ( !count( $this->stack ) ) {
- throw new MWException( __METHOD__.': no elements remaining' );
- }
- $temp = array_pop( $this->stack );
-
- if ( count( $this->stack ) ) {
- $this->top = $this->stack[ count( $this->stack ) - 1 ];
- $this->accum = $this->top->getAccum();
- } else {
- $this->top = self::$false;
- $this->accum = $this->rootAccum;
- }
- return $temp;
- }
-
- function addPart( $s = '' ) {
- $this->top->addPart( $s );
- $this->accum = $this->top->getAccum();
- }
-
- /**
- * @return array
- */
- function getFlags() {
- if ( !count( $this->stack ) ) {
- return array(
- 'findEquals' => false,
- 'findPipe' => false,
- 'inHeading' => false,
- );
- } else {
- return $this->top->getFlags();
- }
- }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDStackElement_HipHop {
- var $open, // Opening character (\n for heading)
- $close, // Matching closing character
- $count, // Number of opening characters found (number of "=" for heading)
- $parts, // Array of PPDPart objects describing pipe-separated parts.
- $lineStart; // True if the open char appeared at the start of the input line. Not set for headings.
-
- /**
- * @param $obj PPDStackElement_HipHop
- * @return PPDStackElement_HipHop
- */
- static function cast( PPDStackElement_HipHop $obj ) {
- return $obj;
- }
-
- /**
- * @param $data array
- */
- function __construct( $data = array() ) {
- $this->parts = array( new PPDPart_HipHop );
-
- foreach ( $data as $name => $value ) {
- $this->$name = $value;
- }
- }
-
- /**
- * @return PPDAccum_HipHop
- */
- function getAccum() {
- return PPDAccum_HipHop::cast( $this->parts[count($this->parts) - 1]->out );
- }
-
- /**
- * @param $s string
- */
- function addPart( $s = '' ) {
- $this->parts[] = new PPDPart_HipHop( $s );
- }
-
- /**
- * @return PPDPart_HipHop
- */
- function getCurrentPart() {
- return PPDPart_HipHop::cast( $this->parts[count($this->parts) - 1] );
- }
-
- /**
- * @return array
- */
- function getFlags() {
- $partCount = count( $this->parts );
- $findPipe = $this->open !== "\n" && $this->open !== '[';
- return array(
- 'findPipe' => $findPipe,
- 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
- 'inHeading' => $this->open === "\n",
- );
- }
-
- /**
- * Get the accumulator that would result if the close is not found.
- *
- * @param $openingCount bool
- * @return PPDAccum_HipHop
- */
- function breakSyntax( $openingCount = false ) {
- if ( $this->open === "\n" ) {
- $accum = PPDAccum_HipHop::cast( $this->parts[0]->out );
- } else {
- if ( $openingCount === false ) {
- $openingCount = $this->count;
- }
- $accum = new PPDAccum_HipHop;
- $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
- $first = true;
- foreach ( $this->parts as $part ) {
- if ( $first ) {
- $first = false;
- } else {
- $accum->addLiteral( '|' );
- }
- $accum->addAccum( $part->out );
- }
- }
- return $accum;
- }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDPart_HipHop {
- var $out; // Output accumulator object
-
- // Optional member variables:
- // eqpos Position of equals sign in output accumulator
- // commentEnd Past-the-end input pointer for the last comment encountered
- // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
-
- function __construct( $out = '' ) {
- $this->out = new PPDAccum_HipHop;
- if ( $out !== '' ) {
- $this->out->addLiteral( $out );
- }
- }
-
- static function cast( PPDPart_HipHop $obj ) {
- return $obj;
- }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDAccum_HipHop {
- var $firstNode, $lastNode;
-
- function __construct() {
- $this->firstNode = $this->lastNode = false;
- }
-
- static function cast( PPDAccum_HipHop $obj ) {
- return $obj;
- }
-
- /**
- * Append a string literal
- */
- function addLiteral( string $s ) {
- if ( $this->lastNode === false ) {
- $this->firstNode = $this->lastNode = new PPNode_HipHop_Text( $s );
- } elseif ( $this->lastNode instanceof PPNode_HipHop_Text ) {
- $this->lastNode->value .= $s;
- } else {
- $this->lastNode->nextSibling = new PPNode_HipHop_Text( $s );
- $this->lastNode = $this->lastNode->nextSibling;
- }
- }
-
- /**
- * Append a PPNode
- */
- function addNode( PPNode $node ) {
- if ( $this->lastNode === false ) {
- $this->firstNode = $this->lastNode = $node;
- } else {
- $this->lastNode->nextSibling = $node;
- $this->lastNode = $node;
- }
- }
-
- /**
- * Append a tree node with text contents
- */
- function addNodeWithText( string $name, string $value ) {
- $node = PPNode_HipHop_Tree::newWithText( $name, $value );
- $this->addNode( $node );
- }
-
- /**
- * Append a PPDAccum_HipHop
- * Takes over ownership of the nodes in the source argument. These nodes may
- * subsequently be modified, especially nextSibling.
- */
- function addAccum( PPDAccum_HipHop $accum ) {
- if ( $accum->lastNode === false ) {
- // nothing to add
- } elseif ( $this->lastNode === false ) {
- $this->firstNode = $accum->firstNode;
- $this->lastNode = $accum->lastNode;
- } else {
- $this->lastNode->nextSibling = $accum->firstNode;
- $this->lastNode = $accum->lastNode;
- }
- }
-}
-
-/**
- * An expansion frame, used as a context to expand the result of preprocessToObj()
- * @ingroup Parser
- */
-class PPFrame_HipHop implements PPFrame {
-
- /**
- * @var Parser
- */
- var $parser;
-
- /**
- * @var Preprocessor
- */
- var $preprocessor;
-
- /**
- * @var Title
- */
- var $title;
- var $titleCache;
-
- /**
- * Hashtable listing templates which are disallowed for expansion in this frame,
- * having been encountered previously in parent frames.
- */
- var $loopCheckHash;
-
- /**
- * Recursion depth of this frame, top = 0
- * Note that this is NOT the same as expansion depth in expand()
- */
- var $depth;
-
- /**
- * Construct a new preprocessor frame.
- * @param $preprocessor Preprocessor: the parent preprocessor
- */
- function __construct( $preprocessor ) {
- $this->preprocessor = $preprocessor;
- $this->parser = $preprocessor->parser;
- $this->title = $this->parser->mTitle;
- $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
- $this->loopCheckHash = array();
- $this->depth = 0;
- }
-
- /**
- * Create a new child frame
- * $args is optionally a multi-root PPNode or array containing the template arguments
- *
- * @param $args PPNode_HipHop_Array|array|bool
- * @param $title Title|bool
- * @param $indexOffset A number subtracted from the index attributes of the arguments
- *
- * @throws MWException
- * @return PPTemplateFrame_HipHop
- */
- function newChild( $args = false, $title = false, $indexOffset = 0 ) {
- $namedArgs = array();
- $numberedArgs = array();
- if ( $title === false ) {
- $title = $this->title;
- }
- if ( $args !== false ) {
- if ( $args instanceof PPNode_HipHop_Array ) {
- $args = $args->value;
- } elseif ( !is_array( $args ) ) {
- throw new MWException( __METHOD__ . ': $args must be array or PPNode_HipHop_Array' );
- }
- foreach ( $args as $arg ) {
- $bits = $arg->splitArg();
- if ( $bits['index'] !== '' ) {
- // Numbered parameter
- $numberedArgs[$bits['index']] = $bits['value'];
- unset( $namedArgs[$bits['index']] );
- } else {
- // Named parameter
- $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
- $namedArgs[$name] = $bits['value'];
- unset( $numberedArgs[$name] );
- }
- }
- }
- return new PPTemplateFrame_HipHop( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
- }
-
- /**
- * @throws MWException
- * @param $root
- * @param $flags int
- * @return string
- */
- function expand( $root, $flags = 0 ) {
- static $expansionDepth = 0;
- if ( is_string( $root ) ) {
- return $root;
- }
-
- if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
- $this->parser->limitationWarn( 'node-count-exceeded',
- $this->parser->mPPNodeCount,
- $this->parser->mOptions->getMaxPPNodeCount()
- );
- return '<span class="error">Node-count limit exceeded</span>';
- }
- if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
- $this->parser->limitationWarn( 'expansion-depth-exceeded',
- $expansionDepth,
- $this->parser->mOptions->getMaxPPExpandDepth()
- );
- return '<span class="error">Expansion depth limit exceeded</span>';
- }
- ++$expansionDepth;
- if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
- $this->parser->mHighestExpansionDepth = $expansionDepth;
- }
-
- $outStack = array( '', '' );
- $iteratorStack = array( false, $root );
- $indexStack = array( 0, 0 );
-
- while ( count( $iteratorStack ) > 1 ) {
- $level = count( $outStack ) - 1;
- $iteratorNode =& $iteratorStack[ $level ];
- $out =& $outStack[$level];
- $index =& $indexStack[$level];
-
- if ( is_array( $iteratorNode ) ) {
- if ( $index >= count( $iteratorNode ) ) {
- // All done with this iterator
- $iteratorStack[$level] = false;
- $contextNode = false;
- } else {
- $contextNode = $iteratorNode[$index];
- $index++;
- }
- } elseif ( $iteratorNode instanceof PPNode_HipHop_Array ) {
- if ( $index >= $iteratorNode->getLength() ) {
- // All done with this iterator
- $iteratorStack[$level] = false;
- $contextNode = false;
- } else {
- $contextNode = $iteratorNode->item( $index );
- $index++;
- }
- } else {
- // Copy to $contextNode and then delete from iterator stack,
- // because this is not an iterator but we do have to execute it once
- $contextNode = $iteratorStack[$level];
- $iteratorStack[$level] = false;
- }
-
- $newIterator = false;
-
- if ( $contextNode === false ) {
- // nothing to do
- } elseif ( is_string( $contextNode ) ) {
- $out .= $contextNode;
- } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_HipHop_Array ) {
- $newIterator = $contextNode;
- } elseif ( $contextNode instanceof PPNode_HipHop_Attr ) {
- // No output
- } elseif ( $contextNode instanceof PPNode_HipHop_Text ) {
- $out .= $contextNode->value;
- } elseif ( $contextNode instanceof PPNode_HipHop_Tree ) {
- if ( $contextNode->name === 'template' ) {
- # Double-brace expansion
- $bits = $contextNode->splitTemplate();
- if ( $flags & PPFrame::NO_TEMPLATES ) {
- $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
- } else {
- $ret = $this->parser->braceSubstitution( $bits, $this );
- if ( isset( $ret['object'] ) ) {
- $newIterator = $ret['object'];
- } else {
- $out .= $ret['text'];
- }
- }
- } elseif ( $contextNode->name === 'tplarg' ) {
- # Triple-brace expansion
- $bits = $contextNode->splitTemplate();
- if ( $flags & PPFrame::NO_ARGS ) {
- $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
- } else {
- $ret = $this->parser->argSubstitution( $bits, $this );
- if ( isset( $ret['object'] ) ) {
- $newIterator = $ret['object'];
- } else {
- $out .= $ret['text'];
- }
- }
- } elseif ( $contextNode->name === 'comment' ) {
- # HTML-style comment
- # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
- if ( $this->parser->ot['html']
- || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
- || ( $flags & PPFrame::STRIP_COMMENTS ) )
- {
- $out .= '';
- }
- # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
- # Not in RECOVER_COMMENTS mode (extractSections) though
- elseif ( $this->parser->ot['wiki'] && ! ( $flags & PPFrame::RECOVER_COMMENTS ) ) {
- $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
- }
- # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
- else {
- $out .= $contextNode->firstChild->value;
- }
- } elseif ( $contextNode->name === 'ignore' ) {
- # Output suppression used by <includeonly> etc.
- # OT_WIKI will only respect <ignore> in substed templates.
- # The other output types respect it unless NO_IGNORE is set.
- # extractSections() sets NO_IGNORE and so never respects it.
- if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & PPFrame::NO_IGNORE ) ) {
- $out .= $contextNode->firstChild->value;
- } else {
- //$out .= '';
- }
- } elseif ( $contextNode->name === 'ext' ) {
- # Extension tag
- $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
- $out .= $this->parser->extensionSubstitution( $bits, $this );
- } elseif ( $contextNode->name === 'h' ) {
- # Heading
- if ( $this->parser->ot['html'] ) {
- # Expand immediately and insert heading index marker
- $s = '';
- for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
- $s .= $this->expand( $node, $flags );
- }
-
- $bits = $contextNode->splitHeading();
- $titleText = $this->title->getPrefixedDBkey();
- $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
- $serial = count( $this->parser->mHeadings ) - 1;
- $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
- $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
- $this->parser->mStripState->addGeneral( $marker, '' );
- $out .= $s;
- } else {
- # Expand in virtual stack
- $newIterator = $contextNode->getChildren();
- }
- } else {
- # Generic recursive expansion
- $newIterator = $contextNode->getChildren();
- }
- } else {
- throw new MWException( __METHOD__.': Invalid parameter type' );
- }
-
- if ( $newIterator !== false ) {
- $outStack[] = '';
- $iteratorStack[] = $newIterator;
- $indexStack[] = 0;
- } elseif ( $iteratorStack[$level] === false ) {
- // Return accumulated value to parent
- // With tail recursion
- while ( $iteratorStack[$level] === false && $level > 0 ) {
- $outStack[$level - 1] .= $out;
- array_pop( $outStack );
- array_pop( $iteratorStack );
- array_pop( $indexStack );
- $level--;
- }
- }
- }
- --$expansionDepth;
- return $outStack[0];
- }
-
- /**
- * @param $sep
- * @param $flags
- * @return string
- */
- function implodeWithFlags( $sep, $flags /*, ... */ ) {
- $args = array_slice( func_get_args(), 2 );
-
- $first = true;
- $s = '';
- foreach ( $args as $root ) {
- if ( $root instanceof PPNode_HipHop_Array ) {
- $root = $root->value;
- }
- if ( !is_array( $root ) ) {
- $root = array( $root );
- }
- foreach ( $root as $node ) {
- if ( $first ) {
- $first = false;
- } else {
- $s .= $sep;
- }
- $s .= $this->expand( $node, $flags );
- }
- }
- return $s;
- }
-
- /**
- * Implode with no flags specified
- * This previously called implodeWithFlags but has now been inlined to reduce stack depth
- * @param $sep
- * @return string
- */
- function implode( $sep /*, ... */ ) {
- $args = array_slice( func_get_args(), 1 );
-
- $first = true;
- $s = '';
- foreach ( $args as $root ) {
- if ( $root instanceof PPNode_HipHop_Array ) {
- $root = $root->value;
- }
- if ( !is_array( $root ) ) {
- $root = array( $root );
- }
- foreach ( $root as $node ) {
- if ( $first ) {
- $first = false;
- } else {
- $s .= $sep;
- }
- $s .= $this->expand( $node );
- }
- }
- return $s;
- }
-
- /**
- * Makes an object that, when expand()ed, will be the same as one obtained
- * with implode()
- *
- * @param $sep
- * @return PPNode_HipHop_Array
- */
- function virtualImplode( $sep /*, ... */ ) {
- $args = array_slice( func_get_args(), 1 );
- $out = array();
- $first = true;
-
- foreach ( $args as $root ) {
- if ( $root instanceof PPNode_HipHop_Array ) {
- $root = $root->value;
- }
- if ( !is_array( $root ) ) {
- $root = array( $root );
- }
- foreach ( $root as $node ) {
- if ( $first ) {
- $first = false;
- } else {
- $out[] = $sep;
- }
- $out[] = $node;
- }
- }
- return new PPNode_HipHop_Array( $out );
- }
-
- /**
- * Virtual implode with brackets
- *
- * @param $start
- * @param $sep
- * @param $end
- * @return PPNode_HipHop_Array
- */
- function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
- $args = array_slice( func_get_args(), 3 );
- $out = array( $start );
- $first = true;
-
- foreach ( $args as $root ) {
- if ( $root instanceof PPNode_HipHop_Array ) {
- $root = $root->value;
- }
- if ( !is_array( $root ) ) {
- $root = array( $root );
- }
- foreach ( $root as $node ) {
- if ( $first ) {
- $first = false;
- } else {
- $out[] = $sep;
- }
- $out[] = $node;
- }
- }
- $out[] = $end;
- return new PPNode_HipHop_Array( $out );
- }
-
- function __toString() {
- return 'frame{}';
- }
-
- /**
- * @param $level bool
- * @return array|bool|String
- */
- function getPDBK( $level = false ) {
- if ( $level === false ) {
- return $this->title->getPrefixedDBkey();
- } else {
- return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
- }
- }
-
- /**
- * @return array
- */
- function getArguments() {
- return array();
- }
-
- /**
- * @return array
- */
- function getNumberedArguments() {
- return array();
- }
-
- /**
- * @return array
- */
- function getNamedArguments() {
- return array();
- }
-
- /**
- * Returns true if there are no arguments in this frame
- *
- * @return bool
- */
- function isEmpty() {
- return true;
- }
-
- /**
- * @param $name
- * @return bool
- */
- function getArgument( $name ) {
- return false;
- }
-
- /**
- * Returns true if the infinite loop check is OK, false if a loop is detected
- *
- * @param $title Title
- *
- * @return bool
- */
- function loopCheck( $title ) {
- return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
- }
-
- /**
- * Return true if the frame is a template frame
- *
- * @return bool
- */
- function isTemplate() {
- return false;
- }
-
- /**
- * Get a title of frame
- *
- * @return Title
- */
- function getTitle() {
- return $this->title;
- }
-}
-
-/**
- * Expansion frame with template arguments
- * @ingroup Parser
- */
-class PPTemplateFrame_HipHop extends PPFrame_HipHop {
- var $numberedArgs, $namedArgs, $parent;
- var $numberedExpansionCache, $namedExpansionCache;
-
- /**
- * @param $preprocessor Preprocessor_HipHop
- * @param $parent bool
- * @param $numberedArgs array
- * @param $namedArgs array
- * @param $title Title|bool
- */
- function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
- parent::__construct( $preprocessor );
-
- $this->parent = $parent;
- $this->numberedArgs = $numberedArgs;
- $this->namedArgs = $namedArgs;
- $this->title = $title;
- $pdbk = $title ? $title->getPrefixedDBkey() : false;
- $this->titleCache = $parent->titleCache;
- $this->titleCache[] = $pdbk;
- $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
- if ( $pdbk !== false ) {
- $this->loopCheckHash[$pdbk] = true;
- }
- $this->depth = $parent->depth + 1;
- $this->numberedExpansionCache = $this->namedExpansionCache = array();
- }
-
- function __toString() {
- $s = 'tplframe{';
- $first = true;
- $args = $this->numberedArgs + $this->namedArgs;
- foreach ( $args as $name => $value ) {
- if ( $first ) {
- $first = false;
- } else {
- $s .= ', ';
- }
- $s .= "\"$name\":\"" .
- str_replace( '"', '\\"', $value->__toString() ) . '"';
- }
- $s .= '}';
- return $s;
- }
- /**
- * Returns true if there are no arguments in this frame
- *
- * @return bool
- */
- function isEmpty() {
- return !count( $this->numberedArgs ) && !count( $this->namedArgs );
- }
-
- /**
- * @return array
- */
- function getArguments() {
- $arguments = array();
- foreach ( array_merge(
- array_keys($this->numberedArgs),
- array_keys($this->namedArgs)) as $key ) {
- $arguments[$key] = $this->getArgument($key);
- }
- return $arguments;
- }
-
- /**
- * @return array
- */
- function getNumberedArguments() {
- $arguments = array();
- foreach ( array_keys($this->numberedArgs) as $key ) {
- $arguments[$key] = $this->getArgument($key);
- }
- return $arguments;
- }
-
- /**
- * @return array
- */
- function getNamedArguments() {
- $arguments = array();
- foreach ( array_keys($this->namedArgs) as $key ) {
- $arguments[$key] = $this->getArgument($key);
- }
- return $arguments;
- }
-
- /**
- * @param $index
- * @return array|bool
- */
- function getNumberedArgument( $index ) {
- if ( !isset( $this->numberedArgs[$index] ) ) {
- return false;
- }
- if ( !isset( $this->numberedExpansionCache[$index] ) ) {
- # No trimming for unnamed arguments
- $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], PPFrame::STRIP_COMMENTS );
- }
- return $this->numberedExpansionCache[$index];
- }
-
- /**
- * @param $name
- * @return bool
- */
- function getNamedArgument( $name ) {
- if ( !isset( $this->namedArgs[$name] ) ) {
- return false;
- }
- if ( !isset( $this->namedExpansionCache[$name] ) ) {
- # Trim named arguments post-expand, for backwards compatibility
- $this->namedExpansionCache[$name] = trim(
- $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
- }
- return $this->namedExpansionCache[$name];
- }
-
- /**
- * @param $name
- * @return array|bool
- */
- function getArgument( $name ) {
- $text = $this->getNumberedArgument( $name );
- if ( $text === false ) {
- $text = $this->getNamedArgument( $name );
- }
- return $text;
- }
-
- /**
- * Return true if the frame is a template frame
- *
- * @return bool
- */
- function isTemplate() {
- return true;
- }
-}
-
-/**
- * Expansion frame with custom arguments
- * @ingroup Parser
- */
-class PPCustomFrame_HipHop extends PPFrame_HipHop {
- var $args;
-
- function __construct( $preprocessor, $args ) {
- parent::__construct( $preprocessor );
- $this->args = $args;
- }
-
- function __toString() {
- $s = 'cstmframe{';
- $first = true;
- foreach ( $this->args as $name => $value ) {
- if ( $first ) {
- $first = false;
- } else {
- $s .= ', ';
- }
- $s .= "\"$name\":\"" .
- str_replace( '"', '\\"', $value->__toString() ) . '"';
- }
- $s .= '}';
- return $s;
- }
-
- /**
- * @return bool
- */
- function isEmpty() {
- return !count( $this->args );
- }
-
- /**
- * @param $index
- * @return bool
- */
- function getArgument( $index ) {
- if ( !isset( $this->args[$index] ) ) {
- return false;
- }
- return $this->args[$index];
- }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_HipHop_Tree implements PPNode {
- var $name, $firstChild, $lastChild, $nextSibling;
-
- function __construct( $name ) {
- $this->name = $name;
- $this->firstChild = $this->lastChild = $this->nextSibling = false;
- }
-
- function __toString() {
- $inner = '';
- $attribs = '';
- for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
- if ( $node instanceof PPNode_HipHop_Attr ) {
- $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
- } else {
- $inner .= $node->__toString();
- }
- }
- if ( $inner === '' ) {
- return "<{$this->name}$attribs/>";
- } else {
- return "<{$this->name}$attribs>$inner</{$this->name}>";
- }
- }
-
- /**
- * @param $name
- * @param $text
- * @return PPNode_HipHop_Tree
- */
- static function newWithText( $name, $text ) {
- $obj = new self( $name );
- $obj->addChild( new PPNode_HipHop_Text( $text ) );
- return $obj;
- }
-
- function addChild( $node ) {
- if ( $this->lastChild === false ) {
- $this->firstChild = $this->lastChild = $node;
- } else {
- $this->lastChild->nextSibling = $node;
- $this->lastChild = $node;
- }
- }
-
- /**
- * @return PPNode_HipHop_Array
- */
- function getChildren() {
- $children = array();
- for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
- $children[] = $child;
- }
- return new PPNode_HipHop_Array( $children );
- }
-
- function getFirstChild() {
- return $this->firstChild;
- }
-
- function getNextSibling() {
- return $this->nextSibling;
- }
-
- /**
- * @param $name string
- * @return array
- */
- function getChildrenOfType( $name ) {
- $children = array();
- for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
- if ( isset( $child->name ) && $child->name === $name ) {
- $children[] = $child;
- }
- }
- return $children;
- }
-
- /**
- * @return bool
- */
- function getLength() {
- return false;
- }
-
- /**
- * @param $i
- * @return bool
- */
- function item( $i ) {
- return false;
- }
-
- /**
- * @return string
- */
- function getName() {
- return $this->name;
- }
-
- /**
- * Split a <part> node into an associative array containing:
- * name PPNode name
- * index String index
- * value PPNode value
- *
- * @throws MWException
- * @return array
- */
- function splitArg() {
- $bits = array();
- for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
- if ( !isset( $child->name ) ) {
- continue;
- }
- if ( $child->name === 'name' ) {
- $bits['name'] = $child;
- if ( $child->firstChild instanceof PPNode_HipHop_Attr
- && $child->firstChild->name === 'index' )
- {
- $bits['index'] = $child->firstChild->value;
- }
- } elseif ( $child->name === 'value' ) {
- $bits['value'] = $child;
- }
- }
-
- if ( !isset( $bits['name'] ) ) {
- throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
- }
- if ( !isset( $bits['index'] ) ) {
- $bits['index'] = '';
- }
- return $bits;
- }
-
- /**
- * Split an <ext> node into an associative array containing name, attr, inner and close
- * All values in the resulting array are PPNodes. Inner and close are optional.
- *
- * @throws MWException
- * @return array
- */
- function splitExt() {
- $bits = array();
- for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
- if ( !isset( $child->name ) ) {
- continue;
- }
- if ( $child->name === 'name' ) {
- $bits['name'] = $child;
- } elseif ( $child->name === 'attr' ) {
- $bits['attr'] = $child;
- } elseif ( $child->name === 'inner' ) {
- $bits['inner'] = $child;
- } elseif ( $child->name === 'close' ) {
- $bits['close'] = $child;
- }
- }
- if ( !isset( $bits['name'] ) ) {
- throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
- }
- return $bits;
- }
-
- /**
- * Split an <h> node
- *
- * @throws MWException
- * @return array
- */
- function splitHeading() {
- if ( $this->name !== 'h' ) {
- throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
- }
- $bits = array();
- for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
- if ( !isset( $child->name ) ) {
- continue;
- }
- if ( $child->name === 'i' ) {
- $bits['i'] = $child->value;
- } elseif ( $child->name === 'level' ) {
- $bits['level'] = $child->value;
- }
- }
- if ( !isset( $bits['i'] ) ) {
- throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
- }
- return $bits;
- }
-
- /**
- * Split a <template> or <tplarg> node
- *
- * @return array
- */
- function splitTemplate() {
- $parts = array();
- $bits = array( 'lineStart' => '' );
- for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
- if ( !isset( $child->name ) ) {
- continue;
- }
- if ( $child->name === 'title' ) {
- $bits['title'] = $child;
- }
- if ( $child->name === 'part' ) {
- $parts[] = $child;
- }
- if ( $child->name === 'lineStart' ) {
- $bits['lineStart'] = '1';
- }
- }
- if ( !isset( $bits['title'] ) ) {
- throw new MWException( 'Invalid node passed to ' . __METHOD__ );
- }
- $bits['parts'] = new PPNode_HipHop_Array( $parts );
- return $bits;
- }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_HipHop_Text implements PPNode {
- var $value, $nextSibling;
-
- function __construct( $value ) {
- if ( is_object( $value ) ) {
- throw new MWException( __CLASS__ . ' given object instead of string' );
- }
- $this->value = $value;
- }
-
- function __toString() {
- return htmlspecialchars( $this->value );
- }
-
- function getNextSibling() {
- return $this->nextSibling;
- }
-
- function getChildren() { return false; }
- function getFirstChild() { return false; }
- function getChildrenOfType( $name ) { return false; }
- function getLength() { return false; }
- function item( $i ) { return false; }
- function getName() { return '#text'; }
- function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
- function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
- function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_HipHop_Array implements PPNode {
- var $value, $nextSibling;
-
- function __construct( $value ) {
- $this->value = $value;
- }
-
- function __toString() {
- return var_export( $this, true );
- }
-
- function getLength() {
- return count( $this->value );
- }
-
- function item( $i ) {
- return $this->value[$i];
- }
-
- function getName() { return '#nodelist'; }
-
- function getNextSibling() {
- return $this->nextSibling;
- }
-
- function getChildren() { return false; }
- function getFirstChild() { return false; }
- function getChildrenOfType( $name ) { return false; }
- function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
- function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
- function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_HipHop_Attr implements PPNode {
- var $name, $value, $nextSibling;
-
- function __construct( $name, $value ) {
- $this->name = $name;
- $this->value = $value;
- }
-
- function __toString() {
- return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
- }
-
- function getName() {
- return $this->name;
- }
-
- function getNextSibling() {
- return $this->nextSibling;
- }
-
- function getChildren() { return false; }
- function getFirstChild() { return false; }
- function getChildrenOfType( $name ) { return false; }
- function getLength() { return false; }
- function item( $i ) { return false; }
- function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
- function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
- function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
-}
diff --git a/includes/parser/StripState.php b/includes/parser/StripState.php
index ad95d5f7..5f3f18ea 100644
--- a/includes/parser/StripState.php
+++ b/includes/parser/StripState.php
@@ -112,7 +112,7 @@ class StripState {
* @return mixed
*/
protected function unstripType( $type, $text ) {
- // Shortcut
+ // Shortcut
if ( !count( $this->data[$type] ) ) {
return $text;
}
@@ -139,7 +139,7 @@ class StripState {
. '</span>';
}
if ( $this->recursionLevel >= self::UNSTRIP_RECURSION_LIMIT ) {
- return '<span class="error">' .
+ return '<span class="error">' .
wfMessage( 'parser-unstrip-recursion-limit' )
->numParams( self::UNSTRIP_RECURSION_LIMIT )->inContentLanguage()->text() .
'</span>';
@@ -156,7 +156,7 @@ class StripState {
}
/**
- * Get a StripState object which is sufficient to unstrip the given text.
+ * Get a StripState object which is sufficient to unstrip the given text.
* It will contain the minimum subset of strip items necessary.
*
* @param $text string
@@ -233,4 +233,3 @@ class StripState {
return preg_replace( $this->regex, '', $text );
}
}
-
diff --git a/includes/parser/Tidy.php b/includes/parser/Tidy.php
index ed2d436d..32b16aaf 100644
--- a/includes/parser/Tidy.php
+++ b/includes/parser/Tidy.php
@@ -59,12 +59,21 @@ class MWTidyWrapper {
dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
$this->mMarkerIndex = 0;
+ // Replace <mw:editsection> elements with placeholders
$wrappedtext = preg_replace_callback( ParserOutput::EDITSECTION_REGEX,
- array( &$this, 'replaceEditSectionLinksCallback' ), $text );
+ array( &$this, 'replaceCallback' ), $text );
+ // ...and <mw:toc> markers
+ $wrappedtext = preg_replace_callback( '/\<\\/?mw:toc\>/',
+ array( &$this, 'replaceCallback' ), $wrappedtext );
- $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
- ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
- '<head><title>test</title></head><body>'.$wrappedtext.'</body></html>';
+ // Modify inline Microdata <link> and <meta> elements so they say <html-link> and <html-meta> so
+ // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config
+ $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', '<html-$1$2$3', $wrappedtext );
+
+ // Wrap the whole thing in a doctype and body for Tidy.
+ $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' .
+ ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>' .
+ '<head><title>test</title></head><body>' . $wrappedtext . '</body></html>';
return $wrappedtext;
}
@@ -74,7 +83,7 @@ class MWTidyWrapper {
*
* @return string
*/
- function replaceEditSectionLinksCallback( $m ) {
+ function replaceCallback( $m ) {
$marker = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}" . Parser::MARKER_SUFFIX;
$this->mMarkerIndex++;
$this->mTokens->setPair( $marker, $m[0] );
@@ -86,7 +95,13 @@ class MWTidyWrapper {
* @return string
*/
public function postprocess( $text ) {
- return $this->mTokens->replace( $text );
+ // Revert <html-{link,meta}> back to <{link,meta}>
+ $text = preg_replace( '!<html-(link|meta)([^>]*?)(/{0,1}>)!', '<$1$2$3', $text );
+
+ // Restore the contents of placeholder tokens
+ $text = $this->mTokens->replace( $text );
+
+ return $text;
}
}
@@ -106,7 +121,7 @@ class MWTidy {
* If tidy isn't able to correct the markup, the original will be
* returned in all its glory with a warning comment appended.
*
- * @param $text String: hideous HTML input
+ * @param string $text hideous HTML input
* @return String: corrected HTML output
*/
public static function tidy( $text ) {
@@ -146,7 +161,7 @@ class MWTidy {
global $wgTidyInternal;
$retval = 0;
- if( $wgTidyInternal ) {
+ if ( $wgTidyInternal ) {
$errorStr = self::execInternalTidy( $text, true, $retval );
} else {
$errorStr = self::execExternalTidy( $text, true, $retval );
@@ -159,7 +174,7 @@ class MWTidy {
* Spawn an external HTML tidy process and get corrected markup back from it.
* Also called in OutputHandler.php for full page validation
*
- * @param $text String: HTML to check
+ * @param string $text HTML to check
* @param $stderr Boolean: Whether to read result from STDERR rather than STDOUT
* @param &$retval int Exit code (-1 on internal error)
* @return mixed String or null
@@ -223,7 +238,7 @@ class MWTidy {
* Use the HTML tidy extension to use the tidy library in-process,
* saving the overhead of spawning a new process.
*
- * @param $text String: HTML to check
+ * @param string $text HTML to check
* @param $stderr Boolean: Whether to read result from error status instead of output
* @param &$retval int Exit code (-1 on internal error)
* @return mixed String or null
@@ -232,7 +247,7 @@ class MWTidy {
global $wgTidyConf, $wgDebugTidy;
wfProfileIn( __METHOD__ );
- if ( !MWInit::classExists( 'tidy' ) ) {
+ if ( !class_exists( 'tidy' ) ) {
wfWarn( "Unable to load internal tidy class." );
$retval = -1;
@@ -248,24 +263,24 @@ class MWTidy {
wfProfileOut( __METHOD__ );
return $tidy->errorBuffer;
+ }
+
+ $tidy->cleanRepair();
+ $retval = $tidy->getStatus();
+ if ( $retval == 2 ) {
+ // 2 is magic number for fatal error
+ // http://www.php.net/manual/en/function.tidy-get-status.php
+ $cleansource = null;
} else {
- $tidy->cleanRepair();
- $retval = $tidy->getStatus();
- if ( $retval == 2 ) {
- // 2 is magic number for fatal error
- // http://www.php.net/manual/en/function.tidy-get-status.php
- $cleansource = null;
- } else {
- $cleansource = tidy_get_output( $tidy );
- if ( $wgDebugTidy && $retval > 0 ) {
- $cleansource .= "<!--\nTidy reports:\n" .
- str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
- "\n-->";
- }
+ $cleansource = tidy_get_output( $tidy );
+ if ( $wgDebugTidy && $retval > 0 ) {
+ $cleansource .= "<!--\nTidy reports:\n" .
+ str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
+ "\n-->";
}
-
- wfProfileOut( __METHOD__ );
- return $cleansource;
}
+
+ wfProfileOut( __METHOD__ );
+ return $cleansource;
}
}
diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php
index 62be39e4..2282a3af 100644
--- a/includes/profiler/Profiler.php
+++ b/includes/profiler/Profiler.php
@@ -28,23 +28,65 @@
/**
* Begin profiling of a function
- * @param $functionname String: name of the function we will profile
+ * @param string $functionname name of the function we will profile
*/
function wfProfileIn( $functionname ) {
- global $wgProfiler;
- if ( $wgProfiler instanceof Profiler || isset( $wgProfiler['class'] ) ) {
- Profiler::instance()->profileIn( $functionname );
+ if ( Profiler::$__instance === null ) { // use this directly to reduce overhead
+ Profiler::instance();
+ }
+ if ( !( Profiler::$__instance instanceof ProfilerStub ) ) {
+ Profiler::$__instance->profileIn( $functionname );
}
}
/**
* Stop profiling of a function
- * @param $functionname String: name of the function we have profiled
+ * @param string $functionname name of the function we have profiled
*/
function wfProfileOut( $functionname = 'missing' ) {
- global $wgProfiler;
- if ( $wgProfiler instanceof Profiler || isset( $wgProfiler['class'] ) ) {
- Profiler::instance()->profileOut( $functionname );
+ if ( Profiler::$__instance === null ) { // use this directly to reduce overhead
+ Profiler::instance();
+ }
+ if ( !( Profiler::$__instance instanceof ProfilerStub ) ) {
+ Profiler::$__instance->profileOut( $functionname );
+ }
+}
+
+/**
+ * Class for handling function-scope profiling
+ *
+ * @since 1.22
+ */
+class ProfileSection {
+ protected $name; // string; method name
+ protected $enabled = false; // boolean; whether profiling is enabled
+
+ /**
+ * Begin profiling of a function and return an object that ends profiling of
+ * the function when that object leaves scope. As long as the object is not
+ * specifically linked to other objects, it will fall out of scope at the same
+ * moment that the function to be profiled terminates.
+ *
+ * This is typically called like:
+ * <code>$section = new ProfileSection( __METHOD__ );</code>
+ *
+ * @param string $name Name of the function to profile
+ */
+ public function __construct( $name ) {
+ $this->name = $name;
+ if ( Profiler::$__instance === null ) { // use this directly to reduce overhead
+ Profiler::instance();
+ }
+ if ( !( Profiler::$__instance instanceof ProfilerStub ) ) {
+ $this->enabled = true;
+ Profiler::$__instance->profileIn( $this->name );
+ }
+ }
+
+ function __destruct() {
+ if ( $this->enabled ) {
+ Profiler::$__instance->profileOut( $this->name );
+ }
}
}
@@ -53,11 +95,19 @@ function wfProfileOut( $functionname = 'missing' ) {
* @todo document
*/
class Profiler {
- protected $mStack = array(), $mWorkStack = array (), $mCollated = array (),
- $mCalls = array (), $mTotals = array ();
+ protected $mStack = array(), $mWorkStack = array(), $mCollated = array(),
+ $mCalls = array(), $mTotals = array();
protected $mTimeMetric = 'wall';
protected $mProfileID = false, $mCollateDone = false, $mTemplated = false;
- private static $__instance = null;
+
+ protected $mDBLockThreshold = 5.0; // float; seconds
+ /** @var Array DB/server name => (active trx count,timestamp) */
+ protected $mDBTrxHoldingLocks = array();
+ /** @var Array DB/server name => list of (method, elapsed time) */
+ protected $mDBTrxMethodTimes = array();
+
+ /** @var Profiler */
+ public static $__instance = null; // do not call this outside Profiler and ProfileSection
function __construct( $params ) {
if ( isset( $params['timeMetric'] ) ) {
@@ -75,22 +125,18 @@ class Profiler {
* @return Profiler
*/
public static function instance() {
- if( is_null( self::$__instance ) ) {
+ if ( self::$__instance === null ) {
global $wgProfiler;
- if( is_array( $wgProfiler ) ) {
- if( !isset( $wgProfiler['class'] ) ) {
- wfDebug( __METHOD__ . " called without \$wgProfiler['class']"
- . " set, falling back to ProfilerStub for safety\n" );
+ if ( is_array( $wgProfiler ) ) {
+ if ( !isset( $wgProfiler['class'] ) ) {
$class = 'ProfilerStub';
} else {
$class = $wgProfiler['class'];
}
self::$__instance = new $class( $wgProfiler );
- } elseif( $wgProfiler instanceof Profiler ) {
+ } elseif ( $wgProfiler instanceof Profiler ) {
self::$__instance = $wgProfiler; // back-compat
} else {
- wfDebug( __METHOD__ . ' called with bogus $wgProfiler setting,'
- . " falling back to ProfilerStub for safety\n" );
self::$__instance = new ProfilerStub( $wgProfiler );
}
}
@@ -157,7 +203,7 @@ class Profiler {
*/
public function profileIn( $functionname ) {
global $wgDebugFunctionEntry;
- if( $wgDebugFunctionEntry ){
+ if ( $wgDebugFunctionEntry ) {
$this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" );
}
@@ -174,30 +220,28 @@ class Profiler {
$memory = memory_get_usage();
$time = $this->getTime();
- if( $wgDebugFunctionEntry ){
+ if ( $wgDebugFunctionEntry ) {
$this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" );
}
- $bit = array_pop($this->mWorkStack);
+ $bit = array_pop( $this->mWorkStack );
- if (!$bit) {
- $this->debug("Profiling error, !\$bit: $functionname\n");
+ if ( !$bit ) {
+ $this->debug( "Profiling error, !\$bit: $functionname\n" );
} else {
- //if( $wgDebugProfiling ){
- if( $functionname == 'close' ){
- $message = "Profile section ended by close(): {$bit[0]}";
- $this->debug( "$message\n" );
- $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 );
- }
- elseif( $bit[0] != $functionname ){
- $message = "Profiling error: in({$bit[0]}), out($functionname)";
- $this->debug( "$message\n" );
- $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 );
- }
- //}
+ if ( $functionname == 'close' ) {
+ $message = "Profile section ended by close(): {$bit[0]}";
+ $this->debug( "$message\n" );
+ $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 );
+ } elseif ( $bit[0] != $functionname ) {
+ $message = "Profiling error: in({$bit[0]}), out($functionname)";
+ $this->debug( "$message\n" );
+ $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 );
+ }
$bit[] = $time;
$bit[] = $memory;
$this->mStack[] = $bit;
+ $this->updateTrxProfiling( $functionname, $time );
}
}
@@ -205,12 +249,89 @@ class Profiler {
* Close opened profiling sections
*/
public function close() {
- while( count( $this->mWorkStack ) ){
+ while ( count( $this->mWorkStack ) ) {
$this->profileOut( 'close' );
}
}
/**
+ * Mark a DB as in a transaction with one or more writes pending
+ *
+ * Note that there can be multiple connections to a single DB.
+ *
+ * @param string $server DB server
+ * @param string $db DB name
+ */
+ public function transactionWritingIn( $server, $db ) {
+ $name = "{$server} ({$db})";
+ if ( isset( $this->mDBTrxHoldingLocks[$name] ) ) {
+ ++$this->mDBTrxHoldingLocks[$name]['refs'];
+ } else {
+ $this->mDBTrxHoldingLocks[$name] = array( 'refs' => 1, 'start' => microtime( true ) );
+ $this->mDBTrxMethodTimes[$name] = array();
+ }
+ }
+
+ /**
+ * Register the name and time of a method for slow DB trx detection
+ *
+ * @param string $method Function name
+ * @param float $realtime Wal time ellapsed
+ */
+ protected function updateTrxProfiling( $method, $realtime ) {
+ if ( !$this->mDBTrxHoldingLocks ) {
+ return; // short-circuit
+ // @TODO: hardcoded check is a tad janky (what about FOR UPDATE?)
+ } elseif ( !preg_match( '/^query-m: (?!SELECT)/', $method )
+ && $realtime < $this->mDBLockThreshold )
+ {
+ return; // not a DB master query nor slow enough
+ }
+ $now = microtime( true );
+ foreach ( $this->mDBTrxHoldingLocks as $name => $info ) {
+ // Hacky check to exclude entries from before the first TRX write
+ if ( ( $now - $realtime ) >= $info['start'] ) {
+ $this->mDBTrxMethodTimes[$name][] = array( $method, $realtime );
+ }
+ }
+ }
+
+ /**
+ * Mark a DB as no longer in a transaction
+ *
+ * This will check if locks are possibly held for longer than
+ * needed and log any affected transactions to a special DB log.
+ * Note that there can be multiple connections to a single DB.
+ *
+ * @param string $server DB server
+ * @param string $db DB name
+ */
+ public function transactionWritingOut( $server, $db ) {
+ $name = "{$server} ({$db})";
+ if ( --$this->mDBTrxHoldingLocks[$name]['refs'] <= 0 ) {
+ $slow = false;
+ foreach ( $this->mDBTrxMethodTimes[$name] as $info ) {
+ list( $method, $realtime ) = $info;
+ if ( $realtime >= $this->mDBLockThreshold ) {
+ $slow = true;
+ break;
+ }
+ }
+ if ( $slow ) {
+ $dbs = implode( ', ', array_keys( $this->mDBTrxHoldingLocks ) );
+ $msg = "Sub-optimal transaction on DB(s) {$dbs}:\n";
+ foreach ( $this->mDBTrxMethodTimes[$name] as $i => $info ) {
+ list( $method, $realtime ) = $info;
+ $msg .= sprintf( "%d\t%.6f\t%s\n", $i, $realtime, $method );
+ }
+ wfDebugLog( 'DBPerformance', $msg );
+ }
+ unset( $this->mDBTrxHoldingLocks[$name] );
+ unset( $this->mDBTrxMethodTimes[$name] );
+ }
+ }
+
+ /**
* Mark this call as templated or not
*
* @param $t Boolean
@@ -228,11 +349,11 @@ class Profiler {
global $wgDebugFunctionEntry, $wgProfileCallTree;
$wgDebugFunctionEntry = false;
- if( !count( $this->mStack ) && !count( $this->mCollated ) ){
+ if ( !count( $this->mStack ) && !count( $this->mCollated ) ) {
return "No profiling output\n";
}
- if( $wgProfileCallTree ) {
+ if ( $wgProfileCallTree ) {
return $this->getCallTree();
} else {
return $this->getFunctionReport();
@@ -250,20 +371,20 @@ class Profiler {
/**
* Recursive function the format the current profiling array into a tree
*
- * @param $stack array profiling array
+ * @param array $stack profiling array
* @return array
*/
function remapCallTree( $stack ) {
- if( count( $stack ) < 2 ){
+ if ( count( $stack ) < 2 ) {
return $stack;
}
- $outputs = array ();
- for( $max = count( $stack ) - 1; $max > 0; ){
+ $outputs = array();
+ for ( $max = count( $stack ) - 1; $max > 0; ) {
/* Find all items under this entry */
$level = $stack[$max][1];
- $working = array ();
- for( $i = $max -1; $i >= 0; $i-- ){
- if( $stack[$i][1] > $level ){
+ $working = array();
+ for ( $i = $max -1; $i >= 0; $i-- ) {
+ if ( $stack[$i][1] > $level ) {
$working[] = $stack[$i];
} else {
break;
@@ -271,7 +392,7 @@ class Profiler {
}
$working = $this->remapCallTree( array_reverse( $working ) );
$output = array();
- foreach( $working as $item ){
+ foreach ( $working as $item ) {
array_push( $output, $item );
}
array_unshift( $output, $stack[$max] );
@@ -280,8 +401,8 @@ class Profiler {
array_unshift( $outputs, $output );
}
$final = array();
- foreach( $outputs as $output ){
- foreach( $output as $item ){
+ foreach ( $outputs as $output ) {
+ foreach ( $output as $item ) {
$final[] = $item;
}
}
@@ -293,9 +414,9 @@ class Profiler {
* @return string
*/
function getCallTreeLine( $entry ) {
- list( $fname, $level, $start, /* $x */, $end) = $entry;
+ list( $fname, $level, $start, /* $x */, $end ) = $entry;
$delta = $end - $start;
- $space = str_repeat(' ', $level);
+ $space = str_repeat( ' ', $level );
# The ugly double sprintf is to work around a PHP bug,
# which has been fixed in recent releases.
return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
@@ -305,7 +426,7 @@ class Profiler {
* Get the initial time of the request, based either on $wgRequestTime or
* $wgRUstart. Will return null if not able to find data.
*
- * @param $metric string|false: metric to use, with the following possibilities:
+ * @param string|false $metric metric to use, with the following possibilities:
* - user: User CPU time (without system calls)
* - cpu: Total CPU time (user and system calls)
* - wall (or any other string): elapsed time
@@ -338,7 +459,7 @@ class Profiler {
* Get the initial time of the request, based either on $wgRequestTime or
* $wgRUstart. Will return null if not able to find data.
*
- * @param $metric string|false: metric to use, with the following possibilities:
+ * @param string|false $metric metric to use, with the following possibilities:
* - user: User CPU time (without system calls)
* - cpu: Total CPU time (user and system calls)
* - wall (or any other string): elapsed time
@@ -386,23 +507,22 @@ class Profiler {
$this->mMemory = array();
# Estimate profiling overhead
- $profileCount = count($this->mStack);
+ $profileCount = count( $this->mStack );
self::calculateOverhead( $profileCount );
# First, subtract the overhead!
$overheadTotal = $overheadMemory = $overheadInternal = array();
- foreach( $this->mStack as $entry ){
+ foreach ( $this->mStack as $entry ) {
$fname = $entry[0];
$start = $entry[2];
$end = $entry[4];
$elapsed = $end - $start;
$memory = $entry[5] - $entry[3];
- if( $fname == '-overhead-total' ){
+ if ( $fname == '-overhead-total' ) {
$overheadTotal[] = $elapsed;
$overheadMemory[] = $memory;
- }
- elseif( $fname == '-overhead-internal' ){
+ } elseif ( $fname == '-overhead-internal' ) {
$overheadInternal[] = $elapsed;
}
}
@@ -411,7 +531,7 @@ class Profiler {
$overheadInternal = $overheadInternal ? array_sum( $overheadInternal ) / count( $overheadInternal ) : 0;
# Collate
- foreach( $this->mStack as $index => $entry ){
+ foreach ( $this->mStack as $index => $entry ) {
$fname = $entry[0];
$start = $entry[2];
$end = $entry[4];
@@ -420,16 +540,16 @@ class Profiler {
$memory = $entry[5] - $entry[3];
$subcalls = $this->calltreeCount( $this->mStack, $index );
- if( !preg_match( '/^-overhead/', $fname ) ){
+ if ( !preg_match( '/^-overhead/', $fname ) ) {
# Adjust for profiling overhead (except special values with elapsed=0
- if( $elapsed ) {
+ if ( $elapsed ) {
$elapsed -= $overheadInternal;
- $elapsed -= ($subcalls * $overheadTotal);
- $memory -= ($subcalls * $overheadMemory);
+ $elapsed -= ( $subcalls * $overheadTotal );
+ $memory -= ( $subcalls * $overheadMemory );
}
}
- if( !array_key_exists( $fname, $this->mCollated ) ){
+ if ( !array_key_exists( $fname, $this->mCollated ) ) {
$this->mCollated[$fname] = 0;
$this->mCalls[$fname] = 0;
$this->mMemory[$fname] = 0;
@@ -441,8 +561,8 @@ class Profiler {
$this->mCollated[$fname] += $elapsed;
$this->mCalls[$fname]++;
$this->mMemory[$fname] += $memory;
- $this->mMin[$fname] = min($this->mMin[$fname], $elapsed);
- $this->mMax[$fname] = max($this->mMax[$fname], $elapsed);
+ $this->mMin[$fname] = min( $this->mMin[$fname], $elapsed );
+ $this->mMax[$fname] = max( $this->mMax[$fname], $elapsed );
$this->mOverhead[$fname] += $subcalls;
}
@@ -460,18 +580,28 @@ class Profiler {
$width = 140;
$nameWidth = $width - 65;
- $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n";
+ $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n";
$titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n";
$prof = "\nProfiling data\n";
$prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' );
$total = isset( $this->mCollated['-total'] ) ? $this->mCollated['-total'] : 0;
- foreach( $this->mCollated as $fname => $elapsed ){
+ foreach ( $this->mCollated as $fname => $elapsed ) {
$calls = $this->mCalls[$fname];
$percent = $total ? 100. * $elapsed / $total : 0;
$memory = $this->mMemory[$fname];
- $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]);
+ $prof .= sprintf( $format,
+ substr( $fname, 0, $nameWidth ),
+ $calls,
+ (float)( $elapsed * 1000 ),
+ (float)( $elapsed * 1000 ) / $calls,
+ $percent,
+ $memory,
+ ( $this->mMin[$fname] * 1000.0 ),
+ ( $this->mMax[$fname] * 1000.0 ),
+ $this->mOverhead[$fname]
+ );
}
$prof .= "\nTotal: $total\n\n";
@@ -483,7 +613,7 @@ class Profiler {
*/
protected static function calculateOverhead( $profileCount ) {
wfProfileIn( '-overhead-total' );
- for( $i = 0; $i < $profileCount; $i++ ){
+ for ( $i = 0; $i < $profileCount; $i++ ) {
wfProfileIn( '-overhead-internal' );
wfProfileOut( '-overhead-internal' );
}
@@ -499,10 +629,10 @@ class Profiler {
* @return Integer
* @private
*/
- function calltreeCount($stack, $start) {
+ function calltreeCount( $stack, $start ) {
$level = $stack[$start][1];
$count = 0;
- for ($i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i --) {
+ for ( $i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i-- ) {
$count ++;
}
return $count;
@@ -511,65 +641,63 @@ class Profiler {
/**
* Log the whole profiling data into the database.
*/
- public function logData(){
+ public function logData() {
global $wgProfilePerHost, $wgProfileToDatabase;
# Do not log anything if database is readonly (bug 5375)
- if( wfReadOnly() || !$wgProfileToDatabase ) {
+ if ( wfReadOnly() || !$wgProfileToDatabase ) {
return;
}
$dbw = wfGetDB( DB_MASTER );
- if( !is_object( $dbw ) ) {
+ if ( !is_object( $dbw ) ) {
return;
}
- $errorState = $dbw->ignoreErrors( true );
-
- if( $wgProfilePerHost ){
+ if ( $wgProfilePerHost ) {
$pfhost = wfHostname();
} else {
$pfhost = '';
}
- $this->collateData();
-
- foreach( $this->mCollated as $name => $elapsed ){
- $eventCount = $this->mCalls[$name];
- $timeSum = (float) ($elapsed * 1000);
- $memorySum = (float)$this->mMemory[$name];
- $name = substr($name, 0, 255);
-
- // Kludge
- $timeSum = ($timeSum >= 0) ? $timeSum : 0;
- $memorySum = ($memorySum >= 0) ? $memorySum : 0;
-
- $dbw->update( 'profiling',
- array(
- "pf_count=pf_count+{$eventCount}",
- "pf_time=pf_time+{$timeSum}",
- "pf_memory=pf_memory+{$memorySum}",
- ),
- array(
- 'pf_name' => $name,
- 'pf_server' => $pfhost,
- ),
- __METHOD__ );
-
- $rc = $dbw->affectedRows();
- if ( $rc == 0 ) {
- $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount,
- 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ),
- __METHOD__, array ('IGNORE'));
+ try {
+ $this->collateData();
+
+ foreach ( $this->mCollated as $name => $elapsed ) {
+ $eventCount = $this->mCalls[$name];
+ $timeSum = (float)( $elapsed * 1000 );
+ $memorySum = (float)$this->mMemory[$name];
+ $name = substr( $name, 0, 255 );
+
+ // Kludge
+ $timeSum = $timeSum >= 0 ? $timeSum : 0;
+ $memorySum = $memorySum >= 0 ? $memorySum : 0;
+
+ $dbw->update( 'profiling',
+ array(
+ "pf_count=pf_count+{$eventCount}",
+ "pf_time=pf_time+{$timeSum}",
+ "pf_memory=pf_memory+{$memorySum}",
+ ),
+ array(
+ 'pf_name' => $name,
+ 'pf_server' => $pfhost,
+ ),
+ __METHOD__ );
+
+ $rc = $dbw->affectedRows();
+ if ( $rc == 0 ) {
+ $dbw->insert( 'profiling', array( 'pf_name' => $name, 'pf_count' => $eventCount,
+ 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ),
+ __METHOD__, array( 'IGNORE' ) );
+ }
+ // When we upgrade to mysql 4.1, the insert+update
+ // can be merged into just a insert with this construct added:
+ // "ON DUPLICATE KEY UPDATE ".
+ // "pf_count=pf_count + VALUES(pf_count), ".
+ // "pf_time=pf_time + VALUES(pf_time)";
}
- // When we upgrade to mysql 4.1, the insert+update
- // can be merged into just a insert with this construct added:
- // "ON DUPLICATE KEY UPDATE ".
- // "pf_count=pf_count + VALUES(pf_count), ".
- // "pf_time=pf_time + VALUES(pf_time)";
- }
-
- $dbw->ignoreErrors( $errorState );
+ } catch ( DBError $e ) {}
}
/**
@@ -584,11 +712,25 @@ class Profiler {
/**
* Add an entry in the debug log file
*
- * @param $s String to output
+ * @param string $s to output
*/
function debug( $s ) {
- if( defined( 'MW_COMPILED' ) || function_exists( 'wfDebug' ) ) {
+ if ( function_exists( 'wfDebug' ) ) {
wfDebug( $s );
}
}
+
+ /**
+ * Get the content type sent out to the client.
+ * Used for profilers that output instead of store data.
+ * @return string
+ */
+ protected function getContentType() {
+ foreach ( headers_list() as $header ) {
+ if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
+ return $m[1];
+ }
+ }
+ return null;
+ }
}
diff --git a/includes/profiler/ProfilerSimple.php b/includes/profiler/ProfilerSimple.php
index d1d1c5d9..805c60f4 100644
--- a/includes/profiler/ProfilerSimple.php
+++ b/includes/profiler/ProfilerSimple.php
@@ -29,7 +29,7 @@
class ProfilerSimple extends Profiler {
var $mMinimumTime = 0;
- var $zeroEntry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0);
+ var $zeroEntry = array( 'cpu' => 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0 );
var $errorEntry;
public function isPersistent() {
@@ -57,33 +57,33 @@ class ProfilerSimple extends Profiler {
$this->mMinimumTime = $min;
}
- function profileIn($functionname) {
+ function profileIn( $functionname ) {
global $wgDebugFunctionEntry;
- if ($wgDebugFunctionEntry) {
- $this->debug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n");
+ if ( $wgDebugFunctionEntry ) {
+ $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" );
}
$this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), $this->getTime( 'cpu' ) );
}
- function profileOut($functionname) {
+ function profileOut( $functionname ) {
global $wgDebugFunctionEntry;
- if ($wgDebugFunctionEntry) {
- $this->debug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n");
+ if ( $wgDebugFunctionEntry ) {
+ $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" );
}
- list($ofname, /* $ocount */ ,$ortime,$octime) = array_pop($this->mWorkStack);
+ list( $ofname, /* $ocount */, $ortime, $octime ) = array_pop( $this->mWorkStack );
- if (!$ofname) {
- $this->debug("Profiling error: $functionname\n");
+ if ( !$ofname ) {
+ $this->debug( "Profiling error: $functionname\n" );
} else {
- if ($functionname == 'close') {
+ if ( $functionname == 'close' ) {
$message = "Profile section ended by close(): {$ofname}";
$functionname = $ofname;
$this->debug( "$message\n" );
$this->mCollated[$message] = $this->errorEntry;
}
- elseif ($ofname != $functionname) {
+ elseif ( $ofname != $functionname ) {
$message = "Profiling error: in({$ofname}), out($functionname)";
$this->debug( "$message\n" );
$this->mCollated[$message] = $this->errorEntry;
@@ -91,16 +91,17 @@ class ProfilerSimple extends Profiler {
$entry =& $this->mCollated[$functionname];
$elapsedcpu = $this->getTime( 'cpu' ) - $octime;
$elapsedreal = $this->getTime() - $ortime;
- if (!is_array($entry)) {
+ if ( !is_array( $entry ) ) {
$entry = $this->zeroEntry;
$this->mCollated[$functionname] =& $entry;
}
$entry['cpu'] += $elapsedcpu;
- $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu;
+ $entry['cpu_sq'] += $elapsedcpu * $elapsedcpu;
$entry['real'] += $elapsedreal;
- $entry['real_sq'] += $elapsedreal*$elapsedreal;
+ $entry['real_sq'] += $elapsedreal * $elapsedreal;
$entry['count']++;
+ $this->updateTrxProfiling( $functionname, $elapsedreal );
}
}
diff --git a/includes/profiler/ProfilerSimpleText.php b/includes/profiler/ProfilerSimpleText.php
index 3e7d6fa4..1d57ea8d 100644
--- a/includes/profiler/ProfilerSimpleText.php
+++ b/includes/profiler/ProfilerSimpleText.php
@@ -48,12 +48,20 @@ class ProfilerSimpleText extends ProfilerSimple {
$totalReal = isset( $this->mCollated['-total'] )
? $this->mCollated['-total']['real']
: 0; // profiling mismatch error?
- uasort( $this->mCollated, array('self','sort') );
- array_walk( $this->mCollated, array('self','format'), $totalReal );
- if ( $this->visible ) {
- print '<pre>'.self::$out.'</pre>';
- } else {
- print "<!--\n".self::$out."\n-->\n";
+ uasort( $this->mCollated, array( 'self', 'sort' ) );
+ array_walk( $this->mCollated, array( 'self', 'format' ), $totalReal );
+ if ( PHP_SAPI === 'cli' ) {
+ print "<!--\n" . self::$out . "\n-->\n";
+ } elseif ( $this->getContentType() === 'text/html' ) {
+ if ( $this->visible ) {
+ print '<pre>' . self::$out . '</pre>';
+ } else {
+ print "<!--\n" . self::$out . "\n-->\n";
+ }
+ } elseif ( $this->getContentType() === 'text/javascript' ) {
+ print "\n/*\n" . self::$out . "*/\n";
+ } elseif ( $this->getContentType() === 'text/css' ) {
+ print "\n/*\n" . self::$out . "*/\n";
}
}
}
@@ -63,7 +71,7 @@ class ProfilerSimpleText extends ProfilerSimple {
}
static function format( $item, $key, $totalReal ) {
- $perc = $totalReal ? $item['real']/$totalReal*100 : 0;
+ $perc = $totalReal ? $item['real'] / $totalReal * 100 : 0;
self::$out .= sprintf( "%6.2f%% %3.6f %6d - %s\n",
$perc, $item['real'], $item['count'], $key );
}
diff --git a/includes/profiler/ProfilerSimpleTrace.php b/includes/profiler/ProfilerSimpleTrace.php
index 822e9fe4..5588d1e2 100644
--- a/includes/profiler/ProfilerSimpleTrace.php
+++ b/includes/profiler/ProfilerSimpleTrace.php
@@ -32,18 +32,18 @@ class ProfilerSimpleTrace extends ProfilerSimple {
function profileIn( $functionname ) {
parent::profileIn( $functionname );
- $this->trace .= " " . sprintf("%6.1f",$this->memoryDiff()) .
- str_repeat( " ", count($this->mWorkStack)) . " > " . $functionname . "\n";
+ $this->trace .= " " . sprintf( "%6.1f", $this->memoryDiff() ) .
+ str_repeat( " ", count( $this->mWorkStack ) ) . " > " . $functionname . "\n";
}
- function profileOut($functionname) {
+ function profileOut( $functionname ) {
global $wgDebugFunctionEntry;
if ( $wgDebugFunctionEntry ) {
- $this->debug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n");
+ $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" );
}
- list( $ofname, /* $ocount */ , $ortime ) = array_pop( $this->mWorkStack );
+ list( $ofname, /* $ocount */, $ortime ) = array_pop( $this->mWorkStack );
if ( !$ofname ) {
$this->trace .= "Profiling error: $functionname\n";
@@ -58,7 +58,9 @@ class ProfilerSimpleTrace extends ProfilerSimple {
}
$elapsedreal = $this->getTime() - $ortime;
$this->trace .= sprintf( "%03.6f %6.1f", $elapsedreal, $this->memoryDiff() ) .
- str_repeat(" ", count( $this->mWorkStack ) + 1 ) . " < " . $functionname . "\n";
+ str_repeat( " ", count( $this->mWorkStack ) + 1 ) . " < " . $functionname . "\n";
+
+ $this->updateTrxProfiling( $functionname, $elapsedreal );
}
}
@@ -69,6 +71,14 @@ class ProfilerSimpleTrace extends ProfilerSimple {
}
function logData() {
- print "<!-- \n {$this->trace} \n -->";
+ if ( PHP_SAPI === 'cli' ) {
+ print "<!-- \n {$this->trace} \n -->";
+ } elseif ( $this->getContentType() === 'text/html' ) {
+ print "<!-- \n {$this->trace} \n -->";
+ } elseif ( $this->getContentType() === 'text/javascript' ) {
+ print "\n/*\n {$this->trace}\n*/";
+ } elseif ( $this->getContentType() === 'text/css' ) {
+ print "\n/*\n {$this->trace}\n*/";
+ }
}
}
diff --git a/includes/profiler/ProfilerSimpleUDP.php b/includes/profiler/ProfilerSimpleUDP.php
index a95ccb0d..0a1f3b10 100644
--- a/includes/profiler/ProfilerSimpleUDP.php
+++ b/includes/profiler/ProfilerSimpleUDP.php
@@ -32,7 +32,7 @@ class ProfilerSimpleUDP extends ProfilerSimple {
}
public function logData() {
- global $wgUDPProfilerHost, $wgUDPProfilerPort;
+ global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgUDPProfilerFormatString;
$this->close();
@@ -41,24 +41,24 @@ class ProfilerSimpleUDP extends ProfilerSimple {
return;
}
- if ( !MWInit::functionExists( 'socket_create' ) ) {
+ if ( !function_exists( 'socket_create' ) ) {
# Sockets are not enabled
return;
}
- $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
+ $sock = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
$plength = 0;
$packet = "";
foreach ( $this->mCollated as $entry => $pfdata ) {
- if( !isset($pfdata['count'])
+ if ( !isset( $pfdata['count'] )
|| !isset( $pfdata['cpu'] )
|| !isset( $pfdata['cpu_sq'] )
|| !isset( $pfdata['real'] )
|| !isset( $pfdata['real_sq'] ) ) {
continue;
}
- $pfline = sprintf( "%s %s %d %f %f %f %f %s\n", $this->getProfileID(), "-", $pfdata['count'],
- $pfdata['cpu'], $pfdata['cpu_sq'], $pfdata['real'], $pfdata['real_sq'], $entry);
+ $pfline = sprintf( $wgUDPProfilerFormatString, $this->getProfileID(), $pfdata['count'],
+ $pfdata['cpu'], $pfdata['cpu_sq'], $pfdata['real'], $pfdata['real_sq'], $entry );
$length = strlen( $pfline );
/* printf("<!-- $pfline -->"); */
if ( $length + $plength > 1400 ) {
diff --git a/includes/profiler/ProfilerStub.php b/includes/profiler/ProfilerStub.php
index c0eb0fb4..3697f352 100644
--- a/includes/profiler/ProfilerStub.php
+++ b/includes/profiler/ProfilerStub.php
@@ -39,4 +39,6 @@ class ProfilerStub extends Profiler {
public function close() {}
public function logData() {}
public function getCurrentSection() { return ''; }
+ public function transactionWritingIn( $server, $db ) {}
+ public function transactionWritingOut( $server, $db ) {}
}
diff --git a/includes/rcfeed/IRCColourfulRCFeedFormatter.php b/includes/rcfeed/IRCColourfulRCFeedFormatter.php
new file mode 100644
index 00000000..507369f3
--- /dev/null
+++ b/includes/rcfeed/IRCColourfulRCFeedFormatter.php
@@ -0,0 +1,99 @@
+<?php
+class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
+ /**
+ * Generates a colourful notification intended for humans on IRC.
+ * @see RCFeedFormatter::getLine
+ */
+ public function getLine( array $feed, RecentChange $rc, $actionComment ) {
+ global $wgUseRCPatrol, $wgUseNPPatrol, $wgLocalInterwiki,
+ $wgCanonicalServer, $wgScript;
+ $attribs = $rc->getAttributes();
+ if ( $attribs['rc_type'] == RC_LOG ) {
+ // Don't use SpecialPage::getTitleFor, backwards compatibility with
+ // IRC API which expects "Log".
+ $titleObj = Title::newFromText( 'Log/' . $attribs['rc_log_type'], NS_SPECIAL );
+ } else {
+ $titleObj =& $rc->getTitle();
+ }
+ $title = $titleObj->getPrefixedText();
+ $title = self::cleanupForIRC( $title );
+
+ if ( $attribs['rc_type'] == RC_LOG ) {
+ $url = '';
+ } else {
+ $url = $wgCanonicalServer . $wgScript;
+ if ( $attribs['rc_type'] == RC_NEW ) {
+ $query = '?oldid=' . $attribs['rc_this_oldid'];
+ } else {
+ $query = '?diff=' . $attribs['rc_this_oldid'] . '&oldid=' . $attribs['rc_last_oldid'];
+ }
+ if ( $wgUseRCPatrol || ( $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
+ $query .= '&rcid=' . $attribs['rc_id'];
+ }
+ // HACK: We need this hook for WMF's secure server setup
+ wfRunHooks( 'IRCLineURL', array( &$url, &$query ) );
+ $url .= $query;
+ }
+
+ if ( $attribs['rc_old_len'] !== null && $attribs['rc_new_len'] !== null ) {
+ $szdiff = $attribs['rc_new_len'] - $attribs['rc_old_len'];
+ if ( $szdiff < -500 ) {
+ $szdiff = "\002$szdiff\002";
+ } elseif ( $szdiff >= 0 ) {
+ $szdiff = '+' . $szdiff;
+ }
+ // @todo i18n with parentheses in content language?
+ $szdiff = '(' . $szdiff . ')';
+ } else {
+ $szdiff = '';
+ }
+
+ $user = self::cleanupForIRC( $attribs['rc_user_text'] );
+
+ if ( $attribs['rc_type'] == RC_LOG ) {
+ $targetText = $rc->getTitle()->getPrefixedText();
+ $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $actionComment ) );
+ $flag = $attribs['rc_log_action'];
+ } else {
+ $comment = self::cleanupForIRC( $attribs['rc_comment'] );
+ $flag = '';
+ if ( !$attribs['rc_patrolled'] && ( $wgUseRCPatrol || $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
+ $flag .= '!';
+ }
+ $flag .= ( $attribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $attribs['rc_minor'] ? "M" : "" ) . ( $attribs['rc_bot'] ? "B" : "" );
+ }
+
+ if ( $feed['add_interwiki_prefix'] === true && $wgLocalInterwiki !== false ) {
+ $prefix = $wgLocalInterwiki;
+ } elseif ( $feed['add_interwiki_prefix'] ) {
+ $prefix = $feed['add_interwiki_prefix'];
+ } else {
+ $prefix = false;
+ }
+ if ( $prefix !== false ) {
+ $titleString = "\00314[[\00303$prefix:\00307$title\00314]]";
+ } else {
+ $titleString = "\00314[[\00307$title\00314]]";
+ }
+
+ # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
+ # no colour (\003) switches back to the term default
+ $fullString = "$titleString\0034 $flag\00310 " .
+ "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
+
+ return $fullString;
+ }
+
+ /**
+ * Remove newlines, carriage returns and decode html entites
+ * @param string $text
+ * @return string
+ */
+ public static function cleanupForIRC( $text ) {
+ return Sanitizer::decodeCharReferences( str_replace(
+ array( "\n", "\r" ),
+ array( " ", "" ),
+ $text
+ ) );
+ }
+}
diff --git a/includes/rcfeed/JSONRCFeedFormatter.php b/includes/rcfeed/JSONRCFeedFormatter.php
new file mode 100644
index 00000000..f4cb9921
--- /dev/null
+++ b/includes/rcfeed/JSONRCFeedFormatter.php
@@ -0,0 +1,90 @@
+<?php
+
+class JSONRCFeedFormatter implements RCFeedFormatter {
+ /**
+ * Generates a notification that can be easily interpreted by a machine.
+ * @see RCFeedFormatter::getLine
+ */
+ public function getLine( array $feed, RecentChange $rc, $actionComment ) {
+ global $wgCanonicalServer, $wgScriptPath, $wgDBname;
+ $attrib = $rc->getAttributes();
+
+ $packet = array(
+ // Usually, RC ID is exposed only for patrolling purposes,
+ // but there is no real reason not to expose it in other cases,
+ // and I can see how this may be potentially useful for clients.
+ 'id' => $attrib['rc_id'],
+ 'type' => $attrib['rc_type'],
+ 'namespace' => $rc->getTitle()->getNamespace(),
+ 'title' => $rc->getTitle()->getPrefixedText(),
+ 'comment' => $attrib['rc_comment'],
+ 'timestamp' => (int)wfTimestamp( TS_UNIX, $attrib['rc_timestamp'] ),
+ 'user' => $attrib['rc_user_text'],
+ 'bot' => (bool)$attrib['rc_bot'],
+ );
+
+ if ( isset( $feed['channel'] ) ) {
+ $packet['channel'] = $feed['channel'];
+ }
+
+ $type = $attrib['rc_type'];
+ if ( $type == RC_EDIT || $type == RC_NEW ) {
+ global $wgUseRCPatrol, $wgUseNPPatrol;
+
+ $packet['minor'] = $attrib['rc_minor'];
+ if ( $wgUseRCPatrol || ( $type == RC_NEW && $wgUseNPPatrol ) ) {
+ $packet['patrolled'] = $attrib['rc_patrolled'];
+ }
+ }
+
+ switch ( $type ) {
+ case RC_EDIT:
+ $packet['length'] = array( 'old' => $attrib['rc_old_len'], 'new' => $attrib['rc_new_len'] );
+ $packet['revision'] = array( 'old' => $attrib['rc_last_oldid'], 'new' => $attrib['rc_this_oldid'] );
+ break;
+
+ case RC_NEW:
+ $packet['length'] = array( 'old' => NULL, 'new' => $attrib['rc_new_len'] );
+ $packet['revision'] = array( 'old' => NULL, 'new' => $attrib['rc_this_oldid'] );
+ break;
+
+ case RC_LOG:
+ $packet['log_type'] = $attrib['rc_log_type'];
+ $packet['log_action'] = $attrib['rc_log_action'];
+ if ( $attrib['rc_params'] ) {
+ wfSuppressWarnings();
+ $params = unserialize( $attrib['rc_params'] );
+ wfRestoreWarnings();
+ if (
+ // If it's an actual serialised false...
+ $attrib['rc_params'] == serialize( false ) ||
+ // Or if we did not get false back when trying to unserialise
+ $params !== false
+ ) {
+ // From ApiQueryLogEvents::addLogParams
+ $logParams = array();
+ // Keys like "4::paramname" can't be used for output so we change them to "paramname"
+ foreach ( $params as $key => $value ) {
+ if ( strpos( $key, ':' ) === false ) {
+ $logParams[$key] = $value;
+ continue;
+ }
+ $logParam = explode( ':', $key, 3 );
+ $logParams[$logParam[2]] = $value;
+ }
+ $packet['log_params'] = $logParams;
+ } else {
+ $packet['log_params'] = explode( "\n", $attrib['rc_params'] );
+ }
+ }
+ $packet['log_action_comment'] = $actionComment;
+ break;
+ }
+
+ $packet['server_url'] = $wgCanonicalServer;
+ $packet['server_script_path'] = $wgScriptPath ?: '/';
+ $packet['wiki'] = $wgDBname;
+
+ return FormatJson::encode( $packet );
+ }
+}
diff --git a/includes/rcfeed/RCFeedEngine.php b/includes/rcfeed/RCFeedEngine.php
new file mode 100644
index 00000000..f733bcb7
--- /dev/null
+++ b/includes/rcfeed/RCFeedEngine.php
@@ -0,0 +1,12 @@
+<?php
+interface RCFeedEngine {
+ /**
+ * Sends some text to the specified live feed.
+ *
+ * @see RecentChange::cleanupForIRC
+ * @param array $feed The feed, as configured in an associative array.
+ * @param string $line The text to send.
+ * @return boolean success
+ */
+ public function send( array $feed, $line );
+}
diff --git a/includes/rcfeed/RCFeedFormatter.php b/includes/rcfeed/RCFeedFormatter.php
new file mode 100644
index 00000000..6c9f8042
--- /dev/null
+++ b/includes/rcfeed/RCFeedFormatter.php
@@ -0,0 +1,13 @@
+<?php
+interface RCFeedFormatter {
+ /**
+ * Formats the line for the live feed.
+ *
+ * @param array $feed The feed, as configured in an associative array.
+ * @param RecentChange $rc The RecentChange object showing what sort
+ * of event has taken place.
+ * @param string|null $actionComment
+ * @return string The text to send.
+ */
+ public function getLine( array $feed, RecentChange $rc, $actionComment );
+}
diff --git a/includes/rcfeed/RedisPubSubFeedEngine.php b/includes/rcfeed/RedisPubSubFeedEngine.php
new file mode 100644
index 00000000..4bcc1337
--- /dev/null
+++ b/includes/rcfeed/RedisPubSubFeedEngine.php
@@ -0,0 +1,41 @@
+<?php
+class RedisPubSubFeedEngine implements RCFeedEngine {
+ /**
+ * Emit a recent change notification via Redis Pub/Sub
+ *
+ * If the feed URI contains a path component, it will be used to generate a
+ * channel name by stripping the leading slash and replacing any remaining
+ * slashes with '.'. If no path component is present, the channel is set to
+ * 'rc'. If the URI contains a query string, its parameters will be parsed
+ * as RedisConnectionPool options.
+ *
+ * @example $wgRCFeeds['redis'] = array(
+ * 'formatter' => 'JSONRCFeedFormatter',
+ * 'uri' => "redis://127.0.0.1:6379/rc.$wgDBname",
+ * );
+ *
+ * @since 1.22
+ */
+ public function send( array $feed, $line ) {
+ $parsed = parse_url( $feed['uri'] );
+ $server = $parsed['host'];
+ $options = array( 'serializer' => 'none' );
+ $channel = 'rc';
+
+ if ( isset( $parsed['port'] ) ) {
+ $server .= ":{$parsed['port']}";
+ }
+ if ( isset( $parsed['query'] ) ) {
+ parse_str( $parsed['query'], $options );
+ }
+ if ( isset( $parsed['pass'] ) ) {
+ $options['password'] = $parsed['pass'];
+ }
+ if ( isset( $parsed['path'] ) ) {
+ $channel = str_replace( '/', '.', ltrim( $parsed['path'], '/' ) );
+ }
+ $pool = RedisConnectionPool::singleton( $options );
+ $conn = $pool->getConnection( $server );
+ $conn->publish( $channel, $line );
+ }
+}
diff --git a/includes/rcfeed/UDPRCFeedEngine.php b/includes/rcfeed/UDPRCFeedEngine.php
new file mode 100644
index 00000000..beeb73bd
--- /dev/null
+++ b/includes/rcfeed/UDPRCFeedEngine.php
@@ -0,0 +1,10 @@
+<?php
+class UDPRCFeedEngine implements RCFeedEngine {
+ /**
+ * Sends the notification to the specified host in a UDP packet.
+ * @see RCFeedEngine::send
+ */
+ public function send( array $feed, $line ) {
+ wfErrorLog( $line, $feed['uri'] );
+ }
+}
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php
index 7b87f9d4..6380efcf 100644
--- a/includes/resourceloader/ResourceLoader.php
+++ b/includes/resourceloader/ResourceLoader.php
@@ -39,7 +39,7 @@ class ResourceLoader {
/** Associative array mapping module name to info associative array */
protected $moduleInfos = array();
-
+
/** Associative array mapping framework ids to a list of names of test suite modules */
/** like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) */
protected $testModuleNames = array();
@@ -47,6 +47,9 @@ class ResourceLoader {
/** array( 'source-id' => array( 'loadScript' => 'http://.../load.php' ) ) **/
protected $sources = array();
+ /** @var bool */
+ protected $hasErrors = false;
+
/* Protected Methods */
/**
@@ -60,7 +63,7 @@ class ResourceLoader {
* requests its own information. This sacrifice of modularity yields a substantial
* performance improvement.
*
- * @param $modules Array: List of module names to preload information for
+ * @param array $modules List of module names to preload information for
* @param $context ResourceLoaderContext: Context to load the information within
*/
public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
@@ -127,8 +130,8 @@ class ResourceLoader {
* If $data is empty, only contains whitespace or the filter was unknown,
* $data is returned unmodified.
*
- * @param $filter String: Name of filter to run
- * @param $data String: Text to filter, such as JavaScript or CSS text
+ * @param string $filter Name of filter to run
+ * @param string $data Text to filter, such as JavaScript or CSS text
* @return String: Filtered data, or a comment containing an error message
*/
protected function filter( $filter, $data ) {
@@ -150,6 +153,7 @@ class ResourceLoader {
$cache = wfGetCache( CACHE_ANYTHING );
$cacheEntry = $cache->get( $key );
if ( is_string( $cacheEntry ) ) {
+ wfIncrStats( "rl-$filter-cache-hits" );
wfProfileOut( __METHOD__ );
return $cacheEntry;
}
@@ -157,6 +161,7 @@ class ResourceLoader {
$result = '';
// Run the filter - we've already verified one of these will work
try {
+ wfIncrStats( "rl-$filter-cache-misses" );
switch ( $filter ) {
case 'minify-js':
$result = JavaScriptMinifier::minify( $data,
@@ -173,10 +178,12 @@ class ResourceLoader {
// Save filtered text to Memcached
$cache->set( $key, $result );
- } catch ( Exception $exception ) {
- // Return exception as a comment
- $result = $this->makeComment( $exception->__toString() );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" );
$this->hasErrors = true;
+ // Return exception as a comment
+ $result = self::formatException( $e );
}
wfProfileOut( __METHOD__ );
@@ -201,7 +208,7 @@ class ResourceLoader {
$this->addSource( $wgResourceLoaderSources );
// Register core modules
- $this->register( include( "$IP/resources/Resources.php" ) );
+ $this->register( include "$IP/resources/Resources.php" );
// Register extension modules
wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
$this->register( $wgResourceModules );
@@ -210,7 +217,6 @@ class ResourceLoader {
$this->registerTestModules();
}
-
wfProfileOut( __METHOD__ );
}
@@ -218,7 +224,7 @@ class ResourceLoader {
* Registers a module with the ResourceLoader system.
*
* @param $name Mixed: Name of module as a string or List of name/object pairs as an array
- * @param $info array Module info array. For backwards compatibility with 1.17alpha,
+ * @param array $info Module info array. For backwards compatibility with 1.17alpha,
* this may also be a ResourceLoaderModule object. Optional when using
* multiple-registration calling style.
* @throws MWException: If a duplicate module registration is attempted
@@ -235,6 +241,7 @@ class ResourceLoader {
foreach ( $registrations as $name => $info ) {
// Disallow duplicate registrations
if ( isset( $this->moduleInfos[$name] ) ) {
+ wfProfileOut( __METHOD__ );
// A module has already been registered by this name
throw new MWException(
'ResourceLoader duplicate registration error. ' .
@@ -244,6 +251,7 @@ class ResourceLoader {
// Check $name for validity
if ( !self::isValidModuleName( $name ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
}
@@ -252,6 +260,7 @@ class ResourceLoader {
// Old calling convention
// Validate the input
if ( !( $info instanceof ResourceLoaderModule ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( 'ResourceLoader invalid module error. ' .
'Instances of ResourceLoaderModule expected.' );
}
@@ -274,24 +283,28 @@ class ResourceLoader {
global $IP, $wgEnableJavaScriptTest;
if ( $wgEnableJavaScriptTest !== true ) {
- throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
+ throw new MWException( 'Attempt to register JavaScript test modules but <code>$wgEnableJavaScriptTest</code> is false. Edit your <code>LocalSettings.php</code> to enable it.' );
}
wfProfileIn( __METHOD__ );
// Get core test suites
$testModules = array();
- $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
+ $testModules['qunit'] = include "$IP/tests/qunit/QUnitTestResources.php";
// Get other test suites (e.g. from extensions)
wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
// Add the testrunner (which configures QUnit) to the dependencies.
// Since it must be ready before any of the test suites are executed.
- foreach( $testModules['qunit'] as $moduleName => $moduleProps ) {
- $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
+ foreach ( $testModules['qunit'] as &$module ) {
+ // Make sure all test modules are top-loading so that when QUnit starts
+ // on document-ready, it will run once and finish. If some tests arrive
+ // later (possibly after QUnit has already finished) they will be ignored.
+ $module['position'] = 'top';
+ $module['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
}
- foreach( $testModules as $id => $names ) {
+ foreach ( $testModules as $id => $names ) {
// Register test modules
$this->register( $testModules[$id] );
@@ -309,9 +322,10 @@ class ResourceLoader {
* 'loadScript': URL (either fully-qualified or protocol-relative) of load.php for this source
*
* @param $id Mixed: source ID (string), or array( id1 => props1, id2 => props2, ... )
- * @param $properties Array: source properties
+ * @param array $properties source properties
+ * @throws MWException
*/
- public function addSource( $id, $properties = null) {
+ public function addSource( $id, $properties = null ) {
// Allow multiple sources to be registered in one call
if ( is_array( $id ) ) {
foreach ( $id as $key => $value ) {
@@ -346,18 +360,18 @@ class ResourceLoader {
public function getModuleNames() {
return array_keys( $this->moduleInfos );
}
-
- /**
+
+ /**
* Get a list of test module names for one (or all) frameworks.
* If the given framework id is unknkown, or if the in-object variable is not an array,
* then it will return an empty array.
*
- * @param $framework String: Optional. Get only the test module names for one
+ * @param string $framework Optional. Get only the test module names for one
* particular framework.
* @return Array
*/
public function getTestModuleNames( $framework = 'all' ) {
- /// @TODO: api siteinfo prop testmodulenames modulenames
+ /// @todo api siteinfo prop testmodulenames modulenames
if ( $framework == 'all' ) {
return $this->testModuleNames;
} elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
@@ -370,7 +384,7 @@ class ResourceLoader {
/**
* Get the ResourceLoaderModule object for a given module name.
*
- * @param $name String: Module name
+ * @param string $name Module name
* @return ResourceLoaderModule if module has been registered, null otherwise
*/
public function getModule( $name ) {
@@ -381,6 +395,7 @@ class ResourceLoader {
}
// Construct the requested object
$info = $this->moduleInfos[$name];
+ /** @var ResourceLoaderModule $object */
if ( isset( $info['object'] ) ) {
// Object given in info array
$object = $info['object'];
@@ -435,7 +450,6 @@ class ResourceLoader {
wfProfileIn( __METHOD__ );
$errors = '';
- $this->hasErrors = false;
// Split requested modules into two groups, modules and missing
$modules = array();
@@ -446,11 +460,14 @@ class ResourceLoader {
// Do not allow private modules to be loaded from the web.
// This is a security issue, see bug 34907.
if ( $module->getGroup() === 'private' ) {
- $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $errors .= self::makeComment( "Cannot show private module \"$name\"" );
+
continue;
}
- $modules[$name] = $this->getModule( $name );
+ $modules[$name] = $module;
} else {
$missing[] = $name;
}
@@ -459,13 +476,15 @@ class ResourceLoader {
// Preload information needed to the mtime calculation below
try {
$this->preloadModuleInfo( array_keys( $modules ), $context );
- } catch( Exception $e ) {
- // Add exception to the output as a comment
- $errors .= $this->makeComment( $e->__toString() );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $errors .= self::formatException( $e );
}
- wfProfileIn( __METHOD__.'-getModifiedTime' );
+ wfProfileIn( __METHOD__ . '-getModifiedTime' );
// To send Last-Modified and support If-Modified-Since, we need to detect
// the last modified time
@@ -478,13 +497,15 @@ class ResourceLoader {
// Calculate maximum modified time
$mtime = max( $mtime, $module->getModifiedTime( $context ) );
} catch ( Exception $e ) {
- // Add exception to the output as a comment
- $errors .= $this->makeComment( $e->__toString() );
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $errors .= self::formatException( $e );
}
}
- wfProfileOut( __METHOD__.'-getModifiedTime' );
+ wfProfileOut( __METHOD__ . '-getModifiedTime' );
// If there's an If-Modified-Since header, respond with a 304 appropriately
if ( $this->tryRespondLastModified( $context, $mtime ) ) {
@@ -501,7 +522,7 @@ class ResourceLoader {
// Capture any PHP warnings from the output buffer and append them to the
// response in a comment if we're in debug mode.
if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
- $response = $this->makeComment( $warnings ) . $response;
+ $response = self::makeComment( $warnings ) . $response;
$this->hasErrors = true;
}
@@ -530,8 +551,8 @@ class ResourceLoader {
/**
* Send content type and last modified headers to the client.
* @param $context ResourceLoaderContext
- * @param $mtime string TS_MW timestamp to use for last-modified
- * @param $error bool Whether there are commented-out errors in the response
+ * @param string $mtime TS_MW timestamp to use for last-modified
+ * @param bool $errors Whether there are commented-out errors in the response
* @return void
*/
protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
@@ -540,16 +561,17 @@ class ResourceLoader {
// to propagate to clients quickly
// If there were errors, we also need a shorter expiry time so we can recover quickly
if ( is_null( $context->getVersion() ) || $errors ) {
- $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
+ $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
$smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
// If a version was specified we can use a longer expiry time since changing
// version numbers causes cache misses
} else {
- $maxage = $wgResourceLoaderMaxage['versioned']['client'];
+ $maxage = $wgResourceLoaderMaxage['versioned']['client'];
$smaxage = $wgResourceLoaderMaxage['versioned']['server'];
}
if ( $context->getOnly() === 'styles' ) {
header( 'Content-Type: text/css; charset=utf-8' );
+ header( 'Access-Control-Allow-Origin: *' );
} else {
header( 'Content-Type: text/javascript; charset=utf-8' );
}
@@ -569,8 +591,8 @@ class ResourceLoader {
* If there's an If-Modified-Since header, respond with a 304 appropriately
* and clear out the output buffer. If the client cache is too old then do nothing.
* @param $context ResourceLoaderContext
- * @param $mtime string The TS_MW timestamp to check the header against
- * @return bool True iff 304 header sent and output handled
+ * @param string $mtime The TS_MW timestamp to check the header against
+ * @return bool True if 304 header sent and output handled
*/
protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
// If there's an If-Modified-Since header, respond with a 304 appropriately
@@ -590,13 +612,7 @@ class ResourceLoader {
// See also http://bugs.php.net/bug.php?id=51579
// To work around this, we tear down all output buffering before
// sending the 304.
- // On some setups, ob_get_level() doesn't seem to go down to zero
- // no matter how often we call ob_get_clean(), so instead of doing
- // the more intuitive while ( ob_get_level() > 0 ) ob_get_clean();
- // we have to be safe here and avoid an infinite loop.
- for ( $i = 0; $i < ob_get_level(); $i++ ) {
- ob_end_clean();
- }
+ wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
header( 'HTTP/1.0 304 Not Modified' );
header( 'Status: 304 Not Modified' );
@@ -628,7 +644,7 @@ class ResourceLoader {
if ( !$good ) {
try { // RL always hits the DB on file cache miss...
wfGetDB( DB_SLAVE );
- } catch( DBConnectionError $e ) { // ...check if we need to fallback to cache
+ } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
$good = $fileCache->isCacheGood(); // cache existence check
}
}
@@ -657,22 +673,45 @@ class ResourceLoader {
return false; // cache miss
}
- protected function makeComment( $text ) {
+ /**
+ * Generate a CSS or JS comment block. Only use this for public data,
+ * not error message details.
+ *
+ * @param $text string
+ * @return string
+ */
+ public static function makeComment( $text ) {
$encText = str_replace( '*/', '* /', $text );
return "/*\n$encText\n*/\n";
}
/**
+ * Handle exception display
+ *
+ * @param Exception $e to be shown to the user
+ * @return string sanitized text that can be returned to the user
+ */
+ public static function formatException( $e ) {
+ global $wgShowExceptionDetails;
+
+ if ( $wgShowExceptionDetails ) {
+ return self::makeComment( $e->__toString() );
+ } else {
+ return self::makeComment( wfMessage( 'internalerror' )->text() );
+ }
+ }
+
+ /**
* Generates code for a response
*
* @param $context ResourceLoaderContext: Context in which to generate a response
- * @param $modules Array: List of module objects keyed by module name
- * @param $missing Array: List of unavailable modules (optional)
+ * @param array $modules List of module objects keyed by module name
+ * @param array $missing List of unavailable modules (optional)
* @return String: Response data
*/
public function makeModuleResponse( ResourceLoaderContext $context,
- array $modules, $missing = array() )
- {
+ array $modules, $missing = array()
+ ) {
$out = '';
$exceptions = '';
if ( $modules === array() && $missing === array() ) {
@@ -685,9 +724,11 @@ class ResourceLoader {
try {
$blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
} catch ( Exception $e ) {
- // Add exception to the output as a comment
- $exceptions .= $this->makeComment( $e->__toString() );
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $exceptions .= self::formatException( $e );
}
} else {
$blobs = array();
@@ -791,9 +832,11 @@ class ResourceLoader {
break;
}
} catch ( Exception $e ) {
- // Add exception to the output as a comment
- $exceptions .= $this->makeComment( $e->__toString() );
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $exceptions .= self::formatException( $e );
// Register module as missing
$missing[] = $name;
@@ -834,7 +877,7 @@ class ResourceLoader {
* Returns JS code to call to mw.loader.implement for a module with
* given properties.
*
- * @param $name string Module name
+ * @param string $name Module name
* @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
* @param $styles Mixed: Array of CSS strings keyed by media type, or an array of lists of URLs to
* CSS files keyed by media type
@@ -842,6 +885,7 @@ class ResourceLoader {
* associative array mapping message key to value, or a JSON-encoded message blob containing
* the same data, wrapped in an XmlJsCode object.
*
+ * @throws MWException
* @return string
*/
public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
@@ -862,7 +906,9 @@ class ResourceLoader {
// output javascript "[]" instead of "{}". This fixes that.
(object)$styles,
(object)$messages
- ) );
+ ),
+ ResourceLoader::inDebugMode()
+ );
}
/**
@@ -881,7 +927,7 @@ class ResourceLoader {
* Combines an associative array mapping media type to CSS into a
* single stylesheet with "@media" blocks.
*
- * @param $styles Array: Array keyed by media type containing (arrays of) CSS strings.
+ * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings.
*
* @return Array
*/
@@ -891,7 +937,7 @@ class ResourceLoader {
// ResourceLoaderFileModule::getStyle can return the styles
// as a string or an array of strings. This is to allow separation in
// the front-end.
- $styles = (array) $styles;
+ $styles = (array)$styles;
foreach ( $styles as $style ) {
$style = trim( $style );
// Don't output an empty "@media print { }" block (bug 40498)
@@ -902,7 +948,7 @@ class ResourceLoader {
if ( $media === '' || $media == 'all' ) {
$out[] = $style;
- } else if ( is_string( $media ) ) {
+ } elseif ( is_string( $media ) ) {
$out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
}
// else: skip
@@ -941,12 +987,12 @@ class ResourceLoader {
* which will have values corresponding to $name, $version, $dependencies
* and $group as supplied.
*
- * @param $name String: Module name
+ * @param string $name Module name
* @param $version Integer: Module version number as a timestamp
- * @param $dependencies Array: List of module names on which this module depends
- * @param $group String: Group which the module is in.
- * @param $source String: Source of the module, or 'local' if not foreign.
- * @param $script String: JavaScript code
+ * @param array $dependencies List of module names on which this module depends
+ * @param string $group Group which the module is in.
+ * @param string $source Source of the module, or 'local' if not foreign.
+ * @param string $script JavaScript code
*
* @return string
*/
@@ -974,21 +1020,21 @@ class ResourceLoader {
* ) ):
* Registers modules with the given names and parameters.
*
- * @param $name String: Module name
+ * @param string $name Module name
* @param $version Integer: Module version number as a timestamp
- * @param $dependencies Array: List of module names on which this module depends
- * @param $group String: group which the module is in.
- * @param $source String: source of the module, or 'local' if not foreign
+ * @param array $dependencies List of module names on which this module depends
+ * @param string $group group which the module is in.
+ * @param string $source source of the module, or 'local' if not foreign
*
* @return string
*/
public static function makeLoaderRegisterScript( $name, $version = null,
- $dependencies = null, $group = null, $source = null )
- {
+ $dependencies = null, $group = null, $source = null
+ ) {
if ( is_array( $name ) ) {
return Xml::encodeJsCall( 'mw.loader.register', array( $name ) );
} else {
- $version = (int) $version > 1 ? (int) $version : 1;
+ $version = (int)$version > 1 ? (int)$version : 1;
return Xml::encodeJsCall( 'mw.loader.register',
array( $name, $version, $dependencies, $group, $source ) );
}
@@ -1004,8 +1050,8 @@ class ResourceLoader {
* - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $props1, $id2 => $props2, ... ) );
* Register sources with the given IDs and properties.
*
- * @param $id String: source ID
- * @param $properties Array: source properties (see addSource())
+ * @param string $id source ID
+ * @param array $properties source properties (see addSource())
*
* @return string
*/
@@ -1021,7 +1067,7 @@ class ResourceLoader {
* Returns JS code which runs given JS code if the client-side framework is
* present.
*
- * @param $script String: JavaScript code
+ * @param string $script JavaScript code
*
* @return string
*/
@@ -1033,12 +1079,12 @@ class ResourceLoader {
* Returns JS code which will set the MediaWiki configuration array to
* the given value.
*
- * @param $configuration Array: List of configuration values keyed by variable name
+ * @param array $configuration List of configuration values keyed by variable name
*
* @return string
*/
public static function makeConfigSetScript( array $configuration ) {
- return Xml::encodeJsCall( 'mw.config.set', array( $configuration ) );
+ return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), ResourceLoader::inDebugMode() );
}
/**
@@ -1046,7 +1092,7 @@ class ResourceLoader {
*
* For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' )
* becomes 'foo.bar,baz|bar.baz,quux'
- * @param $modules array of module names (strings)
+ * @param array $modules of module names (strings)
* @return string Packed query string
*/
public static function makePackedModulesString( $modules ) {
@@ -1084,16 +1130,16 @@ class ResourceLoader {
/**
* Build a load.php URL
- * @param $modules array of module names (strings)
- * @param $lang string Language code
- * @param $skin string Skin name
- * @param $user string|null User name. If null, the &user= parameter is omitted
- * @param $version string|null Versioning timestamp
- * @param $debug bool Whether the request should be in debug mode
- * @param $only string|null &only= parameter
- * @param $printable bool Printable mode
- * @param $handheld bool Handheld mode
- * @param $extraQuery array Extra query parameters to add
+ * @param array $modules of module names (strings)
+ * @param string $lang Language code
+ * @param string $skin Skin name
+ * @param string|null $user User name. If null, the &user= parameter is omitted
+ * @param string|null $version Versioning timestamp
+ * @param bool $debug Whether the request should be in debug mode
+ * @param string|null $only &only= parameter
+ * @param bool $printable Printable mode
+ * @param bool $handheld Handheld mode
+ * @param array $extraQuery Extra query parameters to add
* @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative)
*/
public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
@@ -1111,6 +1157,18 @@ class ResourceLoader {
/**
* Build a query array (array representation of query string) for load.php. Helper
* function for makeLoaderURL().
+ *
+ * @param array $modules
+ * @param string $lang
+ * @param string $skin
+ * @param string $user
+ * @param string $version
+ * @param bool $debug
+ * @param string $only
+ * @param bool $printable
+ * @param bool $handheld
+ * @param array $extraQuery
+ *
* @return array
*/
public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
@@ -1149,10 +1207,54 @@ class ResourceLoader {
* Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be
* at most 255 bytes.
*
- * @param $moduleName string Module name to check
+ * @param string $moduleName Module name to check
* @return bool Whether $moduleName is a valid module name
*/
public static function isValidModuleName( $moduleName ) {
return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
}
+
+ /**
+ * Returns LESS compiler set up for use with MediaWiki
+ *
+ * @since 1.22
+ * @return lessc
+ */
+ public static function getLessCompiler() {
+ global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths;
+
+ // When called from the installer, it is possible that a required PHP extension
+ // is missing (at least for now; see bug 47564). If this is the case, throw an
+ // exception (caught by the installer) to prevent a fatal error later on.
+ if ( !function_exists( 'ctype_digit' ) ) {
+ throw new MWException( 'lessc requires the Ctype extension' );
+ }
+
+ $less = new lessc();
+ $less->setPreserveComments( true );
+ $less->setVariables( self::getLESSVars() );
+ $less->setImportDir( $wgResourceLoaderLESSImportPaths );
+ foreach ( $wgResourceLoaderLESSFunctions as $name => $func ) {
+ $less->registerFunction( $name, $func );
+ }
+ return $less;
+ }
+
+ /**
+ * Get global LESS variables.
+ *
+ * $since 1.22
+ * @return array: Map of variable names to string CSS values.
+ */
+ public static function getLESSVars() {
+ global $wgResourceLoaderLESSVars;
+
+ static $lessVars = null;
+ if ( $lessVars === null ) {
+ $lessVars = $wgResourceLoaderLESSVars;
+ // Sort by key to ensure consistent hashing for cache lookups.
+ ksort( $lessVars );
+ }
+ return $lessVars;
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php
index 0e96c6c8..22ff6a7e 100644
--- a/includes/resourceloader/ResourceLoaderContext.php
+++ b/includes/resourceloader/ResourceLoaderContext.php
@@ -58,14 +58,14 @@ class ResourceLoaderContext {
// Interpret request
// List of modules
$modules = $request->getVal( 'modules' );
- $this->modules = $modules ? self::expandModuleNames( $modules ) : array();
+ $this->modules = $modules ? self::expandModuleNames( $modules ) : array();
// Various parameters
- $this->skin = $request->getVal( 'skin' );
- $this->user = $request->getVal( 'user' );
- $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
- $this->only = $request->getVal( 'only' );
- $this->version = $request->getVal( 'version' );
- $this->raw = $request->getFuzzyBool( 'raw' );
+ $this->skin = $request->getVal( 'skin' );
+ $this->user = $request->getVal( 'user' );
+ $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
+ $this->only = $request->getVal( 'only' );
+ $this->version = $request->getVal( 'version' );
+ $this->raw = $request->getFuzzyBool( 'raw' );
$skinnames = Skin::getSkinNames();
// If no skin is specified, or we don't recognize the skin, use the default skin
@@ -78,7 +78,7 @@ class ResourceLoaderContext {
* Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to
* an array of module names like array( 'jquery.foo', 'jquery.bar',
* 'jquery.ui.baz', 'jquery.ui.quux' )
- * @param $modules String Packed module name list
+ * @param string $modules Packed module name list
* @return array of module names
*/
public static function expandModuleNames( $modules ) {
@@ -96,7 +96,7 @@ class ResourceLoaderContext {
$pos = strrpos( $group, '.' );
if ( $pos === false ) {
// Prefixless modules, i.e. without dots
- $retval = explode( ',', $group );
+ $retval = array_merge( $retval, explode( ',', $group ) );
} else {
// We have a prefix and a bunch of suffixes
$prefix = substr( $group, 0, $pos ); // 'foo'
@@ -145,7 +145,7 @@ class ResourceLoaderContext {
public function getLanguage() {
if ( $this->language === null ) {
global $wgLang;
- $this->language = $this->request->getVal( 'lang' );
+ $this->language = $this->request->getVal( 'lang' );
if ( !$this->language ) {
$this->language = $wgLang->getCode();
}
diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php
index 8b9b7277..9ed181ed 100644
--- a/includes/resourceloader/ResourceLoaderFileModule.php
+++ b/includes/resourceloader/ResourceLoaderFileModule.php
@@ -113,6 +113,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
protected $debugRaw = true;
/** Boolean: Whether mw.loader.state() call should be omitted */
protected $raw = false;
+ protected $targets = array( 'desktop' );
+
+ /**
+ * Boolean: Whether getStyleURLsForDebug should return raw file paths,
+ * or return load.php urls
+ */
+ protected $hasGeneratedStyles = false;
+
/**
* Array: Cache for mtime
* @par Usage:
@@ -135,57 +143,58 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Constructs a new module from an options array.
*
- * @param $options Array: List of options; if not given or empty, an empty module will be
+ * @param array $options List of options; if not given or empty, an empty module will be
* constructed
- * @param $localBasePath String: Base path to prepend to all local paths in $options. Defaults
+ * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
* to $IP
- * @param $remoteBasePath String: Base path to prepend to all remote paths in $options. Defaults
+ * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults
* to $wgScriptPath
*
* Below is a description for the $options array:
+ * @throws MWException
* @par Construction options:
* @code
- * array(
- * // Base path to prepend to all local paths in $options. Defaults to $IP
- * 'localBasePath' => [base path],
- * // Base path to prepend to all remote paths in $options. Defaults to $wgScriptPath
- * 'remoteBasePath' => [base path],
- * // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
- * 'remoteExtPath' => [base path],
- * // Scripts to always include
- * 'scripts' => [file path string or array of file path strings],
- * // Scripts to include in specific language contexts
- * 'languageScripts' => array(
- * [language code] => [file path string or array of file path strings],
- * ),
- * // Scripts to include in specific skin contexts
- * 'skinScripts' => array(
- * [skin name] => [file path string or array of file path strings],
- * ),
- * // Scripts to include in debug contexts
- * 'debugScripts' => [file path string or array of file path strings],
- * // Scripts to include in the startup module
- * 'loaderScripts' => [file path string or array of file path strings],
- * // Modules which must be loaded before this module
- * 'dependencies' => [modile name string or array of module name strings],
- * // Styles to always load
- * 'styles' => [file path string or array of file path strings],
- * // Styles to include in specific skin contexts
- * 'skinStyles' => array(
- * [skin name] => [file path string or array of file path strings],
- * ),
- * // Messages to always load
- * 'messages' => [array of message key strings],
- * // Group which this module should be loaded together with
- * 'group' => [group name string],
- * // Position on the page to load this module at
- * 'position' => ['bottom' (default) or 'top']
- * )
+ * array(
+ * // Base path to prepend to all local paths in $options. Defaults to $IP
+ * 'localBasePath' => [base path],
+ * // Base path to prepend to all remote paths in $options. Defaults to $wgScriptPath
+ * 'remoteBasePath' => [base path],
+ * // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
+ * 'remoteExtPath' => [base path],
+ * // Scripts to always include
+ * 'scripts' => [file path string or array of file path strings],
+ * // Scripts to include in specific language contexts
+ * 'languageScripts' => array(
+ * [language code] => [file path string or array of file path strings],
+ * ),
+ * // Scripts to include in specific skin contexts
+ * 'skinScripts' => array(
+ * [skin name] => [file path string or array of file path strings],
+ * ),
+ * // Scripts to include in debug contexts
+ * 'debugScripts' => [file path string or array of file path strings],
+ * // Scripts to include in the startup module
+ * 'loaderScripts' => [file path string or array of file path strings],
+ * // Modules which must be loaded before this module
+ * 'dependencies' => [module name string or array of module name strings],
+ * // Styles to always load
+ * 'styles' => [file path string or array of file path strings],
+ * // Styles to include in specific skin contexts
+ * 'skinStyles' => array(
+ * [skin name] => [file path string or array of file path strings],
+ * ),
+ * // Messages to always load
+ * 'messages' => [array of message key strings],
+ * // Group which this module should be loaded together with
+ * 'group' => [group name string],
+ * // Position on the page to load this module at
+ * 'position' => ['bottom' (default) or 'top']
+ * )
* @endcode
*/
public function __construct( $options = array(), $localBasePath = null,
- $remoteBasePath = null )
- {
+ $remoteBasePath = null
+ ) {
global $IP, $wgScriptPath, $wgResourceBasePath;
$this->localBasePath = $localBasePath === null ? $IP : $localBasePath;
if ( $remoteBasePath !== null ) {
@@ -206,7 +215,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
case 'debugScripts':
case 'loaderScripts':
case 'styles':
- $this->{$member} = (array) $option;
+ $this->{$member} = (array)$option;
break;
// Collated lists of file paths
case 'languageScripts':
@@ -225,25 +234,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
"'$key' given, string expected."
);
}
- $this->{$member}[$key] = (array) $value;
+ $this->{$member}[$key] = (array)$value;
}
break;
// Lists of strings
case 'dependencies':
case 'messages':
- $this->{$member} = (array) $option;
+ case 'targets':
+ $this->{$member} = (array)$option;
break;
// Single strings
case 'group':
case 'position':
case 'localBasePath':
case 'remoteBasePath':
- $this->{$member} = (string) $option;
+ $this->{$member} = (string)$option;
break;
// Single booleans
case 'debugRaw':
case 'raw':
- $this->{$member} = (bool) $option;
+ $this->{$member} = (bool)$option;
break;
}
}
@@ -255,8 +265,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets all scripts for a given context concatenated together.
*
- * @param $context ResourceLoaderContext: Context in which to generate script
- * @return String: JavaScript code for $context
+ * @param ResourceLoaderContext $context Context in which to generate script
+ * @return string: JavaScript code for $context
*/
public function getScript( ResourceLoaderContext $context ) {
$files = $this->getScriptFiles( $context );
@@ -264,7 +274,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return array
*/
public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
@@ -285,7 +295,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets loader script.
*
- * @return String: JavaScript code to be added to startup module
+ * @return string: JavaScript code to be added to startup module
*/
public function getLoaderScript() {
if ( count( $this->loaderScripts ) == 0 ) {
@@ -297,8 +307,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets all styles for a given context concatenated together.
*
- * @param $context ResourceLoaderContext: Context in which to generate styles
- * @return String: CSS code for $context
+ * @param ResourceLoaderContext $context Context in which to generate styles
+ * @return string: CSS code for $context
*/
public function getStyles( ResourceLoaderContext $context ) {
$styles = $this->readStyleFiles(
@@ -320,16 +330,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
);
}
} catch ( Exception $e ) {
- wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
}
return $styles;
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return array
*/
public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
+ if ( $this->hasGeneratedStyles ) {
+ // Do the default behaviour of returning a url back to load.php
+ // but with only=styles.
+ return parent::getStyleURLsForDebug( $context );
+ }
+ // Our module consists entirely of real css files,
+ // in debug mode we can load those directly.
$urls = array();
foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
$urls[$mediaType] = array();
@@ -343,7 +360,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets list of message keys used by this module.
*
- * @return Array: List of message keys
+ * @return array: List of message keys
*/
public function getMessages() {
return $this->messages;
@@ -352,7 +369,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets the name of the group this module should be loaded in.
*
- * @return String: Group name
+ * @return string: Group name
*/
public function getGroup() {
return $this->group;
@@ -368,7 +385,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets list of names of modules this module depends on.
*
- * @return Array: List of module names
+ * @return array: List of module names
*/
public function getDependencies() {
return $this->dependencies;
@@ -390,9 +407,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
* calculations on files relevant to the given language, skin and debug
* mode.
*
- * @param $context ResourceLoaderContext: Context in which to calculate
+ * @param ResourceLoaderContext $context Context in which to calculate
* the modified time
- * @return Integer: UNIX timestamp
+ * @return int: UNIX timestamp
* @see ResourceLoaderModule::getFileDependencies
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
@@ -437,9 +454,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
return $this->modifiedTime[$context->getHash()] = 1;
}
- wfProfileIn( __METHOD__.'-filemtime' );
+ wfProfileIn( __METHOD__ . '-filemtime' );
$filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) );
- wfProfileOut( __METHOD__.'-filemtime' );
+ wfProfileOut( __METHOD__ . '-filemtime' );
$this->modifiedTime[$context->getHash()] = max(
$filesMtime,
$this->getMsgBlobMtime( $context->getLanguage() ) );
@@ -451,7 +468,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/* Protected Methods */
/**
- * @param $path string
+ * @param string $path
* @return string
*/
protected function getLocalPath( $path ) {
@@ -459,7 +476,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * @param $path string
+ * @param string $path
* @return string
*/
protected function getRemotePath( $path ) {
@@ -467,17 +484,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
+ * Infer the stylesheet language from a stylesheet file path.
+ *
+ * @since 1.22
+ * @param string $path
+ * @return string: the stylesheet language name
+ */
+ public function getStyleSheetLang( $path ) {
+ return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
+ }
+
+ /**
* Collates file paths by option (where provided).
*
- * @param $list Array: List of file paths in any combination of index/path
+ * @param array $list List of file paths in any combination of index/path
* or path/options pairs
- * @param $option String: option name
- * @param $default Mixed: default value if the option isn't set
- * @return Array: List of file paths, collated by $option
+ * @param string $option option name
+ * @param mixed $default default value if the option isn't set
+ * @return array: List of file paths, collated by $option
*/
protected static function collateFilePathListByOption( array $list, $option, $default ) {
$collatedFiles = array();
- foreach ( (array) $list as $key => $value ) {
+ foreach ( (array)$list as $key => $value ) {
if ( is_int( $key ) ) {
// File name as the value
if ( !isset( $collatedFiles[$default] ) ) {
@@ -499,10 +527,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets a list of element that match a key, optionally using a fallback key.
*
- * @param $list Array: List of lists to select from
- * @param $key String: Key to look for in $map
- * @param $fallback String: Key to look for in $list if $key doesn't exist
- * @return Array: List of elements from $map which matched $key or $fallback,
+ * @param array $list List of lists to select from
+ * @param string $key Key to look for in $map
+ * @param string $fallback Key to look for in $list if $key doesn't exist
+ * @return array: List of elements from $map which matched $key or $fallback,
* or an empty list in case of no match
*/
protected static function tryForKey( array $list, $key, $fallback = null ) {
@@ -520,8 +548,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets a list of file paths for all scripts in this module, in order of propper execution.
*
- * @param $context ResourceLoaderContext: Context
- * @return Array: List of file paths
+ * @param ResourceLoaderContext $context
+ * @return array: List of file paths
*/
protected function getScriptFiles( ResourceLoaderContext $context ) {
$files = array_merge(
@@ -532,14 +560,15 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
if ( $context->getDebug() ) {
$files = array_merge( $files, $this->debugScripts );
}
- return $files;
+
+ return array_unique( $files );
}
/**
* Gets a list of file paths for all styles in this module, in order of propper inclusion.
*
- * @param $context ResourceLoaderContext: Context
- * @return Array: List of file paths
+ * @param ResourceLoaderContext $context
+ * @return array: List of file paths
*/
protected function getStyleFiles( ResourceLoaderContext $context ) {
return array_merge_recursive(
@@ -551,10 +580,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
+ * Returns all style files used by this module
+ * @return array
+ */
+ public function getAllStyleFiles() {
+ $files = array();
+ foreach( (array)$this->styles as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $path = $key;
+ } else {
+ $path = $value;
+ }
+ $files[] = $this->getLocalPath( $path );
+ }
+ return $files;
+ }
+
+ /**
* Gets the contents of a list of JavaScript files.
*
- * @param $scripts Array: List of file paths to scripts to read, remap and concetenate
- * @return String: Concatenated and remapped JavaScript data from $scripts
+ * @param array $scripts List of file paths to scripts to read, remap and concetenate
+ * @throws MWException
+ * @return string: Concatenated and remapped JavaScript data from $scripts
*/
protected function readScriptFiles( array $scripts ) {
global $wgResourceLoaderValidateStaticJS;
@@ -565,7 +612,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
foreach ( array_unique( $scripts ) as $fileName ) {
$localPath = $this->getLocalPath( $fileName );
if ( !file_exists( $localPath ) ) {
- throw new MWException( __METHOD__.": script file not found: \"$localPath\"" );
+ throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
}
$contents = file_get_contents( $localPath );
if ( $wgResourceLoaderValidateStaticJS ) {
@@ -582,12 +629,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets the contents of a list of CSS files.
*
- * @param $styles Array: List of media type/list of file paths pairs, to read, remap and
+ * @param array $styles List of media type/list of file paths pairs, to read, remap and
* concetenate
*
- * @param $flip bool
+ * @param bool $flip
*
- * @return Array: List of concatenated and remapped CSS data from $styles,
+ * @return array: List of concatenated and remapped CSS data from $styles,
* keyed by media type
*/
protected function readStyleFiles( array $styles, $flip ) {
@@ -613,18 +660,27 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*
* This method can be used as a callback for array_map()
*
- * @param $path String: File path of style file to read
- * @param $flip bool
+ * @param string $path File path of style file to read
+ * @param bool $flip
*
- * @return String: CSS data in script file
+ * @return string: CSS data in script file
* @throws MWException if the file doesn't exist
*/
protected function readStyleFile( $path, $flip ) {
$localPath = $this->getLocalPath( $path );
if ( !file_exists( $localPath ) ) {
- throw new MWException( __METHOD__.": style file not found: \"$localPath\"" );
+ $msg = __METHOD__ . ": style file not found: \"$localPath\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+
+ if ( $this->getStyleSheetLang( $path ) === 'less' ) {
+ $style = $this->compileLESSFile( $localPath );
+ $this->hasGeneratedStyles = true;
+ } else {
+ $style = file_get_contents( $localPath );
}
- $style = file_get_contents( $localPath );
+
if ( $flip ) {
$style = CSSJanus::transform( $style, true, false );
}
@@ -646,28 +702,75 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
- * but returns 1 instead.
- * @param $filename string File name
- * @return int UNIX timestamp, or 1 if the file doesn't exist
- */
- protected static function safeFilemtime( $filename ) {
- if ( file_exists( $filename ) ) {
- return filemtime( $filename );
- } else {
- // We only ever map this function on an array if we're gonna call max() after,
- // so return our standard minimum timestamps here. This is 1, not 0, because
- // wfTimestamp(0) == NOW
- return 1;
- }
- }
-
- /**
* Get whether CSS for this module should be flipped
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return bool
*/
public function getFlip( $context ) {
return $context->getDirection() === 'rtl';
}
+
+ /**
+ * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
+ *
+ * @return array of strings
+ */
+ public function getTargets() {
+ return $this->targets;
+ }
+
+ /**
+ * Generate a cache key for a LESS file.
+ *
+ * The cache key varies on the file name and the names and values of global
+ * LESS variables.
+ *
+ * @since 1.22
+ * @param string $fileName File name of root LESS file.
+ * @return string: Cache key
+ */
+ protected static function getLESSCacheKey( $fileName ) {
+ $vars = json_encode( ResourceLoader::getLESSVars() );
+ $hash = md5( $fileName . $vars );
+ return wfMemcKey( 'resourceloader', 'less', $hash );
+ }
+
+ /**
+ * Compile a LESS file into CSS.
+ *
+ * If invalid, returns replacement CSS source consisting of the compilation
+ * error message encoded as a comment. To save work, we cache a result object
+ * which comprises the compiled CSS and the names & mtimes of the files
+ * that were processed. lessphp compares the cached & current mtimes and
+ * recompiles as necessary.
+ *
+ * @since 1.22
+ * @param string $fileName File path of LESS source
+ * @return string: CSS source
+ */
+ protected function compileLESSFile( $fileName ) {
+ $key = self::getLESSCacheKey( $fileName );
+ $cache = wfGetCache( CACHE_ANYTHING );
+
+ // The input to lessc. Either an associative array representing the
+ // cached results of a previous compilation, or the string file name if
+ // no cache result exists.
+ $source = $cache->get( $key );
+ if ( !is_array( $source ) || !isset( $source['root'] ) ) {
+ $source = $fileName;
+ }
+
+ $compiler = ResourceLoader::getLessCompiler();
+ $result = null;
+
+ $result = $compiler->cachedCompile( $source );
+
+ if ( !is_array( $result ) ) {
+ throw new MWException( 'LESS compiler result has type ' . gettype( $result ) . '; array expected.' );
+ }
+
+ $this->localFileRefs += array_keys( $result['files'] );
+ $cache->set( $key, $result );
+ return $result['compiled'];
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderLESSFunctions.php b/includes/resourceloader/ResourceLoaderLESSFunctions.php
new file mode 100644
index 00000000..c7570f64
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderLESSFunctions.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * PHP-provided functions for LESS; see docs for $wgResourceLoaderLESSFunctions
+ *
+ * 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
+ */
+
+class ResourceLoaderLESSFunctions {
+ /**
+ * Check if an image file reference is suitable for embedding.
+ * An image is embeddable if it (a) exists, (b) has a suitable MIME-type,
+ * (c) does not exceed IE<9 size limit of 32kb. This is a LESS predicate
+ * function; it returns a LESS boolean value and can thus be used as a
+ * mixin guard.
+ *
+ * @par Example:
+ * @code
+ * .background-image(@url) when(embeddable(@url)) {
+ * background-image: url(@url) !ie;
+ * }
+ * @endcode
+ */
+ public static function embeddable( $frame, $less ) {
+ $base = pathinfo( $less->parser->sourceName, PATHINFO_DIRNAME );
+ $url = $frame[2][0];
+ $file = realpath( $base . '/' . $url );
+ return $less->toBool( $file
+ && strpos( $url, '//' ) === false
+ && filesize( $file ) < CSSMin::EMBED_SIZE_LIMIT
+ && CSSMin::getMimeType( $file ) !== false );
+ }
+
+ /**
+ * Convert an image URI to a base64-encoded data URI.
+ *
+ * @par Example:
+ * @code
+ * .fancy-button {
+ * background-image: embed('../images/button-bg.png');
+ * }
+ * @endcode
+ */
+ public static function embed( $frame, $less ) {
+ $base = pathinfo( $less->parser->sourceName, PATHINFO_DIRNAME );
+ $url = $frame[2][0];
+ $file = realpath( $base . '/' . $url );
+
+ $data = CSSMin::encodeImageAsDataURI( $file );
+ $less->addParsedFile( $file );
+ return 'url(' . $data . ')';
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
index c916c4a5..fa0fbf85 100644
--- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php
+++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -28,6 +28,7 @@
class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
protected $language;
+ protected $targets = array( 'desktop', 'mobile' );
/**
* Get the grammar forms for the site content language.
*
@@ -47,33 +48,47 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
}
/**
+ * Get the digit groupin Pattern for the site content language.
+ *
+ * @return array
+ */
+ protected function getDigitGroupingPattern() {
+ return $this->language->digitGroupingPattern();
+ }
+
+ /**
* Get the digit transform table for the content language
- * Seperator transform table also required here to convert
- * the . and , sign to appropriate forms in content language.
*
* @return array
*/
protected function getDigitTransformTable() {
- $digitTransformTable = $this->language->digitTransformTable();
- $separatorTransformTable = $this->language->separatorTransformTable();
- if ( $digitTransformTable ) {
- array_merge( $digitTransformTable, (array)$separatorTransformTable );
- } else {
- return $separatorTransformTable;
- }
- return $digitTransformTable;
+ return $this->language->digitTransformTable();
}
/**
- * Get all the dynamic data for the content language to an array
+ * Get seperator transform table required for converting
+ * the . and , sign to appropriate forms in site content language.
+ *
+ * @return array
+ */
+ protected function getSeparatorTransformTable() {
+ return $this->language->separatorTransformTable();
+ }
+
+ /**
+ * Get all the dynamic data for the content language to an array.
+ *
+ * NOTE: Before calling this you HAVE to make sure $this->language is set.
*
* @return array
*/
protected function getData() {
return array(
'digitTransformTable' => $this->getDigitTransformTable(),
+ 'separatorTransformTable' => $this->getSeparatorTransformTable(),
'grammarForms' => $this->getSiteLangGrammarForms(),
'pluralRules' => $this->getPluralRules(),
+ 'digitGroupingPattern' => $this->getDigitGroupingPattern(),
);
}
@@ -91,26 +106,20 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
/**
* @param $context ResourceLoaderContext
- * @return array|int|Mixed
+ * @return int: UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
- $this->language = Language::factory( $context ->getLanguage() );
- $cache = wfGetCache( CACHE_ANYTHING );
- $key = wfMemcKey( 'resourceloader', 'langdatamodule', 'changeinfo' );
+ return max( 1, $this->getHashMtime( $context ) );
+ }
- $data = $this->getData();
- $hash = md5( serialize( $data ) );
+ /**
+ * @param $context ResourceLoaderContext
+ * @return string: Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ $this->language = Language::factory( $context->getLanguage() );
- $result = $cache->get( $key );
- if ( is_array( $result ) && $result['hash'] === $hash ) {
- return $result['timestamp'];
- }
- $timestamp = wfTimestamp();
- $cache->set( $key, array(
- 'hash' => $hash,
- 'timestamp' => $timestamp,
- ) );
- return $timestamp;
+ return md5( serialize( $this->getData() ) );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php
index 9c49c45f..11264fc8 100644
--- a/includes/resourceloader/ResourceLoaderModule.php
+++ b/includes/resourceloader/ResourceLoaderModule.php
@@ -58,6 +58,7 @@ abstract class ResourceLoaderModule {
/* Protected Members */
protected $name = null;
+ protected $targets = array( 'desktop' );
// In-object cache for file dependencies
protected $fileDeps = array();
@@ -70,17 +71,17 @@ abstract class ResourceLoaderModule {
* Get this module's name. This is set when the module is registered
* with ResourceLoader::register()
*
- * @return Mixed: Name (string) or null if no name was set
+ * @return mixed: Name (string) or null if no name was set
*/
public function getName() {
return $this->name;
}
/**
- * Set this module's name. This is called by ResourceLodaer::register()
+ * Set this module's name. This is called by ResourceLoader::register()
* when registering the module. Other code should not call this.
*
- * @param $name String: Name
+ * @param string $name Name
*/
public function setName( $name ) {
$this->name = $name;
@@ -90,25 +91,25 @@ abstract class ResourceLoaderModule {
* Get this module's origin. This is set when the module is registered
* with ResourceLoader::register()
*
- * @return Int ResourceLoaderModule class constant, the subclass default
- * if not set manuall
+ * @return int: ResourceLoaderModule class constant, the subclass default
+ * if not set manually
*/
public function getOrigin() {
return $this->origin;
}
/**
- * Set this module's origin. This is called by ResourceLodaer::register()
+ * Set this module's origin. This is called by ResourceLoader::register()
* when registering the module. Other code should not call this.
*
- * @param $origin Int origin
+ * @param int $origin origin
*/
public function setOrigin( $origin ) {
$this->origin = $origin;
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return bool
*/
public function getFlip( $context ) {
@@ -121,8 +122,8 @@ abstract class ResourceLoaderModule {
* Get all JS for this module for a given language and skin.
* Includes all relevant JS except loader scripts.
*
- * @param $context ResourceLoaderContext: Context object
- * @return String: JavaScript code
+ * @param ResourceLoaderContext $context
+ * @return string: JavaScript code
*/
public function getScript( ResourceLoaderContext $context ) {
// Stub, override expected
@@ -140,8 +141,8 @@ abstract class ResourceLoaderModule {
* #2 is important to prevent an infinite loop, therefore this function
* MUST return either an only= URL or a non-load.php URL.
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array of URLs
+ * @param ResourceLoaderContext $context
+ * @return array: Array of URLs
*/
public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
$url = ResourceLoader::makeLoaderURL(
@@ -171,8 +172,8 @@ abstract class ResourceLoaderModule {
/**
* Get all CSS for this module for a given skin.
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array: List of CSS strings or array of CSS strings keyed by media type.
+ * @param ResourceLoaderContext $context
+ * @return array: List of CSS strings or array of CSS strings keyed by media type.
* like array( 'screen' => '.foo { width: 0 }' );
* or array( 'screen' => array( '.foo { width: 0 }' ) );
*/
@@ -187,8 +188,8 @@ abstract class ResourceLoaderModule {
* the module, but file-based modules will want to override this to
* load the files directly. See also getScriptURLsForDebug()
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array: array( mediaType => array( URL1, URL2, ... ), ... )
+ * @param ResourceLoaderContext $context
+ * @return array: array( mediaType => array( URL1, URL2, ... ), ... )
*/
public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
$url = ResourceLoader::makeLoaderURL(
@@ -210,7 +211,7 @@ abstract class ResourceLoaderModule {
*
* To get a JSON blob with messages, use MessageBlobStore::get()
*
- * @return Array: List of message keys. Keys may occur more than once
+ * @return array: List of message keys. Keys may occur more than once
*/
public function getMessages() {
// Stub, override expected
@@ -220,7 +221,7 @@ abstract class ResourceLoaderModule {
/**
* Get the group this module is in.
*
- * @return String: Group name
+ * @return string: Group name
*/
public function getGroup() {
// Stub, override expected
@@ -230,7 +231,7 @@ abstract class ResourceLoaderModule {
/**
* Get the origin of this module. Should only be overridden for foreign modules.
*
- * @return String: Origin name, 'local' for local modules
+ * @return string: Origin name, 'local' for local modules
*/
public function getSource() {
// Stub, override expected
@@ -262,7 +263,7 @@ abstract class ResourceLoaderModule {
/**
* Get the loader JS for this module, if set.
*
- * @return Mixed: JavaScript loader code as a string or boolean false if no custom loader set
+ * @return mixed: JavaScript loader code as a string or boolean false if no custom loader set
*/
public function getLoaderScript() {
// Stub, override expected
@@ -273,16 +274,11 @@ abstract class ResourceLoaderModule {
* Get a list of modules this module depends on.
*
* Dependency information is taken into account when loading a module
- * on the client side. When adding a module on the server side,
- * dependency information is NOT taken into account and YOU are
- * responsible for adding dependent modules as well. If you don't do
- * this, the client side loader will send a second request back to the
- * server to fetch the missing modules, which kind of defeats the
- * purpose of the resource loader.
+ * on the client side.
*
* To add dependencies dynamically on the client side, use a custom
* loader script, see getLoaderScript()
- * @return Array: List of module names as strings
+ * @return array: List of module names as strings
*/
public function getDependencies() {
// Stub, override expected
@@ -290,11 +286,20 @@ abstract class ResourceLoaderModule {
}
/**
+ * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
+ *
+ * @return array: Array of strings
+ */
+ public function getTargets() {
+ return $this->targets;
+ }
+
+ /**
* Get the files this module depends on indirectly for a given skin.
* Currently these are only image files referenced by the module's CSS.
*
- * @param $skin String: Skin name
- * @return Array: List of files
+ * @param string $skin Skin name
+ * @return array: List of files
*/
public function getFileDependencies( $skin ) {
// Try in-object cache first
@@ -309,7 +314,7 @@ abstract class ResourceLoaderModule {
), __METHOD__
);
if ( !is_null( $deps ) ) {
- $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
+ $this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true );
} else {
$this->fileDeps[$skin] = array();
}
@@ -319,8 +324,8 @@ abstract class ResourceLoaderModule {
/**
* Set preloaded file dependency information. Used so we can load this
* information for all modules at once.
- * @param $skin String: Skin name
- * @param $deps Array: Array of file names
+ * @param string $skin Skin name
+ * @param array $deps Array of file names
*/
public function setFileDependencies( $skin, $deps ) {
$this->fileDeps[$skin] = $deps;
@@ -329,13 +334,14 @@ abstract class ResourceLoaderModule {
/**
* Get the last modification timestamp of the message blob for this
* module in a given language.
- * @param $lang String: Language code
- * @return Integer: UNIX timestamp, or 0 if the module doesn't have messages
+ * @param string $lang Language code
+ * @return int: UNIX timestamp, or 0 if the module doesn't have messages
*/
public function getMsgBlobMtime( $lang ) {
if ( !isset( $this->msgBlobMtime[$lang] ) ) {
- if ( !count( $this->getMessages() ) )
+ if ( !count( $this->getMessages() ) ) {
return 0;
+ }
$dbr = wfGetDB( DB_SLAVE );
$msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
@@ -356,7 +362,7 @@ abstract class ResourceLoaderModule {
/**
* Set a preloaded message blob last modification timestamp. Used so we
* can load this information for all modules at once.
- * @param $lang String: Language code
+ * @param string $lang Language code
* @param $mtime Integer: UNIX timestamp or 0 if there is no such blob
*/
public function setMsgBlobMtime( $lang, $mtime ) {
@@ -375,9 +381,13 @@ abstract class ResourceLoaderModule {
* NOTE: The mtime of the module's messages is NOT automatically included.
* If you want this to happen, you'll need to call getMsgBlobMtime()
* yourself and take its result into consideration.
- *
- * @param $context ResourceLoaderContext: Context object
- * @return Integer: UNIX timestamp
+ *
+ * NOTE: The mtime of the module's hash is NOT automatically included.
+ * If your module provides a getModifiedHash() method, you'll need to call getHashMtime()
+ * yourself and take its result into consideration.
+ *
+ * @param ResourceLoaderContext $context Context object
+ * @return integer UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
// 0 would mean now
@@ -385,19 +395,59 @@ abstract class ResourceLoaderModule {
}
/**
+ * Helper method for calculating when the module's hash (if it has one) changed.
+ *
+ * @param ResourceLoaderContext $context
+ * @return integer: UNIX timestamp or 0 if there is no hash provided
+ */
+ public function getHashMtime( ResourceLoaderContext $context ) {
+ $hash = $this->getModifiedHash( $context );
+ if ( !is_string( $hash ) ) {
+ return 0;
+ }
+
+ $cache = wfGetCache( CACHE_ANYTHING );
+ $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName() );
+
+ $data = $cache->get( $key );
+ if ( is_array( $data ) && $data['hash'] === $hash ) {
+ // Hash is still the same, re-use the timestamp of when we first saw this hash.
+ return $data['timestamp'];
+ }
+
+ $timestamp = wfTimestamp();
+ $cache->set( $key, array(
+ 'hash' => $hash,
+ 'timestamp' => $timestamp,
+ ) );
+
+ return $timestamp;
+ }
+
+ /**
+ * Get the last modification timestamp of the message blob for this
+ * module in a given language.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string|null: Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ return null;
+ }
+
+ /**
* Check whether this module is known to be empty. If a child class
* has an easy and cheap way to determine that this module is
* definitely going to be empty, it should override this method to
* return true in that case. Callers may optimize the request for this
* module away if this function returns true.
- * @param $context ResourceLoaderContext: Context object
- * @return Boolean
+ * @param ResourceLoaderContext $context
+ * @return bool
*/
public function isKnownEmpty( ResourceLoaderContext $context ) {
return false;
}
-
/** @var JSParser lazy-initialized; use self::javaScriptParser() */
private static $jsParser;
private static $parseCacheVersion = 1;
@@ -408,7 +458,7 @@ abstract class ResourceLoaderModule {
*
* @param string $fileName
* @param string $contents
- * @return string JS with the original, or a replacement error
+ * @return string: JS with the original, or a replacement error
*/
protected function validateScriptFile( $fileName, $contents ) {
global $wgResourceLoaderValidateJS;
@@ -426,10 +476,10 @@ abstract class ResourceLoaderModule {
try {
$parser->parse( $contents, $fileName, 1 );
$result = $contents;
- } catch (Exception $e) {
+ } catch ( Exception $e ) {
// We'll save this to cache to avoid having to validate broken JS over and over...
$err = $e->getMessage();
- $result = "throw new Error(" . Xml::encodeJsVar("JavaScript parse error: $err") . ");";
+ $result = "throw new Error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
}
$cache->set( $key, $result );
@@ -449,4 +499,20 @@ abstract class ResourceLoaderModule {
return self::$jsParser;
}
+ /**
+ * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
+ * but returns 1 instead.
+ * @param string $filename File name
+ * @return int UNIX timestamp, or 1 if the file doesn't exist
+ */
+ protected static function safeFilemtime( $filename ) {
+ if ( file_exists( $filename ) ) {
+ return filemtime( $filename );
+ } else {
+ // We only ever map this function on an array if we're gonna call max() after,
+ // so return our standard minimum timestamps here. This is 1, not 0, because
+ // wfTimestamp(0) == NOW
+ return 1;
+ }
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php
index 8e81c8d9..bd026f3f 100644
--- a/includes/resourceloader/ResourceLoaderNoscriptModule.php
+++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php
@@ -45,7 +45,7 @@ class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule {
/**
* Gets group name
- *
+ *
* @return String: Name of group
*/
public function getGroup() {
diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php
index 03fe1fe5..05754d37 100644
--- a/includes/resourceloader/ResourceLoaderSiteModule.php
+++ b/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -37,20 +37,19 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
* @return Array: List of pages
*/
protected function getPages( ResourceLoaderContext $context ) {
- global $wgHandheldStyle;
+ global $wgUseSiteJs, $wgUseSiteCss;
- $pages = array(
- 'MediaWiki:Common.js' => array( 'type' => 'script' ),
- 'MediaWiki:Common.css' => array( 'type' => 'style' ),
- 'MediaWiki:' . ucfirst( $context->getSkin() ) . '.js' => array( 'type' => 'script' ),
- 'MediaWiki:' . ucfirst( $context->getSkin() ) . '.css' => array( 'type' => 'style' ),
- 'MediaWiki:Print.css' => array( 'type' => 'style', 'media' => 'print' ),
- );
- if ( $wgHandheldStyle ) {
- $pages['MediaWiki:Handheld.css'] = array(
- 'type' => 'style',
- 'media' => 'handheld' );
+ $pages = array();
+ if ( $wgUseSiteJs ) {
+ $pages['MediaWiki:Common.js'] = array( 'type' => 'script' );
+ $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.js'] = array( 'type' => 'script' );
}
+ if ( $wgUseSiteCss ) {
+ $pages['MediaWiki:Common.css'] = array( 'type' => 'style' );
+ $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.css'] = array( 'type' => 'style' );
+
+ }
+ $pages['MediaWiki:Print.css'] = array( 'type' => 'style', 'media' => 'print' );
return $pages;
}
@@ -58,7 +57,7 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
/**
* Gets group name
- *
+ *
* @return String: Name of group
*/
public function getGroup() {
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php
index 20ee83f9..20f6e0ba 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -27,6 +27,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
/* Protected Members */
protected $modifiedTime = array();
+ protected $targets = array( 'desktop', 'mobile' );
/* Protected Methods */
@@ -51,7 +52,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
*/
$namespaceIds = $wgContLang->getNamespaceIds();
$caseSensitiveNamespaces = array();
- foreach( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
+ foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
$namespaceIds[$wgContLang->lc( $name )] = $index;
if ( !MWNamespace::isCapitalized( $index ) ) {
$caseSensitiveNamespaces[] = $index;
@@ -83,7 +84,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
'wgNamespaceIds' => $namespaceIds,
'wgSiteName' => $wgSitename,
- 'wgFileExtensions' => array_values( $wgFileExtensions ),
+ 'wgFileExtensions' => array_values( array_unique( $wgFileExtensions ) ),
'wgDBname' => $wgDBname,
// This sucks, it is only needed on Special:Upload, but I could
// not find a way to add vars only for a certain module
@@ -94,6 +95,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgCookiePrefix' => $wgCookiePrefix,
'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength,
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
+ 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
);
wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) );
@@ -114,6 +116,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
$out = '';
$registrations = array();
$resourceLoader = $context->getResourceLoader();
+ $target = $context->getRequest()->getVal( 'target', 'desktop' );
// Register sources
$out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
@@ -121,6 +124,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Register modules
foreach ( $resourceLoader->getModuleNames() as $name ) {
$module = $resourceLoader->getModule( $name );
+ $moduleTargets = $module->getTargets();
+ if ( !in_array( $target, $moduleTargets ) ) {
+ continue;
+ }
$deps = $module->getDependencies();
$group = $module->getGroup();
$source = $module->getSource();
@@ -130,33 +137,33 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
$version = wfTimestamp( TS_ISO_8601_BASIC,
$module->getModifiedTime( $context ) );
$out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $source, $loader );
+ continue;
}
+
// Automatically register module
+ // getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always
+ // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX
+ $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) );
+ $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
+ // Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to
+ // mw.loader.register()
+ if ( !count( $deps ) && $group === null && $source === 'local' ) {
+ $registrations[] = array( $name, $mtime );
+ }
+ // Modules with dependencies but no group or foreign source pass three arguments
+ // (name, timestamp, dependencies) to mw.loader.register()
+ elseif ( $group === null && $source === 'local' ) {
+ $registrations[] = array( $name, $mtime, $deps );
+ }
+ // Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group)
+ // to mw.loader.register()
+ elseif ( $source === 'local' ) {
+ $registrations[] = array( $name, $mtime, $deps, $group );
+ }
+ // Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source)
+ // to mw.loader.register()
else {
- // getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always
- // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX
- $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) );
- $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
- // Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to
- // mw.loader.register()
- if ( !count( $deps ) && $group === null && $source === 'local' ) {
- $registrations[] = array( $name, $mtime );
- }
- // Modules with dependencies but no group or foreign source pass three arguments
- // (name, timestamp, dependencies) to mw.loader.register()
- elseif ( $group === null && $source === 'local' ) {
- $registrations[] = array( $name, $mtime, $deps );
- }
- // Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group)
- // to mw.loader.register()
- elseif ( $source === 'local' ) {
- $registrations[] = array( $name, $mtime, $deps, $group );
- }
- // Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source)
- // to mw.loader.register()
- else {
- $registrations[] = array( $name, $mtime, $deps, $group, $source );
- }
+ $registrations[] = array( $name, $mtime, $deps, $group, $source );
}
}
$out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
@@ -179,7 +186,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
* @return string
*/
public function getScript( ResourceLoaderContext $context ) {
- global $IP, $wgLoadScript, $wgLegacyJavaScriptGlobals;
+ global $IP, $wgLegacyJavaScriptGlobals;
$out = file_get_contents( "$IP/resources/startup.js" );
if ( $context->getOnly() === 'scripts' ) {
@@ -219,7 +226,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
"};\n";
// Conditional script injection
- $scriptTag = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) );
+ $scriptTag = Html::linkedScript( wfAppendQuery( wfScript( 'load' ), $query ) );
$out .= "if ( isCompatible() ) {\n" .
"\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) .
"}\n" .
diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
index 0e95d964..bda86539 100644
--- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
@@ -48,7 +48,7 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
global $wgUser;
return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
}
-
+
/**
* @param $context ResourceLoaderContext
* @return array
@@ -56,43 +56,44 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
public function getStyles( ResourceLoaderContext $context ) {
global $wgAllowUserCssPrefs, $wgUser;
- if ( $wgAllowUserCssPrefs ) {
- $options = $wgUser->getOptions();
+ if ( !$wgAllowUserCssPrefs ) {
+ return array();
+ }
- // Build CSS rules
- $rules = array();
+ $options = $wgUser->getOptions();
- // Underline: 2 = browser default, 1 = always, 0 = never
- if ( $options['underline'] < 2 ) {
- $rules[] = "a { text-decoration: " .
- ( $options['underline'] ? 'underline' : 'none' ) . "; }";
- } else {
- # The scripts of these languages are very hard to read with underlines
- $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(fa),a:lang(kk-arab), ' .
- 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }';
- }
- if ( $options['justify'] ) {
- $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
- }
- if ( !$options['showtoc'] ) {
- $rules[] = "#toc { display: none; }\n";
- }
- if ( !$options['editsection'] ) {
- $rules[] = ".editsection { display: none; }\n";
- }
- if ( $options['editfont'] !== 'default' ) {
- // Double-check that $options['editfont'] consists of safe characters only
- if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) {
- $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
- }
- }
- $style = implode( "\n", $rules );
- if ( $this->getFlip( $context ) ) {
- $style = CSSJanus::transform( $style, true, false );
+ // Build CSS rules
+ $rules = array();
+
+ // Underline: 2 = browser default, 1 = always, 0 = never
+ if ( $options['underline'] < 2 ) {
+ $rules[] = "a { text-decoration: " .
+ ( $options['underline'] ? 'underline' : 'none' ) . "; }";
+ } else {
+ # The scripts of these languages are very hard to read with underlines
+ $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(kk-arab), ' .
+ 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }';
+ }
+ if ( $options['justify'] ) {
+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
+ }
+ if ( !$options['showtoc'] ) {
+ $rules[] = "#toc { display: none; }\n";
+ }
+ if ( !$options['editsection'] ) {
+ $rules[] = ".mw-editsection { display: none; }\n";
+ }
+ if ( $options['editfont'] !== 'default' ) {
+ // Double-check that $options['editfont'] consists of safe characters only
+ if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) {
+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
}
- return array( 'all' => $style );
}
- return array();
+ $style = implode( "\n", $rules );
+ if ( $this->getFlip( $context ) ) {
+ $style = CSSJanus::transform( $style, true, false );
+ }
+ return array( 'all' => $style );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
index 1316f423..9064263f 100644
--- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
@@ -33,12 +33,15 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
* @return array
*/
protected function getPages( ResourceLoaderContext $context ) {
- global $wgUser;
+ global $wgUser, $wgUseSiteJs, $wgUseSiteCss;
$userName = $context->getUser();
if ( $userName === null ) {
return array();
}
+ if ( !$wgUseSiteJs && !$wgUseSiteCss ) {
+ return array();
+ }
// Use $wgUser is possible; allows to skip a lot of code
if ( is_object( $wgUser ) && $wgUser->getName() == $userName ) {
@@ -51,12 +54,16 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
}
$pages = array();
- foreach( $user->getEffectiveGroups() as $group ) {
+ foreach ( $user->getEffectiveGroups() as $group ) {
if ( in_array( $group, array( '*', 'user' ) ) ) {
continue;
}
- $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' );
- $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' );
+ if ( $wgUseSiteJs ) {
+ $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' );
+ }
+ if ( $wgUseSiteCss ) {
+ $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' );
+ }
}
return $pages;
}
diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php
index 177302c5..7a04e473 100644
--- a/includes/resourceloader/ResourceLoaderUserModule.php
+++ b/includes/resourceloader/ResourceLoaderUserModule.php
@@ -35,11 +35,15 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
* @return array
*/
protected function getPages( ResourceLoaderContext $context ) {
+ global $wgAllowUserJs, $wgAllowUserCss;
$username = $context->getUser();
if ( $username === null ) {
return array();
}
+ if ( !$wgAllowUserJs && !$wgAllowUserCss ) {
+ return array();
+ }
// Get the normalized title of the user's user page
$userpageTitle = Title::makeTitleSafe( NS_USER, $username );
@@ -50,14 +54,15 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
$userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works
- $pages = array(
- "$userpage/common.js" => array( 'type' => 'script' ),
- "$userpage/" . $context->getSkin() . '.js' =>
- array( 'type' => 'script' ),
- "$userpage/common.css" => array( 'type' => 'style' ),
- "$userpage/" . $context->getSkin() . '.css' =>
- array( 'type' => 'style' ),
- );
+ $pages = array();
+ if ( $wgAllowUserJs ) {
+ $pages["$userpage/common.js"] = array( 'type' => 'script' );
+ $pages["$userpage/" . $context->getSkin() . '.js'] = array( 'type' => 'script' );
+ }
+ if ( $wgAllowUserCss ) {
+ $pages["$userpage/common.css"] = array( 'type' => 'style' );
+ $pages["$userpage/" . $context->getSkin() . '.css'] = array( 'type' => 'style' );
+ }
// Hack for bug 26283: if we're on a preview page for a CSS/JS page,
// we need to exclude that page from this module. In that case, the excludepage
diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
index 4624cbce..0b7e1964 100644
--- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -56,7 +56,9 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
public function getScript( ResourceLoaderContext $context ) {
global $wgUser;
return Xml::encodeJsCall( 'mw.user.options.set',
- array( $wgUser->getOptions() ) );
+ array( $wgUser->getOptions() ),
+ ResourceLoader::inDebugMode()
+ );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php
index 62d096a6..92ebbe93 100644
--- a/includes/resourceloader/ResourceLoaderUserTokensModule.php
+++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php
@@ -35,15 +35,15 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
/**
* Fetch the tokens for the current user.
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array: List of tokens keyed by token type
+ * @return array: List of tokens keyed by token type
*/
- protected function contextUserTokens( ResourceLoaderContext $context ) {
+ protected function contextUserTokens() {
global $wgUser;
return array(
'editToken' => $wgUser->getEditToken(),
- 'watchToken' => ApiQueryInfo::getWatchToken(null, null),
+ 'patrolToken' => ApiQueryRecentChanges::getPatrolToken( null, null ),
+ 'watchToken' => ApiQueryInfo::getWatchToken( null, null ),
);
}
@@ -53,7 +53,9 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
*/
public function getScript( ResourceLoaderContext $context ) {
return Xml::encodeJsCall( 'mw.user.tokens.set',
- array( $this->contextUserTokens( $context ) ) );
+ array( $this->contextUserTokens() ),
+ ResourceLoader::inDebugMode()
+ );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php
index ee8dd1e5..3f10ae53 100644
--- a/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -42,7 +42,20 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
/* Abstract Protected Methods */
/**
+ * Subclasses should return an associative array of resources in the module.
+ * Keys should be the title of a page in the MediaWiki or User namespace.
+ *
+ * Values should be a nested array of options. The supported keys are 'type' and
+ * (CSS only) 'media'.
+ *
+ * For scripts, 'type' should be 'script'.
+ *
+ * For stylesheets, 'type' should be 'style'.
+ * There is an optional media key, the value of which can be the
+ * medium ('screen', 'print', etc.) of the stylesheet.
+ *
* @param $context ResourceLoaderContext
+ * @return array
*/
abstract protected function getPages( ResourceLoaderContext $context );
@@ -75,7 +88,22 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
if ( !$revision ) {
return null;
}
- return $revision->getRawText();
+
+ $content = $revision->getContent( Revision::RAW );
+
+ if ( !$content ) {
+ wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
+ return null;
+ }
+
+ $model = $content->getModel();
+
+ if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) {
+ wfDebugLog( 'resourceloader', __METHOD__ . ': bad content model $model for JS/CSS page!' );
+ return null;
+ }
+
+ return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS
}
/* Methods */
@@ -98,7 +126,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
if ( strval( $script ) !== '' ) {
$script = $this->validateScriptFile( $titleText, $script );
if ( strpos( $titleText, '*/' ) === false ) {
- $scripts .= "/* $titleText */\n";
+ $scripts .= "/* $titleText */\n";
}
$scripts .= $script . "\n";
}
@@ -119,7 +147,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
continue;
}
$title = Title::newFromText( $titleText );
- if ( !$title || $title->isRedirect() ) {
+ if ( !$title || $title->isRedirect() ) {
continue;
}
$media = isset( $options['media'] ) ? $options['media'] : 'all';
@@ -135,7 +163,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
$styles[$media] = array();
}
if ( strpos( $titleText, '*/' ) === false ) {
- $style = "/* $titleText */\n" . $style;
+ $style = "/* $titleText */\n" . $style;
}
$styles[$media][] = $style;
}
diff --git a/includes/revisiondelete/RevisionDelete.php b/includes/revisiondelete/RevisionDelete.php
index 6ceadff4..71850877 100644
--- a/includes/revisiondelete/RevisionDelete.php
+++ b/includes/revisiondelete/RevisionDelete.php
@@ -41,6 +41,19 @@ class RevDel_RevisionList extends RevDel_List {
return 'rev_id';
}
+ public static function getRestriction() {
+ return 'deleterevision';
+ }
+
+ public static function getRevdelConstant() {
+ return Revision::DELETED_TEXT;
+ }
+
+ public static function suggestTarget( $target, array $ids ) {
+ $rev = Revision::newFromId( $ids[0] );
+ return $rev ? $rev->getTitle() : $target;
+ }
+
/**
* @param $db DatabaseBase
* @return mixed
@@ -52,7 +65,7 @@ class RevDel_RevisionList extends RevDel_List {
array_merge( Revision::selectFields(), Revision::selectUserFields() ),
array(
'rev_page' => $this->title->getArticleID(),
- 'rev_id' => $ids,
+ 'rev_id' => $ids,
),
__METHOD__,
array( 'ORDER BY' => 'rev_id DESC' ),
@@ -242,8 +255,7 @@ class RevDel_RevisionItem extends RevDel_Item {
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $this->list->msg( 'diff' )->escaped();
} else {
- return
- Linker::linkKnown(
+ return Linker::linkKnown(
$this->list->title,
$this->list->msg( 'diff' )->escaped(),
array(),
@@ -293,7 +305,7 @@ class RevDel_ArchiveList extends RevDel_RevisionList {
return $db->select( 'archive', '*',
array(
'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
+ 'ar_title' => $this->title->getDBkey(),
'ar_timestamp' => $timestamps
),
__METHOD__,
@@ -350,12 +362,12 @@ class RevDel_ArchiveItem extends RevDel_RevisionItem {
$dbw->update( 'archive',
array( 'ar_deleted' => $bits ),
array(
- 'ar_namespace' => $this->list->title->getNamespace(),
- 'ar_title' => $this->list->title->getDBkey(),
+ '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()
+ 'ar_timestamp' => $this->row->ar_timestamp,
+ 'ar_rev_id' => $this->row->ar_rev_id,
+ 'ar_deleted' => $this->getBits()
),
__METHOD__ );
return (bool)$dbw->affectedRows();
@@ -398,7 +410,6 @@ class RevDel_ArchiveItem extends RevDel_RevisionItem {
}
}
-
/**
* Item class for a archive table row by ar_rev_id -- actually
* used via RevDel_RevisionList.
@@ -424,7 +435,7 @@ class RevDel_ArchivedRevisionItem extends RevDel_ArchiveItem {
$dbw->update( 'archive',
array( 'ar_deleted' => $bits ),
array( 'ar_rev_id' => $this->row->ar_rev_id,
- 'ar_deleted' => $this->getBits()
+ 'ar_deleted' => $this->getBits()
),
__METHOD__ );
return (bool)$dbw->affectedRows();
@@ -443,6 +454,14 @@ class RevDel_FileList extends RevDel_List {
return 'oi_archive_name';
}
+ public static function getRestriction() {
+ return 'deleterevision';
+ }
+
+ public static function getRevdelConstant() {
+ return File::DELETED_FILE;
+ }
+
var $storeBatch, $deleteBatch, $cleanupBatch;
/**
@@ -451,12 +470,14 @@ class RevDel_FileList extends RevDel_List {
*/
public function doQuery( $db ) {
$archiveNames = array();
- foreach( $this->ids as $timestamp ) {
+ foreach ( $this->ids as $timestamp ) {
$archiveNames[] = $timestamp . '!' . $this->title->getDBkey();
}
- return $db->select( 'oldimage', '*',
+ return $db->select(
+ 'oldimage',
+ OldLocalFile::selectFields(),
array(
- 'oi_name' => $this->title->getDBkey(),
+ 'oi_name' => $this->title->getDBkey(),
'oi_archive_name' => $archiveNames
),
__METHOD__,
@@ -498,9 +519,20 @@ class RevDel_FileList extends RevDel_List {
}
public function doPostCommitUpdates() {
+ global $wgUseSquid;
$file = wfLocalFile( $this->title );
$file->purgeCache();
$file->purgeDescription();
+ $purgeUrls = array();
+ foreach ( $this->ids as $timestamp ) {
+ $archiveName = $timestamp . '!' . $this->title->getDBkey();
+ $file->purgeOldThumbnails( $archiveName );
+ $purgeUrls[] = $file->getArchiveUrl( $archiveName );
+ }
+ if ( $wgUseSquid ) {
+ // purge full images from cache
+ SquidUpdate::purge( $purgeUrls );
+ }
return Status::newGood();
}
@@ -623,8 +655,8 @@ class RevDel_FileItem extends RevDel_Item {
array(),
array(
'target' => $this->list->title->getPrefixedText(),
- 'file' => $this->file->getArchiveName(),
- 'token' => $this->list->getUser()->getEditToken(
+ 'file' => $this->file->getArchiveName(),
+ 'token' => $this->list->getUser()->getEditToken(
$this->file->getArchiveName() )
)
);
@@ -636,13 +668,13 @@ class RevDel_FileItem extends RevDel_Item {
* @return string HTML
*/
protected function getUserTools() {
- if( $this->file->userCan( Revision::DELETED_USER, $this->list->getUser() ) ) {
+ if ( $this->file->userCan( Revision::DELETED_USER, $this->list->getUser() ) ) {
$link = Linker::userLink( $this->file->user, $this->file->user_text ) .
Linker::userToolLinks( $this->file->user, $this->file->user_text );
} else {
$link = $this->list->msg( 'rev-deleted-user' )->escaped();
}
- if( $this->file->isDeleted( Revision::DELETED_USER ) ) {
+ if ( $this->file->isDeleted( Revision::DELETED_USER ) ) {
return '<span class="history-deleted">' . $link . '</span>';
}
return $link;
@@ -655,12 +687,12 @@ class RevDel_FileItem extends RevDel_Item {
* @return string HTML
*/
protected function getComment() {
- if( $this->file->userCan( File::DELETED_COMMENT, $this->list->getUser() ) ) {
+ if ( $this->file->userCan( File::DELETED_COMMENT, $this->list->getUser() ) ) {
$block = Linker::commentBlock( $this->file->description );
} else {
$block = ' ' . $this->list->msg( 'rev-deleted-comment' )->escaped();
}
- if( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
+ if ( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
return "<span class=\"history-deleted\">$block</span>";
}
return $block;
@@ -673,7 +705,7 @@ class RevDel_FileItem extends RevDel_Item {
' (' . $this->list->msg( 'nbytes' )->numParams( $this->file->getSize() )->text() . ')';
return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' .
- $data . ' ' . $this->getComment(). '</li>';
+ $data . ' ' . $this->getComment() . '</li>';
}
}
@@ -695,10 +727,12 @@ class RevDel_ArchivedFileList extends RevDel_FileList {
*/
public function doQuery( $db ) {
$ids = array_map( 'intval', $this->ids );
- return $db->select( 'filearchive', '*',
+ return $db->select(
+ 'filearchive',
+ ArchivedFile::selectFields(),
array(
'fa_name' => $this->title->getDBkey(),
- 'fa_id' => $ids
+ 'fa_id' => $ids
),
__METHOD__,
array( 'ORDER BY' => 'fa_id DESC' )
@@ -757,7 +791,7 @@ class RevDel_ArchivedFileItem extends RevDel_FileItem {
$this->file->getTimestamp(), $this->list->getUser() ) );
# Hidden files...
- if( !$this->canViewContent() ) {
+ if ( !$this->canViewContent() ) {
$link = $date;
} else {
$undelete = SpecialPage::getTitleFor( 'Undelete' );
@@ -770,7 +804,7 @@ class RevDel_ArchivedFileItem extends RevDel_FileItem {
)
);
}
- if( $this->isDeleted() ) {
+ if ( $this->isDeleted() ) {
$link = '<span class="history-deleted">' . $link . '</span>';
}
return $link;
@@ -789,6 +823,28 @@ class RevDel_LogList extends RevDel_List {
return 'log_id';
}
+ public static function getRestriction() {
+ return 'deletelogentry';
+ }
+
+ public static function getRevdelConstant() {
+ return LogPage::DELETED_ACTION;
+ }
+
+ public static function suggestTarget( $target, array $ids ) {
+ $result = wfGetDB( DB_SLAVE )->select( 'logging',
+ 'log_type',
+ array( 'log_id' => $ids ),
+ __METHOD__,
+ array( 'DISTINCT' )
+ );
+ if ( $result->numRows() == 1 ) {
+ // If there's only one type, the target can be set to include it.
+ return SpecialPage::getTitleFor( 'Log', $result->current()->log_type );
+ }
+ return SpecialPage::getTitleFor( 'Log' );
+ }
+
/**
* @param $db DatabaseBase
* @return mixed
@@ -899,7 +955,7 @@ class RevDel_LogItem extends RevDel_Item {
$action = $formatter->getActionText();
// Comment
$comment = $this->list->getLanguage()->getDirMark() . Linker::commentBlock( $this->row->log_comment );
- if( LogEventsList::isDeleted($this->row,LogPage::DELETED_COMMENT) ) {
+ if ( LogEventsList::isDeleted( $this->row, LogPage::DELETED_COMMENT ) ) {
$comment = '<span class="history-deleted">' . $comment . '</span>';
}
diff --git a/includes/revisiondelete/RevisionDeleteAbstracts.php b/includes/revisiondelete/RevisionDeleteAbstracts.php
index 4f58099f..803467e6 100644
--- a/includes/revisiondelete/RevisionDeleteAbstracts.php
+++ b/includes/revisiondelete/RevisionDeleteAbstracts.php
@@ -37,17 +37,49 @@ abstract class RevDel_List extends RevisionListBase {
* Get the DB field name associated with the ID list.
* This used to populate the log_search table for finding log entries.
* Override this function.
- * @return null
+ * @return string|null
*/
public static function getRelationType() {
return null;
}
/**
+ * Get the user right required for this list type
+ * Override this function.
+ * @since 1.22
+ * @return string|null
+ */
+ public static function getRestriction() {
+ return null;
+ }
+
+ /**
+ * Get the revision deletion constant for this list type
+ * Override this function.
+ * @since 1.22
+ * @return int|null
+ */
+ public static function getRevdelConstant() {
+ return null;
+ }
+
+ /**
+ * Suggest a target for the revision deletion
+ * Optionally override this function.
+ * @since 1.22
+ * @param Title|null $target User-supplied target
+ * @param array $ids
+ * @return Title|null
+ */
+ public static function suggestTarget( $target, array $ids ) {
+ return $target;
+ }
+
+ /**
* Set the visibility for the revisions in this list. Logging and
* transactions are done here.
*
- * @param $params array Associative array of parameters. Members are:
+ * @param array $params Associative array of parameters. Members are:
* value: The integer value to set the visibility to
* comment: The log comment.
* @return Status
@@ -68,11 +100,11 @@ abstract class RevDel_List extends RevisionListBase {
for ( $this->reset(); $this->current(); $this->next() ) {
$item = $this->current();
- unset( $missing[ $item->getId() ] );
+ unset( $missing[$item->getId()] );
$oldBits = $item->getBits();
// Build the actual new rev_deleted bitfield
- $newBits = SpecialRevisionDelete::extractBitfield( $bitPars, $oldBits );
+ $newBits = RevisionDeleter::extractBitfield( $bitPars, $oldBits );
if ( $oldBits == $newBits ) {
$status->warning( 'revdelete-no-change', $item->formatDate(), $item->formatTime() );
@@ -94,14 +126,14 @@ abstract class RevDel_List extends RevisionListBase {
}
if ( !$item->canView() ) {
// Cannot access this revision
- $msg = ($opType == 'show') ?
+ $msg = ( $opType == 'show' ) ?
'revdelete-show-no-access' : 'revdelete-modify-no-access';
$status->error( $msg, $item->formatDate(), $item->formatTime() );
$status->failCount++;
continue;
}
// Cannot just "hide from Sysops" without hiding any fields
- if( $newBits == Revision::DELETED_RESTRICTED ) {
+ if ( $newBits == Revision::DELETED_RESTRICTED ) {
$status->warning( 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
$status->failCount++;
continue;
@@ -113,9 +145,9 @@ abstract class RevDel_List extends RevisionListBase {
if ( $ok ) {
$idsForLog[] = $item->getId();
$status->successCount++;
- if( $item->getAuthorId() > 0 ) {
+ if ( $item->getAuthorId() > 0 ) {
$authorIds[] = $item->getAuthorId();
- } elseif( IP::isIPAddress( $item->getAuthorName() ) ) {
+ } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
$authorIPs[] = $item->getAuthorName();
}
} else {
@@ -176,7 +208,7 @@ abstract class RevDel_List extends RevisionListBase {
/**
* Record a log entry on the action
- * @param $params array Associative array of parameters:
+ * @param array $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
@@ -184,11 +216,12 @@ abstract class RevDel_List extends RevisionListBase {
* comment: The log comment
* authorsIds: The array of the user IDs of the offenders
* authorsIPs: The array of the IP/anon user offenders
+ * @throws MWException
*/
protected function updateLog( $params ) {
// Get the URL param's corresponding DB field
$field = RevisionDeleter::getRelationType( $this->getType() );
- if( !$field ) {
+ if ( !$field ) {
throw new MWException( "Bad log URL param type!" );
}
// Put things hidden from sysops in the oversight log
@@ -202,7 +235,7 @@ abstract class RevDel_List extends RevisionListBase {
// Actually add the deletion log entry
$log = new LogPage( $logType );
$logid = $log->addEntry( $this->getLogAction(), $params['title'],
- $params['comment'], $logParams );
+ $params['comment'], $logParams, $this->getUser() );
// 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 );
@@ -219,7 +252,7 @@ abstract class RevDel_List extends RevisionListBase {
/**
* Get log parameter array.
- * @param $params array Associative array of log parameters, same as updateLog()
+ * @param array $params Associative array of log parameters, same as updateLog()
* @return array
*/
public function getLogParams( $params ) {
diff --git a/includes/revisiondelete/RevisionDeleteUser.php b/includes/revisiondelete/RevisionDeleteUser.php
index c02e9c76..4505ee10 100644
--- a/includes/revisiondelete/RevisionDeleteUser.php
+++ b/includes/revisiondelete/RevisionDeleteUser.php
@@ -54,7 +54,7 @@ class RevisionDeleteUser {
# The same goes for the sysop-restricted *_deleted bit.
$delUser = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
$delAction = LogPage::DELETED_ACTION | Revision::DELETED_RESTRICTED;
- if( $op == '&' ) {
+ if ( $op == '&' ) {
$delUser = "~{$delUser}";
$delAction = "~{$delAction}";
}
diff --git a/includes/revisiondelete/RevisionDeleter.php b/includes/revisiondelete/RevisionDeleter.php
index c59edc2a..dbcb3d7d 100644
--- a/includes/revisiondelete/RevisionDeleter.php
+++ b/includes/revisiondelete/RevisionDeleter.php
@@ -22,25 +22,85 @@
*/
/**
- * Temporary b/c interface, collection of static functions.
- * @ingroup SpecialPage
+ * General controller for RevDel, used by both SpecialRevisiondelete and
+ * ApiRevisionDelete.
* @ingroup RevisionDelete
*/
class RevisionDeleter {
+ /** List of known revdel types, with their corresponding list classes */
+ private static $allowedTypes = array(
+ 'revision' => 'RevDel_RevisionList',
+ 'archive' => 'RevDel_ArchiveList',
+ 'oldimage' => 'RevDel_FileList',
+ 'filearchive' => 'RevDel_ArchivedFileList',
+ 'logging' => 'RevDel_LogList',
+ );
+
+ /** Type map to support old log entries */
+ private static $deprecatedTypeMap = array(
+ 'oldid' => 'revision',
+ 'artimestamp' => 'archive',
+ 'oldimage' => 'oldimage',
+ 'fileid' => 'filearchive',
+ 'logid' => 'logging',
+ );
+
+ /**
+ * Lists the valid possible types for revision deletion.
+ *
+ * @since 1.22
+ * @return array
+ */
+ public static function getTypes() {
+ return array_keys( self::$allowedTypes );
+ }
+
+ /**
+ * Gets the canonical type name, if any.
+ *
+ * @since 1.22
+ * @param string $typeName
+ * @return string|null
+ */
+ public static function getCanonicalTypeName( $typeName ) {
+ if ( isset( self::$deprecatedTypeMap[$typeName] ) ) {
+ $typeName = self::$deprecatedTypeMap[$typeName];
+ }
+ return isset( self::$allowedTypes[$typeName] ) ? $typeName : null;
+ }
+
+ /**
+ * Instantiate the appropriate list class for a given list of IDs.
+ *
+ * @since 1.22
+ * @param string $typeName RevDel type, see RevisionDeleter::getTypes()
+ * @param IContextSource $context
+ * @param Title $title
+ * @param array $ids
+ * @return RevDel_List
+ */
+ public static function createList( $typeName, IContextSource $context, Title $title, array $ids ) {
+ $typeName = self::getCanonicalTypeName( $typeName );
+ if ( !$typeName ) {
+ throw new MWException( __METHOD__ . ": Unknown RevDel type '$typeName'" );
+ }
+ return new self::$allowedTypes[$typeName]( $context, $title, $ids );
+ }
+
/**
* 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
+ * @param string $desc 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.
+ * @param array $arr the array to update.
*/
protected static function checkItem( $desc, $field, $diff, $new, &$arr ) {
- if( $diff & $field ) {
- $arr[ ( $new & $field ) ? 0 : 1 ][] = $desc;
+ if ( $diff & $field ) {
+ $arr[( $new & $field ) ? 0 : 1][] = $desc;
}
}
@@ -73,11 +133,12 @@ class RevisionDeleter {
self::checkItem( 'revdelete-uname',
Revision::DELETED_USER, $diff, $n, $ret );
// Restriction application to sysops
- if( $diff & Revision::DELETED_RESTRICTED ) {
- if( $n & Revision::DELETED_RESTRICTED )
+ if ( $diff & Revision::DELETED_RESTRICTED ) {
+ if ( $n & Revision::DELETED_RESTRICTED ) {
$ret[2][] = 'revdelete-restricted';
- else
+ } else {
$ret[2][] = 'revdelete-unrestricted';
+ }
}
return $ret;
}
@@ -85,20 +146,62 @@ class RevisionDeleter {
/** Get DB field name for URL param...
* Future code for other things may also track
* other types of revision-specific changes.
+ * @param string $typeName
* @return 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];
+ $typeName = self::getCanonicalTypeName( $typeName );
+ if ( !$typeName ) {
+ return null;
}
- if ( isset( SpecialRevisionDelete::$allowedTypes[$typeName] ) ) {
- $class = SpecialRevisionDelete::$allowedTypes[$typeName]['list-class'];
- return call_user_func( array( $class, 'getRelationType' ) );
- } else {
+ return call_user_func( array( self::$allowedTypes[$typeName], 'getRelationType' ) );
+ }
+
+ /**
+ * Get the user right required for the RevDel type
+ * @since 1.22
+ * @param string $typeName
+ * @return string User right
+ */
+ public static function getRestriction( $typeName ) {
+ $typeName = self::getCanonicalTypeName( $typeName );
+ if ( !$typeName ) {
+ return null;
+ }
+ return call_user_func( array( self::$allowedTypes[$typeName], 'getRestriction' ) );
+ }
+
+ /**
+ * Get the revision deletion constant for the RevDel type
+ * @since 1.22
+ * @param string $typeName
+ * @return int RevDel constant
+ */
+ public static function getRevdelConstant( $typeName ) {
+ $typeName = self::getCanonicalTypeName( $typeName );
+ if ( !$typeName ) {
return null;
}
+ return call_user_func( array( self::$allowedTypes[$typeName], 'getRevdelConstant' ) );
+ }
+
+ /**
+ * Suggest a target for the revision deletion
+ * @since 1.22
+ * @param string $typeName
+ * @param Title|null $title User-supplied target
+ * @param array $ids
+ * @return Title|null
+ */
+ public static function suggestTarget( $typeName, $target, array $ids ) {
+ $typeName = self::getCanonicalTypeName( $typeName );
+ if ( !$typeName ) {
+ return $target;
+ }
+ return call_user_func( array( self::$allowedTypes[$typeName], 'suggestTarget' ), $target, $ids );
}
+
/**
* Checks if a revision still exists in the revision table.
* If it doesn't, returns the corresponding ar_timestamp field
@@ -124,4 +227,24 @@ class RevisionDeleter {
return $timestamp;
}
+
+ /**
+ * Put together a rev_deleted bitfield
+ * @since 1.22
+ * @param array $bitPars extractBitParams() params
+ * @param int $oldfield current bitfield
+ * @return array
+ */
+ public static function extractBitfield( $bitPars, $oldfield ) {
+ // Build the actual new rev_deleted bitfield
+ $newBits = 0;
+ foreach ( $bitPars as $const => $val ) {
+ if ( $val == 1 ) {
+ $newBits |= $const; // $const is the *_deleted const
+ } elseif ( $val == -1 ) {
+ $newBits |= ( $oldfield & $const ); // use existing
+ }
+ }
+ return $newBits;
+ }
}
diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php
index 27a321ac..71c05d8b 100644
--- a/includes/search/SearchEngine.php
+++ b/includes/search/SearchEngine.php
@@ -45,7 +45,7 @@ class SearchEngine {
*/
protected $db;
- function __construct($db = null) {
+ function __construct( $db = null ) {
if ( $db ) {
$this->db = $db;
} else {
@@ -58,8 +58,8 @@ class SearchEngine {
* If title searches are not supported or disabled, return null.
* STUB
*
- * @param $term String: raw search term
- * @return SearchResultSet
+ * @param string $term raw search term
+ * @return SearchResultSet|Status|null
*/
function searchText( $term ) {
return null;
@@ -70,8 +70,8 @@ class SearchEngine {
* If title searches are not supported or disabled, return null.
* STUB
*
- * @param $term String: raw search term
- * @return SearchResultSet
+ * @param string $term raw search term
+ * @return SearchResultSet|null
*/
function searchTitle( $term ) {
return null;
@@ -93,8 +93,9 @@ class SearchEngine {
* @return Boolean
*/
public function supports( $feature ) {
- switch( $feature ) {
+ switch ( $feature ) {
case 'list-redirects':
+ case 'search-update':
return true;
case 'title-suffix-filter':
default:
@@ -118,7 +119,7 @@ class SearchEngine {
* on text to be used for searching or updating search index.
* Default implementation does nothing (simply returns $string).
*
- * @param $string string: String to process
+ * @param string $string String to process
* @return string
*/
public function normalizeText( $string ) {
@@ -163,7 +164,7 @@ class SearchEngine {
/**
* Really find the title match.
- * @return null|\Title
+ * @return null|Title
*/
private static function getNearMatchInternal( $searchterm ) {
global $wgContLang, $wgEnableSearchContributorsByIP;
@@ -183,10 +184,15 @@ class SearchEngine {
# Exact match? No need to look further.
$title = Title::newFromText( $term );
- if ( is_null( $title ) ){
+ if ( is_null( $title ) ) {
return null;
}
+ # Try files if searching in the Media: namespace
+ if ( $title->getNamespace() == NS_MEDIA ) {
+ $title = Title::makeTitle( NS_FILE, $title->getText() );
+ }
+
if ( $title->isSpecialPage() || $title->isExternal() || $title->exists() ) {
return $title;
}
@@ -197,22 +203,23 @@ class SearchEngine {
return $title;
}
+ if ( !wfRunHooks( 'SearchAfterNoDirectMatch', array( $term, &$title ) ) ) {
+ 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;
@@ -233,7 +240,6 @@ class SearchEngine {
$title = Title::newFromText( $searchterm );
-
# Entering an IP address goes to the contributions page
if ( $wgEnableSearchContributorsByIP ) {
if ( ( $title->getNamespace() == NS_USER && User::isIP( $title->getText() ) )
@@ -242,7 +248,6 @@ class SearchEngine {
}
}
-
# Entering a user goes to the user page whether it's there or not
if ( $title->getNamespace() == NS_USER ) {
return $title;
@@ -327,8 +332,9 @@ class SearchEngine {
$parsed = substr( $query, strlen( $prefix ) + 1 );
}
}
- if ( trim( $parsed ) == '' )
+ if ( trim( $parsed ) == '' ) {
$parsed = $query; // prefix was the whole query
+ }
wfRunHooks( 'SearchEngineReplacePrefixesComplete', array( $this, $query, &$parsed ) );
@@ -416,8 +422,9 @@ class SearchEngine {
$formatted = array_map( array( $wgContLang, 'getFormattedNsText' ), $namespaces );
foreach ( $formatted as $key => $ns ) {
- if ( empty( $ns ) )
+ if ( empty( $ns ) ) {
$formatted[$key] = wfMessage( 'blanknamespace' )->text();
+ }
}
return $formatted;
}
@@ -447,23 +454,46 @@ class SearchEngine {
* Load up the appropriate search engine class for the currently
* active database backend, and return a configured instance.
*
+ * @param String $type Type of search backend, if not the default
* @return SearchEngine
*/
- public static function create() {
+ public static function create( $type = null ) {
global $wgSearchType;
$dbr = null;
- if ( $wgSearchType ) {
+
+ $alternatives = self::getSearchTypes();
+
+ if ( $type && in_array( $type, $alternatives ) ) {
+ $class = $type;
+ } elseif ( $wgSearchType !== null ) {
$class = $wgSearchType;
} else {
$dbr = wfGetDB( DB_SLAVE );
$class = $dbr->getSearchEngine();
}
+
$search = new $class( $dbr );
$search->setLimitOffset( 0, 0 );
return $search;
}
/**
+ * Return the search engines we support. If only $wgSearchType
+ * is set, it'll be an array of just that one item.
+ *
+ * @return array
+ */
+ public static function getSearchTypes() {
+ global $wgSearchType, $wgSearchTypeAlternatives;
+ static $alternatives = null;
+ if ( $alternatives === null ) {
+ $alternatives = $wgSearchTypeAlternatives ?: array();
+ array_unshift( $alternatives, $wgSearchType );
+ }
+ return $alternatives;
+ }
+
+ /**
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
* STUB
@@ -489,6 +519,18 @@ class SearchEngine {
}
/**
+ * Delete an indexed page
+ * Title should be pre-processed.
+ * STUB
+ *
+ * @param Integer $id Page id that was deleted
+ * @param String $title Title of page that was deleted
+ */
+ function delete( $id, $title ) {
+ // no-op
+ }
+
+ /**
* Get OpenSearch suggestion template
*
* @return String
@@ -505,6 +547,31 @@ class SearchEngine {
return $wgCanonicalServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
}
}
+
+ /**
+ * Get the raw text for updating the index from a content object
+ * Nicer search backends could possibly do something cooler than
+ * just returning raw text
+ *
+ * @todo This isn't ideal, we'd really like to have content-specific handling here
+ * @param Title $t Title we're indexing
+ * @param Content $c Content of the page to index
+ * @return string
+ */
+ public function getTextFromContent( Title $t, Content $c = null ) {
+ return $c ? $c->getTextForSearchIndex() : '';
+ }
+
+ /**
+ * If an implementation of SearchEngine handles all of its own text processing
+ * in getTextFromContent() and doesn't require SearchUpdate::updateText()'s
+ * rather silly handling, it should return true here instead.
+ *
+ * @return bool
+ */
+ public function textAlreadyUpdatedForIndex() {
+ return false;
+ }
}
/**
@@ -637,26 +704,30 @@ class SqlSearchResultSet extends SearchResultSet {
}
function numRows() {
- if ( $this->mResultSet === false )
+ if ( $this->mResultSet === false ) {
return false;
+ }
return $this->mResultSet->numRows();
}
function next() {
- if ( $this->mResultSet === false )
+ if ( $this->mResultSet === false ) {
return false;
+ }
$row = $this->mResultSet->fetchObject();
- if ( $row === false )
+ if ( $row === false ) {
return false;
+ }
return SearchResult::newFromRow( $row );
}
function free() {
- if ( $this->mResultSet === false )
+ if ( $this->mResultSet === false ) {
return false;
+ }
$this->mResultSet->free();
}
@@ -669,7 +740,6 @@ 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
@@ -747,8 +817,9 @@ class SearchResult {
wfRunHooks( 'SearchResultInitFromTitle', array( $title, &$id ) );
$this->mRevision = Revision::newFromTitle(
$this->mTitle, $id, Revision::READ_NORMAL );
- if ( $this->mTitle->getNamespace() === NS_FILE )
+ if ( $this->mTitle->getNamespace() === NS_FILE ) {
$this->mImage = wfFindFile( $this->mTitle );
+ }
}
}
@@ -758,8 +829,9 @@ class SearchResult {
* @return Boolean
*/
function isBrokenTitle() {
- if ( is_null( $this->mTitle ) )
+ if ( is_null( $this->mTitle ) ) {
return true;
+ }
return false;
}
@@ -791,31 +863,35 @@ class SearchResult {
*/
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?
+ if ( $this->mRevision != null ) {
+ $this->mText = SearchEngine::create()
+ ->getTextFromContent( $this->mTitle, $this->mRevision->getContent() );
+ } else { // TODO: can we fetch raw wikitext for commons images?
$this->mText = '';
-
+ }
}
}
/**
- * @param $terms Array: terms to highlight
+ * @param array $terms terms to highlight
* @return String: highlighted text snippet, null (and not '') if not supported
*/
function getTextSnippet( $terms ) {
- global $wgUser, $wgAdvancedSearchHighlighting;
+ global $wgAdvancedSearchHighlighting;
$this->initText();
- list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser );
+
+ // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
+ list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs();
$h = new SearchHighlighter();
- if ( $wgAdvancedSearchHighlighting )
+ if ( $wgAdvancedSearchHighlighting ) {
return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars );
- else
+ } else {
return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars );
+ }
}
/**
- * @param $terms Array: terms to highlight
+ * @param array $terms terms to highlight
* @return String: highlighted title, '' if not supported
*/
function getTitleSnippet( $terms ) {
@@ -823,7 +899,7 @@ class SearchResult {
}
/**
- * @param $terms Array: terms to highlight
+ * @param array $terms terms to highlight
* @return String: highlighted redirect name (redirect to this page), '' if none or not supported
*/
function getRedirectSnippet( $terms ) {
@@ -855,10 +931,11 @@ class SearchResult {
* @return String: timestamp
*/
function getTimestamp() {
- if ( $this->mRevision )
+ if ( $this->mRevision ) {
return $this->mRevision->getTimestamp();
- elseif ( $this->mImage )
+ } elseif ( $this->mImage ) {
return $this->mImage->getTimestamp();
+ }
return '';
}
@@ -934,7 +1011,7 @@ class SearchHighlighter {
* Default implementation of wikitext highlighting
*
* @param $text String
- * @param $terms Array: terms to highlight (unescaped)
+ * @param array $terms terms to highlight (unescaped)
* @param $contextlines Integer
* @param $contextchars Integer
* @return String
@@ -944,8 +1021,9 @@ class SearchHighlighter {
global $wgSearchHighlightBoundaries;
$fname = __METHOD__;
- if ( $text == '' )
+ if ( $text == '' ) {
return '';
+ }
// spli text into text + templates/links/tables
$spat = "/(\\{\\{)|(\\[\\[[^\\]:]+:)|(\n\\{\\|)";
@@ -962,7 +1040,7 @@ class SearchHighlighter {
}
$spat .= '/';
$textExt = array(); // text extracts
- $otherExt = array(); // other extracts
+ $otherExt = array(); // other extracts
wfProfileIn( "$fname-split" );
$start = 0;
$textLen = strlen( $text );
@@ -976,8 +1054,9 @@ class SearchHighlighter {
if ( $key == 2 ) {
// see if this is an image link
$ns = substr( $val[0], 2, - 1 );
- if ( $wgContLang->getNsIndex( $ns ) != NS_FILE )
+ if ( $wgContLang->getNsIndex( $ns ) != NS_FILE ) {
break;
+ }
}
$epat = $endPatterns[$key];
@@ -998,7 +1077,7 @@ class SearchHighlighter {
$len = strlen( $endMatches[2][0] );
$off = $endMatches[2][1];
$this->splitAndAdd( $otherExt, $count,
- substr( $text, $start, $off + $len - $start ) );
+ substr( $text, $start, $off + $len - $start ) );
$start = $off + $len;
$found = true;
break;
@@ -1111,7 +1190,7 @@ class SearchHighlighter {
// 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] );
+ $snippets = array( $first => $snippets[$first] );
}
// calc by how much to extend existing snippets
@@ -1131,8 +1210,8 @@ class SearchHighlighter {
// add more lines
$add = $index + 1;
while ( $len < $targetchars - 20
- && array_key_exists( $add, $all )
- && !array_key_exists( $add, $snippets ) ) {
+ && 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;
@@ -1142,22 +1221,24 @@ class SearchHighlighter {
}
}
- // $snippets = array_map('htmlspecialchars', $extended);
+ // $snippets = array_map( 'htmlspecialchars', $extended );
$snippets = $extended;
$last = - 1;
$extract = '';
foreach ( $snippets as $index => $line ) {
- if ( $last == - 1 )
+ if ( $last == - 1 ) {
$extract .= $line; // first line
- elseif ( $last + 1 == $index && $offsets[$last] + strlen( $snippets[$last] ) >= strlen( $all[$last] ) )
+ } elseif ( $last + 1 == $index && $offsets[$last] + strlen( $snippets[$last] ) >= strlen( $all[$last] ) ) {
$extract .= " " . $line; // continous lines
- else
+ } else {
$extract .= '<b> ... </b>' . $line;
+ }
$last = $index;
}
- if ( $extract )
+ if ( $extract ) {
$extract .= '<b> ... </b>';
+ }
$processed = array();
foreach ( $terms as $term ) {
@@ -1177,7 +1258,7 @@ class SearchHighlighter {
/**
* Split text into lines and add it to extracts array
*
- * @param $extracts Array: index -> $line
+ * @param array $extracts index -> $line
* @param $count Integer
* @param $text String
*/
@@ -1185,8 +1266,9 @@ class SearchHighlighter {
$split = explode( "\n", $this->mCleanWikitext ? $this->removeWiki( $text ) : $text );
foreach ( $split as $line ) {
$tt = trim( $line );
- if ( $tt )
+ if ( $tt ) {
$extracts[$count++] = $tt;
+ }
}
}
@@ -1232,7 +1314,7 @@ class SearchHighlighter {
$posEnd = $end;
}
- if ( $end > $start ) {
+ if ( $end > $start ) {
return substr( $text, $start, $end - $start );
} else {
return '';
@@ -1260,8 +1342,9 @@ class SearchHighlighter {
while ( $char >= 0x80 && $char < 0xc0 ) {
// skip trailing bytes
$point++;
- if ( $point >= strlen( $text ) )
+ if ( $point >= strlen( $text ) ) {
return strlen( $text );
+ }
$char = ord( $text[$point] );
}
return $point;
@@ -1272,33 +1355,37 @@ class SearchHighlighter {
/**
* Search extracts for a pattern, and return snippets
*
- * @param $pattern String: regexp for matching lines
- * @param $extracts Array: extracts to search
+ * @param string $pattern regexp for matching lines
+ * @param array $extracts 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
+ * @param array $out map for highlighted snippets
+ * @param array $offsets map of starting points of snippets
* @protected
*/
function process( $pattern, $extracts, &$linesleft, &$contextchars, &$out, &$offsets ) {
- if ( $linesleft == 0 )
+ if ( $linesleft == 0 ) {
return; // nothing to do
+ }
foreach ( $extracts as $index => $line ) {
- if ( array_key_exists( $index, $out ) )
+ if ( array_key_exists( $index, $out ) ) {
continue; // this line already highlighted
+ }
$m = array();
- if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) )
+ if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) ) {
continue;
+ }
$offset = $m[0][1];
$len = strlen( $m[0][0] );
- if ( $offset + $len < $contextchars )
+ if ( $offset + $len < $contextchars ) {
$begin = 0;
- elseif ( $len > $contextchars )
+ } elseif ( $len > $contextchars ) {
$begin = $offset;
- else
+ } else {
$begin = $offset + intval( ( $len - $contextchars ) / 2 );
+ }
$end = $begin + $contextchars;
@@ -1307,8 +1394,9 @@ class SearchHighlighter {
$out[$index] = $this->extract( $line, $begin, $end, $posBegin );
$offsets[$index] = $posBegin;
$linesleft--;
- if ( $linesleft == 0 )
+ if ( $linesleft == 0 ) {
return;
+ }
}
}
@@ -1321,12 +1409,12 @@ class SearchHighlighter {
$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( "/'{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 );
@@ -1349,16 +1437,17 @@ class SearchHighlighter {
*/
function linkReplace( $matches ) {
$colon = strpos( $matches[1], ':' );
- if ( $colon === false )
+ 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 ) )
+ if ( $index !== false && ( $index == NS_FILE || $index == NS_CATEGORY ) ) {
return $matches[0]; // return the whole thing
- else
+ } else {
return $matches[2];
-
+ }
}
/**
@@ -1408,8 +1497,7 @@ class SearchHighlighter {
$line = htmlspecialchars( $pre . $found . $post );
$pat2 = '/(' . $terms . ")/i";
- $line = preg_replace( $pat2,
- "<span class='searchmatch'>\\1</span>", $line );
+ $line = preg_replace( $pat2, "<span class='searchmatch'>\\1</span>", $line );
$extract .= "${line}\n";
}
diff --git a/includes/search/SearchIBM_DB2.php b/includes/search/SearchIBM_DB2.php
deleted file mode 100644
index 51ed000f..00000000
--- a/includes/search/SearchIBM_DB2.php
+++ /dev/null
@@ -1,234 +0,0 @@
-<?php
-/**
- * IBM DB2 search engine
- *
- * Copyright © 2004 Brion Vibber <brion@pobox.com>
- * http://www.mediawiki.org/
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Search
- */
-
-/**
- * Search engine hook base class for IBM DB2
- * @ingroup Search
- */
-class SearchIBM_DB2 extends SearchEngine {
-
- /**
- * Creates an instance of this class
- * @param $db DatabaseIbm_db2: database object
- */
- function __construct($db) {
- parent::__construct( $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 $filteredTerm String
- * @param $fulltext Boolean
- * @return String
- */
- 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;
- }
-
- /** @todo document
- * @return string
- */
- function parseQuery($filteredText, $fulltext) {
- global $wgContLang;
- $lc = SearchEngine::legalSearchChars();
- $this->searchTerms = array();
-
- # @todo 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/SearchMssql.php b/includes/search/SearchMssql.php
index 69c92ba3..cbc1a7a7 100644
--- a/includes/search/SearchMssql.php
+++ b/includes/search/SearchMssql.php
@@ -38,7 +38,7 @@ class SearchMssql extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return MssqlSearchResultSet
* @access public
*/
@@ -50,7 +50,7 @@ class SearchMssql extends SearchEngine {
/**
* Perform a title-only search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return MssqlSearchResultSet
* @access public
*/
@@ -59,7 +59,6 @@ class SearchMssql extends SearchEngine {
return new MssqlSearchResultSet( $resultSet, $this->searchTerms );
}
-
/**
* Return a partial WHERE clause to exclude redirects, if so set
*
@@ -78,7 +77,7 @@ class SearchMssql extends SearchEngine {
* Return a partial WHERE clause to limit the search to the given namespaces
*
* @return String
- * @private
+ * @private
*/
function queryNamespaces() {
$namespaces = implode( ',', $this->namespaces );
@@ -144,9 +143,9 @@ class SearchMssql extends SearchEngine {
*/
function queryMain( $filteredTerm, $fulltext ) {
$match = $this->parseQuery( $filteredTerm, $fulltext );
- $page = $this->db->tableName( 'page' );
+ $page = $this->db->tableName( 'page' );
$searchindex = $this->db->tableName( 'searchindex' );
-
+
return 'SELECT page_id, page_namespace, page_title, ftindex.[RANK]' .
"FROM $page,FREETEXTTABLE($searchindex , $match, LANGUAGE 'English') as ftindex " .
'WHERE page_id=ftindex.[KEY] ';
@@ -171,8 +170,9 @@ class SearchMssql extends SearchEngine {
if ( !empty( $terms[3] ) ) {
$regexp = preg_quote( $terms[3], '/' );
- if ( $terms[4] )
+ if ( $terms[4] ) {
$regexp .= "[0-9A-Za-z_]+";
+ }
} else {
$regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
}
@@ -192,11 +192,11 @@ class SearchMssql extends SearchEngine {
* @param $id Integer
* @param $title String
* @param $text String
- * @return bool|\ResultWrapper
+ * @return bool|ResultWrapper
*/
function update( $id, $title, $text ) {
// We store the column data as UTF-8 byte order marked binary stream
- // because we are invoking the plain text IFilter on it so that, and we want it
+ // because we are invoking the plain text IFilter on it so that, and we want it
// to properly decode the stream as UTF-8. SQL doesn't support UTF8 as a data type
// but the indexer will correctly handle it by this method. Since all we are doing
// is passing this data to the indexer and never retrieving it via PHP, this will save space
@@ -215,7 +215,7 @@ class SearchMssql extends SearchEngine {
*
* @param $id Integer
* @param $title String
- * @return bool|\ResultWrapper
+ * @return bool|ResultWrapper
*/
function updateTitle( $id, $title ) {
$table = $this->db->tableName( 'searchindex' );
@@ -248,10 +248,9 @@ class MssqlSearchResultSet extends SearchResultSet {
function next() {
$row = $this->mResultSet->fetchObject();
- if ( $row === false )
+ if ( $row === false ) {
return false;
+ }
return new SearchResult( $row );
}
}
-
-
diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php
index 5cee03e0..b2bc1c26 100644
--- a/includes/search/SearchMySQL.php
+++ b/includes/search/SearchMySQL.php
@@ -57,12 +57,12 @@ class SearchMySQL extends SearchEngine {
# @todo FIXME: This doesn't handle parenthetical expressions.
$m = array();
- if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
- $filteredText, $m, PREG_SET_ORDER ) ) {
- foreach( $m as $bits ) {
+ if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach ( $m as $bits ) {
@list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
- if( $nonQuoted != '' ) {
+ if ( $nonQuoted != '' ) {
$term = $nonQuoted;
$quote = '';
} else {
@@ -70,8 +70,10 @@ class SearchMySQL extends SearchEngine {
$quote = '"';
}
- if( $searchon !== '' ) $searchon .= ' ';
- if( $this->strictMatching && ($modifier == '') ) {
+ if ( $searchon !== '' ) {
+ $searchon .= ' ';
+ }
+ if ( $this->strictMatching && ( $modifier == '' ) ) {
// If we leave this out, boolean op defaults to OR which is rarely helpful.
$modifier = '+';
}
@@ -79,7 +81,7 @@ class SearchMySQL extends SearchEngine {
// 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 ) ) {
+ if ( is_array( $convertedVariants ) ) {
$variants = array_unique( array_values( $convertedVariants ) );
} else {
$variants = array( $term );
@@ -99,11 +101,12 @@ class SearchMySQL extends SearchEngine {
$strippedVariants = array_unique( $strippedVariants );
$searchon .= $modifier;
- if( count( $strippedVariants) > 1 )
+ if ( count( $strippedVariants ) > 1 ) {
$searchon .= '(';
- foreach( $strippedVariants as $stripped ) {
+ }
+ foreach ( $strippedVariants as $stripped ) {
$stripped = $this->normalizeText( $stripped );
- if( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
+ 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.
@@ -111,8 +114,9 @@ class SearchMySQL extends SearchEngine {
}
$searchon .= "$quote$stripped$quote$wildcard ";
}
- if( count( $strippedVariants) > 1 )
+ 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!
@@ -134,8 +138,8 @@ class SearchMySQL extends SearchEngine {
global $wgContLang;
$regex = preg_quote( $string, '/' );
- if( $wgContLang->hasWordBreaks() ) {
- if( $wildcard ) {
+ if ( $wgContLang->hasWordBreaks() ) {
+ if ( $wildcard ) {
// Don't cut off the final bit!
$regex = "\b$regex";
} else {
@@ -156,7 +160,7 @@ class SearchMySQL extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return MySQLSearchResultSet
*/
function searchText( $term ) {
@@ -166,7 +170,7 @@ class SearchMySQL extends SearchEngine {
/**
* Perform a title-only search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return MySQLSearchResultSet
*/
function searchTitle( $term ) {
@@ -177,7 +181,9 @@ class SearchMySQL extends SearchEngine {
global $wgCountTotalSearchHits;
// This seems out of place, why is this called with empty term?
- if ( trim( $term ) === '' ) return null;
+ if ( trim( $term ) === '' ) {
+ return null;
+ }
$filteredTerm = $this->filter( $term );
$query = $this->getQuery( $filteredTerm, $fulltext );
@@ -187,7 +193,7 @@ class SearchMySQL extends SearchEngine {
);
$total = null;
- if( $wgCountTotalSearchHits ) {
+ if ( $wgCountTotalSearchHits ) {
$query = $this->getCountQuery( $filteredTerm, $fulltext );
$totalResult = $this->db->select(
$query['tables'], $query['fields'], $query['conds'],
@@ -195,7 +201,7 @@ class SearchMySQL extends SearchEngine {
);
$row = $totalResult->fetchObject();
- if( $row ) {
+ if ( $row ) {
$total = intval( $row->c );
}
$totalResult->free();
@@ -205,12 +211,11 @@ class SearchMySQL extends SearchEngine {
}
public function supports( $feature ) {
- switch( $feature ) {
- case 'list-redirects':
+ switch ( $feature ) {
case 'title-suffix-filter':
return true;
default:
- return false;
+ return parent::supports( $feature );
}
}
@@ -221,9 +226,9 @@ class SearchMySQL extends SearchEngine {
*/
protected function queryFeatures( &$query ) {
foreach ( $this->features as $feature => $value ) {
- if ( $feature === 'list-redirects' && !$value ) {
+ if ( $feature === 'list-redirects' && !$value ) {
$query['conds']['page_is_redirect'] = 0;
- } elseif( $feature === 'title-suffix-filter' && $value ) {
+ } elseif ( $feature === 'title-suffix-filter' && $value ) {
$query['conds'][] = 'page_title' . $this->db->buildLike( $this->db->anyString(), $value );
}
}
@@ -358,12 +363,25 @@ class SearchMySQL extends SearchEngine {
$dbw->update( 'searchindex',
array( 'si_title' => $this->normalizeText( $title ) ),
- array( 'si_page' => $id ),
+ array( 'si_page' => $id ),
__METHOD__,
array( $dbw->lowPriorityOption() ) );
}
/**
+ * Delete an indexed page
+ * Title should be pre-processed.
+ *
+ * @param Integer $id Page id that was deleted
+ * @param String $title Title of page that was deleted
+ */
+ function delete( $id, $title ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->delete( 'searchindex', array( 'si_page' => $id ), __METHOD__ );
+ }
+
+ /**
* Converts some characters for MySQL's indexing to grok it correctly,
* and pads short words to overcome limitations.
* @return mixed|string
@@ -386,7 +404,7 @@ class SearchMySQL extends SearchEngine {
// ignores short words... Pad them so we can pass them
// through without reconfiguring the server...
$minLength = $this->minSearchLength();
- if( $minLength > 1 ) {
+ if ( $minLength > 1 ) {
$n = $minLength - 1;
$out = preg_replace(
"/\b(\w{1,$n})\b/",
@@ -427,7 +445,7 @@ class SearchMySQL extends SearchEngine {
* @return int
*/
protected function minSearchLength() {
- if( is_null( self::$mMinSearchLength ) ) {
+ if ( is_null( self::$mMinSearchLength ) ) {
$sql = "SHOW GLOBAL VARIABLES LIKE 'ft\\_min\\_word\\_len'";
$dbr = wfGetDB( DB_SLAVE );
@@ -435,7 +453,7 @@ class SearchMySQL extends SearchEngine {
$row = $result->fetchObject();
$result->free();
- if( $row && $row->Variable_name == 'ft_min_word_len' ) {
+ if ( $row && $row->Variable_name == 'ft_min_word_len' ) {
self::$mMinSearchLength = intval( $row->Value );
} else {
self::$mMinSearchLength = 0;
@@ -449,7 +467,7 @@ class SearchMySQL extends SearchEngine {
* @ingroup Search
*/
class MySQLSearchResultSet extends SqlSearchResultSet {
- function __construct( $resultSet, $terms, $totalHits=null ) {
+ function __construct( $resultSet, $terms, $totalHits = null ) {
parent::__construct( $resultSet, $terms );
$this->mTotalHits = $totalHits;
}
diff --git a/includes/search/SearchOracle.php b/includes/search/SearchOracle.php
index a2db52f3..a8479654 100644
--- a/includes/search/SearchOracle.php
+++ b/includes/search/SearchOracle.php
@@ -29,77 +29,80 @@
* @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);
+
+ 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,
+ );
/**
* Creates an instance of this class
* @param $db DatabasePostgres: database object
*/
- function __construct($db) {
+ function __construct( $db ) {
parent::__construct( $db );
}
/**
* Perform a full text search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return SqlSearchResultSet
*/
function searchText( $term ) {
- if ($term == '')
- return new SqlSearchResultSet(false, '');
+ 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);
+ $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
+ * @param string $term raw search term
* @return SqlSearchResultSet
*/
- function searchTitle($term) {
- if ($term == '')
- return new SqlSearchResultSet(false, '');
+ 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);
+ $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) {
+ if ( $this->showRedirects ) {
return '';
} else {
return 'AND page_is_redirect=0';
@@ -111,8 +114,9 @@ class SearchOracle extends SearchEngine {
* @return String
*/
function queryNamespaces() {
- if( is_null($this->namespaces) )
+ if ( is_null( $this->namespaces ) ) {
return '';
+ }
if ( !count( $this->namespaces ) ) {
$namespaces = '0';
} else {
@@ -129,7 +133,7 @@ class SearchOracle extends SearchEngine {
* @return String
*/
function queryLimit( $sql ) {
- return $this->db->limitResult($sql, $this->limit, $this->offset);
+ return $this->db->limitResult( $sql, $this->limit, $this->offset );
}
/**
@@ -150,19 +154,18 @@ class SearchOracle extends SearchEngine {
* @return String
*/
function getQuery( $filteredTerm, $fulltext ) {
- return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' .
+ return $this->queryLimit( $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
$this->queryRedirect() . ' ' .
$this->queryNamespaces() . ' ' .
- $this->queryRanking( $filteredTerm, $fulltext ) . ' ');
+ $this->queryRanking( $filteredTerm, $fulltext ) . ' ' );
}
-
/**
* Picks which field to index on, depending on what type of query.
* @param $fulltext Boolean
* @return String
*/
- function getIndexField($fulltext) {
+ function getIndexField( $fulltext ) {
return $fulltext ? 'si_text' : 'si_title';
}
@@ -174,9 +177,9 @@ class SearchOracle extends SearchEngine {
* @return String
*/
function queryMain( $filteredTerm, $fulltext ) {
- $match = $this->parseQuery($filteredTerm, $fulltext);
- $page = $this->db->tableName('page');
- $searchindex = $this->db->tableName('searchindex');
+ $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;
@@ -187,7 +190,7 @@ class SearchOracle extends SearchEngine {
* as part of a WHERE clause
* @return string
*/
- function parseQuery($filteredText, $fulltext) {
+ function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
$lc = SearchEngine::legalSearchChars();
$this->searchTerms = array();
@@ -195,44 +198,44 @@ class SearchOracle extends SearchEngine {
# @todo FIXME: This doesn't handle parenthetical expressions.
$m = array();
$searchon = '';
- if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
- $filteredText, $m, PREG_SET_ORDER)) {
- foreach($m as $terms) {
+ 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 );
+ 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] );
+ $searchon .= ( $terms[1] == '-' ? ' ~' : ' & ' ) . $this->escapeTerm( $terms[2] );
}
- if (!empty($terms[3])) {
+ if ( !empty( $terms[3] ) ) {
$regexp = preg_quote( $terms[3], '/' );
- if ($terms[4])
+ if ( $terms[4] ) {
$regexp .= "[0-9A-Za-z_]+";
+ }
} else {
- $regexp = preg_quote(str_replace('"', '', $terms[2]), '/');
+ $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
}
$this->searchTerms[] = $regexp;
}
}
-
- $searchon = $this->db->addQuotes(ltrim($searchon, ' &'));
- $field = $this->getIndexField($fulltext);
+ $searchon = $this->db->addQuotes( ltrim( $searchon, ' &' ) );
+ $field = $this->getIndexField( $fulltext );
return " CONTAINS($field, $searchon, 1) > 0 ";
}
- private function escapeTerm($t) {
+ 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);
+ $t = $wgContLang->normalizeForSearch( $t );
+ $t = isset( $this->reservedWords[strtoupper( $t )] ) ? '{' . $t . '}' : $t;
+ $t = preg_replace( '/^"(.*)"$/', '($1)', $t );
+ $t = preg_replace( '/([-&|])/', '\\\\$1', $t );
return $t;
}
/**
@@ -243,10 +246,10 @@ class SearchOracle extends SearchEngine {
* @param $title String
* @param $text String
*/
- function update($id, $title, $text) {
- $dbw = wfGetDB(DB_MASTER);
- $dbw->replace('searchindex',
- array('si_page'),
+ function update( $id, $title, $text ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'searchindex',
+ array( 'si_page' ),
array(
'si_page' => $id,
'si_title' => $title,
@@ -254,13 +257,13 @@ class SearchOracle extends SearchEngine {
), 'SearchOracle::update' );
// Sync the index
- // We need to specify the DB name (i.e. user/schema) here so that
+ // We need to specify the DB name (i.e. user/schema) here so that
// it can work from the installer, where
// ALTER SESSION SET CURRENT_SCHEMA = ...
// was used.
- $dbw->query( "CALL ctx_ddl.sync_index(" .
+ $dbw->query( "CALL ctx_ddl.sync_index(" .
$dbw->addQuotes( $dbw->getDBname() . '.' . $dbw->tableName( 'si_text_idx', 'raw' ) ) . ")" );
- $dbw->query( "CALL ctx_ddl.sync_index(" .
+ $dbw->query( "CALL ctx_ddl.sync_index(" .
$dbw->addQuotes( $dbw->getDBname() . '.' . $dbw->tableName( 'si_title_idx', 'raw' ) ) . ")" );
}
@@ -271,17 +274,16 @@ class SearchOracle extends SearchEngine {
* @param $id Integer
* @param $title String
*/
- function updateTitle($id, $title) {
- $dbw = wfGetDB(DB_MASTER);
+ function updateTitle( $id, $title ) {
+ $dbw = wfGetDB( DB_MASTER );
- $dbw->update('searchindex',
- array('si_title' => $title),
- array('si_page' => $id),
+ $dbw->update( 'searchindex',
+ array( 'si_title' => $title ),
+ array( 'si_page' => $id ),
'SearchOracle::updateTitle',
- array());
+ array() );
}
-
public static function legalSearchChars() {
return "\"" . parent::legalSearchChars();
}
diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php
index 68648894..7f19ed13 100644
--- a/includes/search/SearchPostgres.php
+++ b/includes/search/SearchPostgres.php
@@ -47,15 +47,15 @@ class SearchPostgres extends SearchEngine {
* Currently searches a page's current title (page.page_title) and
* latest revision article text (pagecontent.old_text)
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return PostgresSearchResultSet
*/
function searchTitle( $term ) {
- $q = $this->searchQuery( $term , 'titlevector', 'page_title' );
- $olderror = error_reporting(E_ERROR);
+ $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) {
+ error_reporting( $olderror );
+ if ( !$resultSet ) {
// Needed for "Query requires full scan, GIN doesn't support it"
return new SearchResultTooMany();
}
@@ -64,10 +64,10 @@ class SearchPostgres extends SearchEngine {
function searchText( $term ) {
$q = $this->searchQuery( $term, 'textvector', 'old_text' );
- $olderror = error_reporting(E_ERROR);
+ $olderror = error_reporting( E_ERROR );
$resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) );
- error_reporting($olderror);
- if (!$resultSet) {
+ error_reporting( $olderror );
+ if ( !$resultSet ) {
return new SearchResultTooMany();
}
return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
@@ -86,29 +86,29 @@ class SearchPostgres extends SearchEngine {
wfDebug( "parseQuery received: $term \n" );
## No backslashes allowed
- $term = preg_replace('/\\\/', '', $term);
+ $term = preg_replace( '/\\\/', '', $term );
## Collapse parens into nearby words:
- $term = preg_replace('/\s*\(\s*/', ' (', $term);
- $term = preg_replace('/\s*\)\s*/', ') ', $term);
+ $term = preg_replace( '/\s*\(\s*/', ' (', $term );
+ $term = preg_replace( '/\s*\)\s*/', ') ', $term );
## Treat colons as word separators:
- $term = preg_replace('/:/', ' ', $term);
+ $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])) {
+ 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') {
+ if ( strtolower( $terms[2] ) === 'and' ) {
$searchstring .= ' & ';
}
- elseif (strtolower($terms[2]) === 'or' or $terms[2] === '|') {
+ elseif ( strtolower( $terms[2] ) === 'or' or $terms[2] === '|' ) {
$searchstring .= ' | ';
}
- elseif (strtolower($terms[2]) === 'not') {
+ elseif ( strtolower( $terms[2] ) === 'not' ) {
$searchstring .= ' & !';
}
else {
@@ -118,22 +118,22 @@ class SearchPostgres extends SearchEngine {
}
## Strip out leading junk
- $searchstring = preg_replace('/^[\s\&\|]+/', '', $searchstring);
+ $searchstring = preg_replace( '/^[\s\&\|]+/', '', $searchstring );
## Remove any doubled-up operators
- $searchstring = preg_replace('/([\!\&\|]) +(?:[\&\|] +)+/', "$1 ", $searchstring);
+ $searchstring = preg_replace( '/([\!\&\|]) +(?:[\&\|] +)+/', "$1 ", $searchstring );
## Remove any non-spaced operators (e.g. "Zounds!")
- $searchstring = preg_replace('/([^ ])[\!\&\|]/', "$1", $searchstring);
+ $searchstring = preg_replace( '/([^ ])[\!\&\|]/', "$1", $searchstring );
## Remove any trailing whitespace or operators
- $searchstring = preg_replace('/[\s\!\&\|]+$/', '', $searchstring);
+ $searchstring = preg_replace( '/[\s\!\&\|]+$/', '', $searchstring );
## Remove unnecessary quotes around everything
- $searchstring = preg_replace('/^[\'"](.*)[\'"]$/', "$1", $searchstring);
+ $searchstring = preg_replace( '/^[\'"](.*)[\'"]$/', "$1", $searchstring );
## Quote the whole thing
- $searchstring = $this->db->addQuotes($searchstring);
+ $searchstring = $this->db->addQuotes( $searchstring );
wfDebug( "parseQuery returned: $searchstring \n" );
@@ -154,42 +154,43 @@ class SearchPostgres extends SearchEngine {
## We need a separate query here so gin does not complain about empty searches
$SQL = "SELECT to_tsquery($searchstring)";
- $res = $this->db->query($SQL);
- if (!$res) {
+ $res = $this->db->query( $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");
+ die( "Sorry, that was not a valid search string. Please go back and try again" );
}
$top = $res->fetchRow();
$top = $top[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 ".
+ 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 ) {
+ if ( preg_match_all( "/'([^']+)'/", $top, $m, PREG_SET_ORDER ) ) {
+ foreach ( $m as $terms ) {
$this->searchTerms[$terms[1]] = $terms[1];
}
}
- $query = "SELECT page_id, page_namespace, page_title, ".
- "ts_rank($fulltext, to_tsquery($searchstring), 5) AS score ".
+ $query = "SELECT page_id, page_namespace, page_title, " .
+ "ts_rank($fulltext, to_tsquery($searchstring), 5) 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($searchstring)";
}
## Redirects
- if (! $this->showRedirects)
+ 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)
+ if ( !is_null( $this->namespaces ) ) { // null -> search all
+ if ( count( $this->namespaces ) < 1 ) {
$query .= ' AND page_namespace = 0';
- else {
+ } else {
$namespaces = $this->db->makeList( $this->namespaces );
$query .= " AND page_namespace IN ($namespaces)";
}
@@ -208,10 +209,10 @@ class SearchPostgres extends SearchEngine {
function update( $pageid, $title, $text ) {
## We don't want to index older revisions
- $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id IN ".
+ $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->query($SQL);
+ $this->db->query( $SQL );
return true;
}
@@ -226,7 +227,7 @@ class SearchPostgres extends SearchEngine {
*/
class PostgresSearchResult extends SearchResult {
function __construct( $row ) {
- parent::__construct($row);
+ parent::__construct( $row );
$this->score = $row->score;
}
function getScore() {
@@ -244,7 +245,7 @@ class PostgresSearchResultSet extends SqlSearchResultSet {
function next() {
$row = $this->mResultSet->fetchObject();
- if( $row === false ) {
+ if ( $row === false ) {
return false;
} else {
return new PostgresSearchResult( $row );
diff --git a/includes/search/SearchSqlite.php b/includes/search/SearchSqlite.php
index e52e4fe3..554181f6 100644
--- a/includes/search/SearchSqlite.php
+++ b/includes/search/SearchSqlite.php
@@ -61,12 +61,12 @@ class SearchSqlite extends SearchEngine {
$this->searchTerms = array();
$m = array();
- if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
- $filteredText, $m, PREG_SET_ORDER ) ) {
- foreach( $m as $bits ) {
+ if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach ( $m as $bits ) {
@list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
- if( $nonQuoted != '' ) {
+ if ( $nonQuoted != '' ) {
$term = $nonQuoted;
$quote = '';
} else {
@@ -74,14 +74,14 @@ class SearchSqlite extends SearchEngine {
$quote = '"';
}
- if( $searchon !== '' ) {
+ 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 ) ) {
+ if ( is_array( $convertedVariants ) ) {
$variants = array_unique( array_values( $convertedVariants ) );
} else {
$variants = array( $term );
@@ -101,10 +101,11 @@ class SearchSqlite extends SearchEngine {
$strippedVariants = array_unique( $strippedVariants );
$searchon .= $modifier;
- if( count( $strippedVariants) > 1 )
+ if ( count( $strippedVariants ) > 1 ) {
$searchon .= '(';
- foreach( $strippedVariants as $stripped ) {
- if( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
+ }
+ 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.
@@ -112,8 +113,9 @@ class SearchSqlite extends SearchEngine {
}
$searchon .= "$quote$stripped$quote$wildcard ";
}
- if( count( $strippedVariants) > 1 )
+ 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!
@@ -134,8 +136,8 @@ class SearchSqlite extends SearchEngine {
global $wgContLang;
$regex = preg_quote( $string, '/' );
- if( $wgContLang->hasWordBreaks() ) {
- if( $wildcard ) {
+ if ( $wgContLang->hasWordBreaks() ) {
+ if ( $wildcard ) {
// Don't cut off the final bit!
$regex = "\b$regex";
} else {
@@ -156,7 +158,7 @@ class SearchSqlite extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return SqliteSearchResultSet
*/
function searchText( $term ) {
@@ -166,7 +168,7 @@ class SearchSqlite extends SearchEngine {
/**
* Perform a title-only search query and return a result set.
*
- * @param $term String: raw search term
+ * @param string $term raw search term
* @return SqliteSearchResultSet
*/
function searchTitle( $term ) {
@@ -184,10 +186,10 @@ class SearchSqlite extends SearchEngine {
$resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) );
$total = null;
- if( $wgCountTotalSearchHits ) {
+ if ( $wgCountTotalSearchHits ) {
$totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
$row = $totalResult->fetchObject();
- if( $row ) {
+ if ( $row ) {
$total = intval( $row->c );
}
$totalResult->free();
@@ -196,13 +198,12 @@ class SearchSqlite extends SearchEngine {
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 ) {
+ if ( $this->showRedirects ) {
return '';
} else {
return 'AND page_is_redirect=0';
@@ -214,8 +215,9 @@ class SearchSqlite extends SearchEngine {
* @return String
*/
function queryNamespaces() {
- if( is_null($this->namespaces) )
+ if ( is_null( $this->namespaces ) ) {
return ''; # search all
+ }
if ( !count( $this->namespaces ) ) {
$namespaces = '0';
} else {
@@ -266,7 +268,7 @@ class SearchSqlite extends SearchEngine {
*/
function queryMain( $filteredTerm, $fulltext ) {
$match = $this->parseQuery( $filteredTerm, $fulltext );
- $page = $this->db->tableName( 'page' );
+ $page = $this->db->tableName( 'page' );
$searchindex = $this->db->tableName( 'searchindex' );
return "SELECT $searchindex.rowid, page_namespace, page_title " .
"FROM $page,$searchindex " .
@@ -275,7 +277,7 @@ class SearchSqlite extends SearchEngine {
function getCountQuery( $filteredTerm, $fulltext ) {
$match = $this->parseQuery( $filteredTerm, $fulltext );
- $page = $this->db->tableName( 'page' );
+ $page = $this->db->tableName( 'page' );
$searchindex = $this->db->tableName( 'searchindex' );
return "SELECT COUNT(*) AS c " .
"FROM $page,$searchindex " .
@@ -296,7 +298,7 @@ class SearchSqlite extends SearchEngine {
if ( !$this->fulltextSearchSupported() ) {
return;
}
- // @todo: find a method to do it in a single request,
+ // @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 );
@@ -317,7 +319,7 @@ class SearchSqlite extends SearchEngine {
* @param $id Integer
* @param $title String
*/
- function updateTitle( $id, $title ) {
+ function updateTitle( $id, $title ) {
if ( !$this->fulltextSearchSupported() ) {
return;
}
@@ -325,7 +327,7 @@ class SearchSqlite extends SearchEngine {
$dbw->update( 'searchindex',
array( 'si_title' => $title ),
- array( 'rowid' => $id ),
+ array( 'rowid' => $id ),
__METHOD__ );
}
}
@@ -334,7 +336,7 @@ class SearchSqlite extends SearchEngine {
* @ingroup Search
*/
class SqliteSearchResultSet extends SqlSearchResultSet {
- function __construct( $resultSet, $terms, $totalHits=null ) {
+ function __construct( $resultSet, $terms, $totalHits = null ) {
parent::__construct( $resultSet, $terms );
$this->mTotalHits = $totalHits;
}
diff --git a/includes/search/SearchUpdate.php b/includes/search/SearchUpdate.php
index 40dd36c2..82a413e9 100644
--- a/includes/search/SearchUpdate.php
+++ b/includes/search/SearchUpdate.php
@@ -29,52 +29,114 @@
* @ingroup Search
*/
class SearchUpdate implements DeferrableUpdate {
+ /**
+ * Page id being updated
+ * @var int
+ */
+ private $id = 0;
+
+ /**
+ * Title we're updating
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * Content of the page (not text)
+ * @var Content|false
+ */
+ private $content;
+
+ /**
+ * Constructor
+ *
+ * @param int $id Page id to update
+ * @param Title|string $title Title of page to update
+ * @param Content|string|false $c Content of the page to update.
+ * If a Content object, text will be gotten from it. String is for back-compat.
+ * Passing false tells the backend to just update the title, not the content
+ */
+ public function __construct( $id, $title, $c = false ) {
+ if ( is_string( $title ) ) {
+ $nt = Title::newFromText( $title );
+ } else {
+ $nt = $title;
+ }
- private $mId = 0, $mNamespace, $mTitle, $mText;
- private $mTitleWords;
-
- function __construct( $id, $title, $text = false ) {
- $nt = Title::newFromText( $title );
- if( $nt ) {
- $this->mId = $id;
- $this->mText = $text;
-
- $this->mNamespace = $nt->getNamespace();
- $this->mTitle = $nt->getText(); # Discard namespace
-
- $this->mTitleWords = $this->mTextWords = array();
+ if ( $nt ) {
+ $this->id = $id;
+ // is_string() check is back-compat for ApprovedRevs
+ if ( is_string( $c ) ) {
+ $this->content = new TextContent( $c );
+ } else {
+ $this->content = $c ?: false;
+ }
+ $this->title = $nt;
} else {
wfDebug( "SearchUpdate object created with invalid title '$title'\n" );
}
}
- function doUpdate() {
- global $wgContLang, $wgDisableSearchUpdate;
+ /**
+ * Perform actual update for the entry
+ */
+ public function doUpdate() {
+ global $wgDisableSearchUpdate;
- if( $wgDisableSearchUpdate || !$this->mId ) {
+ if ( $wgDisableSearchUpdate || !$this->id ) {
return;
}
wfProfileIn( __METHOD__ );
- $search = SearchEngine::create();
- $lc = SearchEngine::legalSearchChars() . '&#;';
+ $page = WikiPage::newFromId( $this->id, WikiPage::READ_LATEST );
+ $indexTitle = Title::indexTitle( $this->title->getNamespace(), $this->title->getText() );
- if( $this->mText === false ) {
- $search->updateTitle($this->mId,
- $search->normalizeText( Title::indexTitle( $this->mNamespace, $this->mTitle ) ) );
- wfProfileOut( __METHOD__ );
- return;
+ foreach ( SearchEngine::getSearchTypes() as $type ) {
+ $search = SearchEngine::create( $type );
+ if ( !$search->supports( 'search-update' ) ) {
+ continue;
+ }
+
+ $normalTitle = $search->normalizeText( $indexTitle );
+
+ if ( $page === null ) {
+ $search->delete( $this->id, $normalTitle );
+ continue;
+ } elseif ( $this->content === false ) {
+ $search->updateTitle( $this->id, $normalTitle );
+ continue;
+ }
+
+ $text = $search->getTextFromContent( $this->title, $this->content );
+ if ( !$search->textAlreadyUpdatedForIndex() ) {
+ $text = self::updateText( $text );
+ }
+
+ # Perform the actual update
+ $search->update( $this->id, $normalTitle, $search->normalizeText( $text ) );
}
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Clean text for indexing. Only really suitable for indexing in databases.
+ * If you're using a real search engine, you'll probably want to override
+ * this behavior and do something nicer with the original wikitext.
+ */
+ public static function updateText( $text ) {
+ global $wgContLang;
+
# Language-specific strip/conversion
- $text = $wgContLang->normalizeForSearch( $this->mText );
+ $text = $wgContLang->normalizeForSearch( $text );
+ $lc = SearchEngine::legalSearchChars() . '&#;';
wfProfileIn( __METHOD__ . '-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
+ "\\1\\2 \\2 \\2\\3", $text ); # Emphasize headings
# Strip external URLs
$uc = "A-Za-z0-9_\\/:.,~%\\-+&;#?!=()@\\x80-\\xFF";
@@ -92,7 +154,7 @@ class SearchUpdate implements DeferrableUpdate {
$text = preg_replace( $pat2, " \\1 \\3", $text );
$text = preg_replace( "/([^{$lc}])([{$lc}]+)]]([a-z]+)/",
- "\\1\\2 \\2\\3", $text ); # Handle [[game]]s
+ "\\1\\2 \\2\\3", $text ); # Handle [[game]]s
# Strip all remaining non-search characters
$text = preg_replace( "/[^{$lc}]+/", " ", $text );
@@ -118,22 +180,6 @@ class SearchUpdate implements DeferrableUpdate {
# Strip wiki '' and '''
$text = preg_replace( "/''[']*/", " ", $text );
wfProfileOut( __METHOD__ . '-regexps' );
-
- wfRunHooks( 'SearchUpdate', array( $this->mId, $this->mNamespace, $this->mTitle, &$text ) );
-
- # Perform the actual update
- $search->update($this->mId, $search->normalizeText( Title::indexTitle( $this->mNamespace, $this->mTitle ) ),
- $search->normalizeText( $text ) );
-
- wfProfileOut( __METHOD__ );
+ return $text;
}
}
-
-/**
- * Placeholder class
- *
- * @ingroup Search
- */
-class SearchUpdateMyISAM extends SearchUpdate {
- # Inherits everything
-}
diff --git a/includes/site/MediaWikiSite.php b/includes/site/MediaWikiSite.php
new file mode 100644
index 00000000..f3b8a0c7
--- /dev/null
+++ b/includes/site/MediaWikiSite.php
@@ -0,0 +1,352 @@
+<?php
+/**
+ * Class representing a MediaWiki site.
+ *
+ * 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 Site
+ * @license GNU GPL v2+
+ * @author John Erling Blad < jeblad@gmail.com >
+ * @author Daniel Kinzler
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+/**
+ * Class representing a MediaWiki site.
+ *
+ * @since 1.21
+ *
+ * @ingroup Site
+ */
+class MediaWikiSite extends Site {
+
+ const PATH_FILE = 'file_path';
+ const PATH_PAGE = 'page_path';
+
+ /**
+ * @since 1.21
+ * @deprecated Just use the constructor or the factory Site::newForType
+ *
+ * @param integer $globalId
+ *
+ * @return MediaWikiSite
+ */
+ public static function newFromGlobalId( $globalId ) {
+ $site = new static();
+ $site->setGlobalId( $globalId );
+ return $site;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @since 1.21
+ *
+ * @param string $type
+ */
+ public function __construct( $type = self::TYPE_MEDIAWIKI ) {
+ parent::__construct( $type );
+ }
+
+ /**
+ * Returns the database form of the given title.
+ *
+ * @since 1.21
+ *
+ * @param string $title the target page's title, in normalized form.
+ *
+ * @return String
+ */
+ public function toDBKey( $title ) {
+ return str_replace( ' ', '_', $title );
+ }
+
+ /**
+ * Returns the normalized form of the given page title, using the normalization rules of the given site.
+ * If the given title is a redirect, the redirect weill be resolved and the redirect target is returned.
+ *
+ * @note : This actually makes an API request to the remote site, so beware that this function is slow and depends
+ * on an external service.
+ *
+ * @note : If MW_PHPUNIT_TEST is defined, the call to the external site is skipped, and the title
+ * is normalized using the local normalization rules as implemented by the Title class.
+ *
+ * @see Site::normalizePageName
+ *
+ * @since 1.21
+ *
+ * @param string $pageName
+ *
+ * @return string
+ * @throws MWException
+ */
+ public function normalizePageName( $pageName ) {
+
+ // Check if we have strings as arguments.
+ if ( !is_string( $pageName ) ) {
+ throw new MWException( '$pageName must be a string' );
+ }
+
+ // Go on call the external site
+ if ( defined( 'MW_PHPUNIT_TEST' ) ) {
+ // If the code is under test, don't call out to other sites, just normalize locally.
+ // Note: this may cause results to be inconsistent with the actual normalization used by the respective remote site!
+
+ $t = Title::newFromText( $pageName );
+ return $t->getPrefixedText();
+ } else {
+
+ // Make sure the string is normalized into NFC (due to the bug 40017)
+ // but do nothing to the whitespaces, that should work appropriately.
+ // @see https://bugzilla.wikimedia.org/show_bug.cgi?id=40017
+ $pageName = UtfNormal::cleanUp( $pageName );
+
+ // Build the args for the specific call
+ $args = array(
+ 'action' => 'query',
+ 'prop' => 'info',
+ 'redirects' => true,
+ 'converttitles' => true,
+ 'format' => 'json',
+ 'titles' => $pageName,
+ // @todo options for maxlag and maxage
+ // Note that maxlag will lead to a long delay before a reply is made,
+ // but that maxage can avoid the extreme delay. On the other hand
+ // maxage could be nice to use anyhow as it stops unnecessary requests.
+ // Also consider smaxage if maxage is used.
+ );
+
+ $url = wfAppendQuery( $this->getFileUrl( 'api.php' ), $args );
+
+ // Go on call the external site
+ // @todo we need a good way to specify a timeout here.
+ $ret = Http::get( $url );
+ }
+
+ if ( $ret === false ) {
+ wfDebugLog( "MediaWikiSite", "call to external site failed: $url" );
+ return false;
+ }
+
+ $data = FormatJson::decode( $ret, true );
+
+ if ( !is_array( $data ) ) {
+ wfDebugLog( "MediaWikiSite", "call to <$url> returned bad json: " . $ret );
+ return false;
+ }
+
+ $page = static::extractPageRecord( $data, $pageName );
+
+ if ( isset( $page['missing'] ) ) {
+ wfDebugLog( "MediaWikiSite", "call to <$url> returned a marker for a missing page title! " . $ret );
+ return false;
+ }
+
+ if ( isset( $page['invalid'] ) ) {
+ wfDebugLog( "MediaWikiSite", "call to <$url> returned a marker for an invalid page title! " . $ret );
+ return false;
+ }
+
+ if ( !isset( $page['title'] ) ) {
+ wfDebugLog( "MediaWikiSite", "call to <$url> did not return a page title! " . $ret );
+ return false;
+ }
+
+ return $page['title'];
+ }
+
+ /**
+ * Get normalization record for a given page title from an API response.
+ *
+ * @since 1.21
+ *
+ * @param array $externalData A reply from the API on a external server.
+ * @param string $pageTitle Identifies the page at the external site, needing normalization.
+ *
+ * @return array|boolean a 'page' structure representing the page identified by $pageTitle.
+ */
+ private static function extractPageRecord( $externalData, $pageTitle ) {
+ // If there is a special case with only one returned page
+ // we can cheat, and only return
+ // the single page in the "pages" substructure.
+ if ( isset( $externalData['query']['pages'] ) ) {
+ $pages = array_values( $externalData['query']['pages'] );
+ if ( count( $pages ) === 1 ) {
+ return $pages[0];
+ }
+ }
+ // This is only used during internal testing, as it is assumed
+ // a more optimal (and lossfree) storage.
+ // Make initial checks and return if prerequisites are not meet.
+ if ( !is_array( $externalData ) || !isset( $externalData['query'] ) ) {
+ return false;
+ }
+ // Loop over the tree different named structures, that otherwise are similar
+ $structs = array(
+ 'normalized' => 'from',
+ 'converted' => 'from',
+ 'redirects' => 'from',
+ 'pages' => 'title'
+ );
+ foreach ( $structs as $listId => $fieldId ) {
+ // Check if the substructure exist at all.
+ if ( !isset( $externalData['query'][$listId] ) ) {
+ continue;
+ }
+ // Filter the substructure down to what we actually are using.
+ $collectedHits = array_filter(
+ array_values( $externalData['query'][$listId] ),
+ function( $a ) use ( $fieldId, $pageTitle ) {
+ return $a[$fieldId] === $pageTitle;
+ }
+ );
+ // If still looping over normalization, conversion or redirects,
+ // then we need to keep the new page title for later rounds.
+ if ( $fieldId === 'from' && is_array( $collectedHits ) ) {
+ switch ( count( $collectedHits ) ) {
+ case 0:
+ break;
+ case 1:
+ $pageTitle = $collectedHits[0]['to'];
+ break;
+ default:
+ return false;
+ }
+ }
+ // If on the pages structure we should prepare for returning.
+ elseif ( $fieldId === 'title' && is_array( $collectedHits ) ) {
+ switch ( count( $collectedHits ) ) {
+ case 0:
+ return false;
+ case 1:
+ return array_shift( $collectedHits );
+ default:
+ return false;
+ }
+ }
+ }
+ // should never be here
+ return false;
+ }
+
+ /**
+ * @see Site::getLinkPathType
+ * Returns Site::PATH_PAGE
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getLinkPathType() {
+ return self::PATH_PAGE;
+ }
+
+ /**
+ * Returns the relative page path.
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getRelativePagePath() {
+ return parse_url( $this->getPath( self::PATH_PAGE ), PHP_URL_PATH );
+ }
+
+ /**
+ * Returns the relative file path.
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getRelativeFilePath() {
+ return parse_url( $this->getPath( self::PATH_FILE ), PHP_URL_PATH );
+ }
+
+ /**
+ * Sets the relative page path.
+ *
+ * @since 1.21
+ *
+ * @param string $path
+ */
+ public function setPagePath( $path ) {
+ $this->setPath( self::PATH_PAGE, $path );
+ }
+
+ /**
+ * Sets the relative file path.
+ *
+ * @since 1.21
+ *
+ * @param string $path
+ */
+ public function setFilePath( $path ) {
+ $this->setPath( self::PATH_FILE, $path );
+ }
+
+ /**
+ * @see Site::getPageUrl
+ *
+ * This implementation returns a URL constructed using the path returned by getLinkPath().
+ * In addition to the default behavior implemented by Site::getPageUrl(), this
+ * method converts the $pageName to DBKey-format by replacing spaces with underscores
+ * before using it in the URL.
+ *
+ * @since 1.21
+ *
+ * @param string|boolean $pageName Page name or false (default: false)
+ *
+ * @return string
+ */
+ public function getPageUrl( $pageName = false ) {
+ $url = $this->getLinkPath();
+
+ if ( $url === false ) {
+ return false;
+ }
+
+ if ( $pageName !== false ) {
+ $pageName = $this->toDBKey( trim( $pageName ) );
+ $url = str_replace( '$1', wfUrlencode( $pageName ), $url );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns the full file path (ie site url + relative file path).
+ * The path should go at the $1 marker. If the $path
+ * argument is provided, the marker will be replaced by it's value.
+ *
+ * @since 1.21
+ *
+ * @param string|boolean $path
+ *
+ * @return string
+ */
+ public function getFileUrl( $path = false ) {
+ $filePath = $this->getPath( self::PATH_FILE );
+
+ if ( $filePath !== false ) {
+ $filePath = str_replace( '$1', $path, $filePath );
+ }
+
+ return $filePath;
+ }
+
+}
diff --git a/includes/site/Site.php b/includes/site/Site.php
new file mode 100644
index 00000000..076dc88c
--- /dev/null
+++ b/includes/site/Site.php
@@ -0,0 +1,702 @@
+<?php
+
+/**
+ * Represents a single site.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Site
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class Site implements Serializable {
+
+ const TYPE_UNKNOWN = 'unknown';
+ const TYPE_MEDIAWIKI = 'mediawiki';
+
+ const GROUP_NONE = 'none';
+
+ const ID_INTERWIKI = 'interwiki';
+ const ID_EQUIVALENT = 'equivalent';
+
+ const SOURCE_LOCAL = 'local';
+
+ const PATH_LINK = 'link';
+
+ /**
+ * A version ID that identifies the serialization structure used by getSerializationData()
+ * and unserialize(). This is useful for constructing cache keys in cases where the cache relies
+ * on serialization for storing the SiteList.
+ *
+ * @var string A string uniquely identifying the version of the serialization structure.
+ */
+ const SERIAL_VERSION_ID = '2013-01-23';
+
+ /**
+ * @since 1.21
+ *
+ * @var string|null
+ */
+ protected $globalId = null;
+
+ /**
+ * @since 1.21
+ *
+ * @var string
+ */
+ protected $type = self::TYPE_UNKNOWN;
+
+ /**
+ * @since 1.21
+ *
+ * @var string
+ */
+ protected $group = self::GROUP_NONE;
+
+ /**
+ * @since 1.21
+ *
+ * @var string
+ */
+ protected $source = self::SOURCE_LOCAL;
+
+ /**
+ * @since 1.21
+ *
+ * @var string|null
+ */
+ protected $languageCode = null;
+
+ /**
+ * Holds the local ids for this site.
+ * local id type => [ ids for this type (strings) ]
+ *
+ * @since 1.21
+ *
+ * @var array[]
+ */
+ protected $localIds = array();
+
+ /**
+ * @since 1.21
+ *
+ * @var array
+ */
+ protected $extraData = array();
+
+ /**
+ * @since 1.21
+ *
+ * @var array
+ */
+ protected $extraConfig = array();
+
+ /**
+ * @since 1.21
+ *
+ * @var bool
+ */
+ protected $forward = false;
+
+ /**
+ * @since 1.21
+ *
+ * @var int|null
+ */
+ protected $internalId = null;
+
+ /**
+ * Constructor.
+ *
+ * @since 1.21
+ *
+ * @param string $type
+ */
+ public function __construct( $type = self::TYPE_UNKNOWN ) {
+ $this->type = $type;
+ }
+
+ /**
+ * Returns the global site identifier (ie enwiktionary).
+ *
+ * @since 1.21
+ *
+ * @return string|null
+ */
+ public function getGlobalId() {
+ return $this->globalId;
+ }
+
+ /**
+ * Sets the global site identifier (ie enwiktionary).
+ *
+ * @since 1.21
+ *
+ * @param string|null $globalId
+ *
+ * @throws MWException
+ */
+ public function setGlobalId( $globalId ) {
+ if ( $globalId !== null && !is_string( $globalId ) ) {
+ throw new MWException( '$globalId needs to be string or null' );
+ }
+
+ $this->globalId = $globalId;
+ }
+
+ /**
+ * Returns the type of the site (ie mediawiki).
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * Gets the type of the site (ie wikipedia).
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getGroup() {
+ return $this->group;
+ }
+
+ /**
+ * Sets the type of the site (ie wikipedia).
+ *
+ * @since 1.21
+ *
+ * @param string $group
+ *
+ * @throws MWException
+ */
+ public function setGroup( $group ) {
+ if ( !is_string( $group ) ) {
+ throw new MWException( '$group needs to be a string' );
+ }
+
+ $this->group = $group;
+ }
+
+ /**
+ * Returns the source of the site data (ie 'local', 'wikidata', 'my-magical-repo').
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getSource() {
+ return $this->source;
+ }
+
+ /**
+ * Sets the source of the site data (ie 'local', 'wikidata', 'my-magical-repo').
+ *
+ * @since 1.21
+ *
+ * @param string $source
+ *
+ * @throws MWException
+ */
+ public function setSource( $source ) {
+ if ( !is_string( $source ) ) {
+ throw new MWException( '$source needs to be a string' );
+ }
+
+ $this->source = $source;
+ }
+
+ /**
+ * Gets if site.tld/path/key:pageTitle should forward users to the page on
+ * the actual site, where "key" is the local identifier.
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function shouldForward() {
+ return $this->forward;
+ }
+
+ /**
+ * Sets if site.tld/path/key:pageTitle should forward users to the page on
+ * the actual site, where "key" is the local identifier.
+ *
+ * @since 1.21
+ *
+ * @param boolean $shouldForward
+ *
+ * @throws MWException
+ */
+ public function setForward( $shouldForward ) {
+ if ( !is_bool( $shouldForward ) ) {
+ throw new MWException( '$shouldForward needs to be a boolean' );
+ }
+
+ $this->forward = $shouldForward;
+ }
+
+ /**
+ * Returns the domain of the site, ie en.wikipedia.org
+ * Or false if it's not known.
+ *
+ * @since 1.21
+ *
+ * @return string|null
+ */
+ public function getDomain() {
+ $path = $this->getLinkPath();
+
+ if ( $path === null ) {
+ return null;
+ }
+
+ return parse_url( $path, PHP_URL_HOST );
+ }
+
+ /**
+ * Returns the protocol of the site.
+ *
+ * @since 1.21
+ *
+ * @throws MWException
+ * @return string
+ */
+ public function getProtocol() {
+ $path = $this->getLinkPath();
+
+ if ( $path === null ) {
+ return '';
+ }
+
+ $protocol = parse_url( $path, PHP_URL_SCHEME );
+
+ // Malformed URL
+ if ( $protocol === false ) {
+ throw new MWException( "failed to parse URL '$path'" );
+ }
+
+ // No schema
+ if ( $protocol === null ) {
+ // Used for protocol relative URLs
+ $protocol = '';
+ }
+
+ return $protocol;
+ }
+
+ /**
+ * Sets the path used to construct links with.
+ * Shall be equivalent to setPath( getLinkPathType(), $fullUrl ).
+ *
+ * @param string $fullUrl
+ *
+ * @since 1.21
+ *
+ * @throws MWException
+ */
+ public function setLinkPath( $fullUrl ) {
+ $type = $this->getLinkPathType();
+
+ if ( $type === null ) {
+ throw new MWException( "This Site does not support link paths." );
+ }
+
+ $this->setPath( $type, $fullUrl );
+ }
+
+ /**
+ * Returns the path used to construct links with or false if there is no such path.
+ *
+ * Shall be equivalent to getPath( getLinkPathType() ).
+ *
+ * @return string|null
+ */
+ public function getLinkPath() {
+ $type = $this->getLinkPathType();
+ return $type === null ? null: $this->getPath( $type );
+ }
+
+ /**
+ * Returns the main path type, that is the type of the path that should generally be used to construct links
+ * to the target site.
+ *
+ * This default implementation returns Site::PATH_LINK as the default path type. Subclasses can override this
+ * to define a different default path type, or return false to disable site links.
+ *
+ * @since 1.21
+ *
+ * @return string|null
+ */
+ public function getLinkPathType() {
+ return self::PATH_LINK;
+ }
+
+ /**
+ * Returns the full URL for the given page on the site.
+ * Or false if the needed information is not known.
+ *
+ * This generated URL is usually based upon the path returned by getLinkPath(),
+ * but this is not a requirement.
+ *
+ * This implementation returns a URL constructed using the path returned by getLinkPath().
+ *
+ * @since 1.21
+ *
+ * @param bool|String $pageName
+ *
+ * @return string|boolean false
+ */
+ public function getPageUrl( $pageName = false ) {
+ $url = $this->getLinkPath();
+
+ if ( $url === false ) {
+ return false;
+ }
+
+ if ( $pageName !== false ) {
+ $url = str_replace( '$1', rawurlencode( $pageName ), $url );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns $pageName without changes.
+ * Subclasses may override this to apply some kind of normalization.
+ *
+ * @see Site::normalizePageName
+ *
+ * @since 1.21
+ *
+ * @param string $pageName
+ *
+ * @return string
+ */
+ public function normalizePageName( $pageName ) {
+ return $pageName;
+ }
+
+ /**
+ * Returns the type specific fields.
+ *
+ * @since 1.21
+ *
+ * @return array
+ */
+ public function getExtraData() {
+ return $this->extraData;
+ }
+
+ /**
+ * Sets the type specific fields.
+ *
+ * @since 1.21
+ *
+ * @param array $extraData
+ */
+ public function setExtraData( array $extraData ) {
+ $this->extraData = $extraData;
+ }
+
+ /**
+ * Returns the type specific config.
+ *
+ * @since 1.21
+ *
+ * @return array
+ */
+ public function getExtraConfig() {
+ return $this->extraConfig;
+ }
+
+ /**
+ * Sets the type specific config.
+ *
+ * @since 1.21
+ *
+ * @param array $extraConfig
+ */
+ public function setExtraConfig( array $extraConfig ) {
+ $this->extraConfig = $extraConfig;
+ }
+
+ /**
+ * Returns language code of the sites primary language.
+ * Or null if it's not known.
+ *
+ * @since 1.21
+ *
+ * @return string|null
+ */
+ public function getLanguageCode() {
+ return $this->languageCode;
+ }
+
+ /**
+ * Sets language code of the sites primary language.
+ *
+ * @since 1.21
+ *
+ * @param string $languageCode
+ */
+ public function setLanguageCode( $languageCode ) {
+ $this->languageCode = $languageCode;
+ }
+
+ /**
+ * Returns the set internal identifier for the site.
+ *
+ * @since 1.21
+ *
+ * @return string|null
+ */
+ public function getInternalId() {
+ return $this->internalId;
+ }
+
+ /**
+ * Sets the internal identifier for the site.
+ * This typically is a primary key in a db table.
+ *
+ * @since 1.21
+ *
+ * @param int|null $internalId
+ */
+ public function setInternalId( $internalId = null ) {
+ $this->internalId = $internalId;
+ }
+
+ /**
+ * Adds a local identifier.
+ *
+ * @since 1.21
+ *
+ * @param string $type
+ * @param string $identifier
+ */
+ public function addLocalId( $type, $identifier ) {
+ if ( $this->localIds === false ) {
+ $this->localIds = array();
+ }
+
+ if ( !array_key_exists( $type, $this->localIds ) ) {
+ $this->localIds[$type] = array();
+ }
+
+ if ( !in_array( $identifier, $this->localIds[$type] ) ) {
+ $this->localIds[$type][] = $identifier;
+ }
+ }
+
+ /**
+ * Adds an interwiki id to the site.
+ *
+ * @since 1.21
+ *
+ * @param string $identifier
+ */
+ public function addInterwikiId( $identifier ) {
+ $this->addLocalId( self::ID_INTERWIKI, $identifier );
+ }
+
+ /**
+ * Adds a navigation id to the site.
+ *
+ * @since 1.21
+ *
+ * @param string $identifier
+ */
+ public function addNavigationId( $identifier ) {
+ $this->addLocalId( self::ID_EQUIVALENT, $identifier );
+ }
+
+ /**
+ * Returns the interwiki link identifiers that can be used for this site.
+ *
+ * @since 1.21
+ *
+ * @return string[]
+ */
+ public function getInterwikiIds() {
+ return array_key_exists( self::ID_INTERWIKI, $this->localIds ) ? $this->localIds[self::ID_INTERWIKI] : array();
+ }
+
+ /**
+ * Returns the equivalent link identifiers that can be used to make
+ * the site show up in interfaces such as the "language links" section.
+ *
+ * @since 1.21
+ *
+ * @return string[]
+ */
+ public function getNavigationIds() {
+ return array_key_exists( self::ID_EQUIVALENT, $this->localIds ) ? $this->localIds[self::ID_EQUIVALENT] : array();
+ }
+
+ /**
+ * Returns all local ids
+ *
+ * @since 1.21
+ *
+ * @return array[]
+ */
+ public function getLocalIds() {
+ return $this->localIds;
+ }
+
+ /**
+ * Sets the path used to construct links with.
+ * Shall be equivalent to setPath( getLinkPathType(), $fullUrl ).
+ *
+ * @since 1.21
+ *
+ * @param string $pathType
+ * @param string $fullUrl
+ *
+ * @throws MWException
+ */
+ public function setPath( $pathType, $fullUrl ) {
+ if ( !is_string( $fullUrl ) ) {
+ throw new MWException( '$fullUrl needs to be a string' );
+ }
+
+ if ( !array_key_exists( 'paths', $this->extraData ) ) {
+ $this->extraData['paths'] = array();
+ }
+
+ $this->extraData['paths'][$pathType] = $fullUrl;
+ }
+
+ /**
+ * Returns the path of the provided type or false if there is no such path.
+ *
+ * @since 1.21
+ *
+ * @param string $pathType
+ *
+ * @return string|null
+ */
+ public function getPath( $pathType ) {
+ $paths = $this->getAllPaths();
+ return array_key_exists( $pathType, $paths ) ? $paths[$pathType] : null;
+ }
+
+ /**
+ * Returns the paths as associative array.
+ * The keys are path types, the values are the path urls.
+ *
+ * @since 1.21
+ *
+ * @return string[]
+ */
+ public function getAllPaths() {
+ return array_key_exists( 'paths', $this->extraData ) ? $this->extraData['paths'] : array();
+ }
+
+ /**
+ * Removes the path of the provided type if it's set.
+ *
+ * @since 1.21
+ *
+ * @param string $pathType
+ */
+ public function removePath( $pathType ) {
+ if ( array_key_exists( 'paths', $this->extraData ) ) {
+ unset( $this->extraData['paths'][$pathType] );
+ }
+ }
+
+ /**
+ * @since 1.21
+ *
+ * @param string $siteType
+ *
+ * @return Site
+ */
+ public static function newForType( $siteType ) {
+ global $wgSiteTypes;
+
+ if ( array_key_exists( $siteType, $wgSiteTypes ) ) {
+ return new $wgSiteTypes[$siteType]();
+ }
+
+ return new Site();
+ }
+
+ /**
+ * @see Serializable::serialize
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function serialize() {
+ $fields = array(
+ 'globalid' => $this->globalId,
+ 'type' => $this->type,
+ 'group' => $this->group,
+ 'source' => $this->source,
+ 'language' => $this->languageCode,
+ 'localids' => $this->localIds,
+ 'config' => $this->extraConfig,
+ 'data' => $this->extraData,
+ 'forward' => $this->forward,
+ 'internalid' => $this->internalId,
+
+ );
+
+ return serialize( $fields );
+ }
+
+ /**
+ * @see Serializable::unserialize
+ *
+ * @since 1.21
+ *
+ * @param string $serialized
+ */
+ public function unserialize( $serialized ) {
+ $fields = unserialize( $serialized );
+
+ $this->__construct( $fields['type'] );
+
+ $this->setGlobalId( $fields['globalid'] );
+ $this->setGroup( $fields['group'] );
+ $this->setSource( $fields['source'] );
+ $this->setLanguageCode( $fields['language'] );
+ $this->localIds = $fields['localids'];
+ $this->setExtraConfig( $fields['config'] );
+ $this->setExtraData( $fields['data'] );
+ $this->setForward( $fields['forward'] );
+ $this->setInternalId( $fields['internalid'] );
+ }
+
+}
+
+/**
+ * @deprecated
+ */
+class SiteObject extends Site {}
diff --git a/includes/site/SiteList.php b/includes/site/SiteList.php
new file mode 100644
index 00000000..b0d1f95b
--- /dev/null
+++ b/includes/site/SiteList.php
@@ -0,0 +1,300 @@
+<?php
+
+/**
+ * Collection of Site objects.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Site
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteList extends GenericArrayObject {
+
+ /**
+ * Internal site identifiers pointing to their sites offset value.
+ *
+ * @since 1.21
+ *
+ * @var array of integer
+ */
+ protected $byInternalId = array();
+
+ /**
+ * Global site identifiers pointing to their sites offset value.
+ *
+ * @since 1.21
+ *
+ * @var array of string
+ */
+ protected $byGlobalId = array();
+
+ /**
+ * @see GenericArrayObject::getObjectType
+ *
+ * @since 1.21
+ *
+ * @return string
+ */
+ public function getObjectType() {
+ return 'Site';
+ }
+
+ /**
+ * @see GenericArrayObject::preSetElement
+ *
+ * @since 1.21
+ *
+ * @param int|string $index
+ * @param Site $site
+ *
+ * @return boolean
+ */
+ protected function preSetElement( $index, $site ) {
+ if ( $this->hasSite( $site->getGlobalId() ) ) {
+ $this->removeSite( $site->getGlobalId() );
+ }
+
+ $this->byGlobalId[$site->getGlobalId()] = $index;
+ $this->byInternalId[$site->getInternalId()] = $index;
+
+ return true;
+ }
+
+ /**
+ * @see ArrayObject::offsetUnset()
+ *
+ * @since 1.21
+ *
+ * @param mixed $index
+ */
+ public function offsetUnset( $index ) {
+ if ( $this->offsetExists( $index ) ) {
+ /**
+ * @var Site $site
+ */
+ $site = $this->offsetGet( $index );
+
+ unset( $this->byGlobalId[$site->getGlobalId()] );
+ unset( $this->byInternalId[$site->getInternalId()] );
+ }
+
+ parent::offsetUnset( $index );
+ }
+
+ /**
+ * Returns all the global site identifiers.
+ * Optionally only those belonging to the specified group.
+ *
+ * @since 1.21
+ *
+ * @return array
+ */
+ public function getGlobalIdentifiers() {
+ return array_keys( $this->byGlobalId );
+ }
+
+ /**
+ * Returns if the list contains the site with the provided global site identifier.
+ *
+ * @param string $globalSiteId
+ *
+ * @return boolean
+ */
+ public function hasSite( $globalSiteId ) {
+ return array_key_exists( $globalSiteId, $this->byGlobalId );
+ }
+
+ /**
+ * Returns the Site with the provided global site identifier.
+ * The site needs to exist, so if not sure, call hasGlobalId first.
+ *
+ * @since 1.21
+ *
+ * @param string $globalSiteId
+ *
+ * @return Site
+ */
+ public function getSite( $globalSiteId ) {
+ return $this->offsetGet( $this->byGlobalId[$globalSiteId] );
+ }
+
+ /**
+ * Removes the site with the specified global site identifier.
+ * The site needs to exist, so if not sure, call hasGlobalId first.
+ *
+ * @since 1.21
+ *
+ * @param string $globalSiteId
+ */
+ public function removeSite( $globalSiteId ) {
+ $this->offsetUnset( $this->byGlobalId[$globalSiteId] );
+ }
+
+ /**
+ * Returns if the list contains no sites.
+ *
+ * @since 1.21
+ *
+ * @return boolean
+ */
+ public function isEmpty() {
+ return $this->byGlobalId === array();
+ }
+
+ /**
+ * Returns if the list contains the site with the provided site id.
+ *
+ * @param integer $id
+ *
+ * @return boolean
+ */
+ public function hasInternalId( $id ) {
+ return array_key_exists( $id, $this->byInternalId );
+ }
+
+ /**
+ * Returns the Site with the provided site id.
+ * The site needs to exist, so if not sure, call has first.
+ *
+ * @since 1.21
+ *
+ * @param integer $id
+ *
+ * @return Site
+ */
+ public function getSiteByInternalId( $id ) {
+ return $this->offsetGet( $this->byInternalId[$id] );
+ }
+
+ /**
+ * Removes the site with the specified site id.
+ * The site needs to exist, so if not sure, call has first.
+ *
+ * @since 1.21
+ *
+ * @param integer $id
+ */
+ public function removeSiteByInternalId( $id ) {
+ $this->offsetUnset( $this->byInternalId[$id] );
+ }
+
+ /**
+ * Sets a site in the list. If the site was not there,
+ * it will be added. If it was, it will be updated.
+ *
+ * @since 1.21
+ *
+ * @param Site $site
+ */
+ public function setSite( Site $site ) {
+ $this[] = $site;
+ }
+
+ /**
+ * Returns the sites that are in the provided group.
+ *
+ * @since 1.21
+ *
+ * @param string $groupName
+ *
+ * @return SiteList
+ */
+ public function getGroup( $groupName ) {
+ $group = new self();
+
+ /**
+ * @var \Site $site
+ */
+ foreach ( $this as $site ) {
+ if ( $site->getGroup() === $groupName ) {
+ $group[] = $site;
+ }
+ }
+
+ return $group;
+ }
+
+ /**
+ * A version ID that identifies the serialization structure used by getSerializationData()
+ * and unserialize(). This is useful for constructing cache keys in cases where the cache relies
+ * on serialization for storing the SiteList.
+ *
+ * @var string A string uniquely identifying the version of the serialization structure,
+ * not including any sub-structures.
+ */
+ const SERIAL_VERSION_ID = '2013-02-07';
+
+ /**
+ * Returns the version ID that identifies the serialization structure used by
+ * getSerializationData() and unserialize(), including the structure of any nested structures.
+ * This is useful for constructing cache keys in cases where the cache relies
+ * on serialization for storing the SiteList.
+ *
+ * @return string A string uniquely identifying the version of the serialization structure,
+ * including any sub-structures.
+ */
+ public static function getSerialVersionId() {
+ return self::SERIAL_VERSION_ID . '+Site:' . Site::SERIAL_VERSION_ID;
+ }
+
+ /**
+ * @see GenericArrayObject::getSerializationData
+ *
+ * @since 1.21
+ *
+ * @return array
+ */
+ protected function getSerializationData() {
+ //NOTE: When changing the structure, either implement unserialize() to handle the
+ // old structure too, or update SERIAL_VERSION_ID to kill any caches.
+ return array_merge(
+ parent::getSerializationData(),
+ array(
+ 'internalIds' => $this->byInternalId,
+ 'globalIds' => $this->byGlobalId,
+ )
+ );
+ }
+
+ /**
+ * @see GenericArrayObject::unserialize
+ *
+ * @since 1.21
+ *
+ * @param string $serialization
+ *
+ * @return array
+ */
+ public function unserialize( $serialization ) {
+ $serializationData = parent::unserialize( $serialization );
+
+ $this->byInternalId = $serializationData['internalIds'];
+ $this->byGlobalId = $serializationData['globalIds'];
+
+ return $serializationData;
+ }
+
+}
+
+/**
+ * @deprecated
+ */
+class SiteArray extends SiteList {}
diff --git a/includes/site/SiteSQLStore.php b/includes/site/SiteSQLStore.php
new file mode 100644
index 00000000..11141e07
--- /dev/null
+++ b/includes/site/SiteSQLStore.php
@@ -0,0 +1,507 @@
+<?php
+
+/**
+ * Represents the site configuration of a wiki.
+ * Holds a list of sites (ie SiteList) and takes care
+ * of retrieving and caching site information when appropriate.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Site
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteSQLStore implements SiteStore {
+
+ /**
+ * @since 1.21
+ *
+ * @var SiteList|null
+ */
+ protected $sites = null;
+
+ /**
+ * @var ORMTable
+ */
+ protected $sitesTable;
+
+ /**
+ * @var string|null
+ */
+ private $cacheKey = null;
+
+ /**
+ * @var int
+ */
+ private $cacheTimeout = 3600;
+
+ /**
+ * @since 1.21
+ *
+ * @param ORMTable|null $sitesTable
+ *
+ * @return SiteStore
+ */
+ public static function newInstance( ORMTable $sitesTable = null ) {
+ return new static( $sitesTable );
+ }
+
+ /**
+ * Constructor.
+ *
+ * @since 1.21
+ *
+ * @param ORMTable|null $sitesTable
+ */
+ protected function __construct( ORMTable $sitesTable = null ) {
+ if ( $sitesTable === null ) {
+ $sitesTable = $this->newSitesTable();
+ }
+
+ $this->sitesTable = $sitesTable;
+ }
+
+ /**
+ * Constructs a cache key to use for caching the list of sites.
+ *
+ * This includes the concrete class name of the site list as well as a version identifier
+ * for the list's serialization, to avoid problems when unserializing site lists serialized
+ * by an older version, e.g. when reading from a cache.
+ *
+ * The cache key also includes information about where the sites were loaded from, e.g.
+ * the name of a database table.
+ *
+ * @see SiteList::getSerialVersionId
+ *
+ * @return String The cache key.
+ */
+ protected function getCacheKey() {
+ wfProfileIn( __METHOD__ );
+
+ if ( $this->cacheKey === null ) {
+ $type = 'SiteList#' . SiteList::getSerialVersionId();
+ $source = $this->sitesTable->getName();
+
+ if ( $this->sitesTable->getTargetWiki() !== false ) {
+ $source = $this->sitesTable->getTargetWiki() . '.' . $source;
+ }
+
+ $this->cacheKey = wfMemcKey( "$source/$type" );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $this->cacheKey;
+ }
+
+ /**
+ * @see SiteStore::getSites
+ *
+ * @since 1.21
+ *
+ * @param string $source either 'cache' or 'recache'
+ *
+ * @return SiteList
+ */
+ public function getSites( $source = 'cache' ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( $source === 'cache' ) {
+ if ( $this->sites === null ) {
+ $cache = wfGetMainCache();
+ $sites = $cache->get( $this->getCacheKey() );
+
+ if ( is_object( $sites ) ) {
+ $this->sites = $sites;
+ } else {
+ $this->loadSites();
+ }
+ }
+ }
+ else {
+ $this->loadSites();
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $this->sites;
+ }
+
+ /**
+ * Returns a new Site object constructed from the provided ORMRow.
+ *
+ * @since 1.21
+ *
+ * @param ORMRow $siteRow
+ *
+ * @return Site
+ */
+ protected function siteFromRow( ORMRow $siteRow ) {
+ wfProfileIn( __METHOD__ );
+
+ $site = Site::newForType( $siteRow->getField( 'type', Site::TYPE_UNKNOWN ) );
+
+ $site->setGlobalId( $siteRow->getField( 'global_key' ) );
+
+ $site->setInternalId( $siteRow->getField( 'id' ) );
+
+ if ( $siteRow->hasField( 'forward' ) ) {
+ $site->setForward( $siteRow->getField( 'forward' ) );
+ }
+
+ if ( $siteRow->hasField( 'group' ) ) {
+ $site->setGroup( $siteRow->getField( 'group' ) );
+ }
+
+ if ( $siteRow->hasField( 'language' ) ) {
+ $site->setLanguageCode( $siteRow->getField( 'language' ) === '' ? null : $siteRow->getField( 'language' ) );
+ }
+
+ if ( $siteRow->hasField( 'source' ) ) {
+ $site->setSource( $siteRow->getField( 'source' ) );
+ }
+
+ if ( $siteRow->hasField( 'data' ) ) {
+ $site->setExtraData( $siteRow->getField( 'data' ) );
+ }
+
+ if ( $siteRow->hasField( 'config' ) ) {
+ $site->setExtraConfig( $siteRow->getField( 'config' ) );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $site;
+ }
+
+ /**
+ * Get a new ORMRow from a Site object
+ *
+ * @since 1.22
+ *
+ * @param Site
+ *
+ * @return ORMRow
+ */
+ protected function getRowFromSite( Site $site ) {
+ $fields = array(
+ // Site data
+ 'global_key' => $site->getGlobalId(), // TODO: check not null
+ 'type' => $site->getType(),
+ 'group' => $site->getGroup(),
+ 'source' => $site->getSource(),
+ 'language' => $site->getLanguageCode() === null ? '' : $site->getLanguageCode(),
+ 'protocol' => $site->getProtocol(),
+ 'domain' => strrev( $site->getDomain() ) . '.',
+ 'data' => $site->getExtraData(),
+
+ // Site config
+ 'forward' => $site->shouldForward(),
+ 'config' => $site->getExtraConfig(),
+ );
+
+ if ( $site->getInternalId() !== null ) {
+ $fields['id'] = $site->getInternalId();
+ }
+
+ return new ORMRow( $this->sitesTable, $fields );
+ }
+
+ /**
+ * Fetches the site from the database and loads them into the sites field.
+ *
+ * @since 1.21
+ */
+ protected function loadSites() {
+ wfProfileIn( __METHOD__ );
+
+ $this->sites = new SiteList();
+
+ foreach ( $this->sitesTable->select() as $siteRow ) {
+ $this->sites[] = $this->siteFromRow( $siteRow );
+ }
+
+ // Batch load the local site identifiers.
+ $ids = wfGetDB( $this->sitesTable->getReadDb() )->select(
+ 'site_identifiers',
+ array(
+ 'si_site',
+ 'si_type',
+ 'si_key',
+ ),
+ array(),
+ __METHOD__
+ );
+
+ foreach ( $ids as $id ) {
+ if ( $this->sites->hasInternalId( $id->si_site ) ) {
+ $site = $this->sites->getSiteByInternalId( $id->si_site );
+ $site->addLocalId( $id->si_type, $id->si_key );
+ $this->sites->setSite( $site );
+ }
+ }
+
+ $cache = wfGetMainCache();
+ $cache->set( $this->getCacheKey(), $this->sites, $this->cacheTimeout );
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * @see SiteStore::getSite
+ *
+ * @since 1.21
+ *
+ * @param string $globalId
+ * @param string $source
+ *
+ * @return Site|null
+ */
+ public function getSite( $globalId, $source = 'cache' ) {
+ wfProfileIn( __METHOD__ );
+
+ $sites = $this->getSites( $source );
+
+ wfProfileOut( __METHOD__ );
+ return $sites->hasSite( $globalId ) ? $sites->getSite( $globalId ) : null;
+ }
+
+ /**
+ * @see SiteStore::saveSite
+ *
+ * @since 1.21
+ *
+ * @param Site $site
+ *
+ * @return boolean Success indicator
+ */
+ public function saveSite( Site $site ) {
+ return $this->saveSites( array( $site ) );
+ }
+
+ /**
+ * @see SiteStore::saveSites
+ *
+ * @since 1.21
+ *
+ * @param Site[] $sites
+ *
+ * @return boolean Success indicator
+ */
+ public function saveSites( array $sites ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( empty( $sites ) ) {
+ wfProfileOut( __METHOD__ );
+ return true;
+ }
+
+ $dbw = $this->sitesTable->getWriteDbConnection();
+
+ $trx = $dbw->trxLevel();
+
+ if ( $trx == 0 ) {
+ $dbw->begin( __METHOD__ );
+ }
+
+ $success = true;
+
+ $internalIds = array();
+ $localIds = array();
+
+ foreach ( $sites as $site ) {
+ if ( $site->getInternalId() !== null ) {
+ $internalIds[] = $site->getInternalId();
+ }
+
+ $siteRow = $this->getRowFromSite( $site );
+ $success = $siteRow->save( __METHOD__ ) && $success;
+
+ foreach ( $site->getLocalIds() as $idType => $ids ) {
+ foreach ( $ids as $id ) {
+ $localIds[] = array( $siteRow->getId(), $idType, $id );
+ }
+ }
+ }
+
+ if ( $internalIds !== array() ) {
+ $dbw->delete(
+ 'site_identifiers',
+ array( 'si_site' => $internalIds ),
+ __METHOD__
+ );
+ }
+
+ foreach ( $localIds as $localId ) {
+ $dbw->insert(
+ 'site_identifiers',
+ array(
+ 'si_site' => $localId[0],
+ 'si_type' => $localId[1],
+ 'si_key' => $localId[2],
+ ),
+ __METHOD__
+ );
+ }
+
+ if ( $trx == 0 ) {
+ $dbw->commit( __METHOD__ );
+ }
+
+ // purge cache
+ $this->reset();
+
+ wfProfileOut( __METHOD__ );
+ return $success;
+ }
+
+ /**
+ * Purges the internal and external cache of the site list, forcing the list
+ * of sites to be re-read from the database.
+ *
+ * @since 1.21
+ */
+ public function reset() {
+ wfProfileIn( __METHOD__ );
+ // purge cache
+ $cache = wfGetMainCache();
+ $cache->delete( $this->getCacheKey() );
+ $this->sites = null;
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Clears the list of sites stored in the database.
+ *
+ * @see SiteStore::clear()
+ *
+ * @return bool success
+ */
+ public function clear() {
+ wfProfileIn( __METHOD__ );
+ $dbw = $this->sitesTable->getWriteDbConnection();
+
+ $trx = $dbw->trxLevel();
+
+ if ( $trx == 0 ) {
+ $dbw->begin( __METHOD__ );
+ }
+
+ $ok = $dbw->delete( 'sites', '*', __METHOD__ );
+ $ok = $dbw->delete( 'site_identifiers', '*', __METHOD__ ) && $ok;
+
+ if ( $trx == 0 ) {
+ $dbw->commit( __METHOD__ );
+ }
+
+ $this->reset();
+
+ wfProfileOut( __METHOD__ );
+ return $ok;
+ }
+
+ /**
+ * @since 1.21
+ *
+ * @return ORMTable
+ */
+ protected function newSitesTable() {
+ return new ORMTable(
+ 'sites',
+ array(
+ 'id' => 'id',
+
+ // Site data
+ 'global_key' => 'str',
+ 'type' => 'str',
+ 'group' => 'str',
+ 'source' => 'str',
+ 'language' => 'str',
+ 'protocol' => 'str',
+ 'domain' => 'str',
+ 'data' => 'array',
+
+ // Site config
+ 'forward' => 'bool',
+ 'config' => 'array',
+ ),
+ array(
+ 'type' => Site::TYPE_UNKNOWN,
+ 'group' => Site::GROUP_NONE,
+ 'source' => Site::SOURCE_LOCAL,
+ 'data' => array(),
+
+ 'forward' => false,
+ 'config' => array(),
+ 'language' => '',
+ ),
+ 'ORMRow',
+ 'site_'
+ );
+ }
+
+}
+
+/**
+ * @deprecated
+ */
+class Sites extends SiteSQLStore {
+
+ /**
+ * Factory for creating new site objects.
+ *
+ * @since 1.21
+ * @deprecated
+ *
+ * @param string|boolean false $globalId
+ *
+ * @return Site
+ */
+ public static function newSite( $globalId = false ) {
+ $site = new Site();
+
+ if ( $globalId !== false ) {
+ $site->setGlobalId( $globalId );
+ }
+
+ return $site;
+ }
+
+ /**
+ * @deprecated
+ * @return SiteStore
+ */
+ public static function singleton() {
+ static $singleton;
+
+ if ( $singleton === null ) {
+ $singleton = new static();
+ }
+
+ return $singleton;
+ }
+
+ /**
+ * @deprecated
+ * @return SiteList
+ */
+ public function getSiteGroup( $group ) {
+ return $this->getSites()->getGroup( $group );
+ }
+
+}
diff --git a/includes/site/SiteStore.php b/includes/site/SiteStore.php
new file mode 100644
index 00000000..52ba8fbf
--- /dev/null
+++ b/includes/site/SiteStore.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * Interface for service objects providing a storage interface for Site objects.
+ *
+ * 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
+ *
+ * @since 1.21
+ *
+ * @file
+ * @ingroup Site
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+interface SiteStore {
+
+ /**
+ * Saves the provided site.
+ *
+ * @since 1.21
+ *
+ * @param Site $site
+ *
+ * @return boolean Success indicator
+ */
+ public function saveSite( Site $site );
+
+ /**
+ * Saves the provided sites.
+ *
+ * @since 1.21
+ *
+ * @param Site[] $sites
+ *
+ * @return boolean Success indicator
+ */
+ public function saveSites( array $sites );
+
+ /**
+ * Returns the site with provided global id, or null if there is no such site.
+ *
+ * @since 1.21
+ *
+ * @param string $globalId
+ * @param string $source either 'cache' or 'recache'.
+ * If 'cache', the values are allowed (but not obliged) to come from a cache.
+ *
+ * @return Site|null
+ */
+ public function getSite( $globalId, $source = 'cache' );
+
+ /**
+ * Returns a list of all sites. By default this site is
+ * fetched from the cache, which can be changed to loading
+ * the list from the database using the $useCache parameter.
+ *
+ * @since 1.21
+ *
+ * @param string $source either 'cache' or 'recache'.
+ * If 'cache', the values are allowed (but not obliged) to come from a cache.
+ *
+ * @return SiteList
+ */
+ public function getSites( $source = 'cache' );
+
+ /**
+ * Deletes all sites from the database. After calling clear(), getSites() will return an empty
+ * list and getSite() will return null until saveSite() or saveSites() is called.
+ */
+ public function clear();
+}
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php
index c5aa2389..705dab55 100644
--- a/includes/specials/SpecialActiveusers.php
+++ b/includes/specials/SpecialActiveusers.php
@@ -50,7 +50,7 @@ class ActiveUsersPager extends UsersPager {
/**
* @param $context IContextSource
* @param $group null Unused
- * @param $par string Parameter passed to the page
+ * @param string $par Parameter passed to the page
*/
function __construct( IContextSource $context = null, $group = null, $par = null ) {
global $wgActiveUserDays;
@@ -62,7 +62,7 @@ class ActiveUsersPager extends UsersPager {
$this->requestedUser = '';
if ( $un != '' ) {
$username = Title::makeTitleSafe( NS_USER, $un );
- if( !is_null( $username ) ) {
+ if ( !is_null( $username ) ) {
$this->requestedUser = $username->getText();
}
}
@@ -91,39 +91,60 @@ class ActiveUsersPager extends UsersPager {
}
function getQueryInfo() {
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = $this->getDatabase();
+
$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 != ' . $dbr->addQuotes( 'newusers' );
- $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $this->RCMaxAge*24*3600 ) );
+ $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes(
+ $dbr->timestamp( wfTimestamp( TS_UNIX ) - $this->RCMaxAge * 24 * 3600 ) );
- if( $this->requestedUser != '' ) {
+ if ( $this->requestedUser != '' ) {
$conds[] = 'rc_user_text >= ' . $dbr->addQuotes( $this->requestedUser );
}
- $query = array(
- 'tables' => array( 'recentchanges', 'user', 'ipblocks' ),
- 'fields' => array( 'user_name' => 'rc_user_text', // inheritance
+ if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
+ $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText(
+ 'ipblocks', '1', array( 'rc_user=ipb_user', 'ipb_deleted' => 1 )
+ ) . ')';
+ }
+
+ return array(
+ 'tables' => array( 'recentchanges' ),
+ 'fields' => array(
+ 'user_name' => 'rc_user_text', // for Pager inheritance
'rc_user_text', // for Pager
- 'user_id',
- 'recentedits' => 'COUNT(*)',
- 'blocked' => 'MAX(ipb_user)'
+ 'user_id' => 'MAX(rc_user)', // Postgres
+ 'recentedits' => 'COUNT(*)'
),
'options' => array(
- 'GROUP BY' => array( 'rc_user_text', 'user_id' ),
+ 'GROUP BY' => array( 'rc_user_text' ),
'USE INDEX' => array( 'recentchanges' => 'rc_user_text' )
),
- 'join_conds' => array(
- 'user' => array( 'INNER JOIN', 'rc_user_text=user_name' ),
- 'ipblocks' => array( 'LEFT JOIN', array(
- 'user_id=ipb_user',
- 'ipb_auto' => 0,
- 'ipb_deleted' => 1
- )),
- ),
'conds' => $conds
);
- return $query;
+ }
+
+ function doBatchLookups() {
+ $uids = array();
+ foreach ( $this->mResult as $row ) {
+ $uids[] = $row->user_id;
+ }
+ // Fetch the block status of the user for showing "(blocked)" text and for
+ // striking out names of suppressed users when privileged user views the list.
+ // Although the first query already hits the block table for un-privileged, this
+ // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct.
+ $dbr = $this->getDatabase();
+ $res = $dbr->select( 'ipblocks',
+ array( 'ipb_user', 'MAX(ipb_deleted) AS block_status' ),
+ array( 'ipb_user' => $uids ),
+ __METHOD__,
+ array( 'GROUP BY' => array( 'ipb_user' ) )
+ );
+ $this->blockStatusByUid = array();
+ foreach ( $res as $row ) {
+ $this->blockStatusByUid[$row->ipb_user] = $row->block_status; // 0 or 1
+ }
+ $this->mResult->seek( 0 );
}
function formatRow( $row ) {
@@ -138,7 +159,7 @@ class ActiveUsersPager extends UsersPager {
$user = User::newFromId( $row->user_id );
// User right filter
- foreach( $this->hideRights as $right ) {
+ foreach ( $this->hideRights as $right ) {
// Calling User::getRights() within the loop so that
// if the hideRights() filter is empty, we don't have to
// trigger the lazy-init of the big userrights array in the
@@ -152,7 +173,7 @@ class ActiveUsersPager extends UsersPager {
// Note: This is a different loop than for user rights,
// because we're reusing it to build the group links
// at the same time
- foreach( $user->getGroups() as $group ) {
+ foreach ( $user->getGroups() as $group ) {
if ( in_array( $group, $this->hideGroups ) ) {
return '';
}
@@ -162,9 +183,14 @@ class ActiveUsersPager extends UsersPager {
$groups = $lang->commaList( $list );
$item = $lang->specialList( $ulinks, $groups );
+
+ $isBlocked = isset( $this->blockStatusByUid[$row->user_id] );
+ if ( $isBlocked && $this->blockStatusByUid[$row->user_id] == 1 ) {
+ $item = "<span class=\"deleted\">$item</span>";
+ }
$count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits )
->params( $userName )->numParams( $this->RCMaxAge )->escaped();
- $blocked = $row->blocked ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : '';
+ $blocked = $isBlocked ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : '';
return Html::rawElement( 'li', array(), "{$item} [{$count}]{$blocked}" );
}
@@ -180,15 +206,15 @@ class ActiveUsersPager extends UsersPager {
$out .= Html::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n";
$out .= Xml::inputLabel( $this->msg( 'activeusers-from' )->text(),
- 'username', 'offset', 20, $this->requestedUser ) . '<br />';# Username field
+ 'username', 'offset', 20, $this->requestedUser, array( 'tabindex' => 1 ) ) . '<br />';# Username field
$out .= Xml::checkLabel( $this->msg( 'activeusers-hidebots' )->text(),
- 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ) );
+ 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ), array( 'tabindex' => 2 ) );
$out .= Xml::checkLabel( $this->msg( 'activeusers-hidesysops' )->text(),
- 'hidesysops', 'hidesysops', $this->opts->getValue( 'hidesysops' ) ) . '<br />';
+ 'hidesysops', 'hidesysops', $this->opts->getValue( 'hidesysops' ), array( 'tabindex' => 3 ) ) . '<br />';
- $out .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n";# Submit button and form bottom
+ $out .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text(), array( 'tabindex' => 4 ) ) . "\n";# Submit button and form bottom
$out .= Xml::closeElement( 'fieldset' );
$out .= Xml::closeElement( 'form' );
@@ -240,4 +266,7 @@ class SpecialActiveUsers extends SpecialPage {
}
}
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php
index fe9d41e5..35d6a0c0 100644
--- a/includes/specials/SpecialAllmessages.php
+++ b/includes/specials/SpecialAllmessages.php
@@ -28,7 +28,6 @@
* @ingroup SpecialPage
*/
class SpecialAllmessages extends SpecialPage {
-
/**
* @var AllmessagesTablePager
*/
@@ -53,8 +52,9 @@ class SpecialAllmessages extends SpecialPage {
$this->setHeaders();
global $wgUseDatabaseMessages;
- if( !$wgUseDatabaseMessages ) {
+ if ( !$wgUseDatabaseMessages ) {
$out->addWikiMsg( 'allmessagesnotsupportedDB' );
+
return;
} else {
$this->outputHeader( 'allmessagestext' );
@@ -74,9 +74,11 @@ class SpecialAllmessages extends SpecialPage {
$this->table->getNavigationBar() .
$this->table->getBody() .
$this->table->getNavigationBar() );
-
}
+ protected function getGroupName() {
+ return 'wiki';
+ }
}
/**
@@ -84,7 +86,6 @@ class SpecialAllmessages extends SpecialPage {
* getting data from a table when in fact not all of it comes from the database.
*/
class AllmessagesTablePager extends TablePager {
-
protected $filter, $prefix, $langcode, $displayPrefix;
public $mLimitsShown;
@@ -113,20 +114,20 @@ class AllmessagesTablePager extends TablePager {
$this->lang = ( $langObj ? $langObj : $wgContLang );
$this->langcode = $this->lang->getCode();
- $this->foreign = $this->langcode != $wgContLang->getCode();
+ $this->foreign = $this->langcode != $wgContLang->getCode();
$request = $this->getRequest();
$this->filter = $request->getVal( 'filter', 'all' );
- if( $this->filter === 'all' ){
+ if ( $this->filter === 'all' ) {
$this->custom = null; // So won't match in either case
} else {
- $this->custom = ($this->filter == 'unmodified');
+ $this->custom = ( $this->filter == 'unmodified' );
}
$prefix = $this->getLanguage()->ucfirst( $request->getVal( 'prefix', '' ) );
$prefix = $prefix != '' ? Title::makeTitleSafe( NS_MEDIAWIKI, $request->getVal( 'prefix', null ) ) : null;
- if( $prefix !== null ){
+ if ( $prefix !== null ) {
$this->displayPrefix = $prefix->getDBkey();
$this->prefix = '/^' . preg_quote( $this->displayPrefix ) . '/i';
} else {
@@ -136,7 +137,7 @@ class AllmessagesTablePager extends TablePager {
// 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 ) {
+ if ( $this->foreign ) {
$this->suffix = '/' . $this->langcode;
} else {
$this->suffix = '';
@@ -150,42 +151,42 @@ class AllmessagesTablePager extends TablePager {
$msg = wfMessage( 'allmessages-language' );
$langSelect = Xml::languageSelector( $this->langcode, false, null, $attrs, $msg );
- $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-allmessages-form' ) ) .
+ $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-allmessages-form' ) ) .
Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
Xml::openElement( 'table', array( 'class' => 'mw-allmessages-table' ) ) . "\n" .
'<tr>
<td class="mw-label">' .
- Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) .
- "</td>\n
- <td class=\"mw-input\">" .
- Xml::input( 'prefix', 20, str_replace( '_', ' ', $this->displayPrefix ), array( 'id' => 'mw-allmessages-form-prefix' ) ) .
- "</td>\n
+ Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) .
+ "</td>\n
+ <td class=\"mw-input\">" .
+ Xml::input( 'prefix', 20, str_replace( '_', ' ', $this->displayPrefix ), array( 'id' => 'mw-allmessages-form-prefix' ) ) .
+ "</td>\n
</tr>
<tr>\n
- <td class='mw-label'>" .
- $this->msg( 'allmessages-filter' )->escaped() .
- "</td>\n
+ <td class='mw-label'>" .
+ $this->msg( 'allmessages-filter' )->escaped() .
+ "</td>\n
<td class='mw-input'>" .
- Xml::radioLabel( $this->msg( 'allmessages-filter-unmodified' )->text(),
- 'filter',
- 'unmodified',
- 'mw-allmessages-form-filter-unmodified',
- ( $this->filter == 'unmodified' )
- ) .
- Xml::radioLabel( $this->msg( 'allmessages-filter-all' )->text(),
- 'filter',
- 'all',
- 'mw-allmessages-form-filter-all',
- ( $this->filter == 'all' )
- ) .
- Xml::radioLabel( $this->msg( 'allmessages-filter-modified' )->text(),
- 'filter',
- 'modified',
- 'mw-allmessages-form-filter-modified',
- ( $this->filter == 'modified' )
- ) .
- "</td>\n
+ Xml::radioLabel( $this->msg( 'allmessages-filter-unmodified' )->text(),
+ 'filter',
+ 'unmodified',
+ 'mw-allmessages-form-filter-unmodified',
+ ( $this->filter == 'unmodified' )
+ ) .
+ Xml::radioLabel( $this->msg( 'allmessages-filter-all' )->text(),
+ 'filter',
+ 'all',
+ 'mw-allmessages-form-filter-all',
+ ( $this->filter == 'all' )
+ ) .
+ Xml::radioLabel( $this->msg( 'allmessages-filter-modified' )->text(),
+ 'filter',
+ 'modified',
+ 'mw-allmessages-form-filter-modified',
+ ( $this->filter == 'modified' )
+ ) .
+ "</td>\n
</tr>
<tr>\n
<td class=\"mw-label\">" . $langSelect[0] . "</td>\n
@@ -194,29 +195,30 @@ class AllmessagesTablePager extends TablePager {
'<tr>
<td class="mw-label">' .
- Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) .
- '</td>
- <td class="mw-input">' .
- $this->getLimitSelect() .
- '</td>
+ Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) .
+ '</td>
+ <td class="mw-input">' .
+ $this->getLimitSelect() .
+ '</td>
<tr>
<td></td>
<td>' .
- Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) .
- "</td>\n
+ Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) .
+ "</td>\n
</tr>" .
Xml::closeElement( 'table' ) .
$this->getHiddenFields( array( 'title', 'prefix', 'filter', 'lang', 'limit' ) ) .
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' );
+
return $out;
}
function getAllMessages( $descending ) {
wfProfileIn( __METHOD__ );
$messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' );
- if( $descending ){
+ if ( $descending ) {
rsort( $messageNames );
} else {
asort( $messageNames );
@@ -226,6 +228,7 @@ class AllmessagesTablePager extends TablePager {
$messageNames = array_map( array( $this->lang, 'ucfirst' ), $messageNames );
wfProfileOut( __METHOD__ );
+
return $messageNames;
}
@@ -257,18 +260,19 @@ class AllmessagesTablePager extends TablePager {
foreach ( $res as $s ) {
$exists = false;
- if( $foreign ) {
+ if ( $foreign ) {
$title = explode( '/', $s->page_title );
- if( count( $title ) === 2 && $langcode == $title[1]
- && isset( $xNames[$title[0]] ) ) {
+ if ( count( $title ) === 2 && $langcode == $title[1]
+ && isset( $xNames[$title[0]] )
+ ) {
$exists = $title[0];
}
- } elseif( isset( $xNames[$s->page_title] ) ) {
+ } elseif ( isset( $xNames[$s->page_title] ) ) {
$exists = $s->page_title;
}
- if( $exists && $s->page_namespace == NS_MEDIAWIKI ) {
+ if ( $exists && $s->page_namespace == NS_MEDIAWIKI ) {
$pageFlags[$exists] = true;
- } elseif( $exists && $s->page_namespace == NS_MEDIAWIKI_TALK ) {
+ } elseif ( $exists && $s->page_namespace == NS_MEDIAWIKI_TALK ) {
$talkFlags[$exists] = true;
}
}
@@ -281,6 +285,9 @@ class AllmessagesTablePager extends TablePager {
/**
* This function normally does a database query to get the results; we need
* to make a pretend result using a FakeResultWrapper.
+ * @param string $offset
+ * @param int $limit
+ * @param bool $descending
* @return FakeResultWrapper
*/
function reallyDoQuery( $offset, $limit, $descending ) {
@@ -290,27 +297,29 @@ class AllmessagesTablePager extends TablePager {
$statuses = self::getCustomisedStatuses( $messageNames, $this->langcode, $this->foreign );
$count = 0;
- foreach( $messageNames as $key ) {
+ foreach ( $messageNames as $key ) {
$customised = isset( $statuses['pages'][$key] );
- if( $customised !== $this->custom &&
+ if ( $customised !== $this->custom &&
( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) &&
( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false )
) {
$actual = wfMessage( $key )->inLanguage( $this->langcode )->plain();
$default = wfMessage( $key )->inLanguage( $this->langcode )->useDatabase( false )->plain();
$result->result[] = array(
- 'am_title' => $key,
- 'am_actual' => $actual,
- 'am_default' => $default,
+ 'am_title' => $key,
+ 'am_actual' => $actual,
+ 'am_default' => $default,
'am_customised' => $customised,
'am_talk_exists' => isset( $statuses['talks'][$key] )
);
$count++;
}
- if( $count == $limit ) {
+
+ if ( $count == $limit ) {
break;
}
}
+
return $result;
}
@@ -318,28 +327,26 @@ class AllmessagesTablePager extends TablePager {
return Xml::openElement( 'table', array( 'class' => 'mw-datatable TablePager', 'id' => 'mw-allmessagestable' ) ) . "\n" .
"<thead><tr>
<th rowspan=\"2\">" .
- $this->msg( 'allmessagesname' )->escaped() . "
+ $this->msg( 'allmessagesname' )->escaped() . "
</th>
<th>" .
- $this->msg( 'allmessagesdefault' )->escaped() .
- "</th>
+ $this->msg( 'allmessagesdefault' )->escaped() .
+ "</th>
</tr>\n
<tr>
<th>" .
- $this->msg( 'allmessagescurrent' )->escaped() .
- "</th>
+ $this->msg( 'allmessagescurrent' )->escaped() .
+ "</th>
</tr></thead><tbody>\n";
}
- function formatValue( $field, $value ){
- switch( $field ){
-
+ function formatValue( $field, $value ) {
+ switch ( $field ) {
case 'am_title' :
-
$title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix );
- $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix );
+ $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix );
- if( $this->mCurrentRow->am_customised ){
+ if ( $this->mCurrentRow->am_customised ) {
$title = Linker::linkKnown( $title, $this->getLanguage()->lcfirst( $value ) );
} else {
$title = Linker::link(
@@ -351,7 +358,7 @@ class AllmessagesTablePager extends TablePager {
);
}
if ( $this->mCurrentRow->am_talk_exists ) {
- $talk = Linker::linkKnown( $talk , $this->talk );
+ $talk = Linker::linkKnown( $talk, $this->talk );
} else {
$talk = Linker::link(
$talk,
@@ -361,6 +368,7 @@ class AllmessagesTablePager extends TablePager {
array( 'broken' )
);
}
+
return $title . ' ' . $this->msg( 'parentheses' )->rawParams( $talk )->escaped();
case 'am_default' :
@@ -370,12 +378,12 @@ class AllmessagesTablePager extends TablePager {
return '';
}
- function formatRow( $row ){
+ 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 ){
+ if ( $row->am_customised ) {
$s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) );
$formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) );
if ( $formatted == '' ) {
@@ -384,24 +392,26 @@ class AllmessagesTablePager extends TablePager {
$s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted )
. "</tr>\n";
}
+
return $s;
}
- function getRowAttrs( $row, $isSecond = false ){
+ function getRowAttrs( $row, $isSecond = false ) {
$arr = array();
- if( $row->am_customised ){
+ if ( $row->am_customised ) {
$arr['class'] = 'allmessages-customised';
}
- if( !$isSecond ){
+ if ( !$isSecond ) {
$arr['id'] = Sanitizer::escapeId( 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) );
}
+
return $arr;
}
- function getCellAttrs( $field, $value ){
- if( $this->mCurrentRow->am_customised && $field == 'am_title' ){
+ function getCellAttrs( $field, $value ) {
+ if ( $this->mCurrentRow->am_customised && $field == 'am_title' ) {
return array( 'rowspan' => '2', 'class' => $field );
- } elseif( $field == 'am_title' ) {
+ } elseif ( $field == 'am_title' ) {
return array( 'class' => $field );
} else {
return array( 'lang' => $this->langcode, 'dir' => $this->lang->getDir(), 'class' => $field );
@@ -420,16 +430,15 @@ class AllmessagesTablePager extends TablePager {
return SpecialPage::getTitleFor( 'Allmessages', false );
}
- function isFieldSortable( $x ){
+ function isFieldSortable( $x ) {
return false;
}
- function getDefaultSort(){
+ function getDefaultSort() {
return '';
}
- function getQueryInfo(){
+ function getQueryInfo() {
return '';
}
}
-
diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php
index 0f8b2557..a0820493 100644
--- a/includes/specials/SpecialAllpages.php
+++ b/includes/specials/SpecialAllpages.php
@@ -59,19 +59,18 @@ class SpecialAllpages extends IncludableSpecialPage {
/**
* Constructor
*
- * @param $name string: name of the special page, as seen in links and URLs (default: 'Allpages')
+ * @param string $name name of the special page, as seen in links and URLs (default: 'Allpages')
*/
- function __construct( $name = 'Allpages' ){
+ function __construct( $name = 'Allpages' ) {
parent::__construct( $name );
}
/**
* Entry point : initialise variables and call subfunctions.
*
- * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
+ * @param string $par becomes "FOO" when called like Special:Allpages/FOO (default NULL)
*/
function execute( $par ) {
- global $wgContLang;
$request = $this->getRequest();
$out = $this->getOutput();
@@ -85,18 +84,18 @@ class SpecialAllpages extends IncludableSpecialPage {
$namespace = $request->getInt( 'namespace' );
$hideredirects = $request->getBool( 'hideredirects', false );
- $namespaces = $wgContLang->getNamespaces();
+ $namespaces = $this->getContext()->getLanguage()->getNamespaces();
$out->setPageTitle(
- ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ?
- $this->msg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
- $this->msg( 'allarticles' )
+ ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) ) ?
+ $this->msg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
+ $this->msg( 'allarticles' )
);
$out->addModuleStyles( 'mediawiki.special' );
- if( $par !== null ) {
+ if ( $par !== null ) {
$this->showChunk( $namespace, $par, $to, $hideredirects );
- } elseif( $from !== null && $to === null ) {
+ } elseif ( $from !== null && $to === null ) {
$this->showChunk( $namespace, $from, $to, $hideredirects );
} else {
$this->showToplevel( $namespace, $from, $to, $hideredirects );
@@ -107,16 +106,16 @@ class SpecialAllpages extends IncludableSpecialPage {
* HTML for the top form
*
* @param $namespace Integer: a namespace constant (default NS_MAIN).
- * @param $from String: dbKey we are starting listing at.
- * @param $to String: dbKey we are ending listing at.
- * @param $hideredirects Bool: dont show redirects (default FALSE)
+ * @param string $from dbKey we are starting listing at.
+ * @param string $to dbKey we are ending listing at.
+ * @param bool $hideredirects dont show redirects (default FALSE)
* @return string
*/
function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) {
global $wgScript;
$t = $this->getTitle();
- $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
+ $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
$out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
$out .= Html::hidden( 'title', $t->getPrefixedText() );
$out .= Xml::openElement( 'fieldset' );
@@ -127,7 +126,7 @@ class SpecialAllpages extends IncludableSpecialPage {
Xml::label( $this->msg( 'allpagesfrom' )->text(), 'nsfrom' ) .
" </td>
<td class='mw-input'>" .
- Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
+ Xml::input( 'from', 30, str_replace( '_', ' ', $from ), array( 'id' => 'nsfrom' ) ) .
" </td>
</tr>
<tr>
@@ -135,7 +134,7 @@ class SpecialAllpages extends IncludableSpecialPage {
Xml::label( $this->msg( 'allpagesto' )->text(), 'nsto' ) .
" </td>
<td class='mw-input'>" .
- Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) .
+ Xml::input( 'to', 30, str_replace( '_', ' ', $to ), array( 'id' => 'nsto' ) ) .
" </td>
</tr>
<tr>
@@ -160,14 +159,15 @@ class SpecialAllpages extends IncludableSpecialPage {
$out .= Xml::closeElement( 'fieldset' );
$out .= Xml::closeElement( 'form' );
$out .= Xml::closeElement( 'div' );
+
return $out;
}
/**
* @param $namespace Integer (default NS_MAIN)
- * @param $from String: list all pages from this name
- * @param $to String: list all pages to this name
- * @param $hideredirects Bool: dont show redirects (default FALSE)
+ * @param string $from list all pages from this name
+ * @param string $to list all pages to this name
+ * @param bool $hideredirects dont show redirects (default FALSE)
*/
function showToplevel( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) {
$output = $this->getOutput();
@@ -180,7 +180,7 @@ class SpecialAllpages extends IncludableSpecialPage {
$where = array( 'page_namespace' => $namespace );
if ( $hideredirects ) {
- $where[ 'page_is_redirect' ] = 0;
+ $where['page_is_redirect'] = 0;
}
$from = Title::makeTitleSafe( $namespace, $from );
@@ -188,20 +188,23 @@ class SpecialAllpages extends IncludableSpecialPage {
$from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null;
$to = ( $to && $to->isLocal() ) ? $to->getDBkey() : null;
- if( isset($from) )
- $where[] = 'page_title >= '.$dbr->addQuotes( $from );
- if( isset($to) )
- $where[] = 'page_title <= '.$dbr->addQuotes( $to );
+ if ( isset( $from ) ) {
+ $where[] = 'page_title >= ' . $dbr->addQuotes( $from );
+ }
+
+ if ( isset( $to ) ) {
+ $where[] = 'page_title <= ' . $dbr->addQuotes( $to );
+ }
global $wgMemc;
- $key = wfMemcKey( 'allpages', 'ns', $namespace, $from, $to );
+ $key = wfMemcKey( 'allpages', 'ns', $namespace, sha1( $from ), sha1( $to ) );
$lines = $wgMemc->get( $key );
$count = $dbr->estimateRowCount( 'page', '*', $where, __METHOD__ );
- $maxPerSubpage = intval($count/$this->maxLineCount);
- $maxPerSubpage = max($maxPerSubpage,$this->maxPerPage);
+ $maxPerSubpage = intval( $count / $this->maxLineCount );
+ $maxPerSubpage = max( $maxPerSubpage, $this->maxPerPage );
- if( !is_array( $lines ) ) {
+ if ( !is_array( $lines ) ) {
$options = array( 'LIMIT' => 1 );
$options['ORDER BY'] = 'page_title ASC';
$firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
@@ -210,31 +213,32 @@ class SpecialAllpages extends IncludableSpecialPage {
$lines = array( $firstTitle );
# If we are going to show n rows, we need n+1 queries to find the relevant titles.
$done = false;
- while( !$done ) {
+ while ( !$done ) {
// Fetch the last title of this chunk and the first of the next
$chunk = ( $lastTitle === false )
? array()
: array( 'page_title >= ' . $dbr->addQuotes( $lastTitle ) );
$res = $dbr->select( 'page', /* FROM */
'page_title', /* WHAT */
- array_merge($where,$chunk),
+ array_merge( $where, $chunk ),
__METHOD__,
- array ('LIMIT' => 2, 'OFFSET' => $maxPerSubpage - 1, 'ORDER BY' => 'page_title ASC')
+ array( 'LIMIT' => 2, 'OFFSET' => $maxPerSubpage - 1, 'ORDER BY' => 'page_title ASC' )
);
$s = $dbr->fetchObject( $res );
- if( $s ) {
+ if ( $s ) {
array_push( $lines, $s->page_title );
} else {
// Final chunk, but ended prematurely. Go back and find the end.
$endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
- array_merge($where,$chunk),
+ array_merge( $where, $chunk ),
__METHOD__ );
array_push( $lines, $endTitle );
$done = true;
}
+
$s = $res->fetchObject();
- if( $s ) {
+ if ( $s ) {
array_push( $lines, $s->page_title );
$lastTitle = $s->page_title;
} else {
@@ -249,18 +253,19 @@ class SpecialAllpages extends IncludableSpecialPage {
// If there are only two or less sections, don't even display them.
// Instead, display the first section directly.
- if( count( $lines ) <= 2 ) {
- if( !empty($lines) ) {
+ if ( count( $lines ) <= 2 ) {
+ if ( !empty( $lines ) ) {
$this->showChunk( $namespace, $from, $to, $hideredirects );
} else {
$output->addHTML( $this->namespaceForm( $namespace, $from, $to, $hideredirects ) );
}
+
return;
}
# At this point, $lines should contain an even number of elements.
$out .= Xml::openElement( 'table', array( 'class' => 'allpageslist' ) );
- while( count ( $lines ) > 0 ) {
+ while ( count( $lines ) > 0 ) {
$inpoint = array_shift( $lines );
$outpoint = array_shift( $lines );
$out .= $this->showline( $inpoint, $outpoint, $namespace, $hideredirects );
@@ -269,19 +274,19 @@ class SpecialAllpages extends IncludableSpecialPage {
$nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects );
# Is there more?
- if( $this->including() ) {
+ if ( $this->including() ) {
$out2 = '';
} else {
- if( isset($from) || isset($to) ) {
- $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ).
- '<tr>
+ if ( isset( $from ) || isset( $to ) ) {
+ $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ) .
+ '<tr>
<td>' .
- $nsForm .
- '</td>
+ $nsForm .
+ '</td>
<td class="mw-allpages-nav">' .
- Linker::link( $this->getTitle(), $this->msg( 'allpages' )->escaped(),
- array(), array(), 'known' ) .
- "</td>
+ Linker::link( $this->getTitle(), $this->msg( 'allpages' )->escaped(),
+ array(), array(), 'known' ) .
+ "</td>
</tr>" .
Xml::closeElement( 'table' );
} else {
@@ -294,50 +299,59 @@ class SpecialAllpages extends IncludableSpecialPage {
/**
* Show a line of "ABC to DEF" ranges of articles
*
- * @param $inpoint String: lower limit of pagenames
- * @param $outpoint String: upper limit of pagenames
+ * @param string $inpoint lower limit of pagenames
+ * @param string $outpoint upper limit of pagenames
* @param $namespace Integer (Default NS_MAIN)
- * @param $hideredirects Bool: dont show redirects (default FALSE)
+ * @param bool $hideRedirects don't show redirects. Default: false
* @return string
*/
- function showline( $inpoint, $outpoint, $namespace = NS_MAIN, $hideredirects ) {
+ function showline( $inpoint, $outpoint, $namespace = NS_MAIN, $hideRedirects = false ) {
+ // Use content language since page titles are considered to use content language
global $wgContLang;
- $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
- $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
+
+ $inpointf = str_replace( '_', ' ', $inpoint );
+ $outpointf = str_replace( '_', ' ', $outpoint );
+
// Don't let the length runaway
$inpointf = $wgContLang->truncate( $inpointf, $this->maxPageLength );
$outpointf = $wgContLang->truncate( $outpointf, $this->maxPageLength );
- $queryparams = $namespace ? "namespace=$namespace&" : '';
+ $queryParams = array(
+ 'from' => $inpoint,
+ 'to' => $outpoint,
+ );
- $queryhideredirects = array();
- if ($hideredirects) {
- $queryhideredirects[ 'hideredirects' ] = 1;
+ if ( $namespace ) {
+ $queryParams['namespace'] = $namespace;
+ }
+ if ( $hideRedirects ) {
+ $queryParams['hideredirects'] = 1;
}
- $special = $this->getTitle();
- $link = htmlspecialchars( $special->getLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint), $queryhideredirects ) );
+ $url = $this->getTitle()->getLocalURL( $queryParams );
+ $inlink = Html::element( 'a', array( 'href' => $url ), $inpointf );
+ $outlink = Html::element( 'a', array( 'href' => $url ), $outpointf );
$out = $this->msg( 'alphaindexline' )->rawParams(
- "<a href=\"$link\">$inpointf</a></td><td>",
- "</td><td><a href=\"$link\">$outpointf</a>"
+ "$inlink</td><td>",
+ "</td><td>$outlink"
)->escaped();
+
return '<tr><td class="mw-allpages-alphaindexline">' . $out . '</td></tr>';
}
/**
- * @param $namespace Integer (Default NS_MAIN)
- * @param $from String: list all pages from this name (default FALSE)
- * @param $to String: list all pages to this name (default FALSE)
- * @param $hideredirects Bool: dont show redirects (default FALSE)
+ * @param int $namespace Namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ * @param string $to list all pages to this name (default FALSE)
+ * @param bool $hideredirects dont show redirects (default FALSE)
*/
function showChunk( $namespace = NS_MAIN, $from = false, $to = false, $hideredirects = false ) {
- global $wgContLang;
$output = $this->getOutput();
- $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $fromList = $this->getNamespaceKeyAndText( $namespace, $from );
$toList = $this->getNamespaceKeyAndText( $namespace, $to );
- $namespaces = $wgContLang->getNamespaces();
+ $namespaces = $this->getContext()->getLanguage()->getNamespaces();
$n = 0;
if ( !$fromList || !$toList ) {
@@ -357,10 +371,10 @@ class SpecialAllpages extends IncludableSpecialPage {
);
if ( $hideredirects ) {
- $conds[ 'page_is_redirect' ] = 0;
+ $conds['page_is_redirect'] = 0;
}
- if( $toKey !== "" ) {
+ if ( $toKey !== "" ) {
$conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey );
}
@@ -369,33 +383,36 @@ class SpecialAllpages extends IncludableSpecialPage {
$conds,
__METHOD__,
array(
- 'ORDER BY' => 'page_title',
- 'LIMIT' => $this->maxPerPage + 1,
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
'USE INDEX' => 'name_title',
)
);
- if( $res->numRows() > 0 ) {
+ if ( $res->numRows() > 0 ) {
$out = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-chunk' ) );
- while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
+ while ( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$t = Title::newFromRow( $s );
- if( $t ) {
+ if ( $t ) {
$link = ( $s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
Linker::link( $t ) .
- ($s->page_is_redirect ? '</div>' : '' );
+ ( $s->page_is_redirect ? '</div>' : '' );
} else {
$link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
}
- if( $n % 3 == 0 ) {
+
+ if ( $n % 3 == 0 ) {
$out .= '<tr>';
}
+
$out .= "<td style=\"width:33%\">$link</td>";
$n++;
- if( $n % 3 == 0 ) {
+ if ( $n % 3 == 0 ) {
$out .= "</tr>\n";
}
}
- if( ($n % 3) != 0 ) {
+
+ if ( ( $n % 3 ) != 0 ) {
$out .= "</tr>\n";
}
$out .= Xml::closeElement( 'table' );
@@ -407,7 +424,7 @@ class SpecialAllpages extends IncludableSpecialPage {
if ( $this->including() ) {
$out2 = '';
} else {
- if( $from == '' ) {
+ if ( $from == '' ) {
// First chunk; no previous link.
$prevTitle = null;
} else {
@@ -416,29 +433,29 @@ class SpecialAllpages extends IncludableSpecialPage {
$res_prev = $dbr->select(
'page',
'page_title',
- array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
+ array( 'page_namespace' => $namespace, 'page_title < ' . $dbr->addQuotes( $from ) ),
__METHOD__,
array( 'ORDER BY' => 'page_title DESC',
- 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 )
+ 'LIMIT' => $this->maxPerPage, 'OFFSET' => ( $this->maxPerPage - 1 )
)
);
# Get first title of previous complete chunk
- if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
+ if ( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
$pt = $dbr->fetchObject( $res_prev );
$prevTitle = Title::makeTitle( $namespace, $pt->page_title );
} else {
# The previous chunk is not complete, need to link to the very first title
# available in the database
$options = array( 'LIMIT' => 1 );
- if ( ! $dbr->implicitOrderby() ) {
+ if ( !$dbr->implicitOrderby() ) {
$options['ORDER BY'] = 'page_title';
}
$reallyFirstPage_title = $dbr->selectField( 'page', 'page_title',
array( 'page_namespace' => $namespace ), __METHOD__, $options );
# Show the previous link if it s not the current requested chunk
- if( $from != $reallyFirstPage_title ) {
- $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
+ if ( $from != $reallyFirstPage_title ) {
+ $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
} else {
$prevTitle = null;
}
@@ -448,23 +465,25 @@ class SpecialAllpages extends IncludableSpecialPage {
$self = $this->getTitle();
$nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects );
- $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ).
- '<tr>
+ $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ) .
+ '<tr>
<td>' .
- $nsForm .
- '</td>
+ $nsForm .
+ '</td>
<td class="mw-allpages-nav">' .
- Linker::link( $self, $this->msg( 'allpages' )->escaped() );
+ Linker::link( $self, $this->msg( 'allpages' )->escaped() );
# Do we put a previous link ?
- if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) {
+ if ( isset( $prevTitle ) && $pt = $prevTitle->getText() ) {
$query = array( 'from' => $prevTitle->getText() );
- if( $namespace )
+ if ( $namespace ) {
$query['namespace'] = $namespace;
+ }
- if( $hideredirects )
+ if ( $hideredirects ) {
$query['hideredirects'] = $hideredirects;
+ }
$prevLink = Linker::linkKnown(
$self,
@@ -475,16 +494,18 @@ class SpecialAllpages extends IncludableSpecialPage {
$out2 = $this->getLanguage()->pipeList( array( $out2, $prevLink ) );
}
- if( $n == $this->maxPerPage && $s = $res->fetchObject() ) {
+ if ( $n == $this->maxPerPage && $s = $res->fetchObject() ) {
# $s is the first link of the next chunk
- $t = Title::makeTitle($namespace, $s->page_title);
+ $t = Title::makeTitle( $namespace, $s->page_title );
$query = array( 'from' => $t->getText() );
- if( $namespace )
+ if ( $namespace ) {
$query['namespace'] = $namespace;
+ }
- if( $hideredirects )
+ if ( $hideredirects ) {
$query['hideredirects'] = $hideredirects;
+ }
$nextLink = Linker::linkKnown(
$self,
@@ -500,29 +521,36 @@ class SpecialAllpages extends IncludableSpecialPage {
$output->addHTML( $out2 . $out );
$links = array();
- if ( isset( $prevLink ) ) $links[] = $prevLink;
- if ( isset( $nextLink ) ) $links[] = $nextLink;
+ if ( isset( $prevLink ) ) {
+ $links[] = $prevLink;
+ }
+
+ if ( isset( $nextLink ) ) {
+ $links[] = $nextLink;
+ }
if ( count( $links ) ) {
$output->addHTML(
Html::element( 'hr' ) .
- Html::rawElement( 'div', array( 'class' => 'mw-allpages-nav' ),
- $this->getLanguage()->pipeList( $links )
- ) );
+ Html::rawElement( 'div', array( 'class' => 'mw-allpages-nav' ),
+ $this->getLanguage()->pipeList( $links )
+ )
+ );
}
-
}
/**
* @param $ns Integer: the namespace of the article
- * @param $text String: the name of the article
+ * @param string $text the name of the article
* @return array( int namespace, string dbkey, string pagename ) or NULL on error
*/
- protected function getNamespaceKeyAndText($ns, $text) {
- if ( $text == '' )
- return array( $ns, '', '' ); # shortcut for common case
+ protected function getNamespaceKeyAndText( $ns, $text ) {
+ if ( $text == '' ) {
+ # shortcut for common case
+ return array( $ns, '', '' );
+ }
- $t = Title::makeTitleSafe($ns, $text);
+ $t = Title::makeTitleSafe( $ns, $text );
if ( $t && $t->isLocal() ) {
return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
} elseif ( $t ) {
@@ -530,12 +558,16 @@ class SpecialAllpages extends IncludableSpecialPage {
}
# try again, in case the problem was an empty pagename
- $text = preg_replace('/(#|$)/', 'X$1', $text);
- $t = Title::makeTitleSafe($ns, $text);
+ $text = preg_replace( '/(#|$)/', 'X$1', $text );
+ $t = Title::makeTitleSafe( $ns, $text );
if ( $t && $t->isLocal() ) {
return array( $t->getNamespace(), '', '' );
} else {
return null;
}
}
+
+ protected function getGroupName() {
+ return 'pages';
+ }
}
diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php
index 6e3d49bd..b0830327 100644
--- a/includes/specials/SpecialAncientpages.php
+++ b/includes/specials/SpecialAncientpages.php
@@ -36,17 +36,23 @@ class AncientPagesPage extends QueryPage {
return true;
}
- function isSyndicated() { return false; }
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
return array(
'tables' => array( 'page', 'revision' ),
- 'fields' => array( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'rev_timestamp' ),
- 'conds' => array( 'page_namespace' => MWNamespace::getContentNamespaces(),
- 'page_is_redirect' => 0,
- 'page_latest=rev_id' )
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'rev_timestamp'
+ ),
+ 'conds' => array(
+ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_is_redirect' => 0,
+ 'page_latest=rev_id'
+ )
);
}
@@ -58,6 +64,11 @@ class AncientPagesPage extends QueryPage {
return false;
}
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
global $wgContLang;
@@ -67,6 +78,11 @@ class AncientPagesPage extends QueryPage {
$title,
htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) )
);
+
return $this->getLanguage()->specialList( $link, htmlspecialchars( $d ) );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialBlankpage.php b/includes/specials/SpecialBlankpage.php
index 42d33779..e61f12b9 100644
--- a/includes/specials/SpecialBlankpage.php
+++ b/includes/specials/SpecialBlankpage.php
@@ -31,8 +31,9 @@ class SpecialBlankpage extends UnlistedSpecialPage {
public function __construct() {
parent::__construct( 'Blankpage' );
}
+
public function execute( $par ) {
$this->setHeaders();
- $this->getOutput()->addWikiMsg('intentionallyblankpage');
+ $this->getOutput()->addWikiMsg( 'intentionallyblankpage' );
}
}
diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php
index 1d6656ab..3b73a374 100644
--- a/includes/specials/SpecialBlock.php
+++ b/includes/specials/SpecialBlock.php
@@ -62,7 +62,7 @@ class SpecialBlock extends FormSpecialPage {
* @throws ErrorPageError
*/
protected function checkExecutePermissions( User $user ) {
- parent::checkExecutePermissions( $user );
+ parent::checkExecutePermissions( $user );
# bug 15810: blocked admins should have limited access here
$status = self::checkUnblockSelf( $this->target, $user );
@@ -110,10 +110,10 @@ class SpecialBlock extends FormSpecialPage {
$s = HTMLForm::formatErrors( $this->preErrors );
if ( $s ) {
$form->addHeaderText( Html::rawElement(
- 'div',
- array( 'class' => 'error' ),
- $s
- ) );
+ 'div',
+ array( 'class' => 'error' ),
+ $s
+ ) );
}
}
}
@@ -127,6 +127,8 @@ class SpecialBlock extends FormSpecialPage {
$user = $this->getUser();
+ $suggestedDurations = self::getSuggestedDurations();
+
$a = array(
'Target' => array(
'type' => 'text',
@@ -134,15 +136,16 @@ class SpecialBlock extends FormSpecialPage {
'tabindex' => '1',
'id' => 'mw-bi-target',
'size' => '45',
+ 'autofocus' => true,
'required' => true,
'validation-callback' => array( __CLASS__, 'validateTargetField' ),
),
'Expiry' => array(
- 'type' => !count( self::getSuggestedDurations() ) ? 'text' : 'selectorother',
+ 'type' => !count( $suggestedDurations ) ? 'text' : 'selectorother',
'label-message' => 'ipbexpiry',
'required' => true,
'tabindex' => '2',
- 'options' => self::getSuggestedDurations(),
+ 'options' => $suggestedDurations,
'other' => $this->msg( 'ipbother' )->text(),
'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(),
),
@@ -224,7 +227,7 @@ class SpecialBlock extends FormSpecialPage {
/**
* If the user has already been blocked with similar settings, load that block
* and change the defaults for the form fields to match the existing settings.
- * @param $fields Array HTMLForm descriptor array
+ * @param array $fields HTMLForm descriptor array
* @return Bool whether fields were altered (that is, whether the target is
* already blocked)
*/
@@ -239,9 +242,8 @@ class SpecialBlock extends FormSpecialPage {
if ( $block instanceof Block && !$block->mAuto # The block exists and isn't an autoblock
&& ( $this->type != Block::TYPE_RANGE # The block isn't a rangeblock
- || $block->getTarget() == $this->target ) # or if it is, the range is what we're about to block
- )
- {
+ || $block->getTarget() == $this->target ) # or if it is, the range is what we're about to block
+ ) {
$fields['HardBlock']['default'] = $block->isHardblock();
$fields['CreateAccount']['default'] = $block->prevents( 'createaccount' );
$fields['AutoBlock']['default'] = $block->isAutoblocking();
@@ -386,7 +388,7 @@ class SpecialBlock extends FormSpecialPage {
);
}
- $text = Html::rawElement(
+ $text = Html::rawElement(
'p',
array( 'class' => 'mw-ipb-conveniencelinks' ),
$this->getLanguage()->pipeList( $links )
@@ -444,13 +446,14 @@ class SpecialBlock extends FormSpecialPage {
} elseif ( IP::isIPAddress( $target ) ) {
return Title::makeTitleSafe( NS_USER, $target );
}
+
return null;
}
/**
* Determine the target of the block, and the type of target
* TODO: should be in Block.php?
- * @param $par String subpage parameter passed to setup, or data value from
+ * @param string $par subpage parameter passed to setup, or data value from
* the HTMLForm
* @param $request WebRequest optionally try and get data from a request too
* @return array( User|string|null, Block::TYPE_ constant|null )
@@ -459,8 +462,8 @@ class SpecialBlock extends FormSpecialPage {
$i = 0;
$target = null;
- while( true ) {
- switch( $i++ ) {
+ while ( true ) {
+ switch ( $i++ ) {
case 0:
# The HTMLForm will check wpTarget first and only if it doesn't get
# a value use the default, which will be generated from the options
@@ -507,53 +510,76 @@ class SpecialBlock extends FormSpecialPage {
* @return Message
*/
public static function validateTargetField( $value, $alldata, $form ) {
+ $status = self::validateTarget( $value, $form->getUser() );
+ if ( !$status->isOK() ) {
+ $errors = $status->getErrorsArray();
+
+ return call_user_func_array( array( $form, 'msg' ), $errors[0] );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Validate a block target.
+ *
+ * @since 1.21
+ * @param string $value Block target to check
+ * @param User $user Performer of the block
+ * @return Status
+ */
+ public static function validateTarget( $value, User $user ) {
global $wgBlockCIDRLimit;
+ /** @var User $target */
list( $target, $type ) = self::getTargetAndType( $value );
+ $status = Status::newGood( $target );
if ( $type == Block::TYPE_USER ) {
- # TODO: why do we not have a User->exists() method?
- if ( !$target->getId() ) {
- return $form->msg( 'nosuchusershort',
- wfEscapeWikiText( $target->getName() ) );
+ if ( $target->isAnon() ) {
+ $status->fatal(
+ 'nosuchusershort',
+ wfEscapeWikiText( $target->getName() )
+ );
}
- $status = self::checkUnblockSelf( $target, $form->getUser() );
- if ( $status !== true ) {
- return $form->msg( 'badaccess', $status );
+ $unblockStatus = self::checkUnblockSelf( $target, $user );
+ if ( $unblockStatus !== true ) {
+ $status->fatal( 'badaccess', $unblockStatus );
}
-
} elseif ( $type == Block::TYPE_RANGE ) {
list( $ip, $range ) = explode( '/', $target, 2 );
- if ( ( IP::isIPv4( $ip ) && $wgBlockCIDRLimit['IPv4'] == 32 )
- || ( IP::isIPv6( $ip ) && $wgBlockCIDRLimit['IPv6'] == 128 ) )
- {
- # Range block effectively disabled
- return $form->msg( 'range_block_disabled' );
+ if (
+ ( IP::isIPv4( $ip ) && $wgBlockCIDRLimit['IPv4'] == 32 ) ||
+ ( IP::isIPv6( $ip ) && $wgBlockCIDRLimit['IPv6'] == 128 )
+ ) {
+ // Range block effectively disabled
+ $status->fatal( 'range_block_disabled' );
}
- if ( ( IP::isIPv4( $ip ) && $range > 32 )
- || ( IP::isIPv6( $ip ) && $range > 128 ) )
- {
- # Dodgy range
- return $form->msg( 'ip_range_invalid' );
+ if (
+ ( IP::isIPv4( $ip ) && $range > 32 ) ||
+ ( IP::isIPv6( $ip ) && $range > 128 )
+ ) {
+ // Dodgy range
+ $status->fatal( 'ip_range_invalid' );
}
if ( IP::isIPv4( $ip ) && $range < $wgBlockCIDRLimit['IPv4'] ) {
- return $form->msg( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] );
+ $status->fatal( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] );
}
if ( IP::isIPv6( $ip ) && $range < $wgBlockCIDRLimit['IPv6'] ) {
- return $form->msg( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] );
+ $status->fatal( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] );
}
} elseif ( $type == Block::TYPE_IP ) {
# All is well
} else {
- return $form->msg( 'badipaddress' );
+ $status->fatal( 'badipaddress' );
}
- return true;
+ return $status;
}
/**
@@ -584,6 +610,7 @@ class SpecialBlock extends FormSpecialPage {
# can come from it
$data['Confirm'] = !in_array( $data['Confirm'], array( '', '0', null, false ), true );
+ /** @var User $target */
list( $target, $type ) = self::getTargetAndType( $data['Target'] );
if ( $type == Block::TYPE_USER ) {
$user = $target;
@@ -598,8 +625,8 @@ class SpecialBlock extends FormSpecialPage {
# but $data['target'] gets overriden by (non-normalized) request variable
# from previous request.
if ( $target === $performer->getName() &&
- ( $data['PreviousTarget'] !== $target || !$data['Confirm'] ) )
- {
+ ( $data['PreviousTarget'] !== $target || !$data['Confirm'] )
+ ) {
return array( 'ipb-blockingself' );
}
} elseif ( $type == Block::TYPE_RANGE ) {
@@ -612,9 +639,9 @@ class SpecialBlock extends FormSpecialPage {
return array( 'badipaddress' );
}
- if ( ( strlen( $data['Expiry'] ) == 0) || ( strlen( $data['Expiry'] ) > 50 )
- || !self::parseExpiryInput( $data['Expiry'] ) )
- {
+ if ( ( strlen( $data['Expiry'] ) == 0 ) || ( strlen( $data['Expiry'] ) > 50 )
+ || !self::parseExpiryInput( $data['Expiry'] )
+ ) {
return array( 'ipb_expiry_invalid' );
}
@@ -629,7 +656,7 @@ class SpecialBlock extends FormSpecialPage {
}
if ( $data['HideUser'] ) {
- if ( !$performer->isAllowed('hideuser') ) {
+ if ( !$performer->isAllowed( 'hideuser' ) ) {
# this codepath is unreachable except by a malicious user spoofing forms,
# or by race conditions (user has oversight and sysop, loads block form,
# and is de-oversighted before submission); so need to fail completely
@@ -672,12 +699,18 @@ class SpecialBlock extends FormSpecialPage {
# Try to insert block. Is there a conflicting block?
$status = $block->insert();
if ( !$status ) {
+ # Indicates whether the user is confirming the block and is aware of
+ # the conflict (did not change the block target in the meantime)
+ $blockNotConfirmed = !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
+ && $data['PreviousTarget'] !== $target );
+
+ # Special case for API - bug 32434
+ $reblockNotAllowed = ( array_key_exists( 'Reblock', $data ) && !$data['Reblock'] );
+
# Show form unless the user is already aware of this...
- if ( !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
- && $data['PreviousTarget'] !== $target ) )
- {
+ if ( $blockNotConfirmed || $reblockNotAllowed ) {
return array( array( 'ipb_already_blocked', $block->getTarget() ) );
- # Otherwise, try to update the block...
+ # Otherwise, try to update the block...
} else {
# This returns direct blocks before autoblocks/rangeblocks, since we should
# be sure the user is blocked by now it should work for our purposes
@@ -720,7 +753,7 @@ class SpecialBlock extends FormSpecialPage {
# Can't watch a rangeblock
if ( $type != Block::TYPE_RANGE && $data['Watch'] ) {
- $performer->addWatch( Title::makeTitle( NS_USER, $target ) );
+ WatchAction::doWatch( Title::makeTitle( NS_USER, $target ), $performer, WatchedItem::IGNORE_USER_RIGHTS );
}
# Block constructor sanitizes certain block options on insert
@@ -739,10 +772,11 @@ class SpecialBlock extends FormSpecialPage {
$logaction,
Title::makeTitle( NS_USER, $target ),
$data['Reason'][0],
- $logParams
+ $logParams,
+ $performer
);
# Relate log ID to block IDs (bug 25763)
- $blockIds = array_merge( array( $status['id'] ), $status['autoIds'] );
+ $blockIds = array_merge( array( $status['id'] ), $status['autoIds'] );
$log->addRelations( 'ipb_id', $blockIds, $log_id );
# Report to the user
@@ -782,7 +816,7 @@ class SpecialBlock extends FormSpecialPage {
/**
* Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute
* ("24 May 2034", etc), into an absolute timestamp we can put into the database.
- * @param $expiry String: whatever was typed into the form
+ * @param string $expiry whatever was typed into the form
* @return String: timestamp or "infinity" string for the DB implementation
*/
public static function parseExpiryInput( $expiry ) {
@@ -837,7 +871,7 @@ class SpecialBlock extends FormSpecialPage {
# User is trying to unblock themselves
if ( $performer->isAllowed( 'unblockself' ) ) {
return true;
- # User blocked themselves and is now trying to reverse it
+ # User blocked themselves and is now trying to reverse it
} elseif ( $performer->blockedBy() === $performer->getName() ) {
return true;
} else {
@@ -855,7 +889,7 @@ class SpecialBlock extends FormSpecialPage {
/**
* Return a comma-delimited list of "flags" to be passed to the log
* reader for this block, to provide more information in the logs
- * @param $data Array from HTMLForm data
+ * @param array $data from HTMLForm data
* @param $type Block::TYPE_ constant (USER, RANGE, or IP)
* @return string
*/
@@ -918,7 +952,12 @@ class SpecialBlock extends FormSpecialPage {
$out->setPageTitle( $this->msg( 'blockipsuccesssub' ) );
$out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( $this->target ) );
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
# BC @since 1.18
-class IPBlockForm extends SpecialBlock {}
+class IPBlockForm extends SpecialBlock {
+}
diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php
index 7143d5bc..f1992c0f 100644
--- a/includes/specials/SpecialBlockList.php
+++ b/includes/specials/SpecialBlockList.php
@@ -37,7 +37,7 @@ class SpecialBlockList extends SpecialPage {
/**
* Main execution point
*
- * @param $par String title fragment
+ * @param string $par title fragment
*/
public function execute( $par ) {
$this->setHeaders();
@@ -55,10 +55,11 @@ class SpecialBlockList extends SpecialPage {
$action = $request->getText( 'action' );
- if( $action == 'unblock' || $action == 'submit' && $request->wasPosted() ) {
+ if ( $action == 'unblock' || $action == 'submit' && $request->wasPosted() ) {
# B/C @since 1.18: Unblock interface is now at Special:Unblock
$title = SpecialPage::getTitleFor( 'Unblock', $this->target );
- $out->redirect( $title->getFullUrl() );
+ $out->redirect( $title->getFullURL() );
+
return;
}
@@ -95,7 +96,9 @@ class SpecialBlockList extends SpecialPage {
'default' => 50,
),
);
- $form = new HTMLForm( $fields, $this->getContext() );
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle() ); // Remove subpage
+ $form = new HTMLForm( $fields, $context );
$form->setMethod( 'get' );
$form->setWrapperLegendMsg( 'ipblocklist-legend' );
$form->setSubmitTextMsg( 'ipblocklist-submit' );
@@ -113,14 +116,14 @@ class SpecialBlockList extends SpecialPage {
$conds = array();
# Is the user allowed to see hidden blocks?
- if ( !$this->getUser()->isAllowed( 'hideuser' ) ){
+ if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
$conds['ipb_deleted'] = 0;
}
- if ( $this->target !== '' ){
+ if ( $this->target !== '' ) {
list( $target, $type ) = Block::parseTarget( $this->target );
- switch( $type ){
+ switch ( $type ) {
case Block::TYPE_ID:
case Block::TYPE_AUTO:
$conds['ipb_id'] = $target;
@@ -141,23 +144,23 @@ class SpecialBlockList extends SpecialPage {
break;
case Block::TYPE_USER:
- $conds['ipb_address'] = (string)$this->target;
+ $conds['ipb_address'] = $target->getName();
$conds['ipb_auto'] = 0;
break;
}
}
# Apply filters
- if( in_array( 'userblocks', $this->options ) ) {
+ if ( in_array( 'userblocks', $this->options ) ) {
$conds['ipb_user'] = 0;
}
- if( in_array( 'tempblocks', $this->options ) ) {
+ if ( in_array( 'tempblocks', $this->options ) ) {
$conds['ipb_expiry'] = 'infinity';
}
- if( in_array( 'addressblocks', $this->options ) ) {
+ if ( in_array( 'addressblocks', $this->options ) ) {
$conds[] = "ipb_user != 0 OR ipb_range_end > ipb_range_start";
}
- if( in_array( 'rangeblocks', $this->options ) ) {
+ if ( in_array( 'rangeblocks', $this->options ) ) {
$conds[] = "ipb_range_end = ipb_range_start";
}
@@ -169,7 +172,7 @@ class SpecialBlockList extends SpecialPage {
# 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 ) ) {
+ if ( count( $otherBlockLink ) ) {
$out->addHTML(
Html::element( 'h2', array(), $this->msg( 'ipblocklist-localblock' )->text() ) . "\n"
);
@@ -179,18 +182,16 @@ class SpecialBlockList extends SpecialPage {
if ( $pager->getNumRows() ) {
$out->addHTML(
$pager->getNavigationBar() .
- $pager->getBody().
- $pager->getNavigationBar()
+ $pager->getBody() .
+ $pager->getNavigationBar()
);
-
} elseif ( $this->target ) {
$out->addWikiMsg( 'ipblocklist-no-results' );
-
} else {
$out->addWikiMsg( 'ipblocklist-empty' );
}
- if( count( $otherBlockLink ) ) {
+ if ( count( $otherBlockLink ) ) {
$out->addHTML(
Html::rawElement(
'h2',
@@ -199,12 +200,16 @@ class SpecialBlockList extends SpecialPage {
) . "\n"
);
$list = '';
- foreach( $otherBlockLink as $link ) {
+ foreach ( $otherBlockLink as $link ) {
$list .= Html::rawElement( 'li', array(), $link ) . "\n";
}
$out->addHTML( Html::rawElement( 'ul', array( 'class' => 'mw-ipblocklist-otherblocks' ), $list ) . "\n" );
}
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
class BlockListPager extends TablePager {
@@ -234,7 +239,7 @@ class BlockListPager extends TablePager {
'ipb_params' => 'blocklist-params',
'ipb_reason' => 'blocklist-reason',
);
- foreach( $headers as $key => $val ) {
+ foreach ( $headers as $key => $val ) {
$headers[$key] = $this->msg( $val )->text();
}
}
@@ -263,17 +268,17 @@ class BlockListPager extends TablePager {
$formatted = '';
- switch( $name ) {
+ switch ( $name ) {
case 'ipb_timestamp':
$formatted = $this->getLanguage()->userTimeAndDate( $value, $this->getUser() );
break;
case 'ipb_target':
- if( $row->ipb_auto ){
+ if ( $row->ipb_auto ) {
$formatted = $this->msg( 'autoblockid', $row->ipb_id )->parse();
} else {
list( $target, $type ) = Block::parseTarget( $row->ipb_address );
- switch( $type ){
+ switch ( $type ) {
case Block::TYPE_USER:
case Block::TYPE_IP:
$formatted = Linker::userLink( $target->getId(), $target );
@@ -291,9 +296,9 @@ class BlockListPager extends TablePager {
break;
case 'ipb_expiry':
- $formatted = $this->getLanguage()->formatExpiry( $value, /* User preference timezone */ true );
- if( $this->getUser()->isAllowed( 'block' ) ){
- if( $row->ipb_auto ){
+ $formatted = $this->getLanguage()->formatExpiry( $value, /* User preference timezone */true );
+ if ( $this->getUser()->isAllowed( 'block' ) ) {
+ if ( $row->ipb_auto ) {
$links[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Unblock' ),
$msg['unblocklink'],
@@ -329,7 +334,7 @@ class BlockListPager extends TablePager {
break;
case 'ipb_reason':
- $formatted = Linker::commentBlock( $value );
+ $formatted = Linker::formatComment( $value );
break;
case 'ipb_params':
@@ -391,14 +396,14 @@ class BlockListPager extends TablePager {
);
# Is the user allowed to see hidden blocks?
- if ( !$this->getUser()->isAllowed( 'hideuser' ) ){
+ if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
$info['conds']['ipb_deleted'] = 0;
}
return $info;
}
- public function getTableClass(){
+ public function getTableClass() {
return 'TablePager mw-blocklist';
}
@@ -416,9 +421,9 @@ class BlockListPager extends TablePager {
/**
* Do a LinkBatch query to minimise database load when generating all these links
- * @param $result
+ * @param ResultWrapper $result
*/
- function preprocessResults( $result ){
+ function preprocessResults( $result ) {
wfProfileIn( __METHOD__ );
# Do a link batch query
$lb = new LinkBatch;
@@ -437,11 +442,11 @@ class BlockListPager extends TablePager {
}
$ua = UserArray::newFromIDs( $userids );
- foreach( $ua as $user ){
+ foreach ( $ua as $user ) {
$name = str_replace( ' ', '_', $user->getName() );
$lb->add( NS_USER, $name );
$lb->add( NS_USER_TALK, $name );
- }
+ }
$lb->execute();
wfProfileOut( __METHOD__ );
@@ -472,11 +477,10 @@ class HTMLBlockedUsersItemSelect extends HTMLSelectField {
// This adds the explicitly requested limit value to the drop-down,
// then makes sure it's sorted correctly so when we output the list
// later, the custom option doesn't just show up last.
- $this->mParams['options'][ $this->mParent->getLanguage()->formatNum( $value ) ] = intval($value);
+ $this->mParams['options'][$this->mParent->getLanguage()->formatNum( $value )] = intval( $value );
asort( $this->mParams['options'] );
}
return true;
}
-
}
diff --git a/includes/specials/SpecialBlockme.php b/includes/specials/SpecialBlockme.php
deleted file mode 100644
index 3840b2ff..00000000
--- a/includes/specials/SpecialBlockme.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * Implements Special:Blockme
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page called by proxy_check.php to block open proxies
- *
- * @ingroup SpecialPage
- */
-class SpecialBlockme extends UnlistedSpecialPage {
-
- function __construct() {
- parent::__construct( 'Blockme' );
- }
-
- function execute( $par ) {
- global $wgBlockOpenProxies, $wgProxyKey;
-
- $this->setHeaders();
- $this->outputHeader();
-
- $ip = $this->getRequest()->getIP();
- if( !$wgBlockOpenProxies || $this->getRequest()->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
- $this->getOutput()->addWikiMsg( 'proxyblocker-disabled' );
- return;
- }
-
- $user = User::newFromName( $this->msg( 'proxyblocker' )->inContentLanguage()->text() );
- # FIXME: newFromName could return false on a badly configured wiki.
- if ( !$user->isLoggedIn() ) {
- $user->addToDatabase();
- }
-
- $block = new Block();
- $block->setTarget( $ip );
- $block->setBlocker( $user );
- $block->mReason = $this->msg( 'proxyblockreason' )->inContentLanguage()->text();
-
- $block->insert();
-
- $this->getOutput()->addWikiMsg( 'proxyblocksuccess' );
- }
-}
diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php
index bf7de3f5..5ad961c3 100644
--- a/includes/specials/SpecialBooksources.php
+++ b/includes/specials/SpecialBooksources.php
@@ -46,15 +46,15 @@ class SpecialBookSources extends SpecialPage {
/**
* Show the special page
*
- * @param $isbn string ISBN passed as a subpage parameter
+ * @param string $isbn ISBN passed as a subpage parameter
*/
public function execute( $isbn ) {
$this->setHeaders();
$this->outputHeader();
$this->isbn = self::cleanIsbn( $isbn ? $isbn : $this->getRequest()->getText( 'isbn' ) );
$this->getOutput()->addHTML( $this->makeForm() );
- if( strlen( $this->isbn ) > 0 ) {
- if( !self::isValidISBN( $this->isbn ) ) {
+ if ( strlen( $this->isbn ) > 0 ) {
+ if ( !self::isValidISBN( $this->isbn ) ) {
$this->getOutput()->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>", 'booksources-invalid-isbn' );
}
$this->showList();
@@ -62,46 +62,47 @@ class SpecialBookSources extends SpecialPage {
}
/**
- * Returns whether a given ISBN (10 or 13) is valid. True indicates validity.
- * @param isbn string ISBN passed for check
+ * Returns whether a given ISBN (10 or 13) is valid. True indicates validity.
+ * @param string $isbn ISBN passed for check
* @return bool
*/
public static function isValidISBN( $isbn ) {
$isbn = self::cleanIsbn( $isbn );
$sum = 0;
- if( strlen( $isbn ) == 13 ) {
- for( $i = 0; $i < 12; $i++ ) {
- if($i % 2 == 0) {
+ if ( strlen( $isbn ) == 13 ) {
+ for ( $i = 0; $i < 12; $i++ ) {
+ if ( $i % 2 == 0 ) {
$sum += $isbn[$i];
} else {
$sum += 3 * $isbn[$i];
}
}
- $check = (10 - ($sum % 10)) % 10;
- if ($check == $isbn[12]) {
+ $check = ( 10 - ( $sum % 10 ) ) % 10;
+ if ( $check == $isbn[12] ) {
return true;
}
- } elseif( strlen( $isbn ) == 10 ) {
- for($i = 0; $i < 9; $i++) {
- $sum += $isbn[$i] * ($i + 1);
+ } elseif ( strlen( $isbn ) == 10 ) {
+ for ( $i = 0; $i < 9; $i++ ) {
+ $sum += $isbn[$i] * ( $i + 1 );
}
$check = $sum % 11;
- if($check == 10) {
+ if ( $check == 10 ) {
$check = "X";
}
- if($check == $isbn[9]) {
+ if ( $check == $isbn[9] ) {
return true;
}
}
+
return false;
}
/**
* Trim ISBN and remove characters which aren't required
*
- * @param $isbn string Unclean ISBN
+ * @param string $isbn Unclean ISBN
* @return string
*/
private static function cleanIsbn( $isbn ) {
@@ -116,13 +117,14 @@ class SpecialBookSources extends SpecialPage {
private function makeForm() {
global $wgScript;
- $form = '<fieldset><legend>' . $this->msg( 'booksources-search-legend' )->escaped() . '</legend>';
- $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
- $form .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() );
- $form .= '<p>' . Xml::inputLabel( $this->msg( 'booksources-isbn' )->text(), 'isbn', 'isbn', 20, $this->isbn );
- $form .= '&#160;' . Xml::submitButton( $this->msg( 'booksources-go' )->text() ) . '</p>';
- $form .= Xml::closeElement( 'form' );
- $form .= '</fieldset>';
+ $form = Html::openElement( 'fieldset' ) . "\n";
+ $form .= Html::element( 'legend', array(), $this->msg( 'booksources-search-legend' )->text() ) . "\n";
+ $form .= Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . "\n";
+ $form .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
+ $form .= '<p>' . Xml::inputLabel( $this->msg( 'booksources-isbn' )->text(), 'isbn', 'isbn', 20, $this->isbn, array( 'autofocus' => true ) );
+ $form .= '&#160;' . Xml::submitButton( $this->msg( 'booksources-go' )->text() ) . "</p>\n";
+ $form .= Html::closeElement( 'form' ) . "\n";
+ $form .= Html::closeElement( 'fieldset' ) . "\n";
return $form;
}
@@ -130,6 +132,7 @@ class SpecialBookSources extends SpecialPage {
* Determine where to get the list of book sources from,
* format and output them
*
+ * @throws MWException
* @return string
*/
private function showList() {
@@ -142,31 +145,49 @@ class SpecialBookSources extends SpecialPage {
# Check for a local page such as Project:Book_sources and use that if available
$page = $this->msg( 'booksources' )->inContentLanguage()->text();
$title = Title::makeTitleSafe( NS_PROJECT, $page ); # Show list in content language
- if( is_object( $title ) && $title->exists() ) {
+ if ( is_object( $title ) && $title->exists() ) {
$rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
- $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
- return true;
+ $content = $rev->getContent();
+
+ if ( $content instanceof TextContent ) {
+ //XXX: in the future, this could be stored as structured data, defining a list of book sources
+
+ $text = $content->getNativeData();
+ $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $text ) );
+
+ return true;
+ } else {
+ throw new MWException( "Unexpected content type for book sources: " . $content->getModel() );
+ }
}
# Fall back to the defaults given in the language file
$this->getOutput()->addWikiMsg( 'booksources-text' );
$this->getOutput()->addHTML( '<ul>' );
$items = $wgContLang->getBookstoreList();
- foreach( $items as $label => $url )
+ foreach ( $items as $label => $url ) {
$this->getOutput()->addHTML( $this->makeListItem( $label, $url ) );
+ }
$this->getOutput()->addHTML( '</ul>' );
+
return true;
}
/**
* Format a book source list item
*
- * @param $label string Book source label
- * @param $url string Book source URL
+ * @param string $label Book source label
+ * @param string $url Book source URL
* @return string
*/
private function makeListItem( $label, $url ) {
$url = str_replace( '$1', $this->isbn, $url );
- return '<li><a href="' . htmlspecialchars( $url ) . '" class="external">' . htmlspecialchars( $label ) . '</a></li>';
+
+ return Html::rawElement( 'li', array(),
+ Html::element( 'a', array( 'href' => $url, 'class' => 'external' ), $label ) );
+ }
+
+ protected function getGroupName() {
+ return 'other';
}
}
diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php
index 8119e6d1..b2ddc220 100644
--- a/includes/specials/SpecialBrokenRedirects.php
+++ b/includes/specials/SpecialBrokenRedirects.php
@@ -33,35 +33,55 @@ class BrokenRedirectsPage extends QueryPage {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
- function sortDescending() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function sortDescending() {
+ return false;
+ }
function getPageHeader() {
return $this->msg( 'brokenredirectstext' )->parseAsBlock();
}
function getQueryInfo() {
+ $dbr = wfGetDB( DB_SLAVE );
+
return array(
- 'tables' => array( 'redirect', 'p1' => 'page',
- 'p2' => 'page' ),
- 'fields' => array( 'namespace' => 'p1.page_namespace',
- 'title' => 'p1.page_title',
- 'value' => 'p1.page_title',
- 'rd_namespace',
- 'rd_title'
+ 'tables' => array(
+ 'redirect',
+ 'p1' => 'page',
+ 'p2' => 'page',
),
- 'conds' => array( 'rd_namespace >= 0',
- 'p2.page_namespace IS NULL'
+ 'fields' => array(
+ 'namespace' => 'p1.page_namespace',
+ 'title' => 'p1.page_title',
+ 'value' => 'p1.page_title',
+ 'rd_namespace',
+ 'rd_title',
+ ),
+ 'conds' => array(
+ // Exclude pages that don't exist locally as wiki pages,
+ // but aren't "broken" either.
+ // Special pages and interwiki links
+ 'rd_namespace >= 0',
+ 'rd_interwiki IS NULL OR rd_interwiki = ' . $dbr->addQuotes( '' ),
+ 'p2.page_namespace IS NULL',
+ ),
+ 'join_conds' => array(
+ 'p1' => array( 'JOIN', array(
+ 'rd_from=p1.page_id',
+ ) ),
+ 'p2' => array( 'LEFT JOIN', array(
+ 'rd_namespace=p2.page_namespace',
+ 'rd_title=p2.page_title'
+ ) ),
),
- 'join_conds' => array( 'p1' => array( 'JOIN', array(
- 'rd_from=p1.page_id',
- ) ),
- 'p2' => array( 'LEFT JOIN', array(
- 'rd_namespace=p2.page_namespace',
- 'rd_title=p2.page_title'
- ) )
- )
);
}
@@ -69,13 +89,13 @@ class BrokenRedirectsPage extends QueryPage {
* @return array
*/
function getOrderFields() {
- return array ( 'rd_namespace', 'rd_title', 'rd_from' );
+ return array( 'rd_namespace', 'rd_title', 'rd_from' );
}
/**
- * @param $skin Skin
- * @param $result
- * @return String
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
*/
function formatResult( $skin, $result ) {
$fromObj = Title::makeTitle( $result->namespace, $result->title );
@@ -119,7 +139,7 @@ class BrokenRedirectsPage extends QueryPage {
$out = $from . $this->msg( 'word-separator' )->escaped();
- if( $this->getUser()->isAllowed( 'delete' ) ) {
+ if ( $this->getUser()->isAllowed( 'delete' ) ) {
$links[] = Linker::linkKnown(
$fromObj,
$this->msg( 'brokenredirects-delete' )->escaped(),
@@ -130,6 +150,11 @@ class BrokenRedirectsPage extends QueryPage {
$out .= $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( $links ) )->escaped();
$out .= " {$arr} {$to}";
+
return $out;
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialCachedPage.php b/includes/specials/SpecialCachedPage.php
index b3f6c720..39305f01 100644
--- a/includes/specials/SpecialCachedPage.php
+++ b/includes/specials/SpecialCachedPage.php
@@ -119,7 +119,7 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper {
*
* @since 1.20
*
- * @param {function} $computeFunction
+ * @param callable $computeFunction
* @param array|mixed $args
* @param string|null $key
*
@@ -137,7 +137,7 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper {
*
* @since 1.20
*
- * @param {function} $computeFunction
+ * @param callable $computeFunction
* @param array $args
* @param string|null $key
*/
@@ -194,5 +194,4 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper {
$this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) );
}
}
-
}
diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php
index 1232e3fa..d01bfd7d 100644
--- a/includes/specials/SpecialCategories.php
+++ b/includes/specials/SpecialCategories.php
@@ -42,14 +42,18 @@ class SpecialCategories extends SpecialPage {
$this->getOutput()->addHTML(
Html::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) .
- $this->msg( 'categoriespagetext', $cap->getNumRows() )->parseAsBlock() .
- $cap->getStartForm( $from ) .
- $cap->getNavigationBar() .
- '<ul>' . $cap->getBody() . '</ul>' .
- $cap->getNavigationBar() .
- Html::closeElement( 'div' )
+ $this->msg( 'categoriespagetext', $cap->getNumRows() )->parseAsBlock() .
+ $cap->getStartForm( $from ) .
+ $cap->getNavigationBar() .
+ '<ul>' . $cap->getBody() . '</ul>' .
+ $cap->getNavigationBar() .
+ Html::closeElement( 'div' )
);
}
+
+ protected function getGroupName() {
+ return 'pages';
+ }
}
/**
@@ -62,7 +66,7 @@ class CategoryPager extends AlphabeticPager {
function __construct( IContextSource $context, $from ) {
parent::__construct( $context );
$from = str_replace( ' ', '_', $from );
- if( $from !== '' ) {
+ if ( $from !== '' ) {
$from = Title::capitalize( $from, NS_CATEGORY );
$this->setOffset( $from );
$this->setIncludeOffset( true );
@@ -72,7 +76,7 @@ class CategoryPager extends AlphabeticPager {
function getQueryInfo() {
return array(
'tables' => array( 'category' ),
- 'fields' => array( 'cat_title','cat_pages' ),
+ 'fields' => array( 'cat_title', 'cat_pages' ),
'conds' => array( 'cat_pages > 0' ),
'options' => array( 'USE INDEX' => 'cat_title' ),
);
@@ -86,8 +90,10 @@ class CategoryPager extends AlphabeticPager {
function getDefaultQuery() {
parent::getDefaultQuery();
unset( $this->mDefaultQuery['from'] );
+
return $this->mDefaultQuery;
}
+
# protected function getOrderTypeMessages() {
# return array( 'abc' => 'special-categories-sort-abc',
# 'count' => 'special-categories-sort-count' );
@@ -109,26 +115,34 @@ class CategoryPager extends AlphabeticPager {
}
$batch->execute();
$this->mResult->rewind();
+
return parent::getBody();
}
- function formatRow($result) {
+ function formatRow( $result ) {
$title = Title::makeTitle( NS_CATEGORY, $result->cat_title );
$titleText = Linker::link( $title, htmlspecialchars( $title->getText() ) );
$count = $this->msg( 'nmembers' )->numParams( $result->cat_pages )->escaped();
+
return Xml::tags( 'li', null, $this->getLanguage()->specialList( $titleText, $count ) ) . "\n";
}
public function getStartForm( $from ) {
global $wgScript;
- return
- Xml::tags( 'form', array( 'method' => 'get', 'action' => $wgScript ),
- Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
- Xml::fieldset( $this->msg( 'categories' )->text(),
- Xml::inputLabel( $this->msg( 'categoriesfrom' )->text(),
+ return Xml::tags(
+ 'form',
+ array( 'method' => 'get', 'action' => $wgScript ),
+ Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ Xml::fieldset(
+ $this->msg( 'categories' )->text(),
+ Xml::inputLabel(
+ $this->msg( 'categoriesfrom' )->text(),
'from', 'from', 20, $from ) .
- ' ' .
- Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) ) );
+ ' ' .
+ Xml::submitButton( $this->msg( 'allpagessubmit' )->text()
+ )
+ )
+ );
}
}
diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php
index fc726106..aab839fd 100644
--- a/includes/specials/SpecialChangeEmail.php
+++ b/includes/specials/SpecialChangeEmail.php
@@ -41,7 +41,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
protected $mNewEmail;
public function __construct() {
- parent::__construct( 'ChangeEmail' );
+ parent::__construct( 'ChangeEmail', 'editmyprivateinfo' );
}
/**
@@ -49,6 +49,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
*/
function isListed() {
global $wgAuth;
+
return $wgAuth->allowPropChange( 'emailaddress' );
}
@@ -67,6 +68,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) {
$this->error( 'cannotchangeemail' );
+
return;
}
@@ -75,22 +77,31 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
if ( !$request->wasPosted() && !$user->isLoggedIn() ) {
$this->error( 'changeemail-no-info' );
+
return;
}
if ( $request->wasPosted() && $request->getBool( 'wpCancel' ) ) {
$this->doReturnTo();
+
return;
}
$this->checkReadOnly();
+ $this->checkPermissions();
+
+ // This could also let someone check the current email address, so
+ // require both permissions.
+ if ( !$this->getUser()->isAllowed( 'viewmyprivateinfo' ) ) {
+ throw new PermissionsError( 'viewmyprivateinfo' );
+ }
$this->mPassword = $request->getVal( 'wpPassword' );
$this->mNewEmail = $request->getVal( 'wpNewEmail' );
if ( $request->wasPosted()
- && $user->matchEditToken( $request->getVal( 'token' ) ) )
- {
+ && $user->matchEditToken( $request->getVal( 'token' ) )
+ ) {
$info = $this->attemptChange( $user, $this->mPassword, $this->mNewEmail );
if ( $info === true ) {
$this->doReturnTo();
@@ -138,38 +149,38 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
$this->getOutput()->addHTML(
Xml::fieldset( $this->msg( 'changeemail-header' )->text() ) .
- Xml::openElement( 'form',
- array(
- 'method' => 'post',
- 'action' => $this->getTitle()->getLocalUrl(),
- 'id' => 'mw-changeemail-form' ) ) . "\n" .
- Html::hidden( 'token', $user->getEditToken() ) . "\n" .
- Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" .
- $this->msg( 'changeemail-text' )->parseAsBlock() . "\n" .
- Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n"
+ Xml::openElement( 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $this->getTitle()->getLocalURL(),
+ 'id' => 'mw-changeemail-form' ) ) . "\n" .
+ Html::hidden( 'token', $user->getEditToken() ) . "\n" .
+ Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" .
+ $this->msg( 'changeemail-text' )->parseAsBlock() . "\n" .
+ Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n"
);
$items = array(
array( 'wpName', 'username', 'text', $user->getName() ),
array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ),
- array( 'wpNewEmail', 'changeemail-newemail', 'input', $this->mNewEmail ),
+ array( 'wpNewEmail', 'changeemail-newemail', 'email', $this->mNewEmail ),
);
if ( $wgRequirePasswordforEmailChange ) {
- $items[] = array( 'wpPassword', 'yourpassword', 'password', $this->mPassword );
+ $items[] = array( 'wpPassword', 'changeemail-password', 'password', $this->mPassword );
}
$this->getOutput()->addHTML(
$this->pretty( $items ) .
- "\n" .
- "<tr>\n" .
+ "\n" .
+ "<tr>\n" .
"<td></td>\n" .
'<td class="mw-input">' .
- Xml::submitButton( $this->msg( 'changeemail-submit' )->text() ) .
- Xml::submitButton( $this->msg( 'changeemail-cancel' )->text(), array( 'name' => 'wpCancel' ) ) .
+ Xml::submitButton( $this->msg( 'changeemail-submit' )->text() ) .
+ Xml::submitButton( $this->msg( 'changeemail-cancel' )->text(), array( 'name' => 'wpCancel' ) ) .
"</td>\n" .
- "</tr>\n" .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' ) . "\n"
+ "</tr>\n" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' ) . "\n"
);
}
@@ -181,7 +192,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
$out = '';
foreach ( $fields as $list ) {
list( $name, $label, $type, $value ) = $list;
- if( $type == 'text' ) {
+ if ( $type == 'text' ) {
$field = htmlspecialchars( $value );
} else {
$attribs = array( 'id' => $name );
@@ -195,7 +206,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
if ( $type != 'text' ) {
$out .= Xml::label( $this->msg( $label )->text(), $name );
} else {
- $out .= $this->msg( $label )->escaped();
+ $out .= $this->msg( $label )->escaped();
}
$out .= "</td>\n";
$out .= "\t<td class='mw-input'>";
@@ -203,6 +214,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
$out .= "</td>\n";
$out .= "</tr>";
}
+
return $out;
}
@@ -213,20 +225,26 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
* @return bool|string true or string on success, false on failure
*/
protected function attemptChange( User $user, $pass, $newaddr ) {
+ global $wgAuth, $wgPasswordAttemptThrottle;
+
if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) {
$this->error( 'invalidemailaddress' );
+
return false;
}
$throttleCount = LoginForm::incLoginThrottle( $user->getName() );
if ( $throttleCount === true ) {
- $this->error( 'login-throttled' );
+ $lang = $this->getLanguage();
+ $this->error( array( 'login-throttled', $lang->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) ) );
+
return false;
}
global $wgRequirePasswordforEmailChange;
if ( $wgRequirePasswordforEmailChange && !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) {
$this->error( 'wrongpassword' );
+
return false;
}
@@ -239,8 +257,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
if ( !$status->isGood() ) {
$this->getOutput()->addHTML(
'<p class="error">' .
- $this->getOutput()->parseInline( $status->getWikiText( 'mailerror' ) ) .
- '</p>' );
+ $this->getOutput()->parseInline( $status->getWikiText( 'mailerror' ) ) .
+ '</p>' );
+
return false;
}
@@ -248,6 +267,12 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
$user->saveSettings();
+ $wgAuth->updateExternalDB( $user );
+
return $status->value;
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialChangePassword.php b/includes/specials/SpecialChangePassword.php
index ba728ac2..c54b5575 100644
--- a/includes/specials/SpecialChangePassword.php
+++ b/includes/specials/SpecialChangePassword.php
@@ -27,8 +27,11 @@
* @ingroup SpecialPage
*/
class SpecialChangePassword extends UnlistedSpecialPage {
+
+ protected $mUserName, $mOldpass, $mNewpass, $mRetype, $mDomain;
+
public function __construct() {
- parent::__construct( 'ChangePassword' );
+ parent::__construct( 'ChangePassword', 'editmyprivateinfo' );
}
/**
@@ -49,64 +52,71 @@ class SpecialChangePassword extends UnlistedSpecialPage {
$this->mDomain = $request->getVal( 'wpDomain' );
$user = $this->getUser();
- if( !$request->wasPosted() && !$user->isLoggedIn() ) {
+ if ( !$request->wasPosted() && !$user->isLoggedIn() ) {
$this->error( $this->msg( 'resetpass-no-info' )->text() );
+
return;
}
- if( $request->wasPosted() && $request->getBool( 'wpCancel' ) ) {
- $this->doReturnTo();
+ if ( $request->wasPosted() && $request->getBool( 'wpCancel' ) ) {
+ $titleObj = Title::newFromText( $request->getVal( 'returnto' ) );
+ if ( !$titleObj instanceof Title ) {
+ $titleObj = Title::newMainPage();
+ }
+ $query = $request->getVal( 'returntoquery' );
+ $this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
+
return;
}
$this->checkReadOnly();
+ $this->checkPermissions();
- if( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'token' ) ) ) {
+ if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'token' ) ) ) {
try {
$this->mDomain = $wgAuth->getDomain();
- if( !$wgAuth->allowPasswordChange() ) {
+ if ( !$wgAuth->allowPasswordChange() ) {
$this->error( $this->msg( 'resetpass_forbidden' )->text() );
+
return;
}
$this->attemptReset( $this->mNewpass, $this->mRetype );
- $this->getOutput()->addWikiMsg( 'resetpass_success' );
- if( !$user->isLoggedIn() ) {
+
+ if ( $user->isLoggedIn() ) {
+ $this->getOutput()->wrapWikiMsg(
+ "<div class=\"successbox\">\n$1\n</div>",
+ 'changepassword-success'
+ );
+ $this->getOutput()->returnToMain();
+ } else {
LoginForm::setLoginToken();
$token = LoginForm::getLoginToken();
$data = array(
- 'action' => 'submitlogin',
- 'wpName' => $this->mUserName,
- 'wpDomain' => $this->mDomain,
+ 'action' => 'submitlogin',
+ 'wpName' => $this->mUserName,
+ 'wpDomain' => $this->mDomain,
'wpLoginToken' => $token,
- 'wpPassword' => $this->mNewpass,
- 'returnto' => $request->getVal( 'returnto' ),
- );
- if( $request->getCheck( 'wpRemember' ) ) {
- $data['wpRemember'] = 1;
- }
- $login = new LoginForm( new FauxRequest( $data, true ) );
+ 'wpPassword' => $request->getVal( 'wpNewPassword' ),
+ ) + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' );
+ $login = new LoginForm( new DerivativeRequest( $request, $data, true ) );
$login->setContext( $this->getContext() );
$login->execute( null );
}
- $this->doReturnTo();
- } catch( PasswordError $e ) {
+
+ return;
+ } catch ( PasswordError $e ) {
$this->error( $e->getMessage() );
}
}
$this->showForm();
}
- function doReturnTo() {
- $titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) );
- if ( !$titleObj instanceof Title ) {
- $titleObj = Title::newMainPage();
- }
- $this->getOutput()->redirect( $titleObj->getFullURL() );
- }
-
+ /**
+ * @param $msg string
+ */
function error( $msg ) {
- $this->getOutput()->addHTML( Xml::element('p', array( 'class' => 'error' ), $msg ) );
+ $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' => 'error' ), $msg ) );
}
function showForm() {
@@ -121,12 +131,12 @@ class SpecialChangePassword extends UnlistedSpecialPage {
$rememberMe = '<tr>' .
'<td></td>' .
'<td class="mw-input">' .
- Xml::checkLabel(
- $this->msg( 'remembermypassword' )->numParams( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) )->text(),
- 'wpRemember', 'wpRemember',
- $this->getRequest()->getCheck( 'wpRemember' ) ) .
+ Xml::checkLabel(
+ $this->msg( 'remembermypassword' )->numParams( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) )->text(),
+ 'wpRemember', 'wpRemember',
+ $this->getRequest()->getCheck( 'wpRemember' ) ) .
'</td>' .
- '</tr>';
+ '</tr>';
$submitMsg = 'resetpass_submit';
$oldpassMsg = 'resetpass-temp-password';
} else {
@@ -136,45 +146,55 @@ class SpecialChangePassword extends UnlistedSpecialPage {
$extraFields = array();
wfRunHooks( 'ChangePasswordForm', array( &$extraFields ) );
$prettyFields = array(
- array( 'wpName', 'username', 'text', $this->mUserName ),
- array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ),
- array( 'wpNewPassword', 'newpassword', 'password', null ),
- array( 'wpRetype', 'retypenew', 'password', null ),
- );
+ array( 'wpName', 'username', 'text', $this->mUserName ),
+ array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ),
+ array( 'wpNewPassword', 'newpassword', 'password', null ),
+ array( 'wpRetype', 'retypenew', 'password', null ),
+ );
$prettyFields = array_merge( $prettyFields, $extraFields );
+ $hiddenFields = array(
+ 'token' => $user->getEditToken(),
+ 'wpName' => $this->mUserName,
+ 'wpDomain' => $this->mDomain,
+ ) + $this->getRequest()->getValues( 'returnto', 'returntoquery' );
+ $hiddenFieldsStr = '';
+ foreach ( $hiddenFields as $fieldname => $fieldvalue ) {
+ $hiddenFieldsStr .= Html::hidden( $fieldname, $fieldvalue ) . "\n";
+ }
$this->getOutput()->addHTML(
Xml::fieldset( $this->msg( 'resetpass_header' )->text() ) .
- Xml::openElement( 'form',
- array(
- 'method' => 'post',
- 'action' => $this->getTitle()->getLocalUrl(),
- 'id' => 'mw-resetpass-form' ) ) . "\n" .
- Html::hidden( 'token', $user->getEditToken() ) . "\n" .
- Html::hidden( 'wpName', $this->mUserName ) . "\n" .
- Html::hidden( 'wpDomain', $this->mDomain ) . "\n" .
- Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" .
- $this->msg( 'resetpass_text' )->parseAsBlock() . "\n" .
- Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . "\n" .
- $this->pretty( $prettyFields ) . "\n" .
- $rememberMe .
- "<tr>\n" .
+ Xml::openElement( 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $this->getTitle()->getLocalURL(),
+ 'id' => 'mw-resetpass-form' ) ) . "\n" .
+ $hiddenFieldsStr .
+ $this->msg( 'resetpass_text' )->parseAsBlock() . "\n" .
+ Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . "\n" .
+ $this->pretty( $prettyFields ) . "\n" .
+ $rememberMe .
+ "<tr>\n" .
"<td></td>\n" .
'<td class="mw-input">' .
- Xml::submitButton( $this->msg( $submitMsg )->text() ) .
- Xml::submitButton( $this->msg( 'resetpass-submit-cancel' )->text(), array( 'name' => 'wpCancel' ) ) .
+ Xml::submitButton( $this->msg( $submitMsg )->text() ) .
+ Xml::submitButton( $this->msg( 'resetpass-submit-cancel' )->text(), array( 'name' => 'wpCancel' ) ) .
"</td>\n" .
- "</tr>\n" .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' ) . "\n"
+ "</tr>\n" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' ) . "\n"
);
}
+ /**
+ * @param $fields array
+ * @return string
+ */
function pretty( $fields ) {
$out = '';
foreach ( $fields as $list ) {
list( $name, $label, $type, $value ) = $list;
- if( $type == 'text' ) {
+ if ( $type == 'text' ) {
$field = htmlspecialchars( $value );
} else {
$attribs = array( 'id' => $name );
@@ -189,16 +209,20 @@ class SpecialChangePassword extends UnlistedSpecialPage {
}
$out .= "<tr>\n";
$out .= "\t<td class='mw-label'>";
- if ( $type != 'text' )
+
+ if ( $type != 'text' ) {
$out .= Xml::label( $this->msg( $label )->text(), $name );
- else
- $out .= $this->msg( $label )->escaped();
+ } else {
+ $out .= $this->msg( $label )->escaped();
+ }
+
$out .= "</td>\n";
$out .= "\t<td class='mw-input'>";
$out .= $field;
$out .= "</td>\n";
$out .= "</tr>";
}
+
return $out;
}
@@ -206,19 +230,31 @@ class SpecialChangePassword extends UnlistedSpecialPage {
* @throws PasswordError when cannot set the new password because requirements not met.
*/
protected function attemptReset( $newpass, $retype ) {
- $user = User::newFromName( $this->mUserName );
- if( !$user || $user->isAnon() ) {
+ global $wgPasswordAttemptThrottle;
+
+ $isSelf = ( $this->mUserName === $this->getUser()->getName() );
+ if ( $isSelf ) {
+ $user = $this->getUser();
+ } else {
+ $user = User::newFromName( $this->mUserName );
+ }
+
+ if ( !$user || $user->isAnon() ) {
throw new PasswordError( $this->msg( 'nosuchusershort', $this->mUserName )->text() );
}
- if( $newpass !== $retype ) {
+ if ( $newpass !== $retype ) {
wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'badretype' ) );
throw new PasswordError( $this->msg( 'badretype' )->text() );
}
$throttleCount = LoginForm::incLoginThrottle( $this->mUserName );
if ( $throttleCount === true ) {
- throw new PasswordError( $this->msg( 'login-throttled' )->text() );
+ $lang = $this->getLanguage();
+ throw new PasswordError( $this->msg( 'login-throttled' )
+ ->params( $lang->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) )
+ ->text()
+ );
}
$abortMsg = 'resetpass-abort-generic';
@@ -227,7 +263,7 @@ class SpecialChangePassword extends UnlistedSpecialPage {
throw new PasswordError( $this->msg( $abortMsg )->text() );
}
- if( !$user->checkTemporaryPassword($this->mOldpass) && !$user->checkPassword($this->mOldpass) ) {
+ if ( !$user->checkTemporaryPassword( $this->mOldpass ) && !$user->checkPassword( $this->mOldpass ) ) {
wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) );
throw new PasswordError( $this->msg( 'resetpass-wrong-oldpass' )->text() );
}
@@ -240,13 +276,22 @@ class SpecialChangePassword extends UnlistedSpecialPage {
try {
$user->setPassword( $this->mNewpass );
wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'success' ) );
- $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
- } catch( PasswordError $e ) {
+ $this->mNewpass = $this->mOldpass = $this->mRetype = '';
+ } catch ( PasswordError $e ) {
wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'error' ) );
throw new PasswordError( $e->getMessage() );
}
- $user->setCookies();
+ if ( $isSelf ) {
+ // This is needed to keep the user connected since
+ // changing the password also modifies the user's token.
+ $user->setCookies();
+ }
+
$user->saveSettings();
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php
index 9e3c52b9..fc6b0c58 100644
--- a/includes/specials/SpecialComparePages.php
+++ b/includes/specials/SpecialComparePages.php
@@ -106,31 +106,37 @@ class SpecialComparePages extends SpecialPage {
$form->trySubmit();
}
- public static function showDiff( $data, HTMLForm $form ){
+ public static function showDiff( $data, HTMLForm $form ) {
$rev1 = self::revOrTitle( $data['Revision1'], $data['Page1'] );
$rev2 = self::revOrTitle( $data['Revision2'], $data['Page2'] );
- if( $rev1 && $rev2 ) {
- $de = new DifferenceEngine( $form->getContext(),
- $rev1,
- $rev2,
- null, // rcid
- ( $data['Action'] == 'purge' ),
- ( $data['Unhide'] == '1' )
- );
- $de->showDiffPage( true );
+ if ( $rev1 && $rev2 ) {
+ $revision = Revision::newFromId( $rev1 );
+
+ if ( $revision ) { // NOTE: $rev1 was already checked, should exist.
+ $contentHandler = $revision->getContentHandler();
+ $de = $contentHandler->createDifferenceEngine( $form->getContext(),
+ $rev1,
+ $rev2,
+ null, // rcid
+ ( $data['Action'] == 'purge' ),
+ ( $data['Unhide'] == '1' )
+ );
+ $de->showDiffPage( true );
+ }
}
}
public static function revOrTitle( $revision, $title ) {
- if( $revision ){
+ if ( $revision ) {
return $revision;
- } elseif( $title ) {
+ } elseif ( $title ) {
$title = Title::newFromText( $title );
- if( $title instanceof Title ){
+ if ( $title instanceof Title ) {
return $title->getLatestRevID();
}
}
+
return null;
}
@@ -145,6 +151,7 @@ class SpecialComparePages extends SpecialPage {
if ( !$title->exists() ) {
return $this->msg( 'compare-title-not-exists' )->parseAsBlock();
}
+
return true;
}
@@ -156,6 +163,11 @@ class SpecialComparePages extends SpecialPage {
if ( $revision === null ) {
return $this->msg( 'compare-revision-not-exists' )->parseAsBlock();
}
+
return true;
}
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
index 3e9ce128..3828b1c6 100644
--- a/includes/specials/SpecialConfirmemail.php
+++ b/includes/specials/SpecialConfirmemail.php
@@ -30,27 +30,30 @@
* @author Rob Church <robchur@gmail.com>
*/
class EmailConfirmation extends UnlistedSpecialPage {
-
- /**
- * Constructor
- */
public function __construct() {
- parent::__construct( 'Confirmemail' );
+ parent::__construct( 'Confirmemail', 'editmyprivateinfo' );
}
/**
* Main execution point
*
- * @param $code Confirmation code passed to the page
+ * @param null|string $code Confirmation code passed to the page
*/
function execute( $code ) {
$this->setHeaders();
$this->checkReadOnly();
+ $this->checkPermissions();
- if( $code === null || $code === '' ) {
- if( $this->getUser()->isLoggedIn() ) {
- if( Sanitizer::validateEmail( $this->getUser()->getEmail() ) ) {
+ // This could also let someone check the current email address, so
+ // require both permissions.
+ if ( !$this->getUser()->isAllowed( 'viewmyprivateinfo' ) ) {
+ throw new PermissionsError( 'viewmyprivateinfo' );
+ }
+
+ if ( $code === null || $code === '' ) {
+ if ( $this->getUser()->isLoggedIn() ) {
+ if ( Sanitizer::validateEmail( $this->getUser()->getEmail() ) ) {
$this->showRequestForm();
} else {
$this->getOutput()->addWikiMsg( 'confirmemail_noemail' );
@@ -62,7 +65,9 @@ class EmailConfirmation extends UnlistedSpecialPage {
array(),
array( 'returnto' => $this->getTitle()->getPrefixedText() )
);
- $this->getOutput()->addHTML( $this->msg( 'confirmemail_needlogin' )->rawParams( $llink )->parse() );
+ $this->getOutput()->addHTML(
+ $this->msg( 'confirmemail_needlogin' )->rawParams( $llink )->parse()
+ );
}
} else {
$this->attemptConfirm( $code );
@@ -75,7 +80,10 @@ class EmailConfirmation extends UnlistedSpecialPage {
function showRequestForm() {
$user = $this->getUser();
$out = $this->getOutput();
- if( $this->getRequest()->wasPosted() && $user->matchEditToken( $this->getRequest()->getText( 'token' ) ) ) {
+
+ if ( $this->getRequest()->wasPosted() &&
+ $user->matchEditToken( $this->getRequest()->getText( 'token' ) )
+ ) {
$status = $user->sendConfirmationMail();
if ( $status->isGood() ) {
$out->addWikiMsg( 'confirmemail_sent' );
@@ -83,7 +91,7 @@ class EmailConfirmation extends UnlistedSpecialPage {
$out->addWikiText( $status->getWikiText( 'confirmemail_sendfailed' ) );
}
} else {
- if( $user->isEmailConfirmed() ) {
+ if ( $user->isEmailConfirmed() ) {
// date and time are separate parameters to facilitate localisation.
// $time is kept for backward compat reasons.
// 'emailauthenticated' is also used in SpecialPreferences.php
@@ -94,14 +102,22 @@ class EmailConfirmation extends UnlistedSpecialPage {
$t = $lang->userTime( $emailAuthenticated, $user );
$out->addWikiMsg( 'emailauthenticated', $time, $d, $t );
}
- if( $user->isEmailConfirmationPending() ) {
- $out->wrapWikiMsg( "<div class=\"error mw-confirmemail-pending\">\n$1\n</div>", 'confirmemail_pending' );
+
+ if ( $user->isEmailConfirmationPending() ) {
+ $out->wrapWikiMsg(
+ "<div class=\"error mw-confirmemail-pending\">\n$1\n</div>",
+ 'confirmemail_pending'
+ );
}
+
$out->addWikiMsg( 'confirmemail_text' );
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl() ) );
- $form .= Html::hidden( 'token', $user->getEditToken() );
- $form .= Xml::submitButton( $this->msg( 'confirmemail_send' )->text() );
- $form .= Xml::closeElement( 'form' );
+ $form = Html::openElement(
+ 'form',
+ array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL() )
+ ) . "\n";
+ $form .= Html::hidden( 'token', $user->getEditToken() ) . "\n";
+ $form .= Xml::submitButton( $this->msg( 'confirmemail_send' )->text() ) . "\n";
+ $form .= Html::closeElement( 'form' ) . "\n";
$out->addHTML( $form );
}
}
@@ -110,24 +126,26 @@ class EmailConfirmation extends UnlistedSpecialPage {
* Attempt to confirm the user's email address and show success or failure
* as needed; if successful, take the user to log in
*
- * @param $code string Confirmation code
+ * @param string $code Confirmation code
*/
function attemptConfirm( $code ) {
$user = User::newFromConfirmationCode( $code );
- if( is_object( $user ) ) {
- $user->confirmEmail();
- $user->saveSettings();
- $message = $this->getUser()->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
- $this->getOutput()->addWikiMsg( $message );
- if( !$this->getUser()->isLoggedIn() ) {
- $title = SpecialPage::getTitleFor( 'Userlogin' );
- $this->getOutput()->returnToMain( true, $title );
- }
- } else {
+ if ( !is_object( $user ) ) {
$this->getOutput()->addWikiMsg( 'confirmemail_invalid' );
+
+ return;
}
- }
+ $user->confirmEmail();
+ $user->saveSettings();
+ $message = $this->getUser()->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
+ $this->getOutput()->addWikiMsg( $message );
+
+ if ( !$this->getUser()->isLoggedIn() ) {
+ $title = SpecialPage::getTitleFor( 'Userlogin' );
+ $this->getOutput()->returnToMain( true, $title );
+ }
+ }
}
/**
@@ -137,18 +155,14 @@ class EmailConfirmation extends UnlistedSpecialPage {
* @ingroup SpecialPage
*/
class EmailInvalidation extends UnlistedSpecialPage {
-
public function __construct() {
- parent::__construct( 'Invalidateemail' );
+ parent::__construct( 'Invalidateemail', 'editmyprivateinfo' );
}
function execute( $code ) {
$this->setHeaders();
-
- if ( wfReadOnly() ) {
- throw new ReadOnlyError;
- }
-
+ $this->checkReadOnly();
+ $this->checkPermissions();
$this->attemptInvalidate( $code );
}
@@ -156,19 +170,22 @@ class EmailInvalidation extends UnlistedSpecialPage {
* Attempt to invalidate the user's email address and show success or failure
* as needed; if successful, link to main page
*
- * @param $code string Confirmation code
+ * @param string $code Confirmation code
*/
function attemptInvalidate( $code ) {
$user = User::newFromConfirmationCode( $code );
- if( is_object( $user ) ) {
- $user->invalidateEmail();
- $user->saveSettings();
- $this->getOutput()->addWikiMsg( 'confirmemail_invalidated' );
- if( !$this->getUser()->isLoggedIn() ) {
- $this->getOutput()->returnToMain();
- }
- } else {
+ if ( !is_object( $user ) ) {
$this->getOutput()->addWikiMsg( 'confirmemail_invalid' );
+
+ return;
+ }
+
+ $user->invalidateEmail();
+ $user->saveSettings();
+ $this->getOutput()->addWikiMsg( 'confirmemail_invalidated' );
+
+ if ( !$this->getUser()->isLoggedIn() ) {
+ $this->getOutput()->returnToMain();
}
}
}
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
index 54f8e261..1fe98190 100644
--- a/includes/specials/SpecialContributions.php
+++ b/includes/specials/SpecialContributions.php
@@ -28,7 +28,6 @@
*/
class SpecialContributions extends SpecialPage {
-
protected $opts;
public function __construct() {
@@ -65,6 +64,7 @@ class SpecialContributions extends SpecialPage {
if ( !strlen( $target ) ) {
$out->addHTML( $this->getForm() );
+
return;
}
@@ -77,11 +77,13 @@ class SpecialContributions extends SpecialPage {
$nt = Title::makeTitleSafe( NS_USER, $target );
if ( !$nt ) {
$out->addHTML( $this->getForm() );
+
return;
}
$userObj = User::newFromName( $nt->getText(), false );
if ( !$userObj ) {
$out->addHTML( $this->getForm() );
+
return;
}
$id = $userObj->getID();
@@ -89,11 +91,17 @@ class SpecialContributions extends SpecialPage {
if ( $this->opts['contribs'] != 'newbie' ) {
$target = $nt->getText();
$out->addSubtitle( $this->contributionsSub( $userObj ) );
- $out->setHTMLTitle( $this->msg( 'pagetitle', $this->msg( 'contributions-title', $target )->plain() ) );
+ $out->setHTMLTitle( $this->msg(
+ 'pagetitle',
+ $this->msg( 'contributions-title', $target )->plain()
+ ) );
$this->getSkin()->setRelevantUser( $userObj );
} else {
$out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
- $out->setHTMLTitle( $this->msg( 'pagetitle', $this->msg( 'sp-contributions-newbies-title' )->plain() ) );
+ $out->setHTMLTitle( $this->msg(
+ 'pagetitle',
+ $this->msg( 'sp-contributions-newbies-title' )->plain()
+ ) );
}
if ( ( $ns = $request->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
@@ -103,10 +111,8 @@ class SpecialContributions extends SpecialPage {
}
$this->opts['associated'] = $request->getBool( 'associated' );
-
- $this->opts['nsInvert'] = (bool) $request->getVal( 'nsInvert' );
-
- $this->opts['tagfilter'] = (string) $request->getVal( 'tagfilter' );
+ $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
+ $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
// Allows reverts to have the bot flag in recent changes. It is just here to
// be passed in the form at the top of the page
@@ -152,9 +158,10 @@ class SpecialContributions extends SpecialPage {
$apiParams['month'] = $this->opts['month'];
}
- $url = wfScript( 'api' ) . '?' . wfArrayToCGI( $apiParams );
+ $url = wfAppendQuery( wfScript( 'api' ), $apiParams );
$out->redirect( $url, '301' );
+
return;
}
@@ -162,13 +169,13 @@ class SpecialContributions extends SpecialPage {
$this->addFeedLinks( array( 'action' => 'feedcontributions', 'user' => $target ) );
if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id ) ) ) {
-
$out->addHTML( $this->getForm() );
$pager = new ContribsPager( $this->getContext(), array(
'target' => $target,
'contribs' => $this->opts['contribs'],
'namespace' => $this->opts['namespace'],
+ 'tagfilter' => $this->opts['tagfilter'],
'year' => $this->opts['year'],
'month' => $this->opts['month'],
'deletedOnly' => $this->opts['deletedOnly'],
@@ -176,36 +183,37 @@ class SpecialContributions extends SpecialPage {
'nsInvert' => $this->opts['nsInvert'],
'associated' => $this->opts['associated'],
) );
+
if ( !$pager->getNumRows() ) {
$out->addWikiMsg( 'nocontribs', $target );
} else {
# Show a message about slave lag, if applicable
$lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
- if ( $lag > 0 )
+ if ( $lag > 0 ) {
$out->showLagWarning( $lag );
+ }
$out->addHTML(
'<p>' . $pager->getNavigationBar() . '</p>' .
- $pager->getBody() .
- '<p>' . $pager->getNavigationBar() . '</p>'
+ $pager->getBody() .
+ '<p>' . $pager->getNavigationBar() . '</p>'
);
}
$out->preventClickjacking( $pager->getPreventClickjacking() );
-
# Show the appropriate "footer" message - WHOIS tools, etc.
if ( $this->opts['contribs'] == 'newbie' ) {
$message = 'sp-contributions-footer-newbies';
- } elseif( IP::isIPAddress( $target ) ) {
+ } elseif ( IP::isIPAddress( $target ) ) {
$message = 'sp-contributions-footer-anon';
- } elseif( $userObj->isAnon() ) {
+ } elseif ( $userObj->isAnon() ) {
// No message for non-existing users
$message = '';
} else {
$message = 'sp-contributions-footer';
}
- if( $message ) {
+ if ( $message ) {
if ( !$this->msg( $message, $target )->isDisabled() ) {
$out->wrapWikiMsg(
"<div class='mw-contributions-footer'>\n$1\n</div>",
@@ -219,7 +227,8 @@ class SpecialContributions extends SpecialPage {
* Generates the subheading with links
* @param $userObj User object 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.
+ * @todo FIXME: Almost the same as getSubTitle in SpecialDeletedContributions.php.
+ * Could be combined.
*/
protected function contributionsSub( $userObj ) {
if ( $userObj->isAnon() ) {
@@ -259,18 +268,7 @@ class SpecialContributions extends SpecialPage {
}
}
- // Old message 'contribsub' had one parameter, but that doesn't work for
- // languages that want to put the "for" bit right after $user but before
- // $links. If 'contribsub' is around, use it for reverse compatibility,
- // otherwise use 'contribsub2'.
- // @todo Should this be removed at some point?
- $oldMsg = $this->msg( 'contribsub' );
- if ( $oldMsg->exists() ) {
- $linksWithParentheses = $this->msg( 'parentheses' )->rawParams( $links )->escaped();
- return $oldMsg->rawParams( "$user $linksWithParentheses" );
- } else {
- return $this->msg( 'contribsub2' )->rawParams( $user, $links );
- }
+ return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
}
/**
@@ -289,7 +287,7 @@ class SpecialContributions extends SpecialPage {
if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
if ( $this->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
- if ( $target->isBlocked() ) {
+ if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
$tools[] = Linker::linkKnown( # Change block link
SpecialPage::getTitleFor( 'Block', $username ),
$this->msg( 'change-blocklink' )->escaped()
@@ -305,14 +303,13 @@ class SpecialContributions extends SpecialPage {
);
}
}
+
# Block log link
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Log', 'block' ),
$this->msg( 'sp-contributions-blocklog' )->escaped(),
array(),
- array(
- 'page' => $userpage->getPrefixedText()
- )
+ array( 'page' => $userpage->getPrefixedText() )
);
}
# Uploads
@@ -346,6 +343,7 @@ class SpecialContributions extends SpecialPage {
}
wfRunHooks( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) );
+
return $tools;
}
@@ -360,7 +358,7 @@ class SpecialContributions extends SpecialPage {
if ( !isset( $this->opts['target'] ) ) {
$this->opts['target'] = '';
} else {
- $this->opts['target'] = str_replace( '_' , ' ' , $this->opts['target'] );
+ $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] );
}
if ( !isset( $this->opts['namespace'] ) ) {
@@ -399,10 +397,28 @@ class SpecialContributions extends SpecialPage {
$this->opts['topOnly'] = false;
}
- $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'class' => 'mw-contributions-form' ) );
+ $form = Html::openElement(
+ 'form',
+ array(
+ 'method' => 'get',
+ 'action' => $wgScript,
+ 'class' => 'mw-contributions-form'
+ )
+ );
# Add hidden params for tracking except for parameters in $skipParameters
- $skipParameters = array( 'namespace', 'nsInvert', 'deletedOnly', 'target', 'contribs', 'year', 'month', 'topOnly', 'associated' );
+ $skipParameters = array(
+ 'namespace',
+ 'nsInvert',
+ 'deletedOnly',
+ 'target',
+ 'contribs',
+ 'year',
+ 'month',
+ 'topOnly',
+ 'associated'
+ );
+
foreach ( $this->opts as $name => $value ) {
if ( in_array( $name, $skipParameters ) ) {
continue;
@@ -413,65 +429,82 @@ class SpecialContributions extends SpecialPage {
$tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] );
if ( $tagFilter ) {
- $filterSelection =
- Xml::tags( 'td', array( 'class' => 'mw-label' ), array_shift( $tagFilter ) ) .
- Xml::tags( 'td', array( 'class' => 'mw-input' ), implode( '&#160', $tagFilter ) );
+ $filterSelection = Html::rawElement(
+ 'td',
+ array( 'class' => 'mw-label' ),
+ array_shift( $tagFilter )
+ );
+ $filterSelection .= Html::rawElement(
+ 'td',
+ array( 'class' => 'mw-input' ),
+ implode( '&#160', $tagFilter )
+ );
} else {
- $filterSelection = Xml::tags( 'td', array( 'colspan' => 2 ), '' );
+ $filterSelection = Html::rawElement( 'td', array( 'colspan' => 2 ), '' );
}
- $targetSelection = Xml::tags( 'td', array( 'colspan' => 2 ),
- Xml::radioLabel(
- $this->msg( 'sp-contributions-newbies' )->text(),
- 'contribs',
- 'newbie' ,
- 'newbie',
- $this->opts['contribs'] == 'newbie',
- array( 'class' => 'mw-input' )
- ) . '<br />' .
- Xml::radioLabel(
- $this->msg( 'sp-contributions-username' )->text(),
- 'contribs',
- 'user',
- 'user',
- $this->opts['contribs'] == 'user',
- array( 'class' => 'mw-input' )
- ) . ' ' .
- Html::input(
- 'target',
- $this->opts['target'],
- 'text',
- array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) +
- ( $this->opts['target'] ? array() : array( 'autofocus' )
- )
- ) . ' '
- ) ;
-
- $namespaceSelection =
- Xml::tags( 'td', array( 'class' => 'mw-label' ),
- Xml::label(
- $this->msg( 'namespace' )->text(),
- 'namespace',
- ''
+ $labelNewbies = Xml::radioLabel(
+ $this->msg( 'sp-contributions-newbies' )->text(),
+ 'contribs',
+ 'newbie',
+ 'newbie',
+ $this->opts['contribs'] == 'newbie',
+ array( 'class' => 'mw-input' )
+ );
+ $labelUsername = Xml::radioLabel(
+ $this->msg( 'sp-contributions-username' )->text(),
+ 'contribs',
+ 'user',
+ 'user',
+ $this->opts['contribs'] == 'user',
+ array( 'class' => 'mw-input' )
+ );
+ $input = Html::input(
+ 'target',
+ $this->opts['target'],
+ 'text',
+ array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) +
+ ( $this->opts['target'] ? array() : array( 'autofocus' )
)
- ) .
- Xml::tags( 'td', null,
- Html::namespaceSelector( array(
- 'selected' => $this->opts['namespace'],
- 'all' => '',
- ), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
+ );
+ $targetSelection = Html::rawElement(
+ 'td',
+ array( 'colspan' => 2 ),
+ $labelNewbies . '<br />' . $labelUsername . ' ' . $input . ' '
+ );
+
+ $namespaceSelection = Xml::tags(
+ 'td',
+ array( 'class' => 'mw-label' ),
+ Xml::label(
+ $this->msg( 'namespace' )->text(),
+ 'namespace',
+ ''
+ )
+ );
+ $namespaceSelection .= Html::rawElement(
+ 'td',
+ null,
+ Html::namespaceSelector(
+ array( 'selected' => $this->opts['namespace'], 'all' => '' ),
+ array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
- ) ) .
- '&#160;' .
- Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
+ )
+ ) . '&#160;' .
+ Html::rawElement(
+ 'span',
+ array( 'style' => 'white-space: nowrap' ),
Xml::checkLabel(
$this->msg( 'invert' )->text(),
'nsInvert',
'nsInvert',
$this->opts['nsInvert'],
- array( 'title' => $this->msg( 'tooltip-invert' )->text(), 'class' => 'mw-input' )
+ array(
+ 'title' => $this->msg( 'tooltip-invert' )->text(),
+ 'class' => 'mw-input'
+ )
) . '&#160;'
) .
Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
@@ -480,13 +513,18 @@ class SpecialContributions extends SpecialPage {
'associated',
'associated',
$this->opts['associated'],
- array( 'title' => $this->msg( 'tooltip-namespace_association' )->text(), 'class' => 'mw-input' )
+ array(
+ 'title' => $this->msg( 'tooltip-namespace_association' )->text(),
+ 'class' => 'mw-input'
+ )
) . '&#160;'
)
- ) ;
+ );
- $extraOptions = Xml::tags( 'td', array( 'colspan' => 2 ),
- Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
+ if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
+ $deletedOnlyCheck = Html::rawElement(
+ 'span',
+ array( 'style' => 'white-space: nowrap' ),
Xml::checkLabel(
$this->msg( 'history-show-deleted' )->text(),
'deletedOnly',
@@ -494,57 +532,61 @@ class SpecialContributions extends SpecialPage {
$this->opts['deletedOnly'],
array( 'class' => 'mw-input' )
)
- ) .
- Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
- Xml::checkLabel(
- $this->msg( 'sp-contributions-toponly' )->text(),
- 'topOnly',
- 'mw-show-top-only',
- $this->opts['topOnly'],
- array( 'class' => 'mw-input' )
- )
+ );
+ } else {
+ $deletedOnlyCheck = '';
+ }
+
+ $checkLabelTopOnly = Html::rawElement(
+ 'span',
+ array( 'style' => 'white-space: nowrap' ),
+ Xml::checkLabel(
+ $this->msg( 'sp-contributions-toponly' )->text(),
+ 'topOnly',
+ 'mw-show-top-only',
+ $this->opts['topOnly'],
+ array( 'class' => 'mw-input' )
)
- ) ;
+ );
+ $extraOptions = Html::rawElement(
+ 'td',
+ array( 'colspan' => 2 ),
+ $deletedOnlyCheck . $checkLabelTopOnly
+ );
$dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ),
Xml::dateMenu(
- $this->opts['year'],
+ $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'],
$this->opts['month']
) . ' ' .
- Xml::submitButton(
- $this->msg( 'sp-contributions-submit' )->text(),
- array( 'class' => 'mw-submit' )
- )
- ) ;
-
- $form .=
- Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() ) .
- Xml::openElement( 'table', array( 'class' => 'mw-contributions-table' ) ) .
- Xml::openElement( 'tr' ) .
- $targetSelection .
- Xml::closeElement( 'tr' ) .
- Xml::openElement( 'tr' ) .
- $namespaceSelection .
- Xml::closeElement( 'tr' ) .
- Xml::openElement( 'tr' ) .
- $filterSelection .
- Xml::closeElement( 'tr' ) .
- Xml::openElement( 'tr' ) .
- $extraOptions .
- Xml::closeElement( 'tr' ) .
- Xml::openElement( 'tr' ) .
- $dateSelectionAndSubmit .
- Xml::closeElement( 'tr' ) .
- Xml::closeElement( 'table' );
+ Xml::submitButton(
+ $this->msg( 'sp-contributions-submit' )->text(),
+ array( 'class' => 'mw-submit' )
+ )
+ );
+
+ $form .= Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() );
+ $form .= Html::rawElement( 'table', array( 'class' => 'mw-contributions-table' ), "\n" .
+ Html::rawElement( 'tr', array(), $targetSelection ) . "\n" .
+ Html::rawElement( 'tr', array(), $namespaceSelection ) . "\n" .
+ Html::rawElement( 'tr', array(), $filterSelection ) . "\n" .
+ Html::rawElement( 'tr', array(), $extraOptions ) . "\n" .
+ Html::rawElement( 'tr', array(), $dateSelectionAndSubmit ) . "\n"
+ );
$explain = $this->msg( 'sp-contributions-explain' );
- if ( $explain->exists() ) {
- $form .= "<p id='mw-sp-contributions-explain'>{$explain}</p>";
+ if ( !$explain->isBlank() ) {
+ $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>";
}
- $form .= Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' );
+
+ $form .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' );
+
return $form;
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
/**
@@ -553,9 +595,11 @@ class SpecialContributions extends SpecialPage {
*/
class ContribsPager extends ReverseChronologicalPager {
public $mDefaultDirection = true;
- var $messages, $target;
- var $namespace = '', $mDb;
- var $preventClickjacking = false;
+ public $messages;
+ public $target;
+ public $namespace = '';
+ public $mDb;
+ public $preventClickjacking = false;
/**
* @var array
@@ -565,7 +609,15 @@ class ContribsPager extends ReverseChronologicalPager {
function __construct( IContextSource $context, array $options ) {
parent::__construct( $context );
- $msgs = array( 'uctop', 'diff', 'newarticle', 'rollbacklink', 'diff', 'hist', 'rev-delundel', 'pipe-separator' );
+ $msgs = array(
+ 'diff',
+ 'hist',
+ 'newarticle',
+ 'pipe-separator',
+ 'rev-delundel',
+ 'rollbacklink',
+ 'uctop'
+ );
foreach ( $msgs as $msg ) {
$this->messages[$msg] = $this->msg( $msg )->escaped();
@@ -591,6 +643,7 @@ class ContribsPager extends ReverseChronologicalPager {
function getDefaultQuery() {
$query = parent::getDefaultQuery();
$query['target'] = $this->target;
+
return $query;
}
@@ -598,13 +651,17 @@ class ContribsPager extends ReverseChronologicalPager {
* This method basically executes the exact same code as the parent class, though with
* a hook added, to allow extentions to add additional queries.
*
- * @param $offset String: index offset, inclusive
+ * @param string $offset index offset, inclusive
* @param $limit Integer: exact query limit
* @param $descending Boolean: query direction, false for ascending, true for descending
* @return ResultWrapper
*/
function reallyDoQuery( $offset, $limit, $descending ) {
- list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $descending );
+ list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
+ $offset,
+ $limit,
+ $descending
+ );
$pager = $this;
/*
@@ -625,13 +682,18 @@ class ContribsPager extends ReverseChronologicalPager {
* $limit: see phpdoc above
* $descending: see phpdoc above
*/
- $data = array( $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ) );
- wfRunHooks( 'ContribsPager::reallyDoQuery', array( &$data, $pager, $offset, $limit, $descending ) );
+ $data = array( $this->mDb->select(
+ $tables, $fields, $conds, $fname, $options, $join_conds
+ ) );
+ wfRunHooks(
+ 'ContribsPager::reallyDoQuery',
+ array( &$data, $pager, $offset, $limit, $descending )
+ );
$result = array();
// loop all results and collect them in an array
- foreach ( $data as $j => $query ) {
+ foreach ( $data as $query ) {
foreach ( $query as $i => $row ) {
// use index column as key, allowing us to easily sort in PHP
$result[$row->{$this->getIndexField()} . "-$i"] = $row;
@@ -674,15 +736,15 @@ class ContribsPager extends ReverseChronologicalPager {
$join_cond['user'] = Revision::userJoinCond();
$queryInfo = array(
- 'tables' => $tables,
- 'fields' => array_merge(
+ 'tables' => $tables,
+ 'fields' => array_merge(
Revision::selectFields(),
Revision::selectUserFields(),
array( 'page_namespace', 'page_title', 'page_is_new',
'page_latest', 'page_is_redirect', 'page_len' )
),
- 'conds' => $conds,
- 'options' => array( 'USE INDEX' => array( 'revision' => $index ) ),
+ 'conds' => $conds,
+ 'options' => array( 'USE INDEX' => array( 'revision' => $index ) ),
'join_conds' => $join_cond
);
@@ -696,6 +758,7 @@ class ContribsPager extends ReverseChronologicalPager {
);
wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
+
return $queryInfo;
}
@@ -710,7 +773,7 @@ class ContribsPager extends ReverseChronologicalPager {
# ignore local groups with the bot right
# @todo FIXME: Global groups may have 'bot' rights
$groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
- if( count( $groupsWithBotPermission ) ) {
+ if ( count( $groupsWithBotPermission ) ) {
$tables[] = 'user_groups';
$condition[] = 'ug_group IS NULL';
$join_conds['user_groups'] = array(
@@ -730,12 +793,15 @@ class ContribsPager extends ReverseChronologicalPager {
$index = 'usertext_timestamp';
}
}
+
if ( $this->deletedOnly ) {
- $condition[] = "rev_deleted != '0'";
+ $condition[] = 'rev_deleted != 0';
}
+
if ( $this->topOnly ) {
- $condition[] = "rev_id = page_latest";
+ $condition[] = 'rev_id = page_latest';
}
+
return array( $tables, $index, $condition, $join_conds );
}
@@ -747,20 +813,20 @@ class ContribsPager extends ReverseChronologicalPager {
if ( !$this->associated ) {
return array( "page_namespace $eq_op $selectedNS" );
- } else {
- $associatedNS = $this->mDb->addQuotes (
- MWNamespace::getAssociated( $this->namespace )
- );
- return array(
- "page_namespace $eq_op $selectedNS " .
- $bool_op .
- " page_namespace $eq_op $associatedNS"
- );
}
- } else {
- return array();
+ $associatedNS = $this->mDb->addQuotes(
+ MWNamespace::getAssociated( $this->namespace )
+ );
+
+ return array(
+ "page_namespace $eq_op $selectedNS " .
+ $bool_op .
+ " page_namespace $eq_op $associatedNS"
+ );
}
+
+ return array();
}
function getIndexField() {
@@ -774,7 +840,7 @@ class ContribsPager extends ReverseChronologicalPager {
$batch = new LinkBatch();
# Give some pointers to make (last) links
foreach ( $this->mResult as $row ) {
- if( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
+ if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
$revIds[] = $row->rev_parent_id;
}
if ( isset( $row->rev_id ) ) {
@@ -831,7 +897,7 @@ class ContribsPager extends ReverseChronologicalPager {
*/
wfSuppressWarnings();
$rev = new Revision( $row );
- $validRevision = $rev->getParentId() !== null;
+ $validRevision = (bool)$rev->getId();
wfRestoreWarnings();
if ( $validRevision ) {
@@ -851,8 +917,8 @@ class ContribsPager extends ReverseChronologicalPager {
$topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
# Add rollback link
if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user )
- && $page->quickUserCan( 'edit', $user ) )
- {
+ && $page->quickUserCan( 'edit', $user )
+ ) {
$this->preventClickjacking();
$topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
}
@@ -882,11 +948,22 @@ class ContribsPager extends ReverseChronologicalPager {
// For some reason rev_parent_id isn't populated for this row.
// Its rumoured this is true on wikipedia for some revisions (bug 34922).
// Next best thing is to have the total number of bytes.
- $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . Linker::formatRevisionSize( $row->rev_len ) . ' <span class="mw-changeslist-separator">. .</span> ';
+ $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
+ $chardiff .= Linker::formatRevisionSize( $row->rev_len );
+ $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
} else {
- $parentLen = isset( $this->mParentLens[$row->rev_parent_id] ) ? $this->mParentLens[$row->rev_parent_id] : 0;
- $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . ChangesList::showCharacterDifference(
- $parentLen, $row->rev_len, $this->getContext() ) . ' <span class="mw-changeslist-separator">. .</span> ';
+ $parentLen = 0;
+ if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
+ $parentLen = $this->mParentLens[$row->rev_parent_id];
+ }
+
+ $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
+ $chardiff .= ChangesList::showCharacterDifference(
+ $parentLen,
+ $row->rev_len,
+ $this->getContext()
+ );
+ $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
}
$lang = $this->getLanguage();
@@ -909,7 +986,7 @@ class ContribsPager extends ReverseChronologicalPager {
# Show user names for /newbies as there may be different users.
# Note that we already excluded rows with hidden user names.
if ( $this->contribs == 'newbie' ) {
- $userlink = ' . . ' . Linker::userLink( $rev->getUser(), $rev->getUserText() );
+ $userlink = ' . . ' . $lang->getDirMark() . Linker::userLink( $rev->getUser(), $rev->getUserText() );
$userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' ';
} else {
@@ -933,16 +1010,24 @@ class ContribsPager extends ReverseChronologicalPager {
$del .= ' ';
}
- $diffHistLinks = $this->msg( 'parentheses' )->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )->escaped();
- $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}";
+ $diffHistLinks = $this->msg( 'parentheses' )
+ ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )
+ ->escaped();
+ $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} ";
+ $ret .= "{$link}{$userlink} {$comment} {$topmarktext}";
# Denote if username is redacted for this edit
if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
- $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
+ $ret .= " <strong>" .
+ $this->msg( 'rev-deleted-user-contribs' )->escaped() .
+ "</strong>";
}
# Tags, if any.
- list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' );
+ list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
+ $row->ts_tags,
+ 'contributions'
+ );
$classes = array_merge( $classes, $newClasses );
$ret .= " $tagSummary";
}
@@ -950,10 +1035,15 @@ class ContribsPager extends ReverseChronologicalPager {
// Let extensions add data
wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) );
- $classes = implode( ' ', $classes );
- $ret = "<li class=\"$classes\">$ret</li>\n";
+ if ( $classes === array() && $ret === '' ) {
+ wfDebug( 'Dropping Special:Contribution row that could not be formatted' );
+ $ret = "<!-- Could not format Special:Contribution row. -->\n";
+ } else {
+ $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n";
+ }
wfProfileOut( __METHOD__ );
+
return $ret;
}
@@ -963,7 +1053,8 @@ class ContribsPager extends ReverseChronologicalPager {
*/
function getSqlComment() {
if ( $this->namespace || $this->deletedOnly ) {
- return 'contributions page filtered for namespace or RevisionDeleted edits'; // potentially slow, see CR r58153
+ // potentially slow, see CR r58153
+ return 'contributions page filtered for namespace or RevisionDeleted edits';
} else {
return 'contributions page unfiltered';
}
diff --git a/includes/specials/SpecialDeadendpages.php b/includes/specials/SpecialDeadendpages.php
index f4904a50..44137c1e 100644
--- a/includes/specials/SpecialDeadendpages.php
+++ b/includes/specials/SpecialDeadendpages.php
@@ -59,27 +59,36 @@ class DeadendPagesPage extends PageQueryPage {
function getQueryInfo() {
return array(
'tables' => array( 'page', 'pagelinks' ),
- 'fields' => array( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'page_title'
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title'
),
- 'conds' => array( 'pl_from IS NULL',
- 'page_namespace' => MWNamespace::getContentNamespaces(),
- 'page_is_redirect' => 0
+ 'conds' => array(
+ 'pl_from IS NULL',
+ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_is_redirect' => 0
),
- 'join_conds' => array( 'pagelinks' => array( 'LEFT JOIN', array(
- 'page_id=pl_from'
- ) ) )
+ 'join_conds' => array(
+ 'pagelinks' => array(
+ 'LEFT JOIN',
+ array( 'page_id=pl_from' )
+ )
+ )
);
}
function getOrderFields() {
// For some crazy reason ordering by a constant
// causes a filesort
- if( count( MWNamespace::getContentNamespaces() ) > 1 ) {
+ if ( count( MWNamespace::getContentNamespaces() ) > 1 ) {
return array( 'page_namespace', 'page_title' );
} else {
return array( 'page_title' );
}
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php
index c880b617..9b9888ad 100644
--- a/includes/specials/SpecialDeletedContributions.php
+++ b/includes/specials/SpecialDeletedContributions.php
@@ -27,13 +27,20 @@
*/
class DeletedContribsPager extends IndexPager {
public $mDefaultDirection = true;
- var $messages, $target;
- var $namespace = '', $mDb;
+ public $messages;
+ public $target;
+ public $namespace = '';
+ public $mDb;
+
+ /**
+ * @var string Navigation bar with paging links.
+ */
+ protected $mNavigationBar;
function __construct( IContextSource $context, $target, $namespace = false ) {
parent::__construct( $context );
$msgs = array( 'deletionlog', 'undeleteviewlink', 'diff' );
- foreach( $msgs as $msg ) {
+ foreach ( $msgs as $msg ) {
$this->messages[$msg] = $this->msg( $msg )->escaped();
}
$this->target = $target;
@@ -44,6 +51,7 @@ class DeletedContribsPager extends IndexPager {
function getDefaultQuery() {
$query = parent::getDefaultQuery();
$query['target'] = $this->target;
+
return $query;
}
@@ -52,17 +60,18 @@ class DeletedContribsPager extends IndexPager {
$conds = array_merge( $userCond, $this->getNamespaceCond() );
$user = $this->getUser();
// Paranoia: avoid brute force searches (bug 17792)
- if( !$user->isAllowed( 'deletedhistory' ) ) {
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
$conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0';
- } elseif( !$user->isAllowed( 'suppressrevision' ) ) {
+ } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
$conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::SUPPRESSED_USER ) .
' != ' . Revision::SUPPRESSED_USER;
}
+
return array(
'tables' => array( 'archive' ),
'fields' => array(
- 'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment', 'ar_minor_edit',
- 'ar_user', 'ar_user_text', 'ar_deleted'
+ 'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment',
+ 'ar_minor_edit', 'ar_user', 'ar_user_text', 'ar_deleted'
),
'conds' => $conds,
'options' => array( 'USE INDEX' => $index )
@@ -107,8 +116,17 @@ class DeletedContribsPager extends IndexPager {
$lang = $this->getLanguage();
$limits = $lang->pipeList( $limitLinks );
- $this->mNavigationBar = "(" . $lang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " .
- $this->msg( 'viewprevnext' )->rawParams( $pagingLinks['prev'], $pagingLinks['next'], $limits )->escaped();
+ $firstLast = $lang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) );
+ $firstLast = $this->msg( 'parentheses' )->rawParams( $firstLast )->escaped();
+ $prevNext = $this->msg( 'viewprevnext' )
+ ->rawParams(
+ $pagingLinks['prev'],
+ $pagingLinks['next'],
+ $limits
+ )->escaped();
+ $separator = $this->msg( 'word-separator' )->escaped();
+ $this->mNavigationBar = $firstLast . $separator . $prevNext;
+
return $this->mNavigationBar;
}
@@ -135,18 +153,19 @@ class DeletedContribsPager extends IndexPager {
function formatRow( $row ) {
wfProfileIn( __METHOD__ );
- $rev = new Revision( array(
- 'id' => $row->ar_rev_id,
- 'comment' => $row->ar_comment,
- 'user' => $row->ar_user,
- 'user_text' => $row->ar_user_text,
- 'timestamp' => $row->ar_timestamp,
- 'minor_edit' => $row->ar_minor_edit,
- 'deleted' => $row->ar_deleted,
- ) );
-
$page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+ $rev = new Revision( array(
+ 'title' => $page,
+ 'id' => $row->ar_rev_id,
+ 'comment' => $row->ar_comment,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'deleted' => $row->ar_deleted,
+ ) );
+
$undelete = SpecialPage::getTitleFor( 'Undelete' );
$logs = SpecialPage::getTitleFor( 'Log' );
@@ -167,7 +186,7 @@ class DeletedContribsPager extends IndexPager {
$user = $this->getUser();
- if( $user->isAllowed('deletedtext') ) {
+ if ( $user->isAllowed( 'deletedtext' ) ) {
$last = Linker::linkKnown(
$undelete,
$this->messages['diff'],
@@ -183,9 +202,10 @@ class DeletedContribsPager extends IndexPager {
}
$comment = Linker::revComment( $rev );
- $date = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $user ) );
+ $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $user );
+ $date = htmlspecialchars( $date );
- if( !$user->isAllowed( 'undelete' ) || !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ if ( !$user->isAllowed( 'undelete' ) || !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
$link = $date; // unusable link
} else {
$link = Linker::linkKnown(
@@ -199,7 +219,7 @@ class DeletedContribsPager extends IndexPager {
);
}
// Style deleted items
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$link = '<span class="history-deleted">' . $link . '</span>';
}
@@ -209,7 +229,7 @@ class DeletedContribsPager extends IndexPager {
array( 'class' => 'mw-changeslist-title' )
);
- if( $rev->isMinor() ) {
+ if ( $rev->isMinor() ) {
$mflag = ChangesList::flag( 'minor' );
} else {
$mflag = '';
@@ -217,7 +237,9 @@ class DeletedContribsPager extends IndexPager {
// Revision delete link
$del = Linker::getRevDeleteLink( $user, $rev, $page );
- if ( $del ) $del .= ' ';
+ if ( $del ) {
+ $del .= ' ';
+ }
$tools = Html::rawElement(
'span',
@@ -230,13 +252,14 @@ class DeletedContribsPager extends IndexPager {
$ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}";
# Denote if username is redacted for this edit
- if( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
$ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
}
$ret = Html::rawElement( 'li', array(), $ret ) . "\n";
wfProfileOut( __METHOD__ );
+
return $ret;
}
@@ -253,17 +276,18 @@ class DeletedContribsPager extends IndexPager {
class DeletedContributionsPage extends SpecialPage {
function __construct() {
parent::__construct( 'DeletedContributions', 'deletedhistory',
- /*listed*/ true, /*function*/ false, /*file*/ false );
+ /*listed*/true, /*function*/false, /*file*/false );
}
/**
* Special page "deleted user contributions".
* Shows a list of the deleted contributions of a user.
*
- * @param $par String: (optional) user name of the user for which to show the contributions
+ * @param string $par (optional) user name of the user for which to show the contributions
*/
function execute( $par ) {
global $wgQueryPageDefaultLimit;
+
$this->setHeaders();
$this->outputHeader();
@@ -271,6 +295,7 @@ class DeletedContributionsPage extends SpecialPage {
if ( !$this->userCanExecute( $user ) ) {
$this->displayRestrictionError();
+
return;
}
@@ -288,6 +313,7 @@ class DeletedContributionsPage extends SpecialPage {
if ( !strlen( $target ) ) {
$out->addHTML( $this->getForm( '' ) );
+
return;
}
@@ -297,6 +323,7 @@ class DeletedContributionsPage extends SpecialPage {
$userObj = User::newFromName( $target, false );
if ( !$userObj ) {
$out->addHTML( $this->getForm( '' ) );
+
return;
}
$this->getSkin()->setRelevantUser( $userObj );
@@ -315,28 +342,33 @@ class DeletedContributionsPage extends SpecialPage {
$pager = new DeletedContribsPager( $this->getContext(), $target, $options['namespace'] );
if ( !$pager->getNumRows() ) {
$out->addWikiMsg( 'nocontribs' );
+
return;
}
# Show a message about slave lag, if applicable
$lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
- if( $lag > 0 )
+ if ( $lag > 0 ) {
$out->showLagWarning( $lag );
+ }
$out->addHTML(
'<p>' . $pager->getNavigationBar() . '</p>' .
- $pager->getBody() .
- '<p>' . $pager->getNavigationBar() . '</p>' );
+ $pager->getBody() .
+ '<p>' . $pager->getNavigationBar() . '</p>' );
# If there were contributions, and it was a valid user or IP, show
# the appropriate "footer" message - WHOIS tools, etc.
- if( $target != 'newbies' ) {
- $message = IP::isIPAddress( $target )
- ? 'sp-contributions-footer-anon'
- : 'sp-contributions-footer';
-
- if( !$this->msg( $message )->isDisabled() ) {
- $out->wrapWikiMsg( "<div class='mw-contributions-footer'>\n$1\n</div>", array( $message, $target ) );
+ if ( $target != 'newbies' ) {
+ $message = IP::isIPAddress( $target ) ?
+ 'sp-contributions-footer-anon' :
+ 'sp-contributions-footer';
+
+ if ( !$this->msg( $message )->isDisabled() ) {
+ $out->wrapWikiMsg(
+ "<div class='mw-contributions-footer'>\n$1\n</div>",
+ array( $message, $target )
+ );
}
}
}
@@ -357,11 +389,12 @@ class DeletedContributionsPage extends SpecialPage {
$nt = $userObj->getUserPage();
$id = $userObj->getID();
$talk = $nt->getTalkPage();
- if( $talk ) {
+ if ( $talk ) {
# Talk page link
$tools[] = Linker::link( $talk, $this->msg( 'sp-contributions-talk' )->escaped() );
- if( ( $id !== null ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) {
- if( $this->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
+ if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) {
+ # Block / Change block / Unblock links
+ if ( $this->getUser()->isAllowed( 'block' ) ) {
if ( $userObj->isBlocked() ) {
$tools[] = Linker::linkKnown( # Change block link
SpecialPage::getTitleFor( 'Block', $nt->getDBkey() ),
@@ -376,8 +409,8 @@ class DeletedContributionsPage extends SpecialPage {
'ip' => $nt->getDBkey()
)
);
- }
- else { # User is not blocked
+ } else {
+ # User is not blocked
$tools[] = Linker::linkKnown( # Block link
SpecialPage::getTitleFor( 'Block', $nt->getDBkey() ),
$this->msg( 'blocklink' )->escaped()
@@ -418,7 +451,7 @@ class DeletedContributionsPage extends SpecialPage {
# Add a link to change user rights for privileged users
$userrightsPage = new UserrightsPage();
$userrightsPage->setContext( $this->getContext() );
- if( $userrightsPage->userCanChangeRights( $userObj ) ) {
+ if ( $userrightsPage->userCanChangeRights( $userObj ) ) {
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ),
$this->msg( 'sp-contributions-userrights' )->escaped()
@@ -431,7 +464,8 @@ class DeletedContributionsPage extends SpecialPage {
// Show a note if the user is blocked and display the last block log entry.
if ( $userObj->isBlocked() ) {
- $out = $this->getOutput(); // LogEventsList::showLogExtract() wants the first parameter by ref
+ // LogEventsList::showLogExtract() wants the first parameter by ref
+ $out = $this->getOutput();
LogEventsList::showLogExtract(
$out,
'block',
@@ -450,21 +484,12 @@ class DeletedContributionsPage extends SpecialPage {
}
}
- // Old message 'contribsub' had one parameter, but that doesn't work for
- // languages that want to put the "for" bit right after $user but before
- // $links. If 'contribsub' is around, use it for reverse compatibility,
- // otherwise use 'contribsub2'.
- $oldMsg = $this->msg( 'contribsub' );
- if ( $oldMsg->exists() ) {
- return $oldMsg->rawParams( "$user ($links)" );
- } else {
- return $this->msg( 'contribsub2' )->rawParams( $user, $links );
- }
+ return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
}
/**
* Generates the namespace selector form with hidden attributes.
- * @param $options Array: the options to be included.
+ * @param array $options the options to be included.
* @return string
*/
function getForm( $options ) {
@@ -474,7 +499,7 @@ class DeletedContributionsPage extends SpecialPage {
if ( !isset( $options['target'] ) ) {
$options['target'] = '';
} else {
- $options['target'] = str_replace( '_' , ' ' , $options['target'] );
+ $options['target'] = str_replace( '_', ' ', $options['target'] );
}
if ( !isset( $options['namespace'] ) ) {
@@ -498,27 +523,42 @@ class DeletedContributionsPage extends SpecialPage {
$f .= "\t" . Html::hidden( $name, $value ) . "\n";
}
- $f .= Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', array(), $this->msg( 'sp-contributions-search' )->text() ) .
- Xml::tags( 'label', array( 'for' => 'target' ), $this->msg( 'sp-contributions-username' )->parse() ) . ' ' .
- Html::input( 'target', $options['target'], 'text', array(
+ $f .= Xml::openElement( 'fieldset' );
+ $f .= Xml::element( 'legend', array(), $this->msg( 'sp-contributions-search' )->text() );
+ $f .= Xml::tags(
+ 'label',
+ array( 'for' => 'target' ),
+ $this->msg( 'sp-contributions-username' )->parse()
+ ) . ' ';
+ $f .= Html::input(
+ 'target',
+ $options['target'],
+ 'text',
+ array(
'size' => '20',
'required' => ''
- ) + ( $options['target'] ? array() : array( 'autofocus' ) ) ) . ' '.
- Html::namespaceSelector(
- array(
- 'selected' => $options['namespace'],
- 'all' => '',
- 'label' => $this->msg( 'namespace' )->text()
- ), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- )
- ) . ' ' .
- Xml::submitButton( $this->msg( 'sp-contributions-submit' )->text() ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' );
+ ) + ( $options['target'] ? array() : array( 'autofocus' ) )
+ ) . ' ';
+ $f .= Html::namespaceSelector(
+ array(
+ 'selected' => $options['namespace'],
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text()
+ ),
+ array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) . ' ';
+ $f .= Xml::submitButton( $this->msg( 'sp-contributions-submit' )->text() );
+ $f .= Xml::closeElement( 'fieldset' );
+ $f .= Xml::closeElement( 'form' );
+
return $f;
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php
deleted file mode 100644
index 48180a77..00000000
--- a/includes/specials/SpecialDisambiguations.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-/**
- * Implements Special:Disambiguations
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page that lists pages containing links to disambiguations pages
- *
- * @ingroup SpecialPage
- */
-class DisambiguationsPage extends QueryPage {
-
- function __construct( $name = 'Disambiguations' ) {
- parent::__construct( $name );
- }
-
- function isExpensive() {
- return true;
- }
-
- function isSyndicated() {
- return false;
- }
-
- function getPageHeader() {
- return $this->msg( 'disambiguations-text' )->parseAsBlock();
- }
-
- /**
- * @return string|bool False on failure
- */
- function getQueryFromLinkBatch() {
- $dbr = wfGetDB( DB_SLAVE );
- $dMsgText = $this->msg( 'disambiguationspage' )->inContentLanguage()->text();
- $linkBatch = new LinkBatch;
-
- # If the text can be treated as a title, use it verbatim.
- # Otherwise, pull the titles from the links table
- $dp = Title::newFromText( $dMsgText );
- if( $dp ) {
- if( $dp->getNamespace() != NS_TEMPLATE ) {
- # @todo FIXME: We assume the disambiguation message is a template but
- # the page can potentially be from another namespace :/
- wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n");
- }
- $linkBatch->addObj( $dp );
- } else {
- # Get all the templates linked from the Mediawiki:Disambiguationspage
- $disPageObj = Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage' );
- $res = $dbr->select(
- array('pagelinks', 'page'),
- 'pl_title',
- array('page_id = pl_from',
- 'pl_namespace' => NS_TEMPLATE,
- 'page_namespace' => $disPageObj->getNamespace(),
- 'page_title' => $disPageObj->getDBkey()),
- __METHOD__ );
-
- foreach ( $res as $row ) {
- $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title ));
- }
- }
- $set = $linkBatch->constructSet( 'tl', $dbr );
-
- if( $set === false ) {
- # We must always return a valid SQL query, but this way
- # the DB will always quickly return an empty result
- $set = 'FALSE';
- wfDebug( "Mediawiki:disambiguationspage message does not link to any templates!\n" );
- }
- return $set;
- }
-
- function getQueryInfo() {
- // @todo FIXME: What are pagelinks and p2 doing here?
- return array (
- 'tables' => array(
- 'templatelinks',
- 'p1' => 'page',
- 'pagelinks',
- 'p2' => 'page'
- ),
- 'fields' => array(
- 'namespace' => 'p1.page_namespace',
- 'title' => 'p1.page_title',
- 'value' => 'pl_from'
- ),
- 'conds' => array(
- $this->getQueryFromLinkBatch(),
- 'p1.page_id = tl_from',
- 'pl_namespace = p1.page_namespace',
- 'pl_title = p1.page_title',
- 'p2.page_id = pl_from',
- 'p2.page_namespace' => MWNamespace::getContentNamespaces()
- )
- );
- }
-
- function getOrderFields() {
- return array( 'tl_namespace', 'tl_title', 'value' );
- }
-
- function sortDescending() {
- return false;
- }
-
- /**
- * Fetch links and cache their existence
- *
- * @param $db DatabaseBase
- * @param $res
- */
- function preprocessResults( $db, $res ) {
- if ( !$res->numRows() ) {
- return;
- }
-
- $batch = new LinkBatch;
- foreach ( $res as $row ) {
- $batch->add( $row->namespace, $row->title );
- }
- $batch->execute();
-
- $res->seek( 0 );
- }
-
- function formatResult( $skin, $result ) {
- $title = Title::newFromID( $result->value );
- $dp = Title::makeTitle( $result->namespace, $result->title );
-
- $from = Linker::link( $title );
- $edit = Linker::link(
- $title,
- $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->escaped(),
- array(),
- array( 'redirect' => 'no', 'action' => 'edit' )
- );
- $arr = $this->getLanguage()->getArrow();
- $to = Linker::link( $dp );
-
- return "$from $edit $arr $to";
- }
-}
diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php
index 5864ca9f..c364f70f 100644
--- a/includes/specials/SpecialDoubleRedirects.php
+++ b/includes/specials/SpecialDoubleRedirects.php
@@ -28,14 +28,21 @@
* @ingroup SpecialPage
*/
class DoubleRedirectsPage extends QueryPage {
-
function __construct( $name = 'DoubleRedirects' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
- function sortDescending() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function sortDescending() {
+ return false;
+ }
function getPageHeader() {
return $this->msg( 'doubleredirectstext' )->parseAsBlock();
@@ -43,28 +50,52 @@ class DoubleRedirectsPage extends QueryPage {
function reallyGetQueryInfo( $namespace = null, $title = null ) {
$limitToTitle = !( $namespace === null && $title === null );
- $retval = array (
- 'tables' => array ( 'ra' => 'redirect',
- 'rb' => 'redirect', 'pa' => 'page',
- 'pb' => 'page', 'pc' => 'page' ),
- 'fields' => array ( 'namespace' => 'pa.page_namespace',
- 'title' => 'pa.page_title',
- 'value' => 'pa.page_title',
- 'nsb' => 'pb.page_namespace',
- 'tb' => 'pb.page_title',
- 'nsc' => 'pc.page_namespace',
- 'tc' => 'pc.page_title' ),
- 'conds' => array ( 'ra.rd_from = pa.page_id',
- 'pb.page_namespace = ra.rd_namespace',
- 'pb.page_title = ra.rd_title',
- 'rb.rd_from = pb.page_id',
- 'pc.page_namespace = rb.rd_namespace',
- 'pc.page_title = rb.rd_title' )
+ $dbr = wfGetDB( DB_SLAVE );
+ $retval = array(
+ 'tables' => array(
+ 'ra' => 'redirect',
+ 'rb' => 'redirect',
+ 'pa' => 'page',
+ 'pb' => 'page'
+ ),
+ 'fields' => array(
+ 'namespace' => 'pa.page_namespace',
+ 'title' => 'pa.page_title',
+ 'value' => 'pa.page_title',
+
+ 'nsb' => 'pb.page_namespace',
+ 'tb' => 'pb.page_title',
+
+ // Select fields from redirect instead of page. Because there may
+ // not actually be a page table row for this target (e.g. for interwiki redirects)
+ 'nsc' => 'rb.rd_namespace',
+ 'tc' => 'rb.rd_title',
+ 'iwc' => 'rb.rd_interwiki',
+ ),
+ 'conds' => array(
+ 'ra.rd_from = pa.page_id',
+
+ // Filter out redirects where the target goes interwiki (bug 40353).
+ // This isn't an optimization, it is required for correct results,
+ // otherwise a non-double redirect like Bar -> w:Foo will show up
+ // like "Bar -> Foo -> w:Foo".
+
+ // Need to check both NULL and "" for some reason,
+ // apparently either can be stored for non-iw entries.
+ 'ra.rd_interwiki IS NULL OR ra.rd_interwiki = ' . $dbr->addQuotes( '' ),
+
+ 'pb.page_namespace = ra.rd_namespace',
+ 'pb.page_title = ra.rd_title',
+
+ 'rb.rd_from = pb.page_id',
+ )
);
+
if ( $limitToTitle ) {
$retval['conds']['pa.page_namespace'] = $namespace;
$retval['conds']['pa.page_title'] = $title;
}
+
return $retval;
}
@@ -73,18 +104,35 @@ class DoubleRedirectsPage extends QueryPage {
}
function getOrderFields() {
- return array ( 'ra.rd_namespace', 'ra.rd_title' );
+ return array( 'ra.rd_namespace', 'ra.rd_title' );
}
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
$titleA = Title::makeTitle( $result->namespace, $result->title );
+ // If only titleA is in the query, it means this came from
+ // querycache (which only saves 3 columns).
+ // That does save the bulk of the query cost, but now we need to
+ // get a little more detail about each individual entry quickly
+ // using the filter of reallyGetQueryInfo.
if ( $result && !isset( $result->nsb ) ) {
$dbr = wfGetDB( DB_SLAVE );
- $qi = $this->reallyGetQueryInfo( $result->namespace,
- $result->title );
- $res = $dbr->select($qi['tables'], $qi['fields'],
- $qi['conds'], __METHOD__ );
+ $qi = $this->reallyGetQueryInfo(
+ $result->namespace,
+ $result->title
+ );
+ $res = $dbr->select(
+ $qi['tables'],
+ $qi['fields'],
+ $qi['conds'],
+ __METHOD__
+ );
+
if ( $res ) {
$result = $dbr->fetchObject( $res );
}
@@ -94,7 +142,7 @@ class DoubleRedirectsPage extends QueryPage {
}
$titleB = Title::makeTitle( $result->nsb, $result->tb );
- $titleC = Title::makeTitle( $result->nsc, $result->tc );
+ $titleC = Title::makeTitle( $result->nsc, $result->tc, '', $result->iwc );
$linkA = Linker::linkKnown(
$titleA,
@@ -125,6 +173,10 @@ class DoubleRedirectsPage extends QueryPage {
$lang = $this->getLanguage();
$arr = $lang->getArrow() . $lang->getDirMark();
- return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
+ return ( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
+ }
+
+ protected function getGroupName() {
+ return 'maintenance';
}
}
diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php
index 23cd9aa6..501552e9 100644
--- a/includes/specials/SpecialEditWatchlist.php
+++ b/includes/specials/SpecialEditWatchlist.php
@@ -35,7 +35,6 @@
* @author Rob Church <robchur@gmail.com>
*/
class SpecialEditWatchlist extends UnlistedSpecialPage {
-
/**
* Editing modes
*/
@@ -49,8 +48,8 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
private $badItems = array();
- public function __construct(){
- parent::__construct( 'EditWatchlist' );
+ public function __construct() {
+ parent::__construct( 'EditWatchlist', 'editmywatchlist' );
}
/**
@@ -64,7 +63,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$out = $this->getOutput();
# Anons don't get a watchlist
- if( $this->getUser()->isAnon() ) {
+ if ( $this->getUser()->isAnon() ) {
$out->setPageTitle( $this->msg( 'watchnologin' ) );
$llink = Linker::linkKnown(
SpecialPage::getTitleFor( 'Userlogin' ),
@@ -73,27 +72,29 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
array( 'returnto' => $this->getTitle()->getPrefixedText() )
);
$out->addHTML( $this->msg( 'watchlistanontext' )->rawParams( $llink )->parse() );
+
return;
}
$this->checkPermissions();
+ $this->checkReadOnly();
$this->outputHeader();
- $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName()
- )->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
+ $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
+ ->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
# B/C: $mode used to be waaay down the parameter list, and the first parameter
# was $wgUser
- if( $mode instanceof User ){
+ if ( $mode instanceof User ) {
$args = func_get_args();
- if( count( $args >= 4 ) ){
+ if ( count( $args ) >= 4 ) {
$mode = $args[3];
}
}
$mode = self::getMode( $this->getRequest(), $mode );
- switch( $mode ) {
+ switch ( $mode ) {
case self::EDIT_CLEAR:
// The "Clear" link scared people too much.
// Pass on to the raw editor, from which it's very easy to clear.
@@ -101,7 +102,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
case self::EDIT_RAW:
$out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
$form = $this->getRawForm();
- if( $form->show() ){
+ if ( $form->show() ) {
$out->addHTML( $this->successMessage );
$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
}
@@ -111,7 +112,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
default:
$out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
$form = $this->getNormalForm();
- if( $form->show() ){
+ if ( $form->show() ) {
$out->addHTML( $this->successMessage );
$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
} elseif ( $this->toc !== false ) {
@@ -130,15 +131,17 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
*/
private function extractTitles( $list ) {
$list = explode( "\n", trim( $list ) );
- if( !is_array( $list ) ) {
+ if ( !is_array( $list ) ) {
return array();
}
+
$titles = array();
- foreach( $list as $text ) {
+
+ foreach ( $list as $text ) {
$text = trim( $text );
- if( strlen( $text ) > 0 ) {
+ if ( strlen( $text ) > 0 ) {
$title = Title::newFromText( $text );
- if( $title instanceof Title && $title->isWatchable() ) {
+ if ( $title instanceof Title && $title->isWatchable() ) {
$titles[] = $title;
}
}
@@ -147,54 +150,57 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
GenderCache::singleton()->doTitlesArray( $titles );
$list = array();
- foreach( $titles as $title ) {
+ /** @var Title $title */
+ foreach ( $titles as $title ) {
$list[] = $title->getPrefixedText();
}
+
return array_unique( $list );
}
- public function submitRaw( $data ){
+ public function submitRaw( $data ) {
$wanted = $this->extractTitles( $data['Titles'] );
$current = $this->getWatchlist();
- if( count( $wanted ) > 0 ) {
+ if ( count( $wanted ) > 0 ) {
$toWatch = array_diff( $wanted, $current );
$toUnwatch = array_diff( $current, $wanted );
$this->watchTitles( $toWatch );
$this->unwatchTitles( $toUnwatch );
$this->getUser()->invalidateCache();
- if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ){
+ if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
$this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
} else {
return false;
}
- if( count( $toWatch ) > 0 ) {
+ if ( count( $toWatch ) > 0 ) {
$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added'
- )->numParams( count( $toWatch ) )->parse();
+ )->numParams( count( $toWatch ) )->parse();
$this->showTitles( $toWatch, $this->successMessage );
}
- if( count( $toUnwatch ) > 0 ) {
+ if ( count( $toUnwatch ) > 0 ) {
$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed'
- )->numParams( count( $toUnwatch ) )->parse();
+ )->numParams( count( $toUnwatch ) )->parse();
$this->showTitles( $toUnwatch, $this->successMessage );
}
} else {
$this->clearWatchlist();
$this->getUser()->invalidateCache();
- if( count( $current ) > 0 ){
+ if ( count( $current ) > 0 ) {
$this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
} else {
return false;
}
- $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed'
- )->numParams( count( $current ) )->parse();
+ $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
+ ->numParams( count( $current ) )->parse();
$this->showTitles( $current, $this->successMessage );
}
+
return true;
}
@@ -204,36 +210,42 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* $titles can be an array of strings or Title objects; the former
* is preferred, since Titles are very memory-heavy
*
- * @param $titles array of strings, or Title objects
+ * @param array $titles of strings, or Title objects
* @param $output String
*/
private function showTitles( $titles, &$output ) {
$talk = $this->msg( 'talkpagelinktext' )->escaped();
// Do a batch existence check
$batch = new LinkBatch();
- foreach( $titles as $title ) {
- if( !$title instanceof Title ) {
+ foreach ( $titles as $title ) {
+ if ( !$title instanceof Title ) {
$title = Title::newFromText( $title );
}
- if( $title instanceof Title ) {
+
+ if ( $title instanceof Title ) {
$batch->addObj( $title );
$batch->addObj( $title->getTalkPage() );
}
}
+
$batch->execute();
+
// Print out the list
$output .= "<ul>\n";
- foreach( $titles as $title ) {
- if( !$title instanceof Title ) {
+
+ foreach ( $titles as $title ) {
+ if ( !$title instanceof Title ) {
$title = Title::newFromText( $title );
}
- if( $title instanceof Title ) {
+
+ if ( $title instanceof Title ) {
$output .= "<li>"
. Linker::link( $title )
. ' (' . Linker::link( $title->getTalkPage(), $talk )
. ")</li>\n";
}
}
+
$output .= "</ul>\n";
}
@@ -246,6 +258,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
private function getWatchlist() {
$list = array();
$dbr = wfGetDB( DB_MASTER );
+
$res = $dbr->select(
'watchlist',
array(
@@ -255,10 +268,12 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
),
__METHOD__
);
- if( $res->numRows() > 0 ) {
+
+ if ( $res->numRows() > 0 ) {
$titles = array();
foreach ( $res as $row ) {
$title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
+
if ( $this->checkTitle( $title, $row->wl_namespace, $row->wl_title )
&& !$title->isTalkPage()
) {
@@ -269,11 +284,13 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
GenderCache::singleton()->doTitlesArray( $titles );
- foreach( $titles as $title ) {
+ foreach ( $titles as $title ) {
$list[] = $title->getPrefixedText();
}
}
+
$this->cleanupWatchlist();
+
return $list;
}
@@ -289,13 +306,14 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$res = $dbr->select(
array( 'watchlist' ),
- array( 'wl_namespace', 'wl_title' ),
+ array( 'wl_namespace', 'wl_title' ),
array( 'wl_user' => $this->getUser()->getId() ),
__METHOD__,
array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) )
);
$lb = new LinkBatch();
+
foreach ( $res as $row ) {
$lb->add( $row->wl_namespace, $row->wl_title );
if ( !MWNamespace::isTalk( $row->wl_namespace ) ) {
@@ -304,6 +322,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
}
$lb->execute();
+
return $titles;
}
@@ -312,7 +331,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
*
* @param Title $title
* @param int $namespace
- * @param String $dbKey
+ * @param string $dbKey
* @return bool: Whether this item is valid
*/
private function checkTitle( $title, $namespace, $dbKey ) {
@@ -323,12 +342,14 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
) {
$title = false; // unrecoverable
}
+
if ( !$title
|| $title->getNamespace() != $namespace
|| $title->getDBkey() != $dbKey
) {
$this->badItems[] = array( $title, $namespace, $dbKey );
}
+
return (bool)$title;
}
@@ -336,16 +357,17 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* Attempts to clean up broken items
*/
private function cleanupWatchlist() {
- if( !count( $this->badItems ) ) {
+ if ( !count( $this->badItems ) ) {
return; //nothing to do
}
+
$dbw = wfGetDB( DB_MASTER );
$user = $this->getUser();
+
foreach ( $this->badItems as $row ) {
list( $title, $namespace, $dbKey ) = $row;
- wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, "
- . ( $title ? 'cleaning up' : 'deleting' ) . ".\n"
- );
+ $action = $title ? 'cleaning up' : 'deleting';
+ wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
$dbw->delete( 'watchlist',
array(
@@ -381,30 +403,33 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* $titles can be an array of strings or Title objects; the former
* is preferred, since Titles are very memory-heavy
*
- * @param $titles Array of strings, or Title objects
+ * @param array $titles of strings, or Title objects
*/
private function watchTitles( $titles ) {
$dbw = wfGetDB( DB_MASTER );
$rows = array();
- foreach( $titles as $title ) {
- if( !$title instanceof Title ) {
+
+ foreach ( $titles as $title ) {
+ if ( !$title instanceof Title ) {
$title = Title::newFromText( $title );
}
- if( $title instanceof Title ) {
+
+ if ( $title instanceof Title ) {
$rows[] = array(
'wl_user' => $this->getUser()->getId(),
- 'wl_namespace' => ( $title->getNamespace() & ~1 ),
+ 'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
'wl_title' => $title->getDBkey(),
'wl_notificationtimestamp' => null,
);
$rows[] = array(
'wl_user' => $this->getUser()->getId(),
- 'wl_namespace' => ( $title->getNamespace() | 1 ),
+ 'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
'wl_title' => $title->getDBkey(),
'wl_notificationtimestamp' => null,
);
}
}
+
$dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
}
@@ -414,33 +439,37 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* $titles can be an array of strings or Title objects; the former
* is preferred, since Titles are very memory-heavy
*
- * @param $titles Array of strings, or Title objects
+ * @param array $titles of strings, or Title objects
*/
private function unwatchTitles( $titles ) {
$dbw = wfGetDB( DB_MASTER );
- foreach( $titles as $title ) {
- if( !$title instanceof Title ) {
+
+ foreach ( $titles as $title ) {
+ if ( !$title instanceof Title ) {
$title = Title::newFromText( $title );
}
- if( $title instanceof Title ) {
+
+ if ( $title instanceof Title ) {
$dbw->delete(
'watchlist',
array(
'wl_user' => $this->getUser()->getId(),
- 'wl_namespace' => ( $title->getNamespace() & ~1 ),
+ 'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
'wl_title' => $title->getDBkey(),
),
__METHOD__
);
+
$dbw->delete(
'watchlist',
array(
'wl_user' => $this->getUser()->getId(),
- 'wl_namespace' => ( $title->getNamespace() | 1 ),
+ 'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
'wl_title' => $title->getDBkey(),
),
__METHOD__
);
+
$page = WikiPage::factory( $title );
wfRunHooks( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) );
}
@@ -450,15 +479,16 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
public function submitNormal( $data ) {
$removed = array();
- foreach( $data as $titles ) {
+ foreach ( $data as $titles ) {
$this->unwatchTitles( $titles );
$removed = array_merge( $removed, $titles );
}
- if( count( $removed ) > 0 ) {
+ if ( count( $removed ) > 0 ) {
$this->successMessage = $this->msg( 'watchlistedit-normal-done'
- )->numParams( count( $removed ) )->parse();
+ )->numParams( count( $removed ) )->parse();
$this->showTitles( $removed, $this->successMessage );
+
return true;
} else {
return false;
@@ -470,26 +500,27 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
*
* @return HTMLForm
*/
- protected function getNormalForm(){
+ protected function getNormalForm() {
global $wgContLang;
$fields = array();
$count = 0;
- foreach( $this->getWatchlistInfo() as $namespace => $pages ){
+ foreach ( $this->getWatchlistInfo() as $namespace => $pages ) {
if ( $namespace >= 0 ) {
- $fields['TitlesNs'.$namespace] = array(
+ $fields['TitlesNs' . $namespace] = array(
'class' => 'EditWatchlistCheckboxSeriesField',
'options' => array(),
'section' => "ns$namespace",
);
}
- foreach( array_keys( $pages ) as $dbkey ){
+ foreach ( array_keys( $pages ) as $dbkey ) {
$title = Title::makeTitleSafe( $namespace, $dbkey );
+
if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
$text = $this->buildRemoveLine( $title );
- $fields['TitlesNs'.$namespace]['options'][$text] = htmlspecialchars( $title->getPrefixedText() );
+ $fields['TitlesNs' . $namespace]['options'][$text] = $title->getPrefixedText();
$count++;
}
}
@@ -499,30 +530,33 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
if ( count( $fields ) > 1 && $count > 30 ) {
$this->toc = Linker::tocIndent();
$tocLength = 0;
- foreach( $fields as $data ) {
+ foreach ( $fields as $data ) {
# strip out the 'ns' prefix from the section name:
$ns = substr( $data['section'], 2 );
- $nsText = ($ns == NS_MAIN)
+ $nsText = ( $ns == NS_MAIN )
? $this->msg( 'blanknamespace' )->escaped()
: htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
$this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
$this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
}
+
$this->toc = Linker::tocList( $this->toc );
} else {
$this->toc = false;
}
- $form = new EditWatchlistNormalHTMLForm( $fields, $this->getContext() );
- $form->setTitle( $this->getTitle() );
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle() ); // Remove subpage
+ $form = new EditWatchlistNormalHTMLForm( $fields, $context );
$form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
# Used message keys: 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
- $form->setSubmitTooltip('watchlistedit-normal-submit');
+ $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
$form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
$form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
$form->setSubmitCallback( array( $this, 'submitNormal' ) );
+
return $form;
}
@@ -534,12 +568,15 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
*/
private function buildRemoveLine( $title ) {
$link = Linker::link( $title );
- if( $title->isRedirect() ) {
+
+ if ( $title->isRedirect() ) {
// Linker already makes class mw-redirect, so this is redundant
$link = '<span class="watchlistredir">' . $link . '</span>';
}
+
$tools[] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() );
- if( $title->exists() ) {
+
+ if ( $title->exists() ) {
$tools[] = Linker::linkKnown(
$title,
$this->msg( 'history_short' )->escaped(),
@@ -547,7 +584,8 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
array( 'action' => 'history' )
);
}
- if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
+
+ if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
$this->msg( 'contributions' )->escaped()
@@ -564,7 +602,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
*
* @return HTMLForm
*/
- protected function getRawForm(){
+ protected function getRawForm() {
$titles = implode( $this->getWatchlist(), "\n" );
$fields = array(
'Titles' => array(
@@ -573,14 +611,16 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
'default' => $titles,
),
);
- $form = new HTMLForm( $fields, $this->getContext() );
- $form->setTitle( $this->getTitle( 'raw' ) );
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle( 'raw' ) ); // Reset subpage
+ $form = new HTMLForm( $fields, $context );
$form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
# Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
- $form->setSubmitTooltip('watchlistedit-raw-submit');
+ $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
$form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
$form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
$form->setSubmitCallback( array( $this, 'submitRaw' ) );
+
return $form;
}
@@ -594,19 +634,17 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
*/
public static function getMode( $request, $par ) {
$mode = strtolower( $request->getVal( 'action', $par ) );
- switch( $mode ) {
+
+ switch ( $mode ) {
case 'clear':
case self::EDIT_CLEAR:
return self::EDIT_CLEAR;
-
case 'raw':
case self::EDIT_RAW:
return self::EDIT_RAW;
-
case 'edit':
case self::EDIT_NORMAL:
return self::EDIT_NORMAL;
-
default:
return false;
}
@@ -628,32 +666,39 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
'edit' => array( 'EditWatchlist', false ),
'raw' => array( 'EditWatchlist', 'raw' ),
);
- foreach( $modes as $mode => $arr ) {
+
+ foreach ( $modes as $mode => $arr ) {
// can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( $arr[0], $arr[1] ),
wfMessage( "watchlisttools-{$mode}" )->escaped()
);
}
- return Html::rawElement( 'span',
- array( 'class' => 'mw-watchlist-toollinks' ),
- wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text() );
+
+ return Html::rawElement(
+ 'span',
+ array( 'class' => 'mw-watchlist-toollinks' ),
+ wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text()
+ );
}
}
# B/C since 1.18
-class WatchlistEditor extends SpecialEditWatchlist {}
+class WatchlistEditor extends SpecialEditWatchlist {
+}
/**
* Extend HTMLForm purely so we can have a more sane way of getting the section headers
*/
class EditWatchlistNormalHTMLForm extends HTMLForm {
- public function getLegend( $namespace ){
+ public function getLegend( $namespace ) {
$namespace = substr( $namespace, 2 );
+
return $namespace == NS_MAIN
? $this->msg( 'blanknamespace' )->escaped()
: htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
}
+
public function getBody() {
return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
}
@@ -667,8 +712,8 @@ class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
* form is open (bug 32126), but we know that invalid items will
* be harmless so we can override it here.
*
- * @param $value String the value the field was submitted with
- * @param $alldata Array the data collected from the form
+ * @param string $value the value the field was submitted with
+ * @param array $alldata the data collected from the form
* @return Mixed Bool true on success, or String error to display.
*/
function validate( $value, $alldata ) {
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
index 4d875e6e..2e90d996 100644
--- a/includes/specials/SpecialEmailuser.php
+++ b/includes/specials/SpecialEmailuser.php
@@ -29,13 +29,18 @@
class SpecialEmailUser extends UnlistedSpecialPage {
protected $mTarget;
+ /**
+ * @var User|string $mTargetObj
+ */
+ protected $mTargetObj;
+
public function __construct() {
parent::__construct( 'Emailuser' );
}
public function getDescription() {
$target = self::getTarget( $this->mTarget );
- if( !$target instanceof User ) {
+ if ( !$target instanceof User ) {
return $this->msg( 'emailuser-title-notarget' )->text();
}
@@ -106,7 +111,11 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$this->outputHeader();
// error out if sending user cannot do this
- $error = self::getPermissionsError( $this->getUser(), $this->getRequest()->getVal( 'wpEditToken' ) );
+ $error = self::getPermissionsError(
+ $this->getUser(),
+ $this->getRequest()->getVal( 'wpEditToken' )
+ );
+
switch ( $error ) {
case null:
# Wahey!
@@ -119,42 +128,45 @@ class SpecialEmailUser extends UnlistedSpecialPage {
throw new ThrottledError;
case 'mailnologin':
case 'usermaildisabled':
- throw new ErrorPageError( $error, "{$error}text" );
+ throw new ErrorPageError( $error, "{$error}text" );
default:
# It's a hook error
list( $title, $msg, $params ) = $error;
- throw new ErrorPageError( $title, $msg, $params );
+ throw new ErrorPageError( $title, $msg, $params );
}
// Got a valid target user name? Else ask for one.
$ret = self::getTarget( $this->mTarget );
- if( !$ret instanceof User ) {
- if( $this->mTarget != '' ) {
+ if ( !$ret instanceof User ) {
+ if ( $this->mTarget != '' ) {
$ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' );
$out->wrapWikiMsg( "<p class='error'>$1</p>", $ret );
}
$out->addHTML( $this->userForm( $this->mTarget ) );
+
return false;
}
$this->mTargetObj = $ret;
- $form = new HTMLForm( $this->getFormFields(), $this->getContext() );
- $form->addPreText( $this->msg( 'emailpagetext' )->parse() );
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle() ); // Remove subpage
+ $form = new HTMLForm( $this->getFormFields(), $context );
+ // By now we are supposed to be sure that $this->mTarget is a user name
+ $form->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() );
$form->setSubmitTextMsg( 'emailsend' );
- $form->setTitle( $this->getTitle() );
$form->setSubmitCallback( array( __CLASS__, 'uiSubmit' ) );
$form->setWrapperLegendMsg( 'email-legend' );
$form->loadData();
- if( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) {
+ if ( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) {
return false;
}
$result = $form->show();
- if( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
+ if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
$out->setPageTitle( $this->msg( 'emailsent' ) );
- $out->addWikiMsg( 'emailsenttext' );
+ $out->addWikiMsg( 'emailsenttext', $this->mTarget );
$out->returnToMain( false, $this->mTargetObj->getUserPage() );
}
}
@@ -162,24 +174,28 @@ class SpecialEmailUser extends UnlistedSpecialPage {
/**
* Validate target User
*
- * @param $target String: target user name
+ * @param string $target target user name
* @return User object on success or a string on error
*/
public static function getTarget( $target ) {
if ( $target == '' ) {
wfDebug( "Target is empty.\n" );
+
return 'notarget';
}
$nu = User::newFromName( $target );
- if( !$nu instanceof User || !$nu->getId() ) {
+ if ( !$nu instanceof User || !$nu->getId() ) {
wfDebug( "Target is invalid user.\n" );
+
return 'notarget';
} elseif ( !$nu->isEmailConfirmed() ) {
wfDebug( "User has no valid email.\n" );
+
return 'noemail';
} elseif ( !$nu->canReceiveEmail() ) {
wfDebug( "User does not allow user emails.\n" );
+
return 'nowikiemail';
}
@@ -190,36 +206,41 @@ class SpecialEmailUser extends UnlistedSpecialPage {
* Check whether a user is allowed to send email
*
* @param $user User object
- * @param $editToken String: edit token
+ * @param string $editToken edit token
* @return null on success or string on error
*/
public static function getPermissionsError( $user, $editToken ) {
global $wgEnableEmail, $wgEnableUserEmail;
- if( !$wgEnableEmail || !$wgEnableUserEmail ) {
+
+ if ( !$wgEnableEmail || !$wgEnableUserEmail ) {
return 'usermaildisabled';
}
- if( !$user->isAllowed( 'sendemail' ) ) {
+ if ( !$user->isAllowed( 'sendemail' ) ) {
return 'badaccess';
}
- if( !$user->isEmailConfirmed() ) {
+ if ( !$user->isEmailConfirmed() ) {
return 'mailnologin';
}
- if( $user->isBlockedFromEmailuser() ) {
+ if ( $user->isBlockedFromEmailuser() ) {
wfDebug( "User is blocked from sending e-mail.\n" );
+
return "blockedemailuser";
}
- if( $user->pingLimiter( 'emailuser' ) ) {
+ if ( $user->pingLimiter( 'emailuser' ) ) {
wfDebug( "Ping limiter triggered.\n" );
+
return 'actionthrottledtext';
}
$hookErr = false;
+
wfRunHooks( 'UserCanSendEmail', array( &$user, &$hookErr ) );
wfRunHooks( 'EmailUserPermissionsErrors', array( $user, $editToken, &$hookErr ) );
+
if ( $hookErr ) {
return $hookErr;
}
@@ -230,19 +251,30 @@ class SpecialEmailUser extends UnlistedSpecialPage {
/**
* Form to ask for target user name.
*
- * @param $name String: user name submitted.
+ * @param string $name user name submitted.
* @return String: form asking for user name.
*/
protected function userForm( $name ) {
global $wgScript;
- $string = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'askusername' ) ) .
+ $string = Xml::openElement(
+ 'form',
+ array( 'method' => 'get', 'action' => $wgScript, 'id' => 'askusername' )
+ ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
Xml::openElement( 'fieldset' ) .
Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) .
- Xml::inputLabel( $this->msg( 'emailusername' )->text(), 'target', 'emailusertarget', 30, $name ) . ' ' .
+ Xml::inputLabel(
+ $this->msg( 'emailusername' )->text(),
+ 'target',
+ 'emailusertarget',
+ 30,
+ $name
+ ) .
+ ' ' .
Xml::submitButton( $this->msg( 'emailusernamesubmit' )->text() ) .
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' ) . "\n";
+
return $string;
}
@@ -263,6 +295,8 @@ class SpecialEmailUser extends UnlistedSpecialPage {
* getPermissionsError(). It is probably also a good
* idea to check the edit token and ping limiter in advance.
*
+ * @param array $data
+ * @param IContextSource $context
* @return Mixed: Status object, or potentially a String on error
* or maybe even true on success if anything uses the EmailUser hook.
*/
@@ -270,9 +304,10 @@ class SpecialEmailUser extends UnlistedSpecialPage {
global $wgUserEmailUseReplyTo;
$target = self::getTarget( $data['Target'] );
- if( !$target instanceof User ) {
+ if ( !$target instanceof User ) {
return $context->msg( $target . 'text' )->parseAsBlock();
}
+
$to = new MailAddress( $target );
$from = new MailAddress( $context->getUser() );
$subject = $data['Subject'];
@@ -284,11 +319,11 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$from->name, $to->name )->inContentLanguage()->text();
$error = '';
- if( !wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) {
+ if ( !wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) {
return $error;
}
- if( $wgUserEmailUseReplyTo ) {
+ if ( $wgUserEmailUseReplyTo ) {
// Put the generic wiki autogenerated address in the From:
// header and reserve the user for Reply-To.
//
@@ -296,6 +331,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
// wiki-borne mails from direct mails and protects against
// SPF and bounce problems with some mailers (see below).
global $wgPasswordSender, $wgPasswordSenderName;
+
$mailFrom = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
$replyTo = $from;
} else {
@@ -318,7 +354,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$status = UserMailer::send( $to, $mailFrom, $subject, $text, $replyTo );
- if( !$status->isGood() ) {
+ if ( !$status->isGood() ) {
return $status;
} else {
// if the user requested a copy of this mail, do this now,
@@ -333,7 +369,12 @@ class SpecialEmailUser extends UnlistedSpecialPage {
}
wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $text ) );
+
return $status;
}
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php
index b4294b32..61ed34d4 100644
--- a/includes/specials/SpecialExport.php
+++ b/includes/specials/SpecialExport.php
@@ -29,7 +29,6 @@
* @ingroup SpecialPage
*/
class SpecialExport extends SpecialPage {
-
private $curonly, $doExport, $pageLinkDepth, $templates;
private $images;
@@ -75,12 +74,11 @@ class SpecialExport extends SpecialPage {
}
}
}
- }
- elseif( $request->getCheck( 'addns' ) && $wgExportFromNamespaces ) {
+ } elseif ( $request->getCheck( 'addns' ) && $wgExportFromNamespaces ) {
$page = $request->getText( 'pages' );
$nsindex = $request->getText( 'nsindex', '' );
- if ( strval( $nsindex ) !== '' ) {
+ if ( strval( $nsindex ) !== '' ) {
/**
* Same implementation as above, so same @todo
*/
@@ -89,8 +87,7 @@ class SpecialExport extends SpecialPage {
$page .= "\n" . implode( "\n", $nspages );
}
}
- }
- elseif( $request->getCheck( 'exportall' ) && $wgExportAllowAll ) {
+ } elseif ( $request->getCheck( 'exportall' ) && $wgExportAllowAll ) {
$this->doExport = true;
$exportall = true;
@@ -100,13 +97,12 @@ class SpecialExport extends SpecialPage {
doExport(...) further down) */
$page = '';
$history = '';
- }
- elseif( $request->wasPosted() && $par == '' ) {
+ } elseif ( $request->wasPosted() && $par == '' ) {
$page = $request->getText( 'pages' );
$this->curonly = $request->getCheck( 'curonly' );
$rawOffset = $request->getVal( 'offset' );
- if( $rawOffset ) {
+ if ( $rawOffset ) {
$offset = wfTimestamp( TS_MW, $rawOffset );
} else {
$offset = null;
@@ -124,18 +120,20 @@ class SpecialExport extends SpecialPage {
if ( $this->curonly ) {
$history = WikiExporter::CURRENT;
} elseif ( !$historyCheck ) {
- if ( $limit > 0 && ($wgExportMaxHistory == 0 || $limit < $wgExportMaxHistory ) ) {
+ if ( $limit > 0 && ( $wgExportMaxHistory == 0 || $limit < $wgExportMaxHistory ) ) {
$history['limit'] = $limit;
}
+
if ( !is_null( $offset ) ) {
$history['offset'] = $offset;
}
+
if ( strtolower( $dir ) == 'desc' ) {
$history['dir'] = 'desc';
}
}
- if( $page != '' ) {
+ if ( $page != '' ) {
$this->doExport = true;
}
} else {
@@ -143,25 +141,25 @@ class SpecialExport extends SpecialPage {
$page = $request->getText( 'pages', $par );
$historyCheck = $request->getCheck( 'history' );
- if( $historyCheck ) {
+ if ( $historyCheck ) {
$history = WikiExporter::FULL;
} else {
$history = WikiExporter::CURRENT;
}
- if( $page != '' ) {
+ if ( $page != '' ) {
$this->doExport = true;
}
}
- if( !$wgExportAllowHistory ) {
+ if ( !$wgExportAllowHistory ) {
// Override
$history = WikiExporter::CURRENT;
}
$list_authors = $request->getCheck( 'listauthors' );
if ( !$this->curonly || !$wgExportAllowListContributors ) {
- $list_authors = false ;
+ $list_authors = false;
}
if ( $this->doExport ) {
@@ -172,7 +170,7 @@ class SpecialExport extends SpecialPage {
wfResetOutputBuffers();
$request->response()->header( "Content-type: application/xml; charset=utf-8" );
- if( $request->getCheck( 'wpDownload' ) ) {
+ if ( $request->getCheck( 'wpDownload' ) ) {
// Provide a sane filename suggestion
$filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
$request->response()->header( "Content-disposition: attachment;filename={$filename}" );
@@ -187,9 +185,17 @@ class SpecialExport extends SpecialPage {
$out->addWikiMsg( 'exporttext' );
$form = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) );
- $form .= Xml::inputLabel( $this->msg( 'export-addcattext' )->text(), 'catname', 'catname', 40 ) . '&#160;';
- $form .= Xml::submitButton( $this->msg( 'export-addcat' )->text(), array( 'name' => 'addcat' ) ) . '<br />';
+ 'action' => $this->getTitle()->getLocalURL( 'action=submit' ) ) );
+ $form .= Xml::inputLabel(
+ $this->msg( 'export-addcattext' )->text(),
+ 'catname',
+ 'catname',
+ 40
+ ) . '&#160;';
+ $form .= Xml::submitButton(
+ $this->msg( 'export-addcat' )->text(),
+ array( 'name' => 'addcat' )
+ ) . '<br />';
if ( $wgExportFromNamespaces ) {
$form .= Html::namespaceSelector(
@@ -197,12 +203,15 @@ class SpecialExport extends SpecialPage {
'selected' => $nsindex,
'label' => $this->msg( 'export-addnstext' )->text()
), array(
- 'name' => 'nsindex',
- 'id' => 'namespace',
+ 'name' => 'nsindex',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
)
) . '&#160;';
- $form .= Xml::submitButton( $this->msg( 'export-addns' )->text(), array( 'name' => 'addns' ) ) . '<br />';
+ $form .= Xml::submitButton(
+ $this->msg( 'export-addns' )->text(),
+ array( 'name' => 'addns' )
+ ) . '<br />';
}
if ( $wgExportAllowAll ) {
@@ -214,10 +223,15 @@ class SpecialExport extends SpecialPage {
) . '<br />';
}
- $form .= Xml::element( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ), $page, false );
+ $form .= Xml::element(
+ 'textarea',
+ array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ),
+ $page,
+ false
+ );
$form .= '<br />';
- if( $wgExportAllowHistory ) {
+ if ( $wgExportAllowHistory ) {
$form .= Xml::checkLabel(
$this->msg( 'exportcuronly' )->text(),
'curonly',
@@ -235,9 +249,16 @@ class SpecialExport extends SpecialPage {
$request->wasPosted() ? $request->getCheck( 'templates' ) : false
) . '<br />';
- if( $wgExportMaxLinkDepth || $this->userCanOverrideExportDepth() ) {
- $form .= Xml::inputLabel( $this->msg( 'export-pagelinks' )->text(), 'pagelink-depth', 'pagelink-depth', 20, 0 ) . '<br />';
+ if ( $wgExportMaxLinkDepth || $this->userCanOverrideExportDepth() ) {
+ $form .= Xml::inputLabel(
+ $this->msg( 'export-pagelinks' )->text(),
+ 'pagelink-depth',
+ 'pagelink-depth',
+ 20,
+ 0
+ ) . '<br />';
}
+
// Enable this when we can do something useful exporting/importing image information. :)
//$form .= Xml::checkLabel( $this->msg( 'export-images' )->text(), 'images', 'wpExportImages', false ) . '<br />';
$form .= Xml::checkLabel(
@@ -256,7 +277,10 @@ class SpecialExport extends SpecialPage {
) . '<br />';
}
- $form .= Xml::submitButton( $this->msg( 'export-submit' )->text(), Linker::tooltipAndAccesskeyAttribs( 'export' ) );
+ $form .= Xml::submitButton(
+ $this->msg( 'export-submit' )->text(),
+ Linker::tooltipAndAccesskeyAttribs( 'export' )
+ );
$form .= Xml::closeElement( 'form' );
$out->addHTML( $form );
@@ -272,7 +296,7 @@ class SpecialExport extends SpecialPage {
/**
* Do the actual page exporting
*
- * @param $page String: user input on what page(s) to export
+ * @param string $page user input on what page(s) to export
* @param $history Mixed: one of the WikiExporter history export constants
* @param $list_authors Boolean: Whether to add distinct author list (when
* not returning full history)
@@ -286,12 +310,12 @@ class SpecialExport extends SpecialPage {
} else {
$pageSet = array(); // Inverted index of all pages to look up
-
+
// Split up and normalize input
- foreach( explode( "\n", $page ) as $pageName ) {
+ foreach ( explode( "\n", $page ) as $pageName ) {
$pageName = trim( $pageName );
$title = Title::newFromText( $pageName );
- if( $title && $title->getInterwiki() == '' && $title->getText() !== '' ) {
+ if ( $title && $title->getInterwiki() == '' && $title->getText() !== '' ) {
// Only record each page once!
$pageSet[$title->getPrefixedText()] = true;
}
@@ -301,25 +325,23 @@ class SpecialExport extends SpecialPage {
$inputPages = array_keys( $pageSet );
// Look up any linked pages if asked...
- if( $this->templates ) {
+ if ( $this->templates ) {
$pageSet = $this->getTemplates( $inputPages, $pageSet );
}
$linkDepth = $this->pageLinkDepth;
- if( $linkDepth ) {
+ if ( $linkDepth ) {
$pageSet = $this->getPageLinks( $inputPages, $pageSet, $linkDepth );
}
- /*
- // Enable this when we can do something useful exporting/importing image information. :)
- if( $this->images ) ) {
- $pageSet = $this->getImages( $inputPages, $pageSet );
- }
- */
+ // Enable this when we can do something useful exporting/importing image information.
+ // if( $this->images ) ) {
+ // $pageSet = $this->getImages( $inputPages, $pageSet );
+ // }
$pages = array_keys( $pageSet );
// Normalize titles to the same format and remove dupes, see bug 17374
- foreach( $pages as $k => $v ) {
+ foreach ( $pages as $k => $v ) {
$pages[$k] = str_replace( " ", "_", $v );
}
@@ -327,7 +349,7 @@ class SpecialExport extends SpecialPage {
}
/* Ok, let's get to it... */
- if( $history == WikiExporter::CURRENT ) {
+ if ( $history == WikiExporter::CURRENT ) {
$lb = false;
$db = wfGetDB( DB_SLAVE );
$buffer = WikiExporter::BUFFER;
@@ -339,7 +361,7 @@ class SpecialExport extends SpecialPage {
// This might take a while... :D
wfSuppressWarnings();
- set_time_limit(0);
+ set_time_limit( 0 );
wfRestoreWarnings();
}
@@ -350,26 +372,17 @@ class SpecialExport extends SpecialPage {
if ( $exportall ) {
$exporter->allPages();
} else {
- foreach( $pages as $page ) {
- /*
- if( $wgExportMaxHistory && !$this->curonly ) {
- $title = Title::newFromText( $page );
- if( $title ) {
- $count = Revision::countByTitle( $db, $title );
- if( $count > $wgExportMaxHistory ) {
- wfDebug( __FUNCTION__ .
- ": Skipped $page, $count revisions too big\n" );
- continue;
- }
- }
- }*/
- #Bug 8824: Only export pages the user can read
+ foreach ( $pages as $page ) {
+ #Bug 8824: Only export pages the user can read
$title = Title::newFromText( $page );
- if( is_null( $title ) ) {
- continue; #TODO: perhaps output an <error> tag or something.
+ if ( is_null( $title ) ) {
+ // @todo Perhaps output an <error> tag or something.
+ continue;
}
- if( !$title->userCan( 'read', $this->getUser() ) ) {
- continue; #TODO: perhaps output an <error> tag or something.
+
+ if ( !$title->userCan( 'read', $this->getUser() ) ) {
+ // @todo Perhaps output an <error> tag or something.
+ continue;
}
$exporter->pageByTitle( $title );
@@ -378,7 +391,7 @@ class SpecialExport extends SpecialPage {
$exporter->closeStream();
- if( $lb ) {
+ if ( $lb ) {
$lb->closeAll();
}
}
@@ -405,13 +418,14 @@ class SpecialExport extends SpecialPage {
foreach ( $res as $row ) {
$n = $row->page_title;
- if ($row->page_namespace) {
+ if ( $row->page_namespace ) {
$ns = $wgContLang->getNsText( $row->page_namespace );
$n = $ns . ':' . $n;
}
$pages[] = $n;
}
+
return $pages;
}
@@ -443,6 +457,7 @@ class SpecialExport extends SpecialPage {
$pages[] = $n;
}
+
return $pages;
}
@@ -468,12 +483,12 @@ class SpecialExport extends SpecialPage {
private function validateLinkDepth( $depth ) {
global $wgExportMaxLinkDepth;
- if( $depth < 0 ) {
+ if ( $depth < 0 ) {
return 0;
}
if ( !$this->userCanOverrideExportDepth() ) {
- if( $depth > $wgExportMaxLinkDepth ) {
+ if ( $depth > $wgExportMaxLinkDepth ) {
return $wgExportMaxLinkDepth;
}
}
@@ -483,6 +498,7 @@ class SpecialExport extends SpecialPage {
* crazy-big export from being done by someone setting the depth
* number too high. In other words, last resort safety net.
*/
+
return intval( min( $depth, 5 ) );
}
@@ -494,7 +510,7 @@ class SpecialExport extends SpecialPage {
* @return array
*/
private function getPageLinks( $inputPages, $pageSet, $depth ) {
- for( ; $depth > 0; --$depth ) {
+ for ( ; $depth > 0; --$depth ) {
$pageSet = $this->getLinks(
$inputPages, $pageSet, 'pagelinks',
array( 'namespace' => 'pl_namespace', 'title' => 'pl_title' ),
@@ -526,15 +542,20 @@ class SpecialExport extends SpecialPage {
/**
* Expand a list of pages to include items used in those pages.
+ * @param array $inputPages Array of page titles
+ * @param array $pageSet
+ * @param string $table
+ * @param array $fields Array of field names
+ * @param array $join
* @return array
*/
private function getLinks( $inputPages, $pageSet, $table, $fields, $join ) {
$dbr = wfGetDB( DB_SLAVE );
- foreach( $inputPages as $page ) {
+ foreach ( $inputPages as $page ) {
$title = Title::newFromText( $page );
- if( $title ) {
+ if ( $title ) {
$pageSet[$title->getPrefixedText()] = true;
/// @todo FIXME: May or may not be more efficient to batch these
/// by namespace when given multiple input pages.
@@ -551,7 +572,7 @@ class SpecialExport extends SpecialPage {
__METHOD__
);
- foreach( $result as $row ) {
+ foreach ( $result as $row ) {
$template = Title::makeTitle( $row->namespace, $row->title );
$pageSet[$template->getPrefixedText()] = true;
}
@@ -561,4 +582,7 @@ class SpecialExport extends SpecialPage {
return $pageSet;
}
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php
index 7e4bc9ce..47a4d75f 100644
--- a/includes/specials/SpecialFewestrevisions.php
+++ b/includes/specials/SpecialFewestrevisions.php
@@ -28,7 +28,6 @@
* @author Martin Drashkov
*/
class FewestrevisionsPage extends QueryPage {
-
function __construct( $name = 'Fewestrevisions' ) {
parent::__construct( $name );
}
@@ -42,41 +41,52 @@ class FewestrevisionsPage extends QueryPage {
}
function getQueryInfo() {
- return array (
- 'tables' => array ( 'revision', 'page' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'COUNT(*)',
- 'redirect' => 'page_is_redirect' ),
- 'conds' => array ( 'page_namespace' => MWNamespace::getContentNamespaces(),
- 'page_id = rev_page' ),
- 'options' => array ( 'HAVING' => 'COUNT(*) > 1',
- // ^^^ This was probably here to weed out redirects.
- // Since we mark them as such now, it might be
- // useful to remove this. People _do_ create pages
- // and never revise them, they aren't necessarily
- // redirects.
- 'GROUP BY' => array( 'page_namespace', 'page_title', 'page_is_redirect' ) )
+ return array(
+ 'tables' => array( 'revision', 'page' ),
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'COUNT(*)',
+ 'redirect' => 'page_is_redirect'
+ ),
+ 'conds' => array(
+ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_id = rev_page' ),
+ 'options' => array(
+ 'HAVING' => 'COUNT(*) > 1',
+ // ^^^ This was probably here to weed out redirects.
+ // Since we mark them as such now, it might be
+ // useful to remove this. People _do_ create pages
+ // and never revise them, they aren't necessarily
+ // redirects.
+ 'GROUP BY' => array( 'page_namespace', 'page_title', 'page_is_redirect' )
+ )
);
}
-
function sortDescending() {
return false;
}
/**
- * @param $skin Skin object
- * @param $result Object: database row
+ * @param Skin $skin
+ * @param object $result Database row
* @return String
*/
function formatResult( $skin, $result ) {
global $wgContLang;
$nt = Title::makeTitleSafe( $result->namespace, $result->title );
- if( !$nt ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
+ if ( !$nt ) {
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $result->namespace,
+ $result->title
+ )
+ );
}
$text = htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) );
@@ -94,4 +104,8 @@ class FewestrevisionsPage extends QueryPage {
return $this->getLanguage()->specialList( $plink, $nlink );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php
index ccf8ba17..4c6593b2 100644
--- a/includes/specials/SpecialFileDuplicateSearch.php
+++ b/includes/specials/SpecialFileDuplicateSearch.php
@@ -40,9 +40,17 @@ class FileDuplicateSearchPage extends QueryPage {
parent::__construct( $name );
}
- function isSyndicated() { return false; }
- function isCacheable() { return false; }
- function isCached() { return false; }
+ function isSyndicated() {
+ return false;
+ }
+
+ function isCacheable() {
+ return false;
+ }
+
+ function isCached() {
+ return false;
+ }
function linkParameters() {
return array( 'filename' => $this->filename );
@@ -51,7 +59,7 @@ class FileDuplicateSearchPage extends QueryPage {
/**
* Fetch dupes from all connected file repositories.
*
- * @return Array of File objects
+ * @return array of File objects
*/
function getDupes() {
return RepoGroup::singleton()->findBySha1( $this->hash );
@@ -59,7 +67,7 @@ class FileDuplicateSearchPage extends QueryPage {
/**
*
- * @param $dupes Array of File objects
+ * @param array $dupes of File objects
*/
function showList( $dupes ) {
$html = array();
@@ -93,11 +101,11 @@ class FileDuplicateSearchPage extends QueryPage {
$this->setHeaders();
$this->outputHeader();
- $this->filename = isset( $par ) ? $par : $this->getRequest()->getText( 'filename' );
+ $this->filename = isset( $par ) ? $par : $this->getRequest()->getText( 'filename' );
$this->file = null;
$this->hash = '';
$title = Title::newFromText( $this->filename, NS_FILE );
- if( $title && $title->getText() != '' ) {
+ if ( $title && $title->getText() != '' ) {
$this->file = wfFindFile( $title );
}
@@ -105,37 +113,46 @@ class FileDuplicateSearchPage extends QueryPage {
# Create the input form
$out->addHTML(
- Xml::openElement( 'form', array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgScript ) ) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, $this->msg( 'fileduplicatesearch-legend' )->text() ) .
- Xml::inputLabel( $this->msg( 'fileduplicatesearch-filename' )->text(), 'filename', 'filename', 50, $this->filename ) . ' ' .
- Xml::submitButton( $this->msg( 'fileduplicatesearch-submit' )->text() ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' )
+ Html::openElement(
+ 'form',
+ array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgScript )
+ ) . "\n" .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" .
+ Html::openElement( 'fieldset' ) . "\n" .
+ Html::element( 'legend', null, $this->msg( 'fileduplicatesearch-legend' )->text() ) . "\n" .
+ Xml::inputLabel(
+ $this->msg( 'fileduplicatesearch-filename' )->text(),
+ 'filename',
+ 'filename',
+ 50,
+ $this->filename
+ ) . "\n" .
+ Xml::submitButton( $this->msg( 'fileduplicatesearch-submit' )->text() ) . "\n" .
+ Html::closeElement( 'fieldset' ) . "\n" .
+ Html::closeElement( 'form' )
);
- if( $this->file ) {
+ if ( $this->file ) {
$this->hash = $this->file->getSha1();
- } elseif( $this->filename !== '' ) {
+ } elseif ( $this->filename !== '' ) {
$out->wrapWikiMsg(
"<p class='mw-fileduplicatesearch-noresults'>\n$1\n</p>",
array( 'fileduplicatesearch-noresults', wfEscapeWikiText( $this->filename ) )
);
}
- if( $this->hash != '' ) {
+ if ( $this->hash != '' ) {
# Show a thumbnail of the file
$img = $this->file;
if ( $img ) {
$thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
- if( $thumb ) {
+ if ( $thumb ) {
$out->addHTML( '<div id="mw-fileduplicatesearch-icon">' .
$thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' .
$this->msg( 'fileduplicatesearch-info' )->numParams(
$img->getWidth(), $img->getHeight() )->params(
- $this->getLanguage()->formatSize( $img->getSize() ),
- $img->getMimeType() )->parseAsBlock() .
+ $this->getLanguage()->formatSize( $img->getSize() ),
+ $img->getMimeType() )->parseAsBlock() .
'</div>' );
}
}
@@ -144,7 +161,7 @@ class FileDuplicateSearchPage extends QueryPage {
$numRows = count( $dupes );
# Show a short summary
- if( $numRows == 1 ) {
+ if ( $numRows == 1 ) {
$out->wrapWikiMsg(
"<p class='mw-fileduplicatesearch-result-1'>\n$1\n</p>",
array( 'fileduplicatesearch-result-1', wfEscapeWikiText( $this->filename ) )
@@ -164,14 +181,16 @@ class FileDuplicateSearchPage extends QueryPage {
function doBatchLookups( $list ) {
$batch = new LinkBatch();
- foreach( $list as $file ) {
+ /** @var File $file */
+ foreach ( $list as $file ) {
$batch->addObj( $file->getTitle() );
- if( $file->isLocal() ) {
+ if ( $file->isLocal() ) {
$userName = $file->getUser( 'text' );
$batch->add( NS_USER, $userName );
$batch->add( NS_USER_TALK, $userName );
}
}
+
$batch->execute();
}
@@ -207,4 +226,8 @@ class FileDuplicateSearchPage extends QueryPage {
return "$plink . . $user . . $time";
}
+
+ protected function getGroupName() {
+ return 'media';
+ }
}
diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php
index e0866504..e7ced52a 100644
--- a/includes/specials/SpecialFilepath.php
+++ b/includes/specials/SpecialFilepath.php
@@ -2,6 +2,7 @@
/**
* Implements Special:Filepath
*
+ * @section LICENSE
* 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
@@ -26,64 +27,19 @@
*
* @ingroup SpecialPage
*/
-class SpecialFilepath extends SpecialPage {
-
+class SpecialFilepath extends RedirectSpecialPage {
function __construct() {
parent::__construct( 'Filepath' );
+ $this->mAllowedRedirectParams = array( 'width', 'height' );
}
- function execute( $par ) {
- $this->setHeaders();
- $this->outputHeader();
-
- $request = $this->getRequest();
- $file = !is_null( $par ) ? $par : $request->getText( 'file' );
-
- $title = Title::newFromText( $file, NS_FILE );
-
- if ( ! $title instanceof Title || $title->getNamespace() != NS_FILE ) {
- $this->showForm( $title );
- } else {
- $file = wfFindFile( $title );
-
- if ( $file && $file->exists() ) {
- // Default behaviour: Use the direct link to the file.
- $url = $file->getURL();
- $width = $request->getInt( 'width', -1 );
- $height = $request->getInt( 'height', -1 );
-
- // If a width is requested...
- if ( $width != -1 ) {
- $mto = $file->transform( array( 'width' => $width, 'height' => $height ) );
- // ... and we can
- if ( $mto && !$mto->isError() ) {
- // ... change the URL to point to a thumbnail.
- $url = $mto->getURL();
- }
- }
- $this->getOutput()->redirect( $url );
- } else {
- $this->getOutput()->setStatusCode( 404 );
- $this->showForm( $title );
- }
- }
+ // implement by redirecting through Special:Redirect/file
+ function getRedirect( $par ) {
+ $file = $par ?: $this->getRequest()->getText( 'file' );
+ return SpecialPage::getSafeTitleFor( 'Redirect', 'file/' . $file );
}
- /**
- * @param $title Title
- */
- function showForm( $title ) {
- global $wgScript;
-
- $this->getOutput()->addHTML(
- Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) .
- Html::openElement( 'fieldset' ) .
- Html::element( 'legend', null, $this->msg( 'filepath' )->text() ) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
- Xml::inputLabel( $this->msg( 'filepath-page' )->text(), 'file', 'file', 25, is_object( $title ) ? $title->getText() : '' ) . ' ' .
- Xml::submitButton( $this->msg( 'filepath-submit' )->text() ) . "\n" .
- Html::closeElement( 'fieldset' ) .
- Html::closeElement( 'form' )
- );
+ protected function getGroupName() {
+ return 'media';
}
}
diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php
index 362fc5cf..d7d860de 100644
--- a/includes/specials/SpecialImport.php
+++ b/includes/specials/SpecialImport.php
@@ -30,12 +30,11 @@
* @ingroup SpecialPage
*/
class SpecialImport extends SpecialPage {
-
private $interwiki = false;
private $namespace;
private $rootpage = '';
private $frompage = '';
- private $logcomment= false;
+ private $logcomment = false;
private $history = true;
private $includeTemplates = false;
private $pageLinkDepth;
@@ -101,20 +100,20 @@ class SpecialImport extends SpecialPage {
$this->logcomment = $request->getText( 'log-comment' );
$this->pageLinkDepth = $wgExportMaxLinkDepth == 0 ? 0 : $request->getIntOrNull( 'pagelink-depth' );
- $this->rootpage = $request->getText( 'rootpage' );
+ $this->rootpage = $request->getText( 'rootpage' );
$user = $this->getUser();
if ( !$user->matchEditToken( $request->getVal( 'editToken' ) ) ) {
$source = Status::newFatal( 'import-token-mismatch' );
} elseif ( $sourceName == 'upload' ) {
$isUpload = true;
- if( $user->isAllowed( 'importupload' ) ) {
+ if ( $user->isAllowed( 'importupload' ) ) {
$source = ImportStreamSource::newFromUpload( "xmlimport" );
} else {
throw new PermissionsError( 'importupload' );
}
} elseif ( $sourceName == "interwiki" ) {
- if( !$user->isAllowed( 'import' ) ){
+ if ( !$user->isAllowed( 'import' ) ) {
throw new PermissionsError( 'import' );
}
$this->interwiki = $request->getVal( 'interwiki' );
@@ -136,24 +135,40 @@ class SpecialImport extends SpecialPage {
}
$out = $this->getOutput();
- if( !$source->isGood() ) {
- $out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'importfailed', $source->getWikiText() ) );
+ if ( !$source->isGood() ) {
+ $out->wrapWikiMsg(
+ "<p class=\"error\">\n$1\n</p>",
+ array( 'importfailed', $source->getWikiText() )
+ );
} else {
$importer = new WikiImporter( $source->value );
- if( !is_null( $this->namespace ) ) {
+ if ( !is_null( $this->namespace ) ) {
$importer->setTargetNamespace( $this->namespace );
}
- if( !is_null( $this->rootpage ) ) {
+ if ( !is_null( $this->rootpage ) ) {
$statusRootPage = $importer->setTargetRootPage( $this->rootpage );
- if( !$statusRootPage->isGood() ) {
- $out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'import-options-wrong', $statusRootPage->getWikiText(), count( $statusRootPage->getErrorsArray() ) ) );
+ if ( !$statusRootPage->isGood() ) {
+ $out->wrapWikiMsg(
+ "<p class=\"error\">\n$1\n</p>",
+ array(
+ 'import-options-wrong',
+ $statusRootPage->getWikiText(),
+ count( $statusRootPage->getErrorsArray() )
+ )
+ );
+
return;
}
}
$out->addWikiMsg( "importstart" );
- $reporter = new ImportReporter( $importer, $isUpload, $this->interwiki , $this->logcomment);
+ $reporter = new ImportReporter(
+ $importer,
+ $isUpload,
+ $this->interwiki,
+ $this->logcomment
+ );
$reporter->setContext( $this->getContext() );
$exception = false;
@@ -167,10 +182,16 @@ class SpecialImport extends SpecialPage {
if ( $exception ) {
# No source or XML parse error
- $out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'importfailed', $exception->getMessage() ) );
- } elseif( !$result->isGood() ) {
+ $out->wrapWikiMsg(
+ "<p class=\"error\">\n$1\n</p>",
+ array( 'importfailed', $exception->getMessage() )
+ );
+ } elseif ( !$result->isGood() ) {
# Zero revisions
- $out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'importfailed', $result->getWikiText() ) );
+ $out->wrapWikiMsg(
+ "<p class=\"error\">\n$1\n</p>",
+ array( 'importfailed', $result->getWikiText() )
+ );
} else {
# Success!
$out->addWikiMsg( 'importsuccess' );
@@ -182,165 +203,202 @@ class SpecialImport extends SpecialPage {
private function showForm() {
global $wgImportSources, $wgExportMaxLinkDepth;
- $action = $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) );
+ $action = $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) );
$user = $this->getUser();
$out = $this->getOutput();
- if( $user->isAllowed( 'importupload' ) ) {
+ if ( $user->isAllowed( 'importupload' ) ) {
$out->addHTML(
- Xml::fieldset( $this->msg( 'import-upload' )->text() ).
- Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post',
- 'action' => $action, 'id' => 'mw-import-upload-form' ) ) .
- $this->msg( 'importtext' )->parseAsBlock() .
- Html::hidden( 'action', 'submit' ) .
- Html::hidden( 'source', 'upload' ) .
- Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
-
- "<tr>
+ Xml::fieldset( $this->msg( 'import-upload' )->text() ) .
+ Xml::openElement(
+ 'form',
+ array(
+ 'enctype' => 'multipart/form-data',
+ 'method' => 'post',
+ 'action' => $action,
+ 'id' => 'mw-import-upload-form'
+ )
+ ) .
+ $this->msg( 'importtext' )->parseAsBlock() .
+ Html::hidden( 'action', 'submit' ) .
+ Html::hidden( 'source', 'upload' ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-import-table-upload' ) ) .
+ "<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-upload-filename' )->text(), 'xmlimport' ) .
+ Xml::label( $this->msg( 'import-upload-filename' )->text(), 'xmlimport' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
+ Html::input( 'xmlimport', '', 'file', array( 'id' => 'xmlimport' ) ) . ' ' .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) .
+ Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'log-comment', 50, '',
- array( 'id' => 'mw-import-comment', 'type' => 'text' ) ) . ' ' .
+ Xml::input( 'log-comment', 50, '',
+ array( 'id' => 'mw-import-comment', 'type' => 'text' ) ) . ' ' .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage' ) .
+ Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage-upload' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'rootpage', 50, $this->rootpage,
- array( 'id' => 'mw-interwiki-rootpage', 'type' => 'text' ) ) . ' ' .
+ Xml::input( 'rootpage', 50, $this->rootpage,
+ array( 'id' => 'mw-interwiki-rootpage-upload', 'type' => 'text' ) ) . ' ' .
"</td>
</tr>
<tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( $this->msg( 'uploadbtn' )->text() ) .
+ Xml::submitButton( $this->msg( 'uploadbtn' )->text() ) .
"</td>
</tr>" .
- Xml::closeElement( 'table' ).
- Html::hidden( 'editToken', $user->getEditToken() ) .
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' )
+ Xml::closeElement( 'table' ) .
+ Html::hidden( 'editToken', $user->getEditToken() ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
);
} else {
- if( empty( $wgImportSources ) ) {
+ if ( empty( $wgImportSources ) ) {
$out->addWikiMsg( 'importnosources' );
}
}
- if( $user->isAllowed( 'import' ) && !empty( $wgImportSources ) ) {
+ if ( $user->isAllowed( 'import' ) && !empty( $wgImportSources ) ) {
# Show input field for import depth only if $wgExportMaxLinkDepth > 0
$importDepth = '';
- if( $wgExportMaxLinkDepth > 0 ) {
+ if ( $wgExportMaxLinkDepth > 0 ) {
$importDepth = "<tr>
<td class='mw-label'>" .
- $this->msg( 'export-pagelinks' )->parse() .
- "</td>
+ $this->msg( 'export-pagelinks' )->parse() .
+ "</td>
<td class='mw-input'>" .
- Xml::input( 'pagelink-depth', 3, 0 ) .
- "</td>
- </tr>";
+ Xml::input( 'pagelink-depth', 3, 0 ) .
+ "</td>
+ </tr>";
}
$out->addHTML(
Xml::fieldset( $this->msg( 'importinterwiki' )->text() ) .
- Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'mw-import-interwiki-form' ) ) .
- $this->msg( 'import-interwiki-text' )->parseAsBlock() .
- Html::hidden( 'action', 'submit' ) .
- Html::hidden( 'source', 'interwiki' ) .
- Html::hidden( 'editToken', $user->getEditToken() ) .
- Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
- "<tr>
+ Xml::openElement(
+ 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $action,
+ 'id' => 'mw-import-interwiki-form'
+ )
+ ) .
+ $this->msg( 'import-interwiki-text' )->parseAsBlock() .
+ Html::hidden( 'action', 'submit' ) .
+ Html::hidden( 'source', 'interwiki' ) .
+ Html::hidden( 'editToken', $user->getEditToken() ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-import-table-interwiki' ) ) .
+ "<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-interwiki-source' )->text(), 'interwiki' ) .
+ Xml::label( $this->msg( 'import-interwiki-source' )->text(), 'interwiki' ) .
"</td>
<td class='mw-input'>" .
- Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
+ Xml::openElement(
+ 'select',
+ array( 'name' => 'interwiki', 'id' => 'interwiki' )
+ )
);
- foreach( $wgImportSources as $prefix ) {
+
+ foreach ( $wgImportSources as $prefix ) {
$selected = ( $this->interwiki === $prefix ) ? ' selected="selected"' : '';
$out->addHTML( Xml::option( $prefix, $prefix, $selected ) );
}
$out->addHTML(
- Xml::closeElement( 'select' ) .
- Xml::input( 'frompage', 50, $this->frompage ) .
+ Xml::closeElement( 'select' ) .
+ Xml::input( 'frompage', 50, $this->frompage, array( 'id' => 'frompage' ) ) .
"</td>
</tr>
<tr>
<td>
</td>
<td class='mw-input'>" .
- Xml::checkLabel( $this->msg( 'import-interwiki-history' )->text(), 'interwikiHistory', 'interwikiHistory', $this->history ) .
+ Xml::checkLabel(
+ $this->msg( 'import-interwiki-history' )->text(),
+ 'interwikiHistory',
+ 'interwikiHistory',
+ $this->history
+ ) .
"</td>
</tr>
<tr>
<td>
</td>
<td class='mw-input'>" .
- Xml::checkLabel( $this->msg( 'import-interwiki-templates' )->text(), 'interwikiTemplates', 'interwikiTemplates', $this->includeTemplates ) .
+ Xml::checkLabel(
+ $this->msg( 'import-interwiki-templates' )->text(),
+ 'interwikiTemplates',
+ 'interwikiTemplates',
+ $this->includeTemplates
+ ) .
"</td>
</tr>
$importDepth
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-interwiki-namespace' )->text(), 'namespace' ) .
+ Xml::label( $this->msg( 'import-interwiki-namespace' )->text(), 'namespace' ) .
"</td>
<td class='mw-input'>" .
- Html::namespaceSelector(
- array(
- 'selected' => $this->namespace,
- 'all' => '',
- ), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- )
- ) .
+ Html::namespaceSelector(
+ array(
+ 'selected' => $this->namespace,
+ 'all' => '',
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-comment' )->text(), 'mw-interwiki-comment' ) .
+ Xml::label( $this->msg( 'import-comment' )->text(), 'mw-interwiki-comment' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'log-comment', 50, '',
- array( 'id' => 'mw-interwiki-comment', 'type' => 'text' ) ) . ' ' .
+ Xml::input( 'log-comment', 50, '',
+ array( 'id' => 'mw-interwiki-comment', 'type' => 'text' ) ) . ' ' .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage' ) .
+ Xml::label(
+ $this->msg( 'import-interwiki-rootpage' )->text(),
+ 'mw-interwiki-rootpage-interwiki'
+ ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'rootpage', 50, $this->rootpage,
- array( 'id' => 'mw-interwiki-rootpage', 'type' => 'text' ) ) . ' ' .
+ Xml::input( 'rootpage', 50, $this->rootpage,
+ array( 'id' => 'mw-interwiki-rootpage-interwiki', 'type' => 'text' ) ) . ' ' .
"</td>
</tr>
<tr>
<td>
</td>
<td class='mw-submit'>" .
- Xml::submitButton( $this->msg( 'import-interwiki-submit' )->text(), Linker::tooltipAndAccesskeyAttribs( 'import' ) ) .
+ Xml::submitButton(
+ $this->msg( 'import-interwiki-submit' )->text(),
+ Linker::tooltipAndAccesskeyAttribs( 'import' )
+ ) .
"</td>
</tr>" .
- Xml::closeElement( 'table' ).
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' )
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
);
}
}
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
/**
@@ -348,14 +406,21 @@ class SpecialImport extends SpecialPage {
* @ingroup SpecialPage
*/
class ImportReporter extends ContextSource {
- private $reason=false;
+ private $reason = false;
private $mOriginalLogCallback = null;
private $mOriginalPageOutCallback = null;
private $mLogItemCount = 0;
- function __construct( $importer, $upload, $interwiki , $reason=false ) {
+
+ /**
+ * @param WikiImporter $importer
+ * @param $upload
+ * @param $interwiki
+ * @param string|bool $reason
+ */
+ function __construct( $importer, $upload, $interwiki, $reason = false ) {
$this->mOriginalPageOutCallback =
- $importer->setPageOutCallback( array( $this, 'reportPage' ) );
+ $importer->setPageOutCallback( array( $this, 'reportPage' ) );
$this->mOriginalLogCallback =
$importer->setLogItemCallback( array( $this, 'reportLogItem' ) );
$importer->setNoticeCallback( array( $this, 'reportNotice' ) );
@@ -399,36 +464,37 @@ class ImportReporter extends ContextSource {
$this->mPageCount++;
- if( $successCount > 0 ) {
- $this->getOutput()->addHTML( "<li>" . Linker::linkKnown( $title ) . " " .
- $this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() .
- "</li>\n"
+ if ( $successCount > 0 ) {
+ $this->getOutput()->addHTML(
+ "<li>" . Linker::linkKnown( $title ) . " " .
+ $this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() .
+ "</li>\n"
);
$log = new LogPage( 'import' );
- if( $this->mIsUpload ) {
+ if ( $this->mIsUpload ) {
$detail = $this->msg( 'import-logentry-upload-detail' )->numParams(
$successCount )->inContentLanguage()->text();
if ( $this->reason ) {
- $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason;
+ $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason;
}
- $log->addEntry( 'upload', $title, $detail );
+ $log->addEntry( 'upload', $title, $detail, array(), $this->getUser() );
} else {
$interwiki = '[[:' . $this->mInterwiki . ':' .
$origTitle->getPrefixedText() . ']]';
$detail = $this->msg( 'import-logentry-interwiki-detail' )->numParams(
$successCount )->params( $interwiki )->inContentLanguage()->text();
if ( $this->reason ) {
- $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason;
+ $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason;
}
- $log->addEntry( 'interwiki', $title, $detail );
+ $log->addEntry( 'interwiki', $title, $detail, array(), $this->getUser() );
}
$comment = $detail; // quick
$dbw = wfGetDB( DB_MASTER );
$latest = $title->getLatestRevID();
$nullRevision = Revision::newNullRevision( $dbw, $title->getArticleID(), $comment, true );
- if (!is_null($nullRevision)) {
+ if ( !is_null( $nullRevision ) ) {
$nullRevision->insertOn( $dbw );
$page = WikiPage::factory( $title );
# Update page record
@@ -446,8 +512,9 @@ class ImportReporter extends ContextSource {
if ( $this->mLogItemCount > 0 ) {
$msg = $this->msg( 'imported-log-entries' )->numParams( $this->mLogItemCount )->parse();
$out->addHTML( Xml::tags( 'li', null, $msg ) );
- } elseif( $this->mPageCount == 0 && $this->mLogItemCount == 0 ) {
+ } elseif ( $this->mPageCount == 0 && $this->mLogItemCount == 0 ) {
$out->addHTML( "</ul>\n" );
+
return Status::newFatal( 'importnopages' );
}
$out->addHTML( "</ul>\n" );
diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php
index c217eccb..7069d527 100644
--- a/includes/specials/SpecialJavaScriptTest.php
+++ b/includes/specials/SpecialJavaScriptTest.php
@@ -40,19 +40,11 @@ class SpecialJavaScriptTest extends SpecialPage {
}
public function execute( $par ) {
- global $wgEnableJavaScriptTest;
-
$out = $this->getOutput();
$this->setHeaders();
$out->disallowUserJs();
- // Abort early if we're disabled
- if ( $wgEnableJavaScriptTest !== true ) {
- $out->addWikiMsg( 'javascripttest-disabled' );
- return;
- }
-
$out->addModules( 'mediawiki.special.javaScriptTest' );
// Determine framework
@@ -63,24 +55,29 @@ class SpecialJavaScriptTest extends SpecialPage {
if ( $par == '' ) {
$out->setPageTitle( $this->msg( 'javascripttest' ) );
$summary = $this->wrapSummaryHtml(
- $this->msg( 'javascripttest-pagetext-noframework' )->escaped() . $this->getFrameworkListHtml(),
+ $this->msg( 'javascripttest-pagetext-noframework' )->escaped() .
+ $this->getFrameworkListHtml(),
'noframework'
);
$out->addHtml( $summary );
-
- // Matched! Display proper title and initialize the framework
} elseif ( isset( self::$frameworks[$framework] ) ) {
- $out->setPageTitle( $this->msg( 'javascripttest-title', $this->msg( "javascripttest-$framework-name" )->plain() ) );
- $out->setSubtitle( $this->msg( 'javascripttest-backlink' )->rawParams( Linker::linkKnown( $this->getTitle() ) ) );
+ // Matched! Display proper title and initialize the framework
+ $out->setPageTitle( $this->msg(
+ 'javascripttest-title',
+ // Messages: javascripttest-qunit-name
+ $this->msg( "javascripttest-$framework-name" )->plain()
+ ) );
+ $out->setSubtitle( $this->msg( 'javascripttest-backlink' )
+ ->rawParams( Linker::linkKnown( $this->getTitle() ) ) );
$this->{self::$frameworks[$framework]}();
-
- // Framework not found, display error
} else {
+ // Framework not found, display error
$out->setPageTitle( $this->msg( 'javascripttest' ) );
- $summary = $this->wrapSummaryHtml( '<p class="error">'
- . $this->msg( 'javascripttest-pagetext-unknownframework', $par )->escaped()
- . '</p>'
- . $this->getFrameworkListHtml(),
+ $summary = $this->wrapSummaryHtml(
+ '<p class="error">' .
+ $this->msg( 'javascripttest-pagetext-unknownframework', $par )->escaped() .
+ '</p>' .
+ $this->getFrameworkListHtml(),
'unknownframework'
);
$out->addHtml( $summary );
@@ -88,40 +85,49 @@ class SpecialJavaScriptTest extends SpecialPage {
}
/**
- * Get a list of frameworks (including introduction paragraph and links to the framework run pages)
- * @return String: HTML
+ * Get a list of frameworks (including introduction paragraph and links
+ * to the framework run pages)
+ *
+ * @return string HTML
*/
private function getFrameworkListHtml() {
$list = '<ul>';
- foreach( self::$frameworks as $framework => $initFn ) {
+ foreach ( self::$frameworks as $framework => $initFn ) {
$list .= Html::rawElement(
'li',
array(),
- Linker::link( $this->getTitle( $framework ), $this->msg( "javascripttest-$framework-name" )->escaped() )
+ Linker::link(
+ $this->getTitle( $framework ),
+ // Message: javascripttest-qunit-name
+ $this->msg( "javascripttest-$framework-name" )->escaped()
+ )
);
}
$list .= '</ul>';
- $msg = $this->msg( 'javascripttest-pagetext-frameworks' )->rawParams( $list )->parseAsBlock();
- return $msg;
+ return $this->msg( 'javascripttest-pagetext-frameworks' )->rawParams( $list )
+ ->parseAsBlock();
}
/**
* Function to wrap the summary.
* It must be given a valid state as a second parameter or an exception will
* be thrown.
- * @param $html String: The raw HTML.
- * @param $state String: State, one of 'noframework', 'unknownframework' or 'frameworkfound'
+ * @param string $html The raw HTML.
+ * @param string $state State, one of 'noframework', 'unknownframework' or 'frameworkfound'
+ * @throws MWException
* @return string
*/
private function wrapSummaryHtml( $html, $state ) {
$validStates = array( 'noframework', 'unknownframework', 'frameworkfound' );
- if( !in_array( $state, $validStates ) ) {
+
+ if ( !in_array( $state, $validStates ) ) {
throw new MWException( __METHOD__
. ' given an invalid state. Must be one of "'
- . join( '", "', $validStates) . '".'
+ . join( '", "', $validStates ) . '".'
);
}
+
return "<div id=\"mw-javascripttest-summary\" class=\"mw-javascripttest-$state\">$html</div>";
}
@@ -162,12 +168,13 @@ HTML;
// Used in ./tests/qunit/data/testrunner.js, see also documentation of
// $wgJavaScriptTestConfig in DefaultSettings.php
- $out->addJsConfigVars( 'QUnitTestSwarmInjectJSPath', $wgJavaScriptTestConfig['qunit']['testswarm-injectjs'] );
+ $out->addJsConfigVars(
+ 'QUnitTestSwarmInjectJSPath',
+ $wgJavaScriptTestConfig['qunit']['testswarm-injectjs']
+ );
}
- public function isListed(){
- global $wgEnableJavaScriptTest;
- return $wgEnableJavaScriptTest === true;
+ protected function getGroupName() {
+ return 'other';
}
-
}
diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php
index 0810ee77..5b0c56e5 100644
--- a/includes/specials/SpecialLinkSearch.php
+++ b/includes/specials/SpecialLinkSearch.php
@@ -22,7 +22,6 @@
* @author Brion Vibber
*/
-
/**
* Special:LinkSearch to search the external-links table.
* @ingroup SpecialPage
@@ -43,7 +42,7 @@ class LinkSearchPage extends QueryPage {
}
function execute( $par ) {
- global $wgUrlProtocols, $wgMiserMode;
+ global $wgUrlProtocols, $wgMiserMode, $wgScript;
$this->setHeaders();
$this->outputHeader();
@@ -56,7 +55,7 @@ class LinkSearchPage extends QueryPage {
$namespace = $request->getIntorNull( 'namespace', null );
$protocols_list = array();
- foreach( $wgUrlProtocols as $prot ) {
+ foreach ( $wgUrlProtocols as $prot ) {
if ( $prot !== '//' ) {
$protocols_list[] = $prot;
}
@@ -64,16 +63,16 @@ class LinkSearchPage extends QueryPage {
$target2 = $target;
$protocol = '';
- $pr_sl = strpos($target2, '//' );
- $pr_cl = strpos($target2, ':' );
+ $pr_sl = strpos( $target2, '//' );
+ $pr_cl = strpos( $target2, ':' );
if ( $pr_sl ) {
// For protocols with '//'
- $protocol = substr( $target2, 0 , $pr_sl+2 );
- $target2 = substr( $target2, $pr_sl+2 );
+ $protocol = substr( $target2, 0, $pr_sl + 2 );
+ $target2 = substr( $target2, $pr_sl + 2 );
} elseif ( !$pr_sl && $pr_cl ) {
// For protocols without '//' like 'mailto:'
- $protocol = substr( $target2, 0 , $pr_cl+1 );
- $target2 = substr( $target2, $pr_cl+1 );
+ $protocol = substr( $target2, 0, $pr_cl + 1 );
+ $target2 = substr( $target2, $pr_cl + 1 );
} elseif ( $protocol == '' && $target2 != '' ) {
// default
$protocol = 'http://';
@@ -84,12 +83,26 @@ class LinkSearchPage extends QueryPage {
$protocol = '';
}
- $out->addWikiMsg( 'linksearch-text', '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>' );
- $s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) .
- '<fieldset>' .
- Xml::element( 'legend', array(), $this->msg( 'linksearch' )->text() ) .
- Xml::inputLabel( $this->msg( 'linksearch-pat' )->text(), 'target', 'target', 50, $target ) . ' ';
+ $out->addWikiMsg(
+ 'linksearch-text',
+ '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>',
+ count( $protocols_list )
+ );
+ $s = Html::openElement(
+ 'form',
+ array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $wgScript )
+ ) . "\n" .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" .
+ Html::openElement( 'fieldset' ) . "\n" .
+ Html::element( 'legend', array(), $this->msg( 'linksearch' )->text() ) . "\n" .
+ Xml::inputLabel(
+ $this->msg( 'linksearch-pat' )->text(),
+ 'target',
+ 'target',
+ 50,
+ $target
+ ) . "\n";
+
if ( !$wgMiserMode ) {
$s .= Html::namespaceSelector(
array(
@@ -97,25 +110,27 @@ class LinkSearchPage extends QueryPage {
'all' => '',
'label' => $this->msg( 'linksearch-ns' )->text()
), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
+ 'name' => 'namespace',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
)
);
}
- $s .= Xml::submitButton( $this->msg( 'linksearch-ok' )->text() ) .
- '</fieldset>' .
- Xml::closeElement( 'form' );
+
+ $s .= Xml::submitButton( $this->msg( 'linksearch-ok' )->text() ) . "\n" .
+ Html::closeElement( 'fieldset' ) . "\n" .
+ Html::closeElement( 'form' ) . "\n";
$out->addHTML( $s );
- if( $target != '' ) {
+ if ( $target != '' ) {
$this->setParams( array(
'query' => $target2,
'namespace' => $namespace,
'protocol' => $protocol ) );
parent::execute( $par );
- if( $this->mMungedQuery === false )
+ if ( $this->mMungedQuery === false ) {
$out->addWikiMsg( 'linksearch-error' );
+ }
}
}
@@ -130,19 +145,23 @@ class LinkSearchPage extends QueryPage {
/**
* Return an appropriately formatted LIKE query and the clause
*
+ * @param string $query
+ * @param string $prot
* @return array
*/
static function mungeQuery( $query, $prot ) {
$field = 'el_index';
- $rv = LinkFilter::makeLikeArray( $query , $prot );
+ $rv = LinkFilter::makeLikeArray( $query, $prot );
if ( $rv === false ) {
// LinkFilter 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)) {
+ $pattern = '/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/';
+ if ( preg_match( $pattern, $query ) ) {
$dbr = wfGetDB( DB_SLAVE );
$rv = array( $prot . rtrim( $query, " \t*" ), $dbr->anyString() );
$field = 'el_to';
}
}
+
return array( $rv, $field );
}
@@ -150,9 +169,10 @@ class LinkSearchPage extends QueryPage {
global $wgMiserMode;
$params = array();
$params['target'] = $this->mProt . $this->mQuery;
- if( isset( $this->mNs ) && !$wgMiserMode ) {
+ if ( isset( $this->mNs ) && !$wgMiserMode ) {
$params['namespace'] = $this->mNs;
}
+
return $params;
}
@@ -161,29 +181,41 @@ class LinkSearchPage extends QueryPage {
$dbr = wfGetDB( DB_SLAVE );
// strip everything past first wildcard, so that
// index-based-only lookup would be done
- list( $this->mMungedQuery, $clause ) = self::mungeQuery(
- $this->mQuery, $this->mProt );
- if( $this->mMungedQuery === false )
+ list( $this->mMungedQuery, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt );
+ if ( $this->mMungedQuery === false ) {
// Invalid query; return no results
return array( 'tables' => 'page', 'fields' => 'page_id', 'conds' => '0=1' );
+ }
$stripped = LinkFilter::keepOneWildcard( $this->mMungedQuery );
$like = $dbr->buildLike( $stripped );
- $retval = array (
- 'tables' => array ( 'page', 'externallinks' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'el_index', 'url' => 'el_to' ),
- 'conds' => array ( 'page_id = el_from',
- "$clause $like" ),
+ $retval = array(
+ 'tables' => array( 'page', 'externallinks' ),
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'el_index',
+ 'url' => 'el_to'
+ ),
+ 'conds' => array(
+ 'page_id = el_from',
+ "$clause $like"
+ ),
'options' => array( 'USE INDEX' => $clause )
);
+
if ( isset( $this->mNs ) && !$wgMiserMode ) {
$retval['conds']['page_namespace'] = $this->mNs;
}
+
return $retval;
}
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
$title = Title::makeTitle( $result->namespace, $result->title );
$url = $result->url;
@@ -197,8 +229,8 @@ class LinkSearchPage extends QueryPage {
* Override to check query validity.
*/
function doQuery( $offset = false, $limit = false ) {
- list( $this->mMungedQuery, ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt );
- if( $this->mMungedQuery === false ) {
+ list( $this->mMungedQuery, ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt );
+ if ( $this->mMungedQuery === false ) {
$this->getOutput()->addWikiMsg( 'linksearch-error' );
} else {
// For debugging
@@ -218,4 +250,8 @@ class LinkSearchPage extends QueryPage {
function getOrderFields() {
return array();
}
+
+ protected function getGroupName() {
+ return 'redirects';
+ }
}
diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php
index cc055221..dff1cf70 100644
--- a/includes/specials/SpecialListfiles.php
+++ b/includes/specials/SpecialListfiles.php
@@ -22,12 +22,11 @@
*/
class SpecialListFiles extends IncludableSpecialPage {
-
- public function __construct(){
+ public function __construct() {
parent::__construct( 'Listfiles' );
}
- public function execute( $par ){
+ public function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
@@ -37,9 +36,16 @@ class SpecialListFiles extends IncludableSpecialPage {
} else {
$userName = $this->getRequest()->getText( 'user', $par );
$search = $this->getRequest()->getText( 'ilsearch', '' );
+ $showAll = $this->getRequest()->getBool( 'ilshowall', false );
}
- $pager = new ImageListPager( $this->getContext(), $userName, $search, $this->including() );
+ $pager = new ImageListPager(
+ $this->getContext(),
+ $userName,
+ $search,
+ $this->including(),
+ $showAll
+ );
if ( $this->including() ) {
$html = $pager->getBody();
@@ -51,6 +57,10 @@ class SpecialListFiles extends IncludableSpecialPage {
}
$this->getOutput()->addHTML( $html );
}
+
+ protected function getGroupName() {
+ return 'media';
+ }
}
/**
@@ -58,27 +68,33 @@ class SpecialListFiles extends IncludableSpecialPage {
*/
class ImageListPager extends TablePager {
var $mFieldNames = null;
+ // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
var $mQueryConds = array();
var $mUserName = null;
var $mSearch = '';
var $mIncluding = false;
+ var $mShowAll = false;
+ var $mTableName = 'image';
- function __construct( IContextSource $context, $userName = null, $search = '', $including = false ) {
+ function __construct( IContextSource $context, $userName = null, $search = '',
+ $including = false, $showAll = false
+ ) {
global $wgMiserMode;
$this->mIncluding = $including;
+ $this->mShowAll = $showAll;
if ( $userName ) {
$nt = Title::newFromText( $userName, NS_USER );
if ( !is_null( $nt ) ) {
$this->mUserName = $nt->getText();
- $this->mQueryConds['img_user_text'] = $this->mUserName;
}
}
- if ( $search != '' && !$wgMiserMode ) {
+ if ( $search !== '' && !$wgMiserMode ) {
$this->mSearch = $search;
$nt = Title::newFromURL( $this->mSearch );
+
if ( $nt ) {
$dbr = wfGetDB( DB_SLAVE );
$this->mQueryConds[] = 'LOWER(img_name)' .
@@ -101,6 +117,42 @@ class ImageListPager extends TablePager {
}
/**
+ * Build the where clause of the query.
+ *
+ * Replaces the older mQueryConds member variable.
+ * @param $table String Either "image" or "oldimage"
+ * @return array The query conditions.
+ */
+ protected function buildQueryConds( $table ) {
+ $prefix = $table === 'image' ? 'img' : 'oi';
+ $conds = array();
+
+ if ( !is_null( $this->mUserName ) ) {
+ $conds[ $prefix . '_user_text' ] = $this->mUserName;
+ }
+
+ if ( $this->mSearch !== '' ) {
+ $nt = Title::newFromURL( $this->mSearch );
+ if ( $nt ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds[] = 'LOWER(' . $prefix . '_name)' .
+ $dbr->buildLike( $dbr->anyString(),
+ strtolower( $nt->getDBkey() ), $dbr->anyString() );
+ }
+ }
+
+ if ( $table === 'oldimage' ) {
+ // Don't want to deal with revdel.
+ // Future fixme: Show partial information as appropriate.
+ // Would have to be careful about filtering by username when username is deleted.
+ $conds['oi_deleted'] = 0;
+ }
+
+ // Add mQueryConds in case anyone was subclassing and using the old variable.
+ return $conds + $this->mQueryConds;
+ }
+
+ /**
* @return Array
*/
function getFieldNames() {
@@ -114,34 +166,94 @@ class ImageListPager extends TablePager {
'img_user_text' => $this->msg( 'listfiles_user' )->text(),
'img_description' => $this->msg( 'listfiles_description' )->text(),
);
- if( !$wgMiserMode ) {
+ if ( !$wgMiserMode && !$this->mShowAll ) {
$this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
}
+ if ( $this->mShowAll ) {
+ $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
+ }
}
+
return $this->mFieldNames;
}
function isFieldSortable( $field ) {
+ global $wgMiserMode;
if ( $this->mIncluding ) {
return false;
}
- static $sortable = array( 'img_timestamp', 'img_name' );
- if ( $field == 'img_size' ) {
- # No index for both img_size and img_user_text
- return !isset( $this->mQueryConds['img_user_text'] );
+ $sortable = array( 'img_timestamp', 'img_name', 'img_size' );
+ /* For reference, the indicies we can use for sorting are:
+ * On the image table: img_usertext_timestamp, img_size, img_timestamp
+ * On oldimage: oi_usertext_timestamp, oi_name_timestamp
+ *
+ * In particular that means we cannot sort by timestamp when not filtering
+ * by user and including old images in the results. Which is sad.
+ */
+ if ( $wgMiserMode && !is_null( $this->mUserName ) ) {
+ // If we're sorting by user, the index only supports sorting by time.
+ if ( $field === 'img_timestamp' ) {
+ return true;
+ } else {
+ return false;
+ }
+ } elseif ( $wgMiserMode && $this->mShowAll /* && mUserName === null */ ) {
+ // no oi_timestamp index, so only alphabetical sorting in this case.
+ if ( $field === 'img_name' ) {
+ return true;
+ } else {
+ return false;
+ }
}
+
return in_array( $field, $sortable );
}
function getQueryInfo() {
- $tables = array( 'image' );
+ // Hacky Hacky Hacky - I want to get query info
+ // for two different tables, without reimplementing
+ // the pager class.
+ $qi = $this->getQueryInfoReal( $this->mTableName );
+ return $qi;
+ }
+
+ /**
+ * Actually get the query info.
+ *
+ * This is to allow displaying both stuff from image and oldimage table.
+ *
+ * This is a bit hacky.
+ *
+ * @param $table String Either 'image' or 'oldimage'
+ * @return array Query info
+ */
+ protected function getQueryInfoReal( $table ) {
+ $prefix = $table === 'oldimage' ? 'oi' : 'img';
+
+ $tables = array( $table );
$fields = array_keys( $this->getFieldNames() );
- $fields[] = 'img_user';
- $fields[array_search('thumb', $fields)] = 'img_name AS thumb';
+
+ if ( $table === 'oldimage' ) {
+ foreach ( $fields as $id => &$field ) {
+ if ( substr( $field, 0, 4 ) !== 'img_' ) {
+ continue;
+ }
+ $field = $prefix . substr( $field, 3 ) . ' AS ' . $field;
+ }
+ $fields[array_search('top', $fields)] = "'no' AS top";
+ } else {
+ if ( $this->mShowAll ) {
+ $fields[array_search( 'top', $fields )] = "'yes' AS top";
+ }
+ }
+ $fields[] = $prefix . '_user AS img_user';
+ $fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb';
+
$options = $join_conds = array();
# Depends on $wgMiserMode
- if( isset( $this->mFieldNames['count'] ) ) {
+ # Will also not happen if mShowAll is true.
+ if ( isset( $this->mFieldNames['count'] ) ) {
$tables[] = 'oldimage';
# Need to rewrite this one
@@ -153,7 +265,7 @@ class ImageListPager extends TablePager {
unset( $field );
$dbr = wfGetDB( DB_SLAVE );
- if( $dbr->implicitGroupby() ) {
+ if ( $dbr->implicitGroupby() ) {
$options = array( 'GROUP BY' => 'img_name' );
} else {
$columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) );
@@ -161,17 +273,107 @@ class ImageListPager extends TablePager {
}
$join_conds = array( 'oldimage' => array( 'LEFT JOIN', 'oi_name = img_name' ) );
}
+
return array(
- 'tables' => $tables,
- 'fields' => $fields,
- 'conds' => $this->mQueryConds,
- 'options' => $options,
+ 'tables' => $tables,
+ 'fields' => $fields,
+ 'conds' => $this->buildQueryConds( $table ),
+ 'options' => $options,
'join_conds' => $join_conds
);
}
+ /**
+ * Override reallyDoQuery to mix together two queries.
+ *
+ * @note $asc is named $descending in IndexPager base class. However
+ * it is true when the order is ascending, and false when the order
+ * is descending, so I renamed it to $asc here.
+ */
+ function reallyDoQuery( $offset, $limit, $asc ) {
+ $prevTableName = $this->mTableName;
+ $this->mTableName = 'image';
+ list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $asc );
+ $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
+ $this->mTableName = $prevTableName;
+
+ if ( !$this->mShowAll ) {
+ return $imageRes;
+ }
+
+ $this->mTableName = 'oldimage';
+
+ # Hacky...
+ $oldIndex = $this->mIndexField;
+ if ( substr( $this->mIndexField, 0, 4 ) !== 'img_' ) {
+ throw new MWException( "Expected to be sorting on an image table field" );
+ }
+ $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 );
+
+ list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $asc );
+ $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
+
+ $this->mTableName = $prevTableName;
+ $this->mIndexField = $oldIndex;
+
+ return $this->combineResult( $imageRes, $oldimageRes, $limit, $asc );
+ }
+
+ /**
+ * Combine results from 2 tables.
+ *
+ * Note: This will throw away some results
+ *
+ * @param $res1 ResultWrapper
+ * @param $res2 ResultWrapper
+ * @param $limit int
+ * @param $ascending boolean See note about $asc in $this->reallyDoQuery
+ * @return FakeResultWrapper $res1 and $res2 combined
+ */
+ protected function combineResult( $res1, $res2, $limit, $ascending ) {
+ $res1->rewind();
+ $res2->rewind();
+ $topRes1 = $res1->next();
+ $topRes2 = $res2->next();
+ $resultArray = array();
+ for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
+ if ( strcmp( $topRes1->{ $this->mIndexField }, $topRes2->{ $this->mIndexField } ) > 0 ) {
+ if ( !$ascending ) {
+ $resultArray[] = $topRes1;
+ $topRes1 = $res1->next();
+ } else {
+ $resultArray[] = $topRes2;
+ $topRes2 = $res2->next();
+ }
+ } else {
+ if ( !$ascending ) {
+ $resultArray[] = $topRes2;
+ $topRes2 = $res2->next();
+ } else {
+ $resultArray[] = $topRes1;
+ $topRes1 = $res1->next();
+ }
+ }
+ }
+ for ( ; $i < $limit && $topRes1; $i++ ) {
+ $resultArray[] = $topRes1;
+ $topRes1 = $res1->next();
+ }
+ for ( ; $i < $limit && $topRes2; $i++ ) {
+ $resultArray[] = $topRes2;
+ $topRes2 = $res2->next();
+ }
+ return new FakeResultWrapper( $resultArray );
+ }
+
function getDefaultSort() {
- return 'img_timestamp';
+ global $wgMiserMode;
+ if ( $this->mShowAll && $wgMiserMode && is_null( $this->mUserName ) ) {
+ // Unfortunately no index on oi_timestamp.
+ return 'img_name';
+ } else {
+ return 'img_timestamp';
+ }
}
function doBatchLookups() {
@@ -187,24 +389,37 @@ class ImageListPager extends TablePager {
function formatValue( $field, $value ) {
switch ( $field ) {
case 'thumb':
- $file = wfLocalFile( $value );
- $thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) );
- return $thumb->toHtml( array( 'desc-link' => true ) );
+ $opt = array( 'time' => $this->mCurrentRow->img_timestamp );
+ $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt );
+ // If statement for paranoia
+ if ( $file ) {
+ $thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) );
+ return $thumb->toHtml( array( 'desc-link' => true ) );
+ } else {
+ return htmlspecialchars( $value );
+ }
case 'img_timestamp':
+ // We may want to make this a link to the "old" version when displaying old files
return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
case 'img_name':
static $imgfile = null;
- if ( $imgfile === null ) $imgfile = $this->msg( 'imgfile' )->text();
+ if ( $imgfile === null ) {
+ $imgfile = $this->msg( 'imgfile' )->text();
+ }
// Weird files can maybe exist? Bug 22227
$filePage = Title::makeTitleSafe( NS_FILE, $value );
- if( $filePage ) {
- $link = Linker::linkKnown( $filePage, htmlspecialchars( $filePage->getText() ) );
+ if ( $filePage ) {
+ $link = Linker::linkKnown(
+ $filePage,
+ htmlspecialchars( $filePage->getText() )
+ );
$download = Xml::element( 'a',
array( 'href' => wfLocalFile( $filePage )->getURL() ),
$imgfile
);
$download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
+
return "$link $download";
} else {
return htmlspecialchars( $value );
@@ -219,39 +434,55 @@ class ImageListPager extends TablePager {
} else {
$link = htmlspecialchars( $value );
}
+
return $link;
case 'img_size':
return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
case 'img_description':
- return Linker::commentBlock( $value );
+ return Linker::formatComment( $value );
case 'count':
return intval( $value ) + 1;
+ case 'top':
+ // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
+ return $this->msg( 'listfiles-latestversion-' . $value );
}
}
function getForm() {
global $wgScript, $wgMiserMode;
$inputForm = array();
- $inputForm['table_pager_limit_label'] = $this->getLimitSelect();
+ $inputForm['table_pager_limit_label'] = $this->getLimitSelect( array( 'tabindex' => 1 ) );
if ( !$wgMiserMode ) {
- $inputForm['listfiles_search_for'] = Html::input( 'ilsearch', $this->mSearch, 'text',
+ $inputForm['listfiles_search_for'] = Html::input(
+ 'ilsearch',
+ $this->mSearch,
+ 'text',
array(
- 'size' => '40',
+ 'size' => '40',
'maxlength' => '255',
- 'id' => 'mw-ilsearch',
- ) );
+ 'id' => 'mw-ilsearch',
+ 'tabindex' => 2,
+ )
+ );
}
$inputForm['username'] = Html::input( 'user', $this->mUserName, 'text', array(
- 'size' => '40',
+ 'size' => '40',
'maxlength' => '255',
- 'id' => 'mw-listfiles-user',
+ 'id' => 'mw-listfiles-user',
+ 'tabindex' => 3,
+ ) );
+
+ $inputForm['listfiles-show-all'] = Html::input( 'ilshowall', 1, 'checkbox', array(
+ 'checked' => $this->mShowAll,
+ 'tabindex' => 4,
) );
return Html::openElement( 'form',
- array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) ) .
+ array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' )
+ ) .
Xml::fieldset( $this->msg( 'listfiles' )->text() ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
- Xml::buildForm( $inputForm, 'table_pager_limit_submit' ) .
- $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title' ) ) .
+ Xml::buildForm( $inputForm, 'table_pager_limit_submit', array( 'tabindex' => 5 ) ) .
+ $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title', 'ilshowall' ) ) .
Html::closeElement( 'fieldset' ) .
Html::closeElement( 'form' ) . "\n";
}
@@ -276,6 +507,7 @@ class ImageListPager extends TablePager {
$query['user'] = $this->mUserName;
}
}
+
return $queries;
}
@@ -284,6 +516,7 @@ class ImageListPager extends TablePager {
if ( !isset( $queries['user'] ) && !is_null( $this->mUserName ) ) {
$queries['user'] = $this->mUserName;
}
+
return $queries;
}
diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php
index 1f95c225..82a4f70f 100644
--- a/includes/specials/SpecialListgrouprights.php
+++ b/includes/specials/SpecialListgrouprights.php
@@ -29,7 +29,6 @@
* @author Petr Kadlec <mormegil@centrum.cz>
*/
class SpecialListGroupRights extends SpecialPage {
-
/**
* Constructor
*/
@@ -51,11 +50,13 @@ class SpecialListGroupRights extends SpecialPage {
$out = $this->getOutput();
$out->addModuleStyles( 'mediawiki.special' );
+ $out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
+
$out->addHTML(
Xml::openElement( 'table', array( 'class' => 'wikitable mw-listgrouprights-table' ) ) .
'<tr>' .
- Xml::element( 'th', null, $this->msg( 'listgrouprights-group' )->text() ) .
- Xml::element( 'th', null, $this->msg( 'listgrouprights-rights' )->text() ) .
+ Xml::element( 'th', null, $this->msg( 'listgrouprights-group' )->text() ) .
+ Xml::element( 'th', null, $this->msg( 'listgrouprights-rights' )->text() ) .
'</tr>'
);
@@ -85,7 +86,7 @@ class SpecialListGroupRights extends SpecialPage {
$msg->text() :
MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
- if( $group == '*' ) {
+ if ( $group == '*' ) {
// Do not make a link for the generic * group
$grouppage = htmlspecialchars( $groupnameLocalized );
} else {
@@ -124,34 +125,31 @@ class SpecialListGroupRights extends SpecialPage {
"
<td>$grouppage$grouplink</td>
<td>" .
- $this->formatPermissions( $permissions, $revoke, $addgroups, $removegroups,
- $addgroupsSelf, $removegroupsSelf ) .
+ $this->formatPermissions( $permissions, $revoke, $addgroups, $removegroups,
+ $addgroupsSelf, $removegroupsSelf ) .
'</td>
'
) );
}
- $out->addHTML(
- Xml::closeElement( 'table' ) . "\n<br /><hr />\n"
- );
- $out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
+ $out->addHTML( Xml::closeElement( 'table' ) );
}
/**
* 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
+ * @param array $permissions of permission => bool (from $wgGroupPermissions items)
+ * @param array $revoke of permission => bool (from $wgRevokePermissions items)
+ * @param array $add of groups this group is allowed to add or true
+ * @param array $remove of groups this group is allowed to remove or true
+ * @param array $addSelf of groups this group is allowed to add to self or true
+ * @param array $removeSelf of group this group is allowed to remove from self or true
* @return string List of all granted permissions, separated by comma separator
*/
- private function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) {
+ private function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) {
$r = array();
- foreach( $permissions as $permission => $granted ) {
+ foreach ( $permissions as $permission => $granted ) {
//show as granted only if it isn't revoked to prevent duplicate display of permissions
- if( $granted && ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) ) {
+ if ( $granted && ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) ) {
$description = $this->msg( 'listgrouprights-right-display',
User::getRightDescription( $permission ),
'<span class="mw-listgrouprights-right-name">' . $permission . '</span>'
@@ -159,8 +157,8 @@ class SpecialListGroupRights extends SpecialPage {
$r[] = $description;
}
}
- foreach( $revoke as $permission => $revoked ) {
- if( $revoked ) {
+ foreach ( $revoke as $permission => $revoked ) {
+ if ( $revoked ) {
$description = $this->msg( 'listgrouprights-right-revoked',
User::getRightDescription( $permission ),
'<span class="mw-listgrouprights-right-name">' . $permission . '</span>'
@@ -168,48 +166,59 @@ class SpecialListGroupRights extends SpecialPage {
$r[] = $description;
}
}
+
sort( $r );
+
$lang = $this->getLanguage();
- if( $add === true ){
+
+ if ( $add === true ) {
$r[] = $this->msg( 'listgrouprights-addgroup-all' )->escaped();
- } elseif( is_array( $add ) && count( $add ) ) {
+ } elseif ( is_array( $add ) && count( $add ) ) {
$add = array_values( array_unique( $add ) );
$r[] = $this->msg( 'listgrouprights-addgroup',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ),
count( $add )
)->parse();
}
- if( $remove === true ){
+
+ if ( $remove === true ) {
$r[] = $this->msg( 'listgrouprights-removegroup-all' )->escaped();
- } elseif( is_array( $remove ) && count( $remove ) ) {
+ } elseif ( is_array( $remove ) && count( $remove ) ) {
$remove = array_values( array_unique( $remove ) );
$r[] = $this->msg( 'listgrouprights-removegroup',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ),
count( $remove )
)->parse();
}
- if( $addSelf === true ){
+
+ if ( $addSelf === true ) {
$r[] = $this->msg( 'listgrouprights-addgroup-self-all' )->escaped();
- } elseif( is_array( $addSelf ) && count( $addSelf ) ) {
+ } elseif ( is_array( $addSelf ) && count( $addSelf ) ) {
$addSelf = array_values( array_unique( $addSelf ) );
$r[] = $this->msg( 'listgrouprights-addgroup-self',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ),
count( $addSelf )
)->parse();
}
- if( $removeSelf === true ){
+
+ if ( $removeSelf === true ) {
$r[] = $this->msg( 'listgrouprights-removegroup-self-all' )->parse();
- } elseif( is_array( $removeSelf ) && count( $removeSelf ) ) {
+ } elseif ( is_array( $removeSelf ) && count( $removeSelf ) ) {
$removeSelf = array_values( array_unique( $removeSelf ) );
$r[] = $this->msg( 'listgrouprights-removegroup-self',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ),
count( $removeSelf )
)->parse();
}
- if( empty( $r ) ) {
+
+ if ( empty( $r ) ) {
return '';
} else {
return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
}
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php
index fe338a08..2c8792ff 100644
--- a/includes/specials/SpecialListredirects.php
+++ b/includes/specials/SpecialListredirects.php
@@ -29,29 +29,36 @@
* @ingroup SpecialPage
*/
class ListredirectsPage extends QueryPage {
-
function __construct( $name = 'Listredirects' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
- function sortDescending() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function sortDescending() {
+ return false;
+ }
function getQueryInfo() {
return array(
'tables' => array( 'p1' => 'page', 'redirect', 'p2' => 'page' ),
'fields' => array( 'namespace' => 'p1.page_namespace',
- 'title' => 'p1.page_title',
- 'value' => 'p1.page_title',
- 'rd_namespace',
- 'rd_title',
- 'rd_fragment',
- 'rd_interwiki',
- 'redirid' => 'p2.page_id' ),
+ 'title' => 'p1.page_title',
+ 'value' => 'p1.page_title',
+ 'rd_namespace',
+ 'rd_title',
+ 'rd_fragment',
+ 'rd_interwiki',
+ 'redirid' => 'p2.page_id' ),
'conds' => array( 'p1.page_is_redirect' => 1 ),
'join_conds' => array( 'redirect' => array(
- 'LEFT JOIN', 'rd_from=p1.page_id' ),
+ 'LEFT JOIN', 'rd_from=p1.page_id' ),
'p2' => array( 'LEFT JOIN', array(
'p2.page_namespace=rd_namespace',
'p2.page_title=rd_title' ) ) )
@@ -59,25 +66,27 @@ class ListredirectsPage extends QueryPage {
}
function getOrderFields() {
- return array ( 'p1.page_namespace', 'p1.page_title' );
+ return array( 'p1.page_namespace', 'p1.page_title' );
}
/**
* Cache page existence for performance
*
- * @param $db DatabaseBase
- * @param $res ResultWrapper
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$batch = new LinkBatch;
+
foreach ( $res as $row ) {
$batch->add( $row->namespace, $row->title );
$batch->addObj( $this->getRedirectTarget( $row ) );
}
+
$batch->execute();
// Back to start for display
- if ( $db->numRows( $res ) > 0 ) {
+ if ( $res->numRows() > 0 ) {
// If there are no rows we get an error seeking.
$db->dataSeek( $res, 0 );
}
@@ -92,10 +101,16 @@ class ListredirectsPage extends QueryPage {
} else {
$title = Title::makeTitle( $row->namespace, $row->title );
$article = WikiPage::factory( $title );
+
return $article->getRedirectTarget();
}
}
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
# Make a link to the redirect itself
$rd_title = Title::makeTitle( $result->namespace, $result->title );
@@ -108,14 +123,19 @@ class ListredirectsPage extends QueryPage {
# Find out where the redirect leads
$target = $this->getRedirectTarget( $result );
- if( $target ) {
+ if ( $target ) {
# Make a link to the destination page
$lang = $this->getLanguage();
$arr = $lang->getArrow() . $lang->getDirMark();
$targetLink = Linker::link( $target );
+
return "$rd_link $arr $targetLink";
} else {
return "<del>$rd_link</del>";
}
}
+
+ protected function getGroupName() {
+ return 'pages';
+ }
}
diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php
index 1089fbbe..8cd9173c 100644
--- a/includes/specials/SpecialListusers.php
+++ b/includes/specials/SpecialListusers.php
@@ -36,7 +36,9 @@ class UsersPager extends AlphabeticPager {
/**
* @param $context IContextSource
- * @param $par null|array
+ * @param array $par (Default null)
+ * @param $including boolean Whether this page is being transcluded in
+ * another page
*/
function __construct( IContextSource $context = null, $par = null, $including = null ) {
if ( $context ) {
@@ -47,7 +49,10 @@ class UsersPager extends AlphabeticPager {
$par = ( $par !== null ) ? $par : '';
$parms = explode( '/', $par );
$symsForAll = array( '*', 'user' );
- if ( $parms[0] != '' && ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) ) ) {
+
+ if ( $parms[0] != '' &&
+ ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) )
+ ) {
$this->requestedGroup = $par;
$un = $request->getText( 'username' );
} elseif ( count( $parms ) == 2 ) {
@@ -57,20 +62,25 @@ class UsersPager extends AlphabeticPager {
$this->requestedGroup = $request->getVal( 'group' );
$un = ( $par != '' ) ? $par : $request->getText( 'username' );
}
+
if ( in_array( $this->requestedGroup, $symsForAll ) ) {
$this->requestedGroup = '';
}
$this->editsOnly = $request->getBool( 'editsOnly' );
$this->creationSort = $request->getBool( 'creationSort' );
$this->including = $including;
+ $this->mDefaultDirection = $request->getBool( 'desc' );
$this->requestedUser = '';
+
if ( $un != '' ) {
$username = Title::makeTitleSafe( NS_USER, $un );
- if( ! is_null( $username ) ) {
+
+ if ( !is_null( $username ) ) {
$this->requestedUser = $username->getText();
}
}
+
parent::__construct();
}
@@ -87,34 +97,35 @@ class UsersPager extends AlphabeticPager {
function getQueryInfo() {
$dbr = wfGetDB( DB_SLAVE );
$conds = array();
+
// Don't show hidden names
- if( !$this->getUser()->isAllowed( 'hideuser' ) ) {
- $conds[] = 'ipb_deleted IS NULL';
+ if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
+ $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0';
}
$options = array();
- if( $this->requestedGroup != '' ) {
+ if ( $this->requestedGroup != '' ) {
$conds['ug_group'] = $this->requestedGroup;
- } else {
- //$options['USE INDEX'] = $this->creationSort ? 'PRIMARY' : 'user_name';
}
- if( $this->requestedUser != '' ) {
+
+ if ( $this->requestedUser != '' ) {
# Sorted either by account creation or name
- if( $this->creationSort ) {
+ if ( $this->creationSort ) {
$conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) );
} else {
$conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
}
}
- if( $this->editsOnly ) {
+
+ if ( $this->editsOnly ) {
$conds[] = 'user_editcount > 0';
}
$options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name';
$query = array(
- 'tables' => array( 'user', 'user_groups', 'ipblocks'),
+ 'tables' => array( 'user', 'user_groups', 'ipblocks' ),
'fields' => array(
'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
@@ -127,16 +138,18 @@ class UsersPager extends AlphabeticPager {
'options' => $options,
'join_conds' => array(
'user_groups' => array( 'LEFT JOIN', 'user_id=ug_user' ),
- 'ipblocks' => array( 'LEFT JOIN', array(
- 'user_id=ipb_user',
- 'ipb_deleted' => 1,
- 'ipb_auto' => 0
- )),
+ 'ipblocks' => array(
+ 'LEFT JOIN', array(
+ 'user_id=ipb_user',
+ 'ipb_auto' => 0
+ )
+ ),
),
'conds' => $conds
);
wfRunHooks( 'SpecialListusersQueryInfo', array( $this, &$query ) );
+
return $query;
}
@@ -152,42 +165,56 @@ class UsersPager extends AlphabeticPager {
$userName = $row->user_name;
$ulinks = Linker::userLink( $row->user_id, $userName );
- $ulinks .= Linker::userToolLinks( $row->user_id, $userName );
+ $ulinks .= Linker::userToolLinksRedContribs(
+ $row->user_id,
+ $userName,
+ (int)$row->edits
+ );
$lang = $this->getLanguage();
$groups = '';
$groups_list = self::getGroups( $row->user_id );
- if( !$this->including && count( $groups_list ) > 0 ) {
+
+ if ( !$this->including && count( $groups_list ) > 0 ) {
$list = array();
- foreach( $groups_list as $group )
+ foreach ( $groups_list as $group ) {
$list[] = self::buildGroupLink( $group, $userName );
+ }
$groups = $lang->commaList( $list );
}
$item = $lang->specialList( $ulinks, $groups );
- if( $row->ipb_deleted ) {
+
+ if ( $row->ipb_deleted ) {
$item = "<span class=\"deleted\">$item</span>";
}
$edits = '';
global $wgEdititis;
if ( !$this->including && $wgEdititis ) {
- $edits = ' [' . $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped() . ']';
+ // @fixme i18n issue: Hardcoded square brackets.
+ $edits = ' [' .
+ $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped() .
+ ']';
}
$created = '';
- # Some rows may be NULL
- if( !$this->including && $row->creation ) {
+ # Some rows may be null
+ if ( !$this->including && $row->creation ) {
$user = $this->getUser();
$d = $lang->userDate( $row->creation, $user );
$t = $lang->userTime( $row->creation, $user );
$created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped();
$created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped();
}
+ $blocked = !is_null( $row->ipb_deleted ) ?
+ ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
+ '';
wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
- return Html::rawElement( 'li', array(), "{$item}{$edits}{$created}" );
+
+ return Html::rawElement( 'li', array(), "{$item}{$edits}{$created}{$blocked}" );
}
function doBatchLookups() {
@@ -204,30 +231,60 @@ class UsersPager extends AlphabeticPager {
/**
* @return string
*/
- function getPageHeader( ) {
+ function getPageHeader() {
global $wgScript;
list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() );
# Form tag
- $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listusers-form' ) ) .
+ $out = Xml::openElement(
+ 'form',
+ array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listusers-form' )
+ ) .
Xml::fieldset( $this->msg( 'listusers' )->text() ) .
Html::hidden( 'title', $self );
# Username field
$out .= Xml::label( $this->msg( 'listusersfrom' )->text(), 'offset' ) . ' ' .
- Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
+ Html::input(
+ 'username',
+ $this->requestedUser,
+ 'text',
+ array(
+ 'id' => 'offset',
+ 'size' => 20,
+ 'autofocus' => $this->requestedUser === ''
+ )
+ ) . ' ';
# Group drop-down list
$out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' ' .
- Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) .
+ Xml::openElement( 'select', array( 'name' => 'group', 'id' => 'group' ) ) .
Xml::option( $this->msg( 'group-all' )->text(), '' );
- foreach( $this->getAllGroups() as $group => $groupText )
+ foreach ( $this->getAllGroups() as $group => $groupText ) {
$out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
+ }
$out .= Xml::closeElement( 'select' ) . '<br />';
- $out .= Xml::checkLabel( $this->msg( 'listusers-editsonly' )->text(), 'editsOnly', 'editsOnly', $this->editsOnly );
+ $out .= Xml::checkLabel(
+ $this->msg( 'listusers-editsonly' )->text(),
+ 'editsOnly',
+ 'editsOnly',
+ $this->editsOnly
+ );
$out .= '&#160;';
- $out .= Xml::checkLabel( $this->msg( 'listusers-creationsort' )->text(), 'creationSort', 'creationSort', $this->creationSort );
+ $out .= Xml::checkLabel(
+ $this->msg( 'listusers-creationsort' )->text(),
+ 'creationSort',
+ 'creationSort',
+ $this->creationSort
+ );
+ $out .= '&#160;';
+ $out .= Xml::checkLabel(
+ $this->msg( 'listusers-desc' )->text(),
+ 'desc',
+ 'desc',
+ $this->mDefaultDirection
+ );
$out .= '<br />';
wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
@@ -248,10 +305,11 @@ class UsersPager extends AlphabeticPager {
*/
function getAllGroups() {
$result = array();
- foreach( User::getAllGroups() as $group ) {
+ foreach ( User::getAllGroups() as $group ) {
$result[$group] = User::getGroupName( $group );
}
asort( $result );
+
return $result;
}
@@ -261,13 +319,14 @@ class UsersPager extends AlphabeticPager {
*/
function getDefaultQuery() {
$query = parent::getDefaultQuery();
- if( $this->requestedGroup != '' ) {
+ if ( $this->requestedGroup != '' ) {
$query['group'] = $this->requestedGroup;
}
- if( $this->requestedUser != '' ) {
+ if ( $this->requestedUser != '' ) {
$query['username'] = $this->requestedUser;
}
wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) );
+
return $query;
}
@@ -280,38 +339,40 @@ class UsersPager extends AlphabeticPager {
protected static function getGroups( $uid ) {
$user = User::newFromId( $uid );
$groups = array_diff( $user->getEffectiveGroups(), User::getImplicitGroups() );
+
return $groups;
}
/**
* Format a link to a group description page
*
- * @param $group String: group name
- * @param $username String Username
+ * @param string $group group name
+ * @param string $username Username
* @return string
*/
protected static function buildGroupLink( $group, $username ) {
- return User::makeGroupLinkHtml( $group, htmlspecialchars( User::getGroupMember( $group, $username ) ) );
+ return User::makeGroupLinkHtml(
+ $group,
+ htmlspecialchars( User::getGroupMember( $group, $username ) )
+ );
}
}
/**
* @ingroup SpecialPage
*/
-class SpecialListUsers extends SpecialPage {
-
+class SpecialListUsers extends IncludableSpecialPage {
/**
* Constructor
*/
public function __construct() {
parent::__construct( 'Listusers' );
- $this->mIncludable = true;
}
/**
* Show the special page
*
- * @param $par string (optional) A group to list users from
+ * @param string $par (optional) A group to list users from
*/
public function execute( $par ) {
$this->setHeaders();
@@ -327,7 +388,7 @@ class SpecialListUsers extends SpecialPage {
$s = $up->getPageHeader();
}
- if( $usersbody ) {
+ if ( $usersbody ) {
$s .= $up->getNavigationBar();
$s .= Html::rawElement( 'ul', array(), $usersbody );
$s .= $up->getNavigationBar();
@@ -337,4 +398,8 @@ class SpecialListUsers extends SpecialPage {
$this->getOutput()->addHTML( $s );
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php
index d71ac6e1..95ef9510 100644
--- a/includes/specials/SpecialLockdb.php
+++ b/includes/specials/SpecialLockdb.php
@@ -102,4 +102,8 @@ class SpecialLockdb extends FormSpecialPage {
$out->addSubtitle( $this->msg( 'lockdbsuccesssub' ) );
$out->addWikiMsg( 'lockdbsuccesstext' );
}
+
+ protected function getGroupName() {
+ return 'wiki';
+ }
}
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
index 7800e566..2ffdd89d 100644
--- a/includes/specials/SpecialLog.php
+++ b/includes/specials/SpecialLog.php
@@ -29,7 +29,6 @@
* @ingroup SpecialPage
*/
class SpecialLog extends SpecialPage {
-
/**
* List log type for which the target is a user
* Thus if the given target is in NS_MAIN we can alter it to be an NS_USER
@@ -75,14 +74,14 @@ class SpecialLog extends SpecialPage {
$opts->setValue( 'month', '' );
}
- // Reset the log type to default (nothing) if it's invalid or if the
- // user does not possess the right to view it
+ // If the user doesn't have the right permission to view the specific
+ // log type, throw a PermissionsError
+ // If the log type is invalid, just show all public logs
$type = $opts->getValue( 'type' );
- if ( !LogPage::isLogType( $type )
- || ( isset( $wgLogRestrictions[$type] )
- && !$this->getUser()->isAllowed( $wgLogRestrictions[$type] ) )
- ) {
+ if ( !LogPage::isLogType( $type ) ) {
$opts->setValue( 'type', '' );
+ } elseif ( isset( $wgLogRestrictions[$type] ) && !$this->getUser()->isAllowed( $wgLogRestrictions[$type] ) ) {
+ throw new PermissionsError( $wgLogRestrictions[$type] );
}
# Handle type-specific inputs
@@ -99,10 +98,10 @@ class SpecialLog extends SpecialPage {
# Some log types are only for a 'User:' title but we might have been given
# only the username instead of the full title 'User:username'. This part try
# to lookup for a user by that name and eventually fix user input. See bug 1697.
- if( in_array( $opts->getValue( 'type' ), $this->typeOnUser ) ) {
+ if ( in_array( $opts->getValue( 'type' ), $this->typeOnUser ) ) {
# ok we have a type of log which expect a user title.
$target = Title::newFromText( $opts->getValue( 'page' ) );
- if( $target && $target->getNamespace() === NS_MAIN ) {
+ if ( $target && $target->getNamespace() === NS_MAIN ) {
# User forgot to add 'User:', we are adding it for him
$opts->setValue( 'page',
Title::makeTitleSafe( NS_USER, $opts->getValue( 'page' ) )
@@ -117,9 +116,11 @@ class SpecialLog extends SpecialPage {
global $wgLogTypes;
# Get parameters
- $parms = explode( '/', ($par = ( $par !== null ) ? $par : '' ) );
+ $parms = explode( '/', ( $par = ( $par !== null ) ? $par : '' ) );
$symsForAll = array( '*', 'all' );
- if ( $parms[0] != '' && ( in_array( $par, $wgLogTypes ) || in_array( $par, $symsForAll ) ) ) {
+ if ( $parms[0] != '' &&
+ ( in_array( $par, $wgLogTypes ) || in_array( $par, $symsForAll ) )
+ ) {
$opts->setValue( 'type', $par );
} elseif ( count( $parms ) == 2 ) {
$opts->setValue( 'type', $parms[0] );
@@ -131,10 +132,22 @@ class SpecialLog extends SpecialPage {
private function show( FormOptions $opts, array $extraConds ) {
# Create a LogPager item to get the results and a LogEventsList item to format them...
- $loglist = new LogEventsList( $this->getContext(), null, LogEventsList::USE_REVDEL_CHECKBOXES );
- $pager = new LogPager( $loglist, $opts->getValue( 'type' ), $opts->getValue( 'user' ),
- $opts->getValue( 'page' ), $opts->getValue( 'pattern' ), $extraConds, $opts->getValue( 'year' ),
- $opts->getValue( 'month' ), $opts->getValue( 'tagfilter' ) );
+ $loglist = new LogEventsList(
+ $this->getContext(),
+ null,
+ LogEventsList::USE_REVDEL_CHECKBOXES
+ );
+ $pager = new LogPager(
+ $loglist,
+ $opts->getValue( 'type' ),
+ $opts->getValue( 'user' ),
+ $opts->getValue( 'page' ),
+ $opts->getValue( 'pattern' ),
+ $extraConds,
+ $opts->getValue( 'year' ),
+ $opts->getValue( 'month' ),
+ $opts->getValue( 'tagfilter' )
+ );
$this->addHeader( $opts->getValue( 'type' ) );
@@ -144,16 +157,28 @@ class SpecialLog extends SpecialPage {
}
# Show form options
- $loglist->showOptions( $pager->getType(), $opts->getValue( 'user' ), $pager->getPage(), $pager->getPattern(),
- $pager->getYear(), $pager->getMonth(), $pager->getFilterParams(), $opts->getValue( 'tagfilter' ) );
+ $loglist->showOptions(
+ $pager->getType(),
+ $opts->getValue( 'user' ),
+ $pager->getPage(),
+ $pager->getPattern(),
+ $pager->getYear(),
+ $pager->getMonth(),
+ $pager->getFilterParams(),
+ $opts->getValue( 'tagfilter' )
+ );
# Insert list
$logBody = $pager->getBody();
if ( $logBody ) {
$this->getOutput()->addHTML(
$pager->getNavigationBar() .
- $this->getRevisionButton( $loglist->beginLogEventsList() . $logBody . $loglist->endLogEventsList() ) .
- $pager->getNavigationBar()
+ $this->getRevisionButton(
+ $loglist->beginLogEventsList() .
+ $logBody .
+ $loglist->endLogEventsList()
+ ) .
+ $pager->getNavigationBar()
);
} else {
$this->getOutput()->addWikiMsg( 'logempty' );
@@ -161,19 +186,27 @@ class SpecialLog extends SpecialPage {
}
private function getRevisionButton( $formcontents ) {
- # If the user doesn't have the ability to delete log entries, don't bother showing him/her the button.
+ # If the user doesn't have the ability to delete log entries,
+ # don't bother showing them the button.
if ( !$this->getUser()->isAllowedAll( 'deletedhistory', 'deletelogentry' ) ) {
return $formcontents;
}
# Show button to hide log entries
global $wgScript;
- $s = Html::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-log-deleterevision-submit' ) ) . "\n";
+ $s = Html::openElement(
+ 'form',
+ array( 'action' => $wgScript, 'id' => 'mw-log-deleterevision-submit' )
+ ) . "\n";
$s .= Html::hidden( 'title', SpecialPage::getTitleFor( 'Revisiondelete' ) ) . "\n";
$s .= Html::hidden( 'target', SpecialPage::getTitleFor( 'Log' ) ) . "\n";
$s .= Html::hidden( 'type', 'logging' ) . "\n";
- $button = Html::element( 'button',
- array( 'type' => 'submit', 'class' => "deleterevision-log-submit mw-log-deleterevision-button" ),
+ $button = Html::element(
+ 'button',
+ array(
+ 'type' => 'submit',
+ 'class' => "deleterevision-log-submit mw-log-deleterevision-button"
+ ),
$this->msg( 'showhideselectedlogentries' )->text()
) . "\n";
$s .= $button . $formcontents . $button;
@@ -182,7 +215,6 @@ class SpecialLog extends SpecialPage {
return $s;
}
-
/**
* Set page title and show header for this log type
* @param $type string
@@ -194,4 +226,7 @@ class SpecialLog extends SpecialPage {
$this->getOutput()->addHTML( $page->getDescription()->parseAsBlock() );
}
+ protected function getGroupName() {
+ return 'changes';
+ }
}
diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php
index 763bbdb1..7c7771d7 100644
--- a/includes/specials/SpecialLonelypages.php
+++ b/includes/specials/SpecialLonelypages.php
@@ -28,7 +28,6 @@
* @ingroup SpecialPage
*/
class LonelyPagesPage extends PageQueryPage {
-
function __construct( $name = 'Lonelypages' ) {
parent::__construct( $name );
}
@@ -44,38 +43,56 @@ class LonelyPagesPage extends PageQueryPage {
function isExpensive() {
return true;
}
- function isSyndicated() { return false; }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'page', 'pagelinks',
- 'templatelinks' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'page_title' ),
- 'conds' => array ( 'pl_namespace IS NULL',
- 'page_namespace' => MWNamespace::getContentNamespaces(),
- 'page_is_redirect' => 0,
- 'tl_namespace IS NULL' ),
- 'join_conds' => array (
- 'pagelinks' => array (
- 'LEFT JOIN', array (
+ return array(
+ 'tables' => array(
+ 'page', 'pagelinks',
+ 'templatelinks'
+ ),
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title'
+ ),
+ 'conds' => array(
+ 'pl_namespace IS NULL',
+ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_is_redirect' => 0,
+ 'tl_namespace IS NULL'
+ ),
+ 'join_conds' => array(
+ 'pagelinks' => array(
+ 'LEFT JOIN', array(
'pl_namespace = page_namespace',
- 'pl_title = page_title' ) ),
- 'templatelinks' => array (
- 'LEFT JOIN', array (
+ 'pl_title = page_title'
+ )
+ ),
+ 'templatelinks' => array(
+ 'LEFT JOIN', array(
'tl_namespace = page_namespace',
- 'tl_title = page_title' ) ) )
+ 'tl_title = page_title'
+ )
+ )
+ )
);
}
function getOrderFields() {
// For some crazy reason ordering by a constant
// causes a filesort in MySQL 5
- if( count( MWNamespace::getContentNamespaces() ) > 1 ) {
+ if ( count( MWNamespace::getContentNamespaces() ) > 1 ) {
return array( 'page_namespace', 'page_title' );
} else {
return array( 'page_title' );
}
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialLongpages.php b/includes/specials/SpecialLongpages.php
index dd60e37d..d90d2718 100644
--- a/includes/specials/SpecialLongpages.php
+++ b/includes/specials/SpecialLongpages.php
@@ -26,7 +26,6 @@
* @ingroup SpecialPage
*/
class LongPagesPage extends ShortPagesPage {
-
function __construct( $name = 'Longpages' ) {
parent::__construct( $name );
}
@@ -34,4 +33,8 @@ class LongPagesPage extends ShortPagesPage {
function sortDescending() {
return true;
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php
index 104c653f..3eeae310 100644
--- a/includes/specials/SpecialMIMEsearch.php
+++ b/includes/specials/SpecialMIMEsearch.php
@@ -34,28 +34,69 @@ class MIMEsearchPage extends QueryPage {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
- function isCacheable() { return false; }
+ function isExpensive() {
+ return false;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function isCacheable() {
+ return false;
+ }
function linkParameters() {
return array( 'mime' => "{$this->major}/{$this->minor}" );
}
public function getQueryInfo() {
- return array(
+ $qi = array(
'tables' => array( 'image' ),
- 'fields' => array( 'namespace' => NS_FILE,
- 'title' => 'img_name',
- 'value' => 'img_major_mime',
- 'img_size',
- 'img_width',
- 'img_height',
- 'img_user_text',
- 'img_timestamp' ),
- 'conds' => array( 'img_major_mime' => $this->major,
- 'img_minor_mime' => $this->minor )
+ 'fields' => array(
+ 'namespace' => NS_FILE,
+ 'title' => 'img_name',
+ // Still have a value field just in case,
+ // but it isn't actually used for sorting.
+ 'value' => 'img_name',
+ 'img_size',
+ 'img_width',
+ 'img_height',
+ 'img_user_text',
+ 'img_timestamp'
+ ),
+ 'conds' => array(
+ 'img_major_mime' => $this->major,
+ 'img_minor_mime' => $this->minor,
+ // This is in order to trigger using
+ // the img_media_mime index in "range" mode.
+ 'img_media_type' => array(
+ MEDIATYPE_BITMAP,
+ MEDIATYPE_DRAWING,
+ MEDIATYPE_AUDIO,
+ MEDIATYPE_VIDEO,
+ MEDIATYPE_MULTIMEDIA,
+ MEDIATYPE_UNKNOWN,
+ MEDIATYPE_OFFICE,
+ MEDIATYPE_TEXT,
+ MEDIATYPE_EXECUTABLE,
+ MEDIATYPE_ARCHIVE,
+ ),
+ ),
);
+ return $qi;
+ }
+
+ /**
+ * The index is on (img_media_type, img_major_mime, img_minor_mime)
+ * which unfortunately doesn't have img_name at the end for sorting.
+ * So tell db to sort it however it wishes (Its not super important
+ * that this report gives results in a logical order). As an aditional
+ * note, mysql seems to by default order things by img_name ASC, which
+ * is what we ideally want, so everything works out fine anyhow.
+ */
+ function getOrderFields() {
+ return array();
}
function execute( $par ) {
@@ -66,25 +107,36 @@ class MIMEsearchPage extends QueryPage {
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addHTML(
- Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgScript ) ) .
- Xml::openElement( 'fieldset' ) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
- Xml::element( 'legend', null, $this->msg( 'mimesearch' )->text() ) .
- Xml::inputLabel( $this->msg( 'mimetype' )->text(), 'mime', 'mime', 20, $mime ) . ' ' .
- Xml::submitButton( $this->msg( 'ilsubmit' )->text() ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' )
+ Xml::openElement(
+ 'form',
+ array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgScript )
+ ) .
+ Xml::openElement( 'fieldset' ) .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ Xml::element( 'legend', null, $this->msg( 'mimesearch' )->text() ) .
+ Xml::inputLabel( $this->msg( 'mimetype' )->text(), 'mime', 'mime', 20, $mime ) .
+ ' ' .
+ Xml::submitButton( $this->msg( 'ilsubmit' )->text() ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
);
list( $this->major, $this->minor ) = File::splitMime( $mime );
+
if ( $this->major == '' || $this->minor == '' || $this->minor == 'unknown' ||
- !self::isValidType( $this->major ) ) {
+ !self::isValidType( $this->major )
+ ) {
return;
}
+
parent::execute( $par );
}
-
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
global $wgContLang;
@@ -101,8 +153,13 @@ class MIMEsearchPage extends QueryPage {
$bytes = htmlspecialchars( $lang->formatSize( $result->img_size ) );
$dimensions = $this->msg( 'widthheight' )->numParams( $result->img_width,
$result->img_height )->escaped();
- $user = Linker::link( Title::makeTitle( NS_USER, $result->img_user_text ), htmlspecialchars( $result->img_user_text ) );
- $time = htmlspecialchars( $lang->userTimeAndDate( $result->img_timestamp, $this->getUser() ) );
+ $user = Linker::link(
+ Title::makeTitle( NS_USER, $result->img_user_text ),
+ htmlspecialchars( $result->img_user_text )
+ );
+
+ $time = $lang->userTimeAndDate( $result->img_timestamp, $this->getUser() );
+ $time = htmlspecialchars( $time );
return "$download $plink . . $dimensions . . $bytes . . $user . . $time";
}
@@ -124,6 +181,11 @@ class MIMEsearchPage extends QueryPage {
'model',
'multipart'
);
+
return in_array( $type, $types );
}
+
+ protected function getGroupName() {
+ return 'media';
+ }
}
diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php
index 1f057499..fb5ea657 100644
--- a/includes/specials/SpecialMergeHistory.php
+++ b/includes/specials/SpecialMergeHistory.php
@@ -52,14 +52,14 @@ class SpecialMergeHistory extends SpecialPage {
$this->mTargetID = intval( $request->getVal( 'targetID' ) );
$this->mDestID = intval( $request->getVal( 'destID' ) );
$this->mTimestamp = $request->getVal( 'mergepoint' );
- if( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
+ if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
$this->mTimestamp = '';
}
$this->mComment = $request->getText( 'wpComment' );
$this->mMerge = $request->wasPosted() && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
// target page
- if( $this->mSubmitted ) {
+ if ( $this->mSubmitted ) {
$this->mTargetObj = Title::newFromURL( $this->mTarget );
$this->mDestObj = Title::newFromURL( $this->mDest );
} else {
@@ -75,7 +75,7 @@ class SpecialMergeHistory extends SpecialPage {
*/
function preCacheMessages() {
// Precache various messages
- if( !isset( $this->message ) ) {
+ if ( !isset( $this->message ) ) {
$this->message['last'] = $this->msg( 'last' )->escaped();
}
}
@@ -89,20 +89,22 @@ class SpecialMergeHistory extends SpecialPage {
$this->setHeaders();
$this->outputHeader();
- if( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
+ if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
$this->merge();
+
return;
}
if ( !$this->mSubmitted ) {
$this->showMergeForm();
+
return;
}
$errors = array();
if ( !$this->mTargetObj instanceof Title ) {
$errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
- } elseif( !$this->mTargetObj->exists() ) {
+ } elseif ( !$this->mTargetObj->exists() ) {
$errors[] = $this->msg( 'mergehistory-no-source', array( 'parse' ),
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
)->parseAsBlock();
@@ -110,7 +112,7 @@ class SpecialMergeHistory extends SpecialPage {
if ( !$this->mDestObj instanceof Title ) {
$errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
- } elseif( !$this->mDestObj->exists() ) {
+ } elseif ( !$this->mDestObj->exists() ) {
$errors[] = $this->msg( 'mergehistory-no-destination', array( 'parse' ),
wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
)->parseAsBlock();
@@ -126,7 +128,6 @@ class SpecialMergeHistory extends SpecialPage {
} else {
$this->showHistory();
}
-
}
function showMergeForm() {
@@ -138,25 +139,25 @@ class SpecialMergeHistory extends SpecialPage {
Xml::openElement( 'form', array(
'method' => 'get',
'action' => $wgScript ) ) .
- '<fieldset>' .
- Xml::element( 'legend', array(),
- $this->msg( 'mergehistory-box' )->text() ) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) .
- Html::hidden( 'submitted', '1' ) .
- Html::hidden( 'mergepoint', $this->mTimestamp ) .
- Xml::openElement( 'table' ) .
- '<tr>
+ '<fieldset>' .
+ Xml::element( 'legend', array(),
+ $this->msg( 'mergehistory-box' )->text() ) .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
+ Html::hidden( 'submitted', '1' ) .
+ Html::hidden( 'mergepoint', $this->mTimestamp ) .
+ Xml::openElement( 'table' ) .
+ '<tr>
<td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
<td>' . Xml::input( 'target', 30, $this->mTarget, array( 'id' => 'target' ) ) . '</td>
</tr><tr>
<td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
<td>' . Xml::input( 'dest', 30, $this->mDest, array( 'id' => 'dest' ) ) . '</td>
</tr><tr><td>' .
- Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
- '</td></tr>' .
- Xml::closeElement( 'table' ) .
- '</fieldset>' .
- '</form>'
+ Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
+ '</td></tr>' .
+ Xml::closeElement( 'table' ) .
+ '</fieldset>' .
+ '</form>'
);
}
@@ -183,40 +184,40 @@ class SpecialMergeHistory extends SpecialPage {
);
$out->addHTML( $top );
- if( $haveRevisions ) {
+ if ( $haveRevisions ) {
# Format the user-visible controls (comment field, submission button)
# in a nice little table
$table =
Xml::openElement( 'fieldset' ) .
- $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
- $this->mDestObj->getPrefixedText() )->parse() .
- Xml::openElement( 'table', array( 'id' => 'mw-mergehistory-table' ) ) .
+ $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
+ $this->mDestObj->getPrefixedText() )->parse() .
+ Xml::openElement( 'table', array( 'id' => 'mw-mergehistory-table' ) ) .
'<tr>
<td class="mw-label">' .
- Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
- '</td>
- <td class="mw-input">' .
- Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
- '</td>
+ Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
+ '</td>
+ <td class="mw-input">' .
+ Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
+ '</td>
</tr>
<tr>
<td>&#160;</td>
<td class="mw-submit">' .
- Xml::submitButton( $this->msg( 'mergehistory-submit' )->text(), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
- '</td>
+ Xml::submitButton( $this->msg( 'mergehistory-submit' )->text(), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
+ '</td>
</tr>' .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'fieldset' );
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
$out->addHTML( $table );
}
$out->addHTML(
'<h2 id="mw-mergehistory">' .
- $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
+ $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
);
- if( $haveRevisions ) {
+ if ( $haveRevisions ) {
$out->addHTML( $revisions->getNavigationBar() );
$out->addHTML( '<ul>' );
$out->addHTML( $revisions->getBody() );
@@ -261,14 +262,14 @@ class SpecialMergeHistory extends SpecialPage {
array(),
array( 'oldid' => $rev->getId() )
);
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
}
# Last link
- if( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
$last = $this->message['last'];
- } elseif( isset( $this->prevId[$row->rev_id] ) ) {
+ } elseif ( isset( $this->prevId[$row->rev_id] ) ) {
$last = Linker::linkKnown(
$rev->getTitle(),
$this->message['last'],
@@ -283,7 +284,7 @@ class SpecialMergeHistory extends SpecialPage {
$userLink = Linker::revUserTools( $rev );
$size = $row->rev_len;
- if( !is_null( $size ) ) {
+ if ( !is_null( $size ) ) {
$stxt = Linker::formatRevisionSize( $size );
}
$comment = Linker::revComment( $rev );
@@ -298,10 +299,10 @@ class SpecialMergeHistory extends SpecialPage {
# keep it consistent...
$targetTitle = Title::newFromID( $this->mTargetID );
$destTitle = Title::newFromID( $this->mDestID );
- if( is_null( $targetTitle ) || is_null( $destTitle ) ) {
+ if ( is_null( $targetTitle ) || is_null( $destTitle ) ) {
return false; // validate these
}
- if( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
+ if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
return false;
}
# Verify that this timestamp is valid
@@ -317,8 +318,9 @@ class SpecialMergeHistory extends SpecialPage {
__METHOD__
);
# Destination page must exist with revisions
- if( !$maxtimestamp ) {
+ if ( !$maxtimestamp ) {
$this->getOutput()->addWikiMsg( 'mergehistory-fail' );
+
return false;
}
# Get the latest timestamp of the source
@@ -329,12 +331,13 @@ class SpecialMergeHistory extends SpecialPage {
__METHOD__
);
# $this->mTimestamp must be older than $maxtimestamp
- if( $this->mTimestamp >= $maxtimestamp ) {
+ if ( $this->mTimestamp >= $maxtimestamp ) {
$this->getOutput()->addWikiMsg( 'mergehistory-fail' );
+
return false;
}
# Update the revisions
- if( $this->mTimestamp ) {
+ if ( $this->mTimestamp ) {
$timewhere = "rev_timestamp <= {$this->mTimestamp}";
$timestampLimit = wfTimestamp( TS_MW, $this->mTimestamp );
} else {
@@ -354,12 +357,12 @@ class SpecialMergeHistory extends SpecialPage {
$haveRevisions = $dbw->selectField(
'revision',
'rev_timestamp',
- array( 'rev_page' => $this->mTargetID ),
+ array( 'rev_page' => $this->mTargetID ),
__METHOD__,
array( 'FOR UPDATE' )
);
- if( !$haveRevisions ) {
- if( $this->mComment ) {
+ if ( !$haveRevisions ) {
+ if ( $this->mComment ) {
$comment = $this->msg(
'mergehistory-comment',
$targetTitle->getPrefixedText(),
@@ -373,40 +376,48 @@ class SpecialMergeHistory extends SpecialPage {
$destTitle->getPrefixedText()
)->inContentLanguage()->text();
}
- $mwRedir = MagicWord::get( 'redirect' );
- $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n";
- $redirectPage = WikiPage::factory( $targetTitle );
- $redirectRevision = new Revision( array(
- 'page' => $this->mTargetID,
- 'comment' => $comment,
- 'text' => $redirectText ) );
- $redirectRevision->insertOn( $dbw );
- $redirectPage->updateRevisionOn( $dbw, $redirectRevision );
-
- # Now, we record the link from the redirect to the new title.
- # It should have no other outgoing links...
- $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
- $dbw->insert( 'pagelinks',
- array(
- 'pl_from' => $this->mDestID,
- 'pl_namespace' => $destTitle->getNamespace(),
- 'pl_title' => $destTitle->getDBkey() ),
- __METHOD__
- );
+
+ $contentHandler = ContentHandler::getForTitle( $targetTitle );
+ $redirectContent = $contentHandler->makeRedirectContent( $destTitle );
+
+ if ( $redirectContent ) {
+ $redirectPage = WikiPage::factory( $targetTitle );
+ $redirectRevision = new Revision( array(
+ 'title' => $targetTitle,
+ 'page' => $this->mTargetID,
+ 'comment' => $comment,
+ 'content' => $redirectContent ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectPage->updateRevisionOn( $dbw, $redirectRevision );
+
+ # Now, we record the link from the redirect to the new title.
+ # It should have no other outgoing links...
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $this->mDestID,
+ 'pl_namespace' => $destTitle->getNamespace(),
+ 'pl_title' => $destTitle->getDBkey() ),
+ __METHOD__
+ );
+ } else {
+ // would be nice to show a warning if we couldn't create a redirect
+ }
} else {
$targetTitle->invalidateCache(); // update histories
}
$destTitle->invalidateCache(); // update histories
# Check if this did anything
- if( !$count ) {
+ if ( !$count ) {
$this->getOutput()->addWikiMsg( 'mergehistory-fail' );
+
return false;
}
# Update our logs
$log = new LogPage( 'merge' );
$log->addEntry(
'merge', $targetTitle, $this->mComment,
- array( $destTitle->getPrefixedText(), $timestampLimit )
+ array( $destTitle->getPrefixedText(), $timestampLimit ), $this->getUser()
);
$this->getOutput()->addWikiMsg( 'mergehistory-success',
@@ -416,6 +427,10 @@ class SpecialMergeHistory extends SpecialPage {
return true;
}
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
class MergeHistoryPager extends ReverseChronologicalPager {
@@ -451,9 +466,9 @@ class MergeHistoryPager extends ReverseChronologicalPager {
$batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
$rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id;
- if( $rev_id > $row->rev_id ) {
+ if ( $rev_id > $row->rev_id ) {
$this->mForm->prevId[$rev_id] = $row->rev_id;
- } elseif( $rev_id < $row->rev_id ) {
+ } elseif ( $rev_id < $row->rev_id ) {
$this->mForm->prevId[$row->rev_id] = $rev_id;
}
@@ -464,6 +479,7 @@ class MergeHistoryPager extends ReverseChronologicalPager {
$this->mResult->seek( 0 );
wfProfileOut( __METHOD__ );
+
return '';
}
@@ -475,10 +491,11 @@ class MergeHistoryPager extends ReverseChronologicalPager {
$conds = $this->mConds;
$conds['rev_page'] = $this->articleID;
$conds[] = "rev_timestamp < {$this->maxTimestamp}";
+
return array(
'tables' => array( 'revision', 'page', 'user' ),
'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
- 'conds' => $conds,
+ 'conds' => $conds,
'join_conds' => array(
'page' => Revision::pageJoinCond(),
'user' => Revision::userJoinCond() )
diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php
index 3f0bafa3..9b67f343 100644
--- a/includes/specials/SpecialMostcategories.php
+++ b/includes/specials/SpecialMostcategories.php
@@ -30,31 +30,43 @@
* @ingroup SpecialPage
*/
class MostcategoriesPage extends QueryPage {
-
function __construct( $name = 'Mostcategories' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'categorylinks', 'page' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'COUNT(*)' ),
- 'conds' => array ( 'page_namespace' => MWNamespace::getContentNamespaces() ),
- 'options' => array ( 'HAVING' => 'COUNT(*) > 1',
- 'GROUP BY' => array( 'page_namespace', 'page_title' ) ),
- 'join_conds' => array ( 'page' => array ( 'LEFT JOIN',
- 'page_id = cl_from' ) )
+ return array(
+ 'tables' => array( 'categorylinks', 'page' ),
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'COUNT(*)'
+ ),
+ 'conds' => array( 'page_namespace' => MWNamespace::getContentNamespaces() ),
+ 'options' => array(
+ 'HAVING' => 'COUNT(*) > 1',
+ 'GROUP BY' => array( 'page_namespace', 'page_title' )
+ ),
+ 'join_conds' => array(
+ 'page' => array(
+ 'LEFT JOIN',
+ 'page_id = cl_from'
+ )
+ )
);
}
/**
- * @param $db DatabaseBase
- * @param $res
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
# There's no point doing a batch check if we aren't caching results;
@@ -73,15 +85,22 @@ class MostcategoriesPage extends QueryPage {
}
/**
- * @param $skin Skin
- * @param $result
+ * @param Skin $skin
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
$title = Title::makeTitleSafe( $result->namespace, $result->title );
if ( !$title ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $result->namespace,
+ $result->title
+ )
+ );
}
if ( $this->isCached() ) {
@@ -94,4 +113,8 @@ class MostcategoriesPage extends QueryPage {
return $this->getLanguage()->specialList( $link, $count );
}
+
+ protected function getGroupName() {
+ return 'highuse';
+ }
}
diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php
index 3d797908..98d8da3a 100644
--- a/includes/specials/SpecialMostimages.php
+++ b/includes/specials/SpecialMostimages.php
@@ -30,22 +30,30 @@
* @ingroup SpecialPage
*/
class MostimagesPage extends ImageQueryPage {
-
function __construct( $name = 'Mostimages' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'imagelinks' ),
- 'fields' => array ( 'namespace' => NS_FILE,
- 'title' => 'il_to',
- 'value' => 'COUNT(*)' ),
- 'options' => array ( 'GROUP BY' => 'il_to',
- 'HAVING' => 'COUNT(*) > 1' )
+ return array(
+ 'tables' => array( 'imagelinks' ),
+ 'fields' => array(
+ 'namespace' => NS_FILE,
+ 'title' => 'il_to',
+ 'value' => 'COUNT(*)'
+ ),
+ 'options' => array(
+ 'GROUP BY' => 'il_to',
+ 'HAVING' => 'COUNT(*) > 1'
+ )
);
}
@@ -53,4 +61,7 @@ class MostimagesPage extends ImageQueryPage {
return $this->msg( 'nimagelinks' )->numParams( $row->value )->escaped() . '<br />';
}
+ protected function getGroupName() {
+ return 'highuse';
+ }
}
diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php
index 894d697b..98dd68e9 100644
--- a/includes/specials/SpecialMostinterwikis.php
+++ b/includes/specials/SpecialMostinterwikis.php
@@ -30,33 +30,37 @@
* @ingroup SpecialPage
*/
class MostinterwikisPage extends QueryPage {
-
function __construct( $name = 'Mostinterwikis' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array (
+ return array(
+ 'tables' => array(
'langlinks',
'page'
- ), 'fields' => array (
+ ), 'fields' => array(
'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'COUNT(*)'
- ), 'conds' => array (
+ ), 'conds' => array(
'page_namespace' => MWNamespace::getContentNamespaces()
- ), 'options' => array (
+ ), 'options' => array(
'HAVING' => 'COUNT(*) > 1',
- 'GROUP BY' => array (
+ 'GROUP BY' => array(
'page_namespace',
'page_title'
)
- ), 'join_conds' => array (
- 'page' => array (
+ ), 'join_conds' => array(
+ 'page' => array(
'LEFT JOIN',
'page_id = ll_from'
)
@@ -67,8 +71,8 @@ class MostinterwikisPage extends QueryPage {
/**
* Pre-fill the link cache
*
- * @param $db DatabaseBase
- * @param $res
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
# There's no point doing a batch check if we aren't caching results;
@@ -95,8 +99,15 @@ class MostinterwikisPage extends QueryPage {
function formatResult( $skin, $result ) {
$title = Title::makeTitleSafe( $result->namespace, $result->title );
if ( !$title ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $result->namespace,
+ $result->title
+ )
+ );
}
if ( $this->isCached() ) {
@@ -109,4 +120,8 @@ class MostinterwikisPage extends QueryPage {
return $this->getLanguage()->specialList( $link, $count );
}
+
+ protected function getGroupName() {
+ return 'highuse';
+ }
}
diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php
index 89c43509..37593bf9 100644
--- a/includes/specials/SpecialMostlinked.php
+++ b/includes/specials/SpecialMostlinked.php
@@ -31,42 +31,60 @@
* @ingroup SpecialPage
*/
class MostlinkedPage extends QueryPage {
-
function __construct( $name = 'Mostlinked' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'pagelinks', 'page' ),
- 'fields' => array ( 'namespace' => 'pl_namespace',
- 'title' => 'pl_title',
- 'value' => 'COUNT(*)',
- 'page_namespace' ),
- 'options' => array ( 'HAVING' => 'COUNT(*) > 1',
- 'GROUP BY' => array( 'pl_namespace', 'pl_title',
- 'page_namespace' ) ),
- 'join_conds' => array ( 'page' => array ( 'LEFT JOIN',
- array ( 'page_namespace = pl_namespace',
- 'page_title = pl_title' ) ) )
+ return array(
+ 'tables' => array( 'pagelinks', 'page' ),
+ 'fields' => array(
+ 'namespace' => 'pl_namespace',
+ 'title' => 'pl_title',
+ 'value' => 'COUNT(*)',
+ 'page_namespace'
+ ),
+ 'options' => array(
+ 'HAVING' => 'COUNT(*) > 1',
+ 'GROUP BY' => array(
+ 'pl_namespace', 'pl_title',
+ 'page_namespace'
+ )
+ ),
+ 'join_conds' => array(
+ 'page' => array(
+ 'LEFT JOIN',
+ array(
+ 'page_namespace = pl_namespace',
+ 'page_title = pl_title'
+ )
+ )
+ )
);
}
/**
* Pre-fill the link cache
*
- * @param $db DatabaseBase
- * @param $res
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
if ( $res->numRows() > 0 ) {
$linkBatch = new LinkBatch();
+
foreach ( $res as $row ) {
$linkBatch->add( $row->namespace, $row->title );
}
+
$res->seek( 0 );
$linkBatch->execute();
}
@@ -76,30 +94,46 @@ 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 string $caption text to display on the link
* @return String
*/
function makeWlhLink( $title, $caption ) {
$wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
+
return Linker::linkKnown( $wlh, $caption );
}
/**
- * Make links to the page corresponding to the item, and the "what links here" page for it
+ * Make links to the page corresponding to the item,
+ * and the "what links here" page for it
*
- * @param $skin Skin to be used
- * @param $result Result row
+ * @param Skin $skin Skin to be used
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
$title = Title::makeTitleSafe( $result->namespace, $result->title );
if ( !$title ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $result->namespace,
+ $result->title )
+ );
}
+
$link = Linker::link( $title );
- $wlh = $this->makeWlhLink( $title,
- $this->msg( 'nlinks' )->numParams( $result->value )->escaped() );
+ $wlh = $this->makeWlhLink(
+ $title,
+ $this->msg( 'nlinks' )->numParams( $result->value )->escaped()
+ );
+
return $this->getLanguage()->specialList( $link, $wlh );
}
+
+ protected function getGroupName() {
+ return 'highuse';
+ }
}
diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php
index dadef8bf..0d4641b1 100644
--- a/includes/specials/SpecialMostlinkedcategories.php
+++ b/includes/specials/SpecialMostlinkedcategories.php
@@ -30,29 +30,32 @@
* @ingroup SpecialPage
*/
class MostlinkedCategoriesPage extends QueryPage {
-
function __construct( $name = 'Mostlinkedcategories' ) {
parent::__construct( $name );
}
- function isSyndicated() { return false; }
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'category' ),
- 'fields' => array ( 'title' => 'cat_title',
- 'namespace' => NS_CATEGORY,
- 'value' => 'cat_pages' ),
+ return array(
+ 'tables' => array( 'category' ),
+ 'fields' => array( 'title' => 'cat_title',
+ 'namespace' => NS_CATEGORY,
+ 'value' => 'cat_pages' ),
);
}
- function sortDescending() { return true; }
+ function sortDescending() {
+ return true;
+ }
/**
* Fetch user page links and cache their existence
*
- * @param $db DatabaseBase
- * @param $res DatabaseResult
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
if ( !$res->numRows() ) {
@@ -70,8 +73,8 @@ class MostlinkedCategoriesPage extends QueryPage {
}
/**
- * @param $skin Skin
- * @param $result
+ * @param Skin $skin
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
@@ -79,15 +82,24 @@ class MostlinkedCategoriesPage extends QueryPage {
$nt = Title::makeTitleSafe( NS_CATEGORY, $result->title );
if ( !$nt ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), NS_CATEGORY, $result->title ) );
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ NS_CATEGORY,
+ $result->title )
+ );
}
$text = $wgContLang->convert( $nt->getText() );
-
$plink = Linker::link( $nt, htmlspecialchars( $text ) );
-
$nlinks = $this->msg( 'nmembers' )->numParams( $result->value )->escaped();
+
return $this->getLanguage()->specialList( $plink, $nlinks );
}
+
+ protected function getGroupName() {
+ return 'highuse';
+ }
}
diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php
index 22932e5c..c90acb1f 100644
--- a/includes/specials/SpecialMostlinkedtemplates.php
+++ b/includes/specials/SpecialMostlinkedtemplates.php
@@ -29,7 +29,6 @@
* @ingroup SpecialPage
*/
class MostlinkedTemplatesPage extends QueryPage {
-
function __construct( $name = 'Mostlinkedtemplates' ) {
parent::__construct( $name );
}
@@ -62,12 +61,14 @@ class MostlinkedTemplatesPage extends QueryPage {
}
public function getQueryInfo() {
- return array (
- 'tables' => array ( 'templatelinks' ),
- 'fields' => array ( 'namespace' => 'tl_namespace',
- 'title' => 'tl_title',
- 'value' => 'COUNT(*)' ),
- 'conds' => array ( 'tl_namespace' => NS_TEMPLATE ),
+ return array(
+ 'tables' => array( 'templatelinks' ),
+ 'fields' => array(
+ 'namespace' => 'tl_namespace',
+ 'title' => 'tl_title',
+ 'value' => 'COUNT(*)'
+ ),
+ 'conds' => array( 'tl_namespace' => NS_TEMPLATE ),
'options' => array( 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) )
);
}
@@ -76,7 +77,7 @@ class MostlinkedTemplatesPage extends QueryPage {
* Pre-cache page existence to speed up link generation
*
* @param $db DatabaseBase connection
- * @param $res ResultWrapper
+ * @param ResultWrapper $res
*/
public function preprocessResults( $db, $res ) {
if ( !$res->numRows() ) {
@@ -95,15 +96,22 @@ class MostlinkedTemplatesPage extends QueryPage {
/**
* Format a result row
*
- * @param $skin Skin to use for UI elements
- * @param $result Result row
- * @return String
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
*/
public function formatResult( $skin, $result ) {
$title = Title::makeTitleSafe( $result->namespace, $result->title );
if ( !$title ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $result->namespace,
+ $result->title
+ )
+ );
}
return $this->getLanguage()->specialList(
@@ -115,14 +123,18 @@ class MostlinkedTemplatesPage extends QueryPage {
/**
* Make a "what links here" link for a given title
*
- * @param $title Title to make the link for
- * @param $result Result row
+ * @param Title $title Title to make the link for
+ * @param object $result Result row
* @return String
*/
private function makeWlhLink( $title, $result ) {
$wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
$label = $this->msg( 'ntransclusions' )->numParams( $result->value )->escaped();
+
return Linker::link( $wlh, $label );
}
-}
+ protected function getGroupName() {
+ return 'highuse';
+ }
+}
diff --git a/includes/specials/SpecialMostrevisions.php b/includes/specials/SpecialMostrevisions.php
index b0253316..ad6b788d 100644
--- a/includes/specials/SpecialMostrevisions.php
+++ b/includes/specials/SpecialMostrevisions.php
@@ -31,4 +31,8 @@ class MostrevisionsPage extends FewestrevisionsPage {
function sortDescending() {
return true;
}
+
+ protected function getGroupName() {
+ return 'highuse';
+ }
}
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
index af3dbf3e..253e6cc3 100644
--- a/includes/specials/SpecialMovepage.php
+++ b/includes/specials/SpecialMovepage.php
@@ -27,13 +27,15 @@
* @ingroup SpecialPage
*/
class MovePageForm extends UnlistedSpecialPage {
-
/**
+ * Objects
* @var Title
*/
- var $oldTitle, $newTitle; # Objects
- var $reason; # Text input
- var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect, $moveOverShared; # Checks
+ var $oldTitle, $newTitle;
+ // Text input
+ var $reason;
+ // Checks
+ var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect, $moveOverShared;
private $watch = false;
@@ -55,10 +57,10 @@ class MovePageForm extends UnlistedSpecialPage {
$oldTitleText = $request->getVal( 'wpOldTitle', $target );
$this->oldTitle = Title::newFromText( $oldTitleText );
- if( is_null( $this->oldTitle ) ) {
+ if ( is_null( $this->oldTitle ) ) {
throw new ErrorPageError( 'notargettitle', 'notargettext' );
}
- if( !$this->oldTitle->exists() ) {
+ if ( !$this->oldTitle->exists() ) {
throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
}
@@ -71,7 +73,6 @@ class MovePageForm extends UnlistedSpecialPage {
? Title::newFromText( $newTitleText_bc )
: Title::makeTitleSafe( $newTitleTextNs, $newTitleTextMain );
-
$user = $this->getUser();
# Check rights
@@ -94,7 +95,8 @@ class MovePageForm extends UnlistedSpecialPage {
$this->watch = $request->getCheck( 'wpWatch' ) && $user->isLoggedIn();
if ( 'submit' == $request->getVal( 'action' ) && $request->wasPosted()
- && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
+ && $user->matchEditToken( $request->getVal( 'wpEditToken' ) )
+ ) {
$this->doSubmit();
} else {
$this->showForm( array() );
@@ -104,7 +106,7 @@ class MovePageForm extends UnlistedSpecialPage {
/**
* Show the form
*
- * @param $err Array: error messages. Each item is an error message.
+ * @param array $err error messages. Each item is an error message.
* It may either be a string message name or array message name and
* parameters, like the second argument to OutputPage::wrapWikiMsg().
*/
@@ -130,7 +132,7 @@ class MovePageForm extends UnlistedSpecialPage {
# link, check for validity. We can then show some diagnostic
# information and save a click.
$newerr = $this->oldTitle->isValidMoveOperation( $newTitle );
- if( is_array( $newerr ) ) {
+ if ( is_array( $newerr ) ) {
$err = $newerr;
}
}
@@ -147,16 +149,26 @@ class MovePageForm extends UnlistedSpecialPage {
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( $this->msg( 'delete_and_move_confirm' )->text(), 'wpConfirm', 'wpConfirm' ) .
- "</td>
+ Xml::checkLabel(
+ $this->msg( 'delete_and_move_confirm' )->text(),
+ 'wpConfirm',
+ 'wpConfirm'
+ ) .
+ "</td>
</tr>";
$err = array();
} else {
- if ($this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) {
- $out->wrapWikiMsg( "<div class=\"error mw-moveuserpage-warning\">\n$1\n</div>", 'moveuserpage-warning' );
+ if ( $this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) {
+ $out->wrapWikiMsg(
+ "<div class=\"error mw-moveuserpage-warning\">\n$1\n</div>",
+ 'moveuserpage-warning'
+ );
}
- $out->addWikiMsg( $wgFixDoubleRedirects ? 'movepagetext' :
- 'movepagetext-noredirectfixer' );
+
+ $out->addWikiMsg( $wgFixDoubleRedirects ?
+ 'movepagetext' :
+ 'movepagetext-noredirectfixer'
+ );
$movepagebtn = $this->msg( 'movepagebtn' )->text();
$submitVar = 'wpMove';
$confirm = false;
@@ -189,7 +201,7 @@ class MovePageForm extends UnlistedSpecialPage {
array(
'rd_namespace' => $this->oldTitle->getNamespace(),
'rd_title' => $this->oldTitle->getDBkey(),
- ) , __METHOD__ );
+ ), __METHOD__ );
} else {
$hasRedirects = false;
}
@@ -206,6 +218,7 @@ class MovePageForm extends UnlistedSpecialPage {
if ( count( $err ) == 1 ) {
$errMsg = $err[0];
$errMsgName = array_shift( $errMsg );
+
if ( $errMsgName == 'hookaborted' ) {
$out->addHTML( "<p>{$errMsg[0]}</p>\n" );
} else {
@@ -213,8 +226,9 @@ class MovePageForm extends UnlistedSpecialPage {
}
} else {
$errStr = array();
- foreach( $err as $errMsg ) {
- if( $errMsg[0] == 'hookaborted' ) {
+
+ foreach ( $err as $errMsg ) {
+ if ( $errMsg[0] == 'hookaborted' ) {
$errStr[] = $errMsg[1];
} else {
$errMsgName = array_shift( $errMsg );
@@ -239,7 +253,13 @@ class MovePageForm extends UnlistedSpecialPage {
}
$out->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
$out->addWikiMsg( $noticeMsg );
- LogEventsList::showLogExtract( $out, 'protect', $this->oldTitle, '', array( 'lim' => 1 ) );
+ LogEventsList::showLogExtract(
+ $out,
+ 'protect',
+ $this->oldTitle,
+ '',
+ array( 'lim' => 1 )
+ );
$out->addHTML( "</div>\n" );
}
@@ -254,14 +274,23 @@ class MovePageForm extends UnlistedSpecialPage {
}
}
+ $handler = ContentHandler::getForTitle( $this->oldTitle );
+
$out->addHTML(
- Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) .
- Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) .
- "<tr>
+ Xml::openElement(
+ 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $this->getTitle()->getLocalURL( 'action=submit' ),
+ 'id' => 'movepage'
+ )
+ ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) .
+ "<tr>
<td class='mw-label'>" .
- $this->msg( 'movearticle' )->escaped() .
+ $this->msg( 'movearticle' )->escaped() .
"</td>
<td class='mw-input'>
<strong>{$oldTitleLink}</strong>
@@ -269,53 +298,70 @@ class MovePageForm extends UnlistedSpecialPage {
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'newtitle' )->text(), 'wpNewTitleMain' ) .
+ Xml::label( $this->msg( 'newtitle' )->text(), 'wpNewTitleMain' ) .
"</td>
<td class='mw-input'>" .
- Html::namespaceSelector(
- array(
- 'selected' => $newTitle->getNamespace(),
- 'exclude' => $immovableNamespaces
- ),
- array( 'name' => 'wpNewTitleNs', 'id' => 'wpNewTitleNs' )
- ) .
- Xml::input( 'wpNewTitleMain', 60, $wgContLang->recodeForEdit( $newTitle->getText() ), array(
+ Html::namespaceSelector(
+ array(
+ 'selected' => $newTitle->getNamespace(),
+ 'exclude' => $immovableNamespaces
+ ),
+ array( 'name' => 'wpNewTitleNs', 'id' => 'wpNewTitleNs' )
+ ) .
+ Xml::input(
+ 'wpNewTitleMain',
+ 60,
+ $wgContLang->recodeForEdit( $newTitle->getText() ),
+ array(
'type' => 'text',
'id' => 'wpNewTitleMain',
- 'maxlength' => 255,
- ) ) .
- Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
+ 'maxlength' => 255
+ )
+ ) .
+ Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'movereason' )->text(), 'wpReason' ) .
+ Xml::label( $this->msg( 'movereason' )->text(), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
- Html::element( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2,
- 'maxlength' => 200 ), $this->reason ) .
+ Xml::input( 'wpReason', 60, $this->reason, array(
+ 'type' => 'text',
+ 'id' => 'wpReason',
+ 'maxlength' => 200,
+ ) ) .
"</td>
</tr>"
);
- if( $considerTalk ) {
+ if ( $considerTalk ) {
$out->addHTML( "
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( $this->msg( 'movetalk' )->text(), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
+ Xml::checkLabel(
+ $this->msg( 'movetalk' )->text(),
+ 'wpMovetalk',
+ 'wpMovetalk',
+ $this->moveTalk
+ ) .
"</td>
</tr>"
);
}
- if ( $user->isAllowed( 'suppressredirect' ) ) {
+ if ( $user->isAllowed( 'suppressredirect' ) && $handler->supportsRedirects() ) {
$out->addHTML( "
<tr>
<td></td>
<td class='mw-input' >" .
- Xml::checkLabel( $this->msg( 'move-leave-redirect' )->text(), 'wpLeaveRedirect',
- 'wpLeaveRedirect', $this->leaveRedirect ) .
+ Xml::checkLabel(
+ $this->msg( 'move-leave-redirect' )->text(),
+ 'wpLeaveRedirect',
+ 'wpLeaveRedirect',
+ $this->leaveRedirect
+ ) .
"</td>
</tr>"
);
@@ -326,48 +372,57 @@ class MovePageForm extends UnlistedSpecialPage {
<tr>
<td></td>
<td class='mw-input' >" .
- Xml::checkLabel( $this->msg( 'fix-double-redirects' )->text(), 'wpFixRedirects',
- 'wpFixRedirects', $this->fixRedirects ) .
+ Xml::checkLabel(
+ $this->msg( 'fix-double-redirects' )->text(),
+ 'wpFixRedirects',
+ 'wpFixRedirects',
+ $this->fixRedirects
+ ) .
"</td>
</tr>"
);
}
- if( $canMoveSubpage ) {
+ if ( $canMoveSubpage ) {
$out->addHTML( "
<tr>
<td></td>
<td class=\"mw-input\">" .
- 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' )
- ) . '&#160;' .
- Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ),
- $this->msg(
- ( $this->oldTitle->hasSubpages()
- ? 'move-subpages'
- : 'move-talk-subpages' )
+ 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' )
+ ) . '&#160;' .
+ Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ),
+ $this->msg(
+ ( $this->oldTitle->hasSubpages()
+ ? 'move-subpages'
+ : 'move-talk-subpages' )
)->numParams( $wgMaximumMovedPages )->params( $wgMaximumMovedPages )->parse()
- ) .
+ ) .
"</td>
</tr>"
);
}
- $watchChecked = $user->isLoggedIn() && ($this->watch || $user->getBoolOption( 'watchmoves' )
+ $watchChecked = $user->isLoggedIn() && ( $this->watch || $user->getBoolOption( 'watchmoves' )
|| $user->isWatched( $this->oldTitle ) );
# Don't allow watching if user is not logged in
- if( $user->isLoggedIn() ) {
+ if ( $user->isLoggedIn() ) {
$out->addHTML( "
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( $this->msg( 'move-watch' )->text(), 'wpWatch', 'watch', $watchChecked ) .
+ Xml::checkLabel(
+ $this->msg( 'move-watch' )->text(),
+ 'wpWatch',
+ 'watch',
+ $watchChecked
+ ) .
"</td>
- </tr>");
+ </tr>" );
}
$out->addHTML( "
@@ -375,19 +430,18 @@ class MovePageForm extends UnlistedSpecialPage {
<tr>
<td>&#160;</td>
<td class='mw-submit'>" .
- Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
+ Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
"</td>
</tr>" .
- Xml::closeElement( 'table' ) .
- Html::hidden( 'wpEditToken', $user->getEditToken() ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' ) .
- "\n"
+ Xml::closeElement( 'table' ) .
+ Html::hidden( 'wpEditToken', $user->getEditToken() ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) .
+ "\n"
);
$this->showLogFragment( $this->oldTitle );
$this->showSubpages( $this->oldTitle );
-
}
function doSubmit() {
@@ -405,6 +459,7 @@ class MovePageForm extends UnlistedSpecialPage {
# don't allow moving to pages with # in
if ( !$nt || $nt->getFragment() != '' ) {
$this->showForm( array( array( 'badtitletext' ) ) );
+
return;
}
@@ -412,11 +467,11 @@ class MovePageForm extends UnlistedSpecialPage {
if ( $nt->getNamespace() == NS_FILE
&& !( $this->moveOverShared && $user->isAllowed( 'reupload-shared' ) )
&& !RepoGroup::singleton()->getLocalRepo()->findFile( $nt )
- && wfFindFile( $nt ) )
- {
+ && wfFindFile( $nt )
+ ) {
$this->showForm( array( array( 'file-exists-sharedrepo' ) ) );
- return;
+ return;
}
# Delete to make way if requested
@@ -425,6 +480,7 @@ class MovePageForm extends UnlistedSpecialPage {
if ( count( $permErrors ) ) {
# Only show the first error
$this->showForm( $permErrors );
+
return;
}
@@ -443,11 +499,16 @@ class MovePageForm extends UnlistedSpecialPage {
$deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
if ( !$deleteStatus->isGood() ) {
$this->showForm( $deleteStatus->getErrorsArray() );
+
return;
}
}
- if ( $user->isAllowed( 'suppressredirect' ) ) {
+ $handler = ContentHandler::getForTitle( $ot );
+
+ if ( !$handler->supportsRedirects() ) {
+ $createRedirect = false;
+ } elseif ( $user->isAllowed( 'suppressredirect' ) ) {
$createRedirect = $this->leaveRedirect;
} else {
$createRedirect = true;
@@ -457,6 +518,7 @@ class MovePageForm extends UnlistedSpecialPage {
$error = $ot->moveTo( $nt, true, $this->reason, $createRedirect );
if ( $error !== true ) {
$this->showForm( $error );
+
return;
}
@@ -464,8 +526,6 @@ class MovePageForm extends UnlistedSpecialPage {
DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
}
- wfRunHooks( 'SpecialMovepageAfterMove', array( &$this, &$ot, &$nt ) );
-
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'pagemovedsub' ) );
@@ -479,15 +539,27 @@ class MovePageForm extends UnlistedSpecialPage {
$oldText = $ot->getPrefixedText();
$newText = $nt->getPrefixedText();
- $msgName = $createRedirect ? 'movepage-moved-redirect' : 'movepage-moved-noredirect';
+ if ( $ot->exists() ) {
+ //NOTE: we assume that if the old title exists, it's because it was re-created as
+ // a redirect to the new title. This is not safe, but what we did before was
+ // even worse: we just determined whether a redirect should have been created,
+ // and reported that it was created if it should have, without any checks.
+ // Also note that isRedirect() is unreliable because of bug 37209.
+ $msgName = 'movepage-moved-redirect';
+ } else {
+ $msgName = 'movepage-moved-noredirect';
+ }
+
$out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink,
$newLink )->params( $oldText, $newText )->parseAsBlock() );
$out->addWikiMsg( $msgName );
+ wfRunHooks( 'SpecialMovepageAfterMove', array( &$this, &$ot, &$nt ) );
+
# Now we move extra pages we've been asked to move: subpages and talk
# pages. First, if the old page or the new page is a talk page, we
# can't move any talk pages: cancel that.
- if( $ot->isTalkPage() || $nt->isTalkPage() ) {
+ if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
$this->moveTalk = false;
}
@@ -509,24 +581,26 @@ class MovePageForm extends UnlistedSpecialPage {
// @todo FIXME: Use Title::moveSubpages() here
$dbr = wfGetDB( DB_MASTER );
- if( $this->moveSubpages && (
+ if ( $this->moveSubpages && (
MWNamespace::hasSubpages( $nt->getNamespace() ) || (
$this->moveTalk &&
- MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+ MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
)
) ) {
$conds = array(
'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() )
- .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
+ . ' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
);
$conds['page_namespace'] = array();
- if( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
- $conds['page_namespace'] []= $ot->getNamespace();
+ if ( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+ $conds['page_namespace'][] = $ot->getNamespace();
}
- if( $this->moveTalk && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) {
- $conds['page_namespace'] []= $ot->getTalkPage()->getNamespace();
+ if ( $this->moveTalk &&
+ MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+ ) {
+ $conds['page_namespace'][] = $ot->getTalkPage()->getNamespace();
}
- } elseif( $this->moveTalk ) {
+ } elseif ( $this->moveTalk ) {
$conds = array(
'page_namespace' => $ot->getTalkPage()->getNamespace(),
'page_title' => $ot->getDBkey()
@@ -537,7 +611,7 @@ class MovePageForm extends UnlistedSpecialPage {
}
$extraPages = array();
- if( !is_null( $conds ) ) {
+ if ( !is_null( $conds ) ) {
$extraPages = TitleArray::newFromResult(
$dbr->select( 'page',
array( 'page_id', 'page_namespace', 'page_title' ),
@@ -549,39 +623,42 @@ class MovePageForm extends UnlistedSpecialPage {
$extraOutput = array();
$count = 1;
- foreach( $extraPages as $oldSubpage ) {
- if( $ot->equals( $oldSubpage ) ) {
+ foreach ( $extraPages as $oldSubpage ) {
+ if ( $ot->equals( $oldSubpage ) || $nt->equals( $oldSubpage ) ) {
# Already did this one.
continue;
}
$newPageName = preg_replace(
- '#^'.preg_quote( $ot->getDBkey(), '#' ).'#',
+ '#^' . preg_quote( $ot->getDBkey(), '#' ) . '#',
StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
$oldSubpage->getDBkey()
);
- if( $oldSubpage->isTalkPage() ) {
+
+ if ( $oldSubpage->isTalkPage() ) {
$newNs = $nt->getTalkPage()->getNamespace();
} else {
$newNs = $nt->getSubjectPage()->getNamespace();
}
+
# Bug 14385: we need makeTitleSafe because the new page names may
# be longer than 255 characters.
$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
- if( !$newSubpage ) {
+ if ( !$newSubpage ) {
$oldLink = Linker::linkKnown( $oldSubpage );
- $extraOutput []= $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink
- )->params( Title::makeName( $newNs, $newPageName ) )->escaped();
+ $extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink )
+ ->params( Title::makeName( $newNs, $newPageName ) )->escaped();
continue;
}
# This was copy-pasted from Renameuser, bleh.
if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) {
$link = Linker::linkKnown( $newSubpage );
- $extraOutput []= $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
+ $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
} else {
$success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect );
- if( $success === true ) {
+
+ if ( $success === true ) {
if ( $this->fixRedirects ) {
DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
}
@@ -591,41 +668,30 @@ class MovePageForm extends UnlistedSpecialPage {
array(),
array( 'redirect' => 'no' )
);
+
$newLink = Linker::linkKnown( $newSubpage );
- $extraOutput []= $this->msg( 'movepage-page-moved' )->rawParams( $oldLink, $newLink )->escaped();
+ $extraOutput[] = $this->msg( 'movepage-page-moved' )->rawParams( $oldLink, $newLink )->escaped();
++$count;
- if( $count >= $wgMaximumMovedPages ) {
- $extraOutput []= $this->msg( 'movepage-max-pages' )->numParams( $wgMaximumMovedPages )->escaped();
+
+ if ( $count >= $wgMaximumMovedPages ) {
+ $extraOutput[] = $this->msg( 'movepage-max-pages' )->numParams( $wgMaximumMovedPages )->escaped();
break;
}
} else {
$oldLink = Linker::linkKnown( $oldSubpage );
$newLink = Linker::link( $newSubpage );
- $extraOutput []= $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink, $newLink )->escaped();
+ $extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink, $newLink )->escaped();
}
}
-
}
- if( $extraOutput !== array() ) {
+ if ( $extraOutput !== array() ) {
$out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
}
# Deal with watches (we don't watch subpages)
- if( $this->watch && $user->isLoggedIn() ) {
- $user->addWatch( $ot );
- $user->addWatch( $nt );
- } else {
- $user->removeWatch( $ot );
- $user->removeWatch( $nt );
- }
-
- # Re-clear the file redirect cache, which may have been polluted by
- # parsing in messages above. See CR r56745.
- # @todo FIXME: Needs a more robust solution inside FileRepo.
- if( $ot->getNamespace() == NS_FILE ) {
- RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $ot );
- }
+ WatchAction::doWatchOrUnwatch( $this->watch, $ot, $user );
+ WatchAction::doWatchOrUnwatch( $this->watch, $nt, $user );
}
function showLogFragment( $title ) {
@@ -636,8 +702,9 @@ class MovePageForm extends UnlistedSpecialPage {
}
function showSubpages( $title ) {
- if( !MWNamespace::hasSubpages( $title->getNamespace() ) )
+ if ( !MWNamespace::hasSubpages( $title->getNamespace() ) ) {
return;
+ }
$subpages = $title->getSubpages();
$count = $subpages instanceof TitleArray ? $subpages->count() : 0;
@@ -648,16 +715,21 @@ class MovePageForm extends UnlistedSpecialPage {
# No subpages.
if ( $count == 0 ) {
$out->addWikiMsg( 'movenosubpage' );
+
return;
}
$out->addWikiMsg( 'movesubpagetext', $this->getLanguage()->formatNum( $count ) );
$out->addHTML( "<ul>\n" );
- foreach( $subpages as $subpage ) {
+ foreach ( $subpages as $subpage ) {
$link = Linker::link( $subpage );
$out->addHTML( "<li>$link</li>\n" );
}
$out->addHTML( "</ul>\n" );
}
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php
index 350aac63..37d29734 100644
--- a/includes/specials/SpecialNewimages.php
+++ b/includes/specials/SpecialNewimages.php
@@ -21,12 +21,11 @@
* @ingroup SpecialPage
*/
class SpecialNewFiles extends IncludableSpecialPage {
-
- public function __construct(){
+ public function __construct() {
parent::__construct( 'Newimages' );
}
- public function execute( $par ){
+ public function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
@@ -37,19 +36,22 @@ class SpecialNewFiles extends IncludableSpecialPage {
$form->prepareForm();
$form->displayForm( '' );
}
+
$this->getOutput()->addHTML( $pager->getBody() );
if ( !$this->including() ) {
$this->getOutput()->addHTML( $pager->getNavigationBar() );
}
}
-}
+ protected function getGroupName() {
+ return 'changes';
+ }
+}
/**
* @ingroup SpecialPage Pager
*/
class NewFilesPager extends ReverseChronologicalPager {
-
/**
* @var ImageGallery
*/
@@ -57,7 +59,7 @@ class NewFilesPager extends ReverseChronologicalPager {
function __construct( IContextSource $context, $par = null ) {
$this->like = $context->getRequest()->getText( 'like' );
- $this->showbots = $context->getRequest()->getBool( 'showbots' , 0 );
+ $this->showbots = $context->getRequest()->getBool( 'showbots', 0 );
if ( is_numeric( $par ) ) {
$this->setLimit( $par );
}
@@ -70,9 +72,10 @@ class NewFilesPager extends ReverseChronologicalPager {
$conds = $jconds = array();
$tables = array( 'image' );
- if( !$this->showbots ) {
+ if ( !$this->showbots ) {
$groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
- if( count( $groupsWithBotPermission ) ) {
+
+ if ( count( $groupsWithBotPermission ) ) {
$tables[] = 'user_groups';
$conds[] = 'ug_group IS NULL';
$jconds['user_groups'] = array(
@@ -85,11 +88,15 @@ class NewFilesPager extends ReverseChronologicalPager {
}
}
- if( !$wgMiserMode && $this->like !== null ){
+ if ( !$wgMiserMode && $this->like !== null ) {
$dbr = wfGetDB( DB_SLAVE );
$likeObj = Title::newFromURL( $this->like );
- if( $likeObj instanceof Title ){
- $like = $dbr->buildLike( $dbr->anyString(), strtolower( $likeObj->getDBkey() ), $dbr->anyString() );
+ if ( $likeObj instanceof Title ) {
+ $like = $dbr->buildLike(
+ $dbr->anyString(),
+ strtolower( $likeObj->getDBkey() ),
+ $dbr->anyString()
+ );
$conds[] = "LOWER(img_name) $like";
}
}
@@ -104,18 +111,27 @@ class NewFilesPager extends ReverseChronologicalPager {
return $query;
}
- function getIndexField(){
+ function getIndexField() {
return 'img_timestamp';
}
- function getStartBody(){
+ function getStartBody() {
if ( !$this->gallery ) {
- $this->gallery = new ImageGallery();
+ // Note that null for mode is taken to mean use default.
+ $mode = $this->getRequest()->getVal( 'gallerymode', null );
+ try {
+ $this->gallery = ImageGalleryBase::factory( $mode );
+ } catch ( MWException $e ) {
+ // User specified something invalid, fallback to default.
+ $this->gallery = ImageGalleryBase::factory();
+ }
+ $this->gallery->setContext( $this->getContext() );
}
+
return '';
}
- function getEndBody(){
+ function getEndBody() {
return $this->gallery->toHTML();
}
@@ -125,11 +141,12 @@ class NewFilesPager extends ReverseChronologicalPager {
$title = Title::makeTitle( NS_FILE, $name );
$ul = Linker::link( $user->getUserpage(), $user->getName() );
+ $time = $this->getLanguage()->userTimeAndDate( $row->img_timestamp, $this->getUser() );
$this->gallery->add(
$title,
"$ul<br />\n<i>"
- . htmlspecialchars( $this->getLanguage()->userTimeAndDate( $row->img_timestamp, $this->getUser() ) )
+ . htmlspecialchars( $time )
. "</i><br />\n"
);
}
@@ -147,7 +164,6 @@ class NewFilesPager extends ReverseChronologicalPager {
'type' => 'check',
'label' => $this->msg( 'showhidebots', $this->msg( 'show' )->plain() )->escaped(),
'name' => 'showbots',
- # 'default' => $this->getRequest()->getBool( 'showbots', 0 ),
),
'limit' => array(
'type' => 'hidden',
@@ -161,12 +177,13 @@ class NewFilesPager extends ReverseChronologicalPager {
),
);
- if( $wgMiserMode ){
+ if ( $wgMiserMode ) {
unset( $fields['like'] );
}
- $form = new HTMLForm( $fields, $this->getContext() );
- $form->setTitle( $this->getTitle() );
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle() ); // Remove subpage
+ $form = new HTMLForm( $fields, $context );
$form->setSubmitTextMsg( 'ilsubmit' );
$form->setMethod( 'get' );
$form->setWrapperLegendMsg( 'newimages-legend' );
diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php
index 8e15d554..43d48558 100644
--- a/includes/specials/SpecialNewpages.php
+++ b/includes/specials/SpecialNewpages.php
@@ -27,7 +27,6 @@
* @ingroup SpecialPage
*/
class SpecialNewpages extends IncludableSpecialPage {
-
// Stored objects
/**
@@ -53,26 +52,29 @@ class SpecialNewpages extends IncludableSpecialPage {
$opts->add( 'hidepatrolled', $this->getUser()->getBoolOption( 'newpageshidepatrolled' ) );
$opts->add( 'hidebots', false );
$opts->add( 'hideredirs', true );
- $opts->add( 'limit', (int)$this->getUser()->getOption( 'rclimit' ) );
+ $opts->add( 'limit', $this->getUser()->getIntOption( 'rclimit' ) );
$opts->add( 'offset', '' );
$opts->add( 'namespace', '0' );
$opts->add( 'username', '' );
$opts->add( 'feed', '' );
$opts->add( 'tagfilter', '' );
+ $opts->add( 'invert', false );
$this->customFilters = array();
wfRunHooks( 'SpecialNewPagesFilters', array( $this, &$this->customFilters ) );
- foreach( $this->customFilters as $key => $params ) {
+ foreach ( $this->customFilters as $key => $params ) {
$opts->add( $key, $params['default'] );
}
// Set values
$opts->fetchValuesFromRequest( $this->getRequest() );
- if ( $par ) $this->parseParams( $par );
+ if ( $par ) {
+ $this->parseParams( $par );
+ }
// Validate
$opts->validateIntBounds( 'limit', 0, 5000 );
- if( !$wgEnableNewpagesUserFilter ) {
+ if ( !$wgEnableNewpagesUserFilter ) {
$opts->setValue( 'username', '' );
}
}
@@ -105,15 +107,15 @@ class SpecialNewpages extends IncludableSpecialPage {
}
// PG offsets not just digits!
if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
- $this->opts->setValue( 'offset', intval( $m[1] ) );
+ $this->opts->setValue( 'offset', intval( $m[1] ) );
}
if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
$this->opts->setValue( 'username', $m[1] );
}
if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
$ns = $this->getLanguage()->getNsIndex( $m[1] );
- if( $ns !== false ) {
- $this->opts->setValue( 'namespace', $ns );
+ if ( $ns !== false ) {
+ $this->opts->setValue( 'namespace', $ns );
}
}
}
@@ -134,25 +136,27 @@ class SpecialNewpages extends IncludableSpecialPage {
$this->showNavigation = !$this->including(); // Maybe changed in setup
$this->setup( $par );
- if( !$this->including() ) {
+ if ( !$this->including() ) {
// Settings
$this->form();
$feedType = $this->opts->getValue( 'feed' );
- if( $feedType ) {
- return $this->feed( $feedType );
+ if ( $feedType ) {
+ $this->feed( $feedType );
+
+ return;
}
$allValues = $this->opts->getAllValues();
unset( $allValues['feed'] );
- $out->setFeedAppendQuery( wfArrayToCGI( $allValues ) );
+ $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
}
$pager = new NewPagesPager( $this, $this->opts );
$pager->mLimit = $this->opts->getValue( 'limit' );
$pager->mOffset = $this->opts->getValue( 'offset' );
- if( $pager->getNumRows() ) {
+ if ( $pager->getNumRows() ) {
$navigation = '';
if ( $this->showNavigation ) {
$navigation = $pager->getNavigationBar();
@@ -164,8 +168,6 @@ class SpecialNewpages extends IncludableSpecialPage {
}
protected function filterLinks() {
- global $wgGroupPermissions;
-
// show/hide links
$showhide = array( $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() );
@@ -181,8 +183,7 @@ class SpecialNewpages extends IncludableSpecialPage {
}
// Disable some if needed
- # @todo FIXME: Throws E_NOTICEs if not set; and doesn't obey hooks etc.
- if ( $wgGroupPermissions['*']['createpage'] !== true ) {
+ if ( !User::groupHasPermission( '*', 'createpage' ) ) {
unset( $filters['hideliu'] );
}
if ( !$this->getUser()->useNPPatrol() ) {
@@ -197,7 +198,7 @@ class SpecialNewpages extends IncludableSpecialPage {
foreach ( $filters as $key => $msg ) {
$onoff = 1 - $this->opts->getValue( $key );
$link = Linker::link( $self, $showhide[$onoff], array(),
- array( $key => $onoff ) + $changed
+ array( $key => $onoff ) + $changed
);
$links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
}
@@ -213,6 +214,7 @@ class SpecialNewpages extends IncludableSpecialPage {
$namespace = $this->opts->consumeValue( 'namespace' );
$username = $this->opts->consumeValue( 'username' );
$tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
+ $nsinvert = $this->opts->consumeValue( 'invert' );
// Check username input validity
$ut = Title::makeTitleSafe( NS_USER, $username );
@@ -236,48 +238,55 @@ class SpecialNewpages extends IncludableSpecialPage {
Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) .
'<tr>
<td class="mw-label">' .
- Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) .
- '</td>
- <td class="mw-input">' .
- Html::namespaceSelector(
- array(
- 'selected' => $namespace,
- 'all' => 'all',
- ), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- )
- ) .
- '</td>
+ Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) .
+ '</td>
+ <td class="mw-input">' .
+ Html::namespaceSelector(
+ array(
+ 'selected' => $namespace,
+ 'all' => 'all',
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) . '&#160;' .
+ Xml::checkLabel(
+ $this->msg( 'invert' )->text(),
+ 'invert',
+ 'nsinvert',
+ $nsinvert,
+ array( 'title' => $this->msg( 'tooltip-invert' )->text() )
+ ) .
+ '</td>
</tr>' . ( $tagFilter ? (
'<tr>
<td class="mw-label">' .
- $tagFilterLabel .
+ $tagFilterLabel .
'</td>
<td class="mw-input">' .
- $tagFilterSelector .
+ $tagFilterSelector .
'</td>
</tr>' ) : '' ) .
( $wgEnableNewpagesUserFilter ?
- '<tr>
+ '<tr>
<td class="mw-label">' .
Xml::label( $this->msg( 'newpages-username' )->text(), 'mw-np-username' ) .
- '</td>
+ '</td>
<td class="mw-input">' .
Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) .
- '</td>
+ '</td>
</tr>' : '' ) .
'<tr> <td></td>
<td class="mw-submit">' .
- Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) .
- '</td>
- </tr>' .
+ Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) .
+ '</td>
+ </tr>' .
'<tr>
<td></td>
<td class="mw-input">' .
- $this->filterLinks() .
- '</td>
+ $this->filterLinks() .
+ '</td>
</tr>' .
Xml::closeElement( 'table' ) .
Xml::closeElement( 'fieldset' ) .
@@ -288,9 +297,10 @@ class SpecialNewpages extends IncludableSpecialPage {
}
/**
- * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment
+ * Format a row, providing the timestamp, links to the page/history,
+ * size, user links, and a comment
*
- * @param $result Result row
+ * @param object $result Result row
* @return String
*/
public function formatRow( $result ) {
@@ -298,11 +308,11 @@ class SpecialNewpages extends IncludableSpecialPage {
# Revision deletion works on revisions, so we should cast one
$row = array(
- 'comment' => $result->rc_comment,
- 'deleted' => $result->rc_deleted,
- 'user_text' => $result->rc_user_text,
- 'user' => $result->rc_user,
- );
+ 'comment' => $result->rc_comment,
+ 'deleted' => $result->rc_deleted,
+ 'user_text' => $result->rc_user_text,
+ 'user' => $result->rc_user,
+ );
$rev = new Revision( $row );
$rev->setTitle( $title );
@@ -324,16 +334,14 @@ class SpecialNewpages extends IncludableSpecialPage {
$query = array( 'redirect' => 'no' );
- if( $this->patrollable( $result ) ) {
- $query['rcid'] = $result->rc_id;
- }
-
- $plink = Linker::linkKnown(
+ // Linker::linkKnown() uses 'known' and 'noclasses' options.
+ // This breaks the colouration for stubs.
+ $plink = Linker::link(
$title,
null,
array( 'class' => 'mw-newpages-pagename' ),
$query,
- array( 'known' ) // Set explicitly to avoid the default of 'known','noclasses'. This breaks the colouration for stubs
+ array( 'known' )
);
$histLink = Linker::linkKnown(
$title,
@@ -344,8 +352,12 @@ class SpecialNewpages extends IncludableSpecialPage {
$hist = Html::rawElement( 'span', array( 'class' => 'mw-newpages-history' ),
$this->msg( 'parentheses' )->rawParams( $histLink )->escaped() );
- $length = Html::element( 'span', array( 'class' => 'mw-newpages-length' ),
- $this->msg( 'brackets' )->params( $this->msg( 'nbytes' )->numParams( $result->length )->text() )
+ $length = Html::element(
+ 'span',
+ array( 'class' => 'mw-newpages-length' ),
+ $this->msg( 'brackets' )->params( $this->msg( 'nbytes' )
+ ->numParams( $result->length )->text()
+ )
);
$ulink = Linker::revUserTools( $rev );
@@ -361,8 +373,11 @@ class SpecialNewpages extends IncludableSpecialPage {
}
# Tags, if any.
- if( isset( $result->ts_tags ) ) {
- list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $result->ts_tags, 'newpages' );
+ if ( isset( $result->ts_tags ) ) {
+ list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
+ $result->ts_tags,
+ 'newpages'
+ );
$classes = array_merge( $classes, $newClasses );
} else {
$tagDisplay = '';
@@ -373,8 +388,10 @@ class SpecialNewpages extends IncludableSpecialPage {
# Display the old title if the namespace/title has been changed
$oldTitleText = '';
$oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
+
if ( !$title->equals( $oldTitle ) ) {
- $oldTitleText = $this->msg( 'rc-old-title' )->params( $oldTitle->getPrefixedText() )->escaped();
+ $oldTitleText = $oldTitle->getPrefixedText();
+ $oldTitleText = $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped();
}
return "<li{$css}>{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}</li>\n";
@@ -383,7 +400,7 @@ class SpecialNewpages extends IncludableSpecialPage {
/**
* Should a specific result row provide "patrollable" links?
*
- * @param $result Result row
+ * @param object $result Result row
* @return Boolean
*/
protected function patrollable( $result ) {
@@ -400,18 +417,20 @@ class SpecialNewpages extends IncludableSpecialPage {
if ( !$wgFeed ) {
$this->getOutput()->addWikiMsg( 'feed-unavailable' );
+
return;
}
- if( !isset( $wgFeedClasses[$type] ) ) {
+ if ( !isset( $wgFeedClasses[$type] ) ) {
$this->getOutput()->addWikiMsg( 'feed-invalid' );
+
return;
}
$feed = new $wgFeedClasses[$type](
$this->feedTitle(),
$this->msg( 'tagline' )->text(),
- $this->getTitle()->getFullUrl()
+ $this->getTitle()->getFullURL()
);
$pager = new NewPagesPager( $this, $this->opts );
@@ -419,7 +438,7 @@ class SpecialNewpages extends IncludableSpecialPage {
$pager->mLimit = min( $limit, $wgFeedLimit );
$feed->outHeader();
- if( $pager->getNumRows() > 0 ) {
+ if ( $pager->getNumRows() > 0 ) {
foreach ( $pager->mResult as $row ) {
$feed->outItem( $this->feedItem( $row ) );
}
@@ -430,12 +449,13 @@ class SpecialNewpages extends IncludableSpecialPage {
protected function feedTitle() {
global $wgLanguageCode, $wgSitename;
$desc = $this->getDescription();
+
return "$wgSitename - $desc [$wgLanguageCode]";
}
protected function feedItem( $row ) {
$title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
- if( $title ) {
+ if ( $title ) {
$date = $row->rc_timestamp;
$comments = $title->getTalkPage()->getFullURL();
@@ -458,15 +478,21 @@ class SpecialNewpages extends IncludableSpecialPage {
protected function feedItemDesc( $row ) {
$revision = Revision::newFromId( $row->rev_id );
- if( $revision ) {
+ if ( $revision ) {
+ //XXX: include content model/type in feed item?
return '<p>' . htmlspecialchars( $revision->getUserText() ) .
$this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
"</p>\n<hr />\n<div>" .
- nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+ nl2br( htmlspecialchars( $revision->getContent()->serialize() ) ) . "</div>";
}
+
return '';
}
+
+ protected function getGroupName() {
+ return 'changes';
+ }
}
/**
@@ -488,7 +514,7 @@ class NewPagesPager extends ReverseChronologicalPager {
}
function getQueryInfo() {
- global $wgEnableNewpagesUserFilter, $wgGroupPermissions;
+ global $wgEnableNewpagesUserFilter;
$conds = array();
$conds['rc_new'] = 1;
@@ -498,26 +524,34 @@ class NewPagesPager extends ReverseChronologicalPager {
$username = $this->opts->getValue( 'username' );
$user = Title::makeTitleSafe( NS_USER, $username );
- if( $namespace !== false ) {
- $conds['rc_namespace'] = $namespace;
+ if ( $namespace !== false ) {
+ if ( $this->opts->getValue( 'invert' ) ) {
+ $conds[] = 'rc_namespace != ' . $this->mDb->addQuotes( $namespace );
+ } else {
+ $conds['rc_namespace'] = $namespace;
+ }
$rcIndexes = array( 'new_name_timestamp' );
} else {
$rcIndexes = array( 'rc_timestamp' );
}
# $wgEnableNewpagesUserFilter - temp WMF hack
- if( $wgEnableNewpagesUserFilter && $user ) {
+ if ( $wgEnableNewpagesUserFilter && $user ) {
$conds['rc_user_text'] = $user->getText();
$rcIndexes = 'rc_user_text';
- # If anons cannot make new pages, don't "exclude logged in users"!
- } elseif( $wgGroupPermissions['*']['createpage'] && $this->opts->getValue( 'hideliu' ) ) {
+ } elseif ( User::groupHasPermission( '*', 'createpage' ) &&
+ $this->opts->getValue( 'hideliu' )
+ ) {
+ # If anons cannot make new pages, don't "exclude logged in users"!
$conds['rc_user'] = 0;
}
+
# If this user cannot see patrolled edits or they are off, don't do dumb queries!
- if( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
+ if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
$conds['rc_patrolled'] = 0;
}
- if( $this->opts->getValue( 'hidebots' ) ) {
+
+ if ( $this->opts->getValue( 'hidebots' ) ) {
$conds['rc_bot'] = 0;
}
@@ -529,7 +563,7 @@ class NewPagesPager extends ReverseChronologicalPager {
$tables = array( 'recentchanges', 'page' );
$fields = array(
'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text',
- 'rc_comment', 'rc_timestamp', 'rc_patrolled','rc_id', 'rc_deleted',
+ 'rc_comment', 'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted',
'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid',
'page_namespace', 'page_title'
);
@@ -539,10 +573,10 @@ class NewPagesPager extends ReverseChronologicalPager {
array( &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ) );
$info = array(
- 'tables' => $tables,
- 'fields' => $fields,
- 'conds' => $conds,
- 'options' => array( 'USE INDEX' => array( 'recentchanges' => $rcIndexes ) ),
+ 'tables' => $tables,
+ 'fields' => $fields,
+ 'conds' => $conds,
+ 'options' => array( 'USE INDEX' => array( 'recentchanges' => $rcIndexes ) ),
'join_conds' => $join_conds
);
@@ -576,6 +610,7 @@ class NewPagesPager extends ReverseChronologicalPager {
$linkBatch->add( $row->rc_namespace, $row->rc_title );
}
$linkBatch->execute();
+
return '<ul>';
}
diff --git a/includes/specials/SpecialPagesWithProp.php b/includes/specials/SpecialPagesWithProp.php
new file mode 100644
index 00000000..e22b42a3
--- /dev/null
+++ b/includes/specials/SpecialPagesWithProp.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Implements Special:PagesWithProp
+ *
+ * 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
+ *
+ * @since 1.21
+ * @file
+ * @ingroup SpecialPage
+ * @author Brad Jorsch
+ */
+
+/**
+ * Special:PagesWithProp to search the page_props table
+ * @ingroup SpecialPage
+ * @since 1.21
+ */
+class SpecialPagesWithProp extends QueryPage {
+ private $propName = null;
+
+ function __construct( $name = 'PagesWithProp' ) {
+ parent::__construct( $name );
+ }
+
+ function isCacheable() {
+ return false;
+ }
+
+ function execute( $par ) {
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->getOutput()->addModuleStyles( 'mediawiki.special.pagesWithProp' );
+
+ $request = $this->getRequest();
+ $propname = $request->getVal( 'propname', $par );
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select(
+ 'page_props',
+ 'pp_propname',
+ '',
+ __METHOD__,
+ array( 'DISTINCT', 'ORDER BY' => 'pp_propname' )
+ );
+ $propnames = array();
+ foreach ( $res as $row ) {
+ $propnames[$row->pp_propname] = $row->pp_propname;
+ }
+
+ $form = new HTMLForm( array(
+ 'propname' => array(
+ 'type' => 'selectorother',
+ 'name' => 'propname',
+ 'options' => $propnames,
+ 'default' => $propname,
+ 'label-message' => 'pageswithprop-prop',
+ 'required' => true,
+ ),
+ ), $this->getContext() );
+ $form->setMethod( 'get' );
+ $form->setSubmitCallback( array( $this, 'onSubmit' ) );
+ $form->setWrapperLegendMsg( 'pageswithprop-legend' );
+ $form->addHeaderText( $this->msg( 'pageswithprop-text' )->parseAsBlock() );
+ $form->setSubmitTextMsg( 'pageswithprop-submit' );
+
+ $form->prepareForm();
+ $form->displayForm( false );
+ if ( $propname !== '' && $propname !== null ) {
+ $form->trySubmit();
+ }
+ }
+
+ public function onSubmit( $data, $form ) {
+ $this->propName = $data['propname'];
+ parent::execute( $data['propname'] );
+ }
+
+ /**
+ * Disable RSS/Atom feeds
+ * @return bool
+ */
+ function isSyndicated() {
+ return false;
+ }
+
+ function getQueryInfo() {
+ return array(
+ 'tables' => array( 'page_props', 'page' ),
+ 'fields' => array(
+ 'page_id' => 'pp_page',
+ 'page_namespace',
+ 'page_title',
+ 'page_len',
+ 'page_is_redirect',
+ 'page_latest',
+ 'pp_value',
+ ),
+ 'conds' => array(
+ 'page_id = pp_page',
+ 'pp_propname' => $this->propName,
+ ),
+ 'options' => array()
+ );
+ }
+
+ function getOrderFields() {
+ return array( 'page_id' );
+ }
+
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
+ function formatResult( $skin, $result ) {
+ $title = Title::newFromRow( $result );
+ $ret = Linker::link( $title, null, array(), array(), array( 'known' ) );
+ if ( $result->pp_value !== '' ) {
+ // Do not show very long or binary values on the special page
+ $valueLength = strlen( $result->pp_value );
+ $isBinary = strpos( $result->pp_value, "\0" ) !== false;
+ $isTooLong = $valueLength > 1024;
+
+ if ( $isBinary || $isTooLong ) {
+ $message = $this
+ ->msg( $isBinary ? 'pageswithprop-prophidden-binary' : 'pageswithprop-prophidden-long' )
+ ->params( $this->getLanguage()->formatSize( $valueLength ) );
+
+ $propValue = Html::element( 'span', array( 'class' => 'prop-value-hidden' ), $message->text() );
+ } else {
+ $propValue = Html::element( 'span', array( 'class' => 'prop-value' ), $result->pp_value );
+ }
+
+ $ret .= $this->msg( 'colon-separator' )->escaped() . $propValue;
+ }
+
+ return $ret;
+ }
+
+ protected function getGroupName() {
+ return 'pages';
+ }
+}
diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php
index efb57657..c486ba01 100644
--- a/includes/specials/SpecialPasswordReset.php
+++ b/includes/specials/SpecialPasswordReset.php
@@ -27,19 +27,23 @@
* @ingroup SpecialPage
*/
class SpecialPasswordReset extends FormSpecialPage {
-
/**
* @var Message
*/
private $email;
/**
+ * @var User
+ */
+ private $firstUser;
+
+ /**
* @var Status
*/
private $result;
public function __construct() {
- parent::__construct( 'PasswordReset' );
+ parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
}
public function userCanExecute( User $user ) {
@@ -65,7 +69,8 @@ class SpecialPasswordReset extends FormSpecialPage {
'type' => 'text',
'label-message' => 'passwordreset-username',
);
- if( $this->getUser()->isLoggedIn() ) {
+
+ if ( $this->getUser()->isLoggedIn() ) {
$a['Username']['default'] = $this->getUser()->getName();
}
}
@@ -86,7 +91,7 @@ class SpecialPasswordReset extends FormSpecialPage {
);
}
- if( $this->getUser()->isAllowed( 'passwordreset' ) ){
+ if ( $this->getUser()->isAllowed( 'passwordreset' ) ) {
$a['Capture'] = array(
'type' => 'check',
'label-message' => 'passwordreset-capture',
@@ -98,11 +103,15 @@ class SpecialPasswordReset extends FormSpecialPage {
}
public function alterForm( HTMLForm $form ) {
- $form->setSubmitTextMsg( 'mailmypassword' );
- }
-
- protected function preText() {
global $wgPasswordResetRoutes;
+
+ $form->setDisplayFormat( 'vform' );
+ // Turn the old-school line around the form off.
+ // XXX This wouldn't be necessary here if we could set the format of
+ // the HTMLForm to 'vform' at its creation, but there's no way to do so
+ // from a FormSpecialPage class.
+ $form->setWrapperLegend( false );
+
$i = 0;
if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) {
$i++;
@@ -113,7 +122,11 @@ class SpecialPasswordReset extends FormSpecialPage {
if ( isset( $wgPasswordResetRoutes['domain'] ) && $wgPasswordResetRoutes['domain'] ) {
$i++;
}
- return $this->msg( 'passwordreset-pretext', $i )->parseAsBlock();
+
+ $message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one';
+
+ $form->setHeaderText( $this->msg( $message, $i )->parseAsBlock() );
+ $form->setSubmitTextMsg( 'mailmypassword' );
}
/**
@@ -121,6 +134,8 @@ class SpecialPasswordReset extends FormSpecialPage {
* userCanExecute(), and if the data array contains 'Username', etc, then Username
* resets are allowed.
* @param $data array
+ * @throws MWException
+ * @throws ThrottledError|PermissionsError
* @return Bool|Array
*/
public function onSubmit( array $data ) {
@@ -134,8 +149,9 @@ class SpecialPasswordReset extends FormSpecialPage {
}
}
- if( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ){
- // The user knows they don't have the passwordreset permission, but they tried to spoof the form. That's naughty
+ if ( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ) {
+ // The user knows they don't have the passwordreset permission,
+ // but they tried to spoof the form. That's naughty
throw new PermissionsError( 'passwordreset' );
}
@@ -149,8 +165,8 @@ class SpecialPasswordReset extends FormSpecialPage {
$users = array( User::newFromName( $data['Username'] ) );
} elseif ( isset( $data['Email'] )
&& $data['Email'] !== ''
- && Sanitizer::validateEmail( $data['Email'] ) )
- {
+ && Sanitizer::validateEmail( $data['Email'] )
+ ) {
$method = 'email';
$res = wfGetDB( DB_SLAVE )->select(
'user',
@@ -158,9 +174,11 @@ class SpecialPasswordReset extends FormSpecialPage {
array( 'user_email' => $data['Email'] ),
__METHOD__
);
+
if ( $res ) {
$users = array();
- foreach( $res as $row ){
+
+ foreach ( $res as $row ) {
$users[] = User::newFromRow( $row );
}
} else {
@@ -178,8 +196,8 @@ class SpecialPasswordReset extends FormSpecialPage {
return array( $error );
}
- if( count( $users ) == 0 ){
- if( $method == 'email' ){
+ if ( count( $users ) == 0 ) {
+ if ( $method == 'email' ) {
// Don't reveal whether or not an email address is in use
return true;
} else {
@@ -202,9 +220,13 @@ class SpecialPasswordReset extends FormSpecialPage {
foreach ( $users as $user ) {
if ( $user->isPasswordReminderThrottled() ) {
global $wgPasswordReminderResendTime;
+
# Round the time in hours to 3 d.p., in case someone is specifying
# minutes or seconds.
- return array( array( 'throttled-mailpassword', round( $wgPasswordReminderResendTime, 3 ) ) );
+ return array( array(
+ 'throttled-mailpassword',
+ round( $wgPasswordReminderResendTime, 3 )
+ ) );
}
}
@@ -237,8 +259,8 @@ class SpecialPasswordReset extends FormSpecialPage {
$password = $user->randomPassword();
$user->setNewpassword( $password );
$user->saveSettings();
- $passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password
- )->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later
+ $passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password )
+ ->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later
}
$passwordBlock = implode( "\n\n", $passwords );
@@ -247,39 +269,44 @@ class SpecialPasswordReset extends FormSpecialPage {
$username,
$passwordBlock,
count( $passwords ),
- Title::newMainPage()->getCanonicalUrl(),
+ '<' . Title::newMainPage()->getCanonicalURL() . '>',
round( $wgNewPasswordExpiry / 86400 )
);
$title = $this->msg( 'passwordreset-emailtitle' );
- $this->result = $firstUser->sendMail( $title->escaped(), $this->email->escaped() );
+ $this->result = $firstUser->sendMail( $title->escaped(), $this->email->text() );
- // Blank the email if the user is not supposed to see it
- if( !isset( $data['Capture'] ) || !$data['Capture'] ) {
+ if ( isset( $data['Capture'] ) && $data['Capture'] ) {
+ // Save the user, will be used if an error occurs when sending the email
+ $this->firstUser = $firstUser;
+ } else {
+ // Blank the email if the user is not supposed to see it
$this->email = null;
}
if ( $this->result->isGood() ) {
return true;
- } elseif( isset( $data['Capture'] ) && $data['Capture'] ){
+ } elseif ( isset( $data['Capture'] ) && $data['Capture'] ) {
// The email didn't send, but maybe they knew that and that's why they captured it
return true;
} else {
- // @todo FIXME: The email didn't send, but we have already set the password throttle
- // timestamp, so they won't be able to try again until it expires... :(
+ // @todo FIXME: The email wasn't sent, but we have already set
+ // the password throttle timestamp, so they won't be able to try
+ // again until it expires... :(
return array( array( 'mailerror', $this->result->getMessage() ) );
}
}
public function onSuccess() {
- if( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ){
- // @todo: Logging
+ if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) {
+ // @todo Logging
- if( $this->result->isGood() ){
+ if ( $this->result->isGood() ) {
$this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' );
} else {
- $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture', $this->result->getMessage() );
+ $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture',
+ $this->result->getMessage(), $this->firstUser->getName() );
}
$this->getOutput()->addHTML( Html::rawElement( 'pre', array(), $this->email->escaped() ) );
@@ -290,12 +317,12 @@ class SpecialPasswordReset extends FormSpecialPage {
}
protected function canChangePassword( User $user ) {
- global $wgPasswordResetRoutes, $wgAuth;
+ global $wgPasswordResetRoutes, $wgEnableEmail, $wgAuth;
// Maybe password resets are disabled, or there are no allowable routes
if ( !is_array( $wgPasswordResetRoutes ) ||
- !in_array( true, array_values( $wgPasswordResetRoutes ) ) )
- {
+ !in_array( true, array_values( $wgPasswordResetRoutes ) )
+ ) {
return 'passwordreset-disabled';
}
@@ -304,6 +331,11 @@ class SpecialPasswordReset extends FormSpecialPage {
return 'resetpass_forbidden';
}
+ // Maybe email features have been disabled
+ if ( !$wgEnableEmail ) {
+ return 'passwordreset-emaildisabled';
+ }
+
// Maybe the user is blocked (check this here rather than relying on the parent
// method as we have a more specific error message to use here
if ( $user->isBlocked() ) {
@@ -324,4 +356,8 @@ class SpecialPasswordReset extends FormSpecialPage {
return false;
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php
index 448d1799..2a80f651 100644
--- a/includes/specials/SpecialPopularpages.php
+++ b/includes/specials/SpecialPopularpages.php
@@ -27,7 +27,6 @@
* @ingroup SpecialPage
*/
class PopularPagesPage extends QueryPage {
-
function __construct( $name = 'Popularpages' ) {
parent::__construct( $name );
}
@@ -37,30 +36,42 @@ class PopularPagesPage extends QueryPage {
return true;
}
- function isSyndicated() { return false; }
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
+ return array(
'tables' => array( 'page' ),
- 'fields' => array( 'namespace' => 'page_namespace',
- 'title' => 'page_title',
- 'value' => 'page_counter'),
- 'conds' => array( 'page_is_redirect' => 0,
- 'page_namespace' => MWNamespace::getContentNamespaces() ) );
+ 'fields' => array(
+ 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_counter' ),
+ 'conds' => array(
+ 'page_is_redirect' => 0,
+ 'page_namespace' => MWNamespace::getContentNamespaces()
+ )
+ );
}
/**
- * @param $skin Skin
- * @param $result
+ * @param Skin $skin
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
global $wgContLang;
$title = Title::makeTitleSafe( $result->namespace, $result->title );
- if( !$title ) {
- return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
+ if ( !$title ) {
+ return Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $result->namespace,
+ $result->title )
+ );
}
$link = Linker::linkKnown(
@@ -68,6 +79,11 @@ class PopularPagesPage extends QueryPage {
htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) )
);
$nv = $this->msg( 'nviews' )->numParams( $result->value )->escaped();
+
return $this->getLanguage()->specialList( $link, $nv );
}
+
+ protected function getGroupName() {
+ return 'wiki';
+ }
}
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
index c6b2bb6b..ecee0bb7 100644
--- a/includes/specials/SpecialPreferences.php
+++ b/includes/specials/SpecialPreferences.php
@@ -35,16 +35,21 @@ class SpecialPreferences extends SpecialPage {
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
- $out->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
+ $out->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
$user = $this->getUser();
if ( $user->isAnon() ) {
- throw new ErrorPageError( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) );
+ throw new ErrorPageError(
+ 'prefsnologin',
+ 'prefsnologintext',
+ array( $this->getTitle()->getPrefixedDBkey() )
+ );
}
$this->checkReadOnly();
if ( $par == 'reset' ) {
$this->showResetForm();
+
return;
}
@@ -52,7 +57,7 @@ class SpecialPreferences extends SpecialPage {
if ( $this->getRequest()->getCheck( 'success' ) ) {
$out->wrapWikiMsg(
- "<div class=\"successbox\"><strong>\n$1\n</strong></div><div id=\"mw-pref-clear\"></div>",
+ "<div class=\"successbox\">\n$1\n</div>",
'savedprefs'
);
}
@@ -64,12 +69,17 @@ class SpecialPreferences extends SpecialPage {
}
private function showResetForm() {
+ if ( !$this->getUser()->isAllowed( 'editmyoptions' ) ) {
+ throw new PermissionsError( 'editmyoptions' );
+ }
+
$this->getOutput()->addWikiMsg( 'prefs-reset-intro' );
- $htmlForm = new HTMLForm( array(), $this->getContext(), 'prefs-restore' );
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle( 'reset' ) ); // Reset subpage
+ $htmlForm = new HTMLForm( array(), $context, 'prefs-restore' );
$htmlForm->setSubmitTextMsg( 'restoreprefs' );
- $htmlForm->setTitle( $this->getTitle( 'reset' ) );
$htmlForm->setSubmitCallback( array( $this, 'submitReset' ) );
$htmlForm->suppressReset();
@@ -77,8 +87,12 @@ class SpecialPreferences extends SpecialPage {
}
public function submitReset( $formData ) {
+ if ( !$this->getUser()->isAllowed( 'editmyoptions' ) ) {
+ throw new PermissionsError( 'editmyoptions' );
+ }
+
$user = $this->getUser();
- $user->resetOptions();
+ $user->resetOptions( 'all', $this->getContext() );
$user->saveSettings();
$url = $this->getTitle()->getFullURL( 'success' );
@@ -87,4 +101,8 @@ class SpecialPreferences extends SpecialPage {
return true;
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php
index 7740b320..28d07ffc 100644
--- a/includes/specials/SpecialPrefixindex.php
+++ b/includes/specials/SpecialPrefixindex.php
@@ -27,15 +27,24 @@
* @ingroup SpecialPage
*/
class SpecialPrefixindex extends SpecialAllpages {
+
+ /**
+ * Whether to remove the searched prefix from the displayed link. Useful
+ * for inclusion of a set of sub pages in a root page.
+ */
+ protected $stripPrefix = false;
+
+ protected $hideRedirects = false;
+
// Inherit $maxPerPage
- function __construct(){
+ function __construct() {
parent::__construct( 'Prefixindex' );
}
/**
* Entry point : initialise variables and call subfunctions.
- * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default null)
+ * @param string $par becomes "FOO" when called like Special:Prefixindex/FOO (default null)
*/
function execute( $par ) {
global $wgContLang;
@@ -52,7 +61,8 @@ class SpecialPrefixindex extends SpecialAllpages {
$prefix = $request->getVal( 'prefix', '' );
$ns = $request->getIntOrNull( 'namespace' );
$namespace = (int)$ns; // if no namespace given, use 0 (NS_MAIN).
- $hideredirects = $request->getBool( 'hideredirects', false );
+ $this->hideRedirects = $request->getBool( 'hideredirects', $this->hideRedirects );
+ $this->stripPrefix = $request->getBool( 'stripprefix', $this->stripPrefix );
$namespaces = $wgContLang->getNamespaces();
$out->setPageTitle(
@@ -62,11 +72,11 @@ class SpecialPrefixindex extends SpecialAllpages {
);
$showme = '';
- if( isset( $par ) ) {
+ if ( isset( $par ) ) {
$showme = $par;
- } elseif( $prefix != '' ) {
+ } elseif ( $prefix != '' ) {
$showme = $prefix;
- } elseif( $from != '' && $ns === null ) {
+ } elseif ( $from != '' && $ns === null ) {
// For back-compat with Special:Allpages
// Don't do this if namespace is passed, so paging works when doing NS views.
$showme = $from;
@@ -74,23 +84,22 @@ class SpecialPrefixindex extends SpecialAllpages {
// Bug 27864: if transcluded, show all pages instead of the form.
if ( $this->including() || $showme != '' || $ns !== null ) {
- $this->showPrefixChunk( $namespace, $showme, $from, $hideredirects );
+ $this->showPrefixChunk( $namespace, $showme, $from );
} else {
- $out->addHTML( $this->namespacePrefixForm( $namespace, null, $hideredirects ) );
+ $out->addHTML( $this->namespacePrefixForm( $namespace, null ) );
}
}
/**
* HTML for the top form
* @param $namespace Integer: a namespace constant (default NS_MAIN).
- * @param $from String: dbKey we are starting listing at.
- * @param $hideredirects Bool: hide redirects (default FALSE)
+ * @param string $from dbKey we are starting listing at.
* @return string
*/
- function namespacePrefixForm( $namespace = NS_MAIN, $from = '', $hideredirects = false ) {
+ protected function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) {
global $wgScript;
- $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
+ $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
$out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
$out .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() );
$out .= Xml::openElement( 'fieldset' );
@@ -98,55 +107,61 @@ class SpecialPrefixindex extends SpecialAllpages {
$out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
$out .= "<tr>
<td class='mw-label'>" .
- Xml::label( $this->msg( 'allpagesprefix' )->text(), 'nsfrom' ) .
- "</td>
+ Xml::label( $this->msg( 'allpagesprefix' )->text(), 'nsfrom' ) .
+ "</td>
<td class='mw-input'>" .
- Xml::input( 'prefix', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
- "</td>
+ Xml::input( 'prefix', 30, str_replace( '_', ' ', $from ), array( 'id' => 'nsfrom' ) ) .
+ "</td>
</tr>
<tr>
- <td class='mw-label'>" .
- Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) .
- "</td>
+ <td class='mw-label'>" .
+ Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) .
+ "</td>
<td class='mw-input'>" .
- Html::namespaceSelector( array(
- 'selected' => $namespace,
- ), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- ) ) .
- Xml::checkLabel(
- $this->msg( 'allpages-hide-redirects' )->text(),
- 'hideredirects',
- 'hideredirects',
- $hideredirects
- ) . ' ' .
- Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) .
- "</td>
+ Html::namespaceSelector( array(
+ 'selected' => $namespace,
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ ) ) .
+ Xml::checkLabel(
+ $this->msg( 'allpages-hide-redirects' )->text(),
+ 'hideredirects',
+ 'hideredirects',
+ $this->hideRedirects
+ ) . ' ' .
+ Xml::checkLabel(
+ $this->msg( 'prefixindex-strip' )->text(),
+ 'stripprefix',
+ 'stripprefix',
+ $this->stripPrefix
+ ) . ' ' .
+ Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) .
+ "</td>
</tr>";
$out .= Xml::closeElement( 'table' );
$out .= Xml::closeElement( 'fieldset' );
$out .= Xml::closeElement( 'form' );
$out .= Xml::closeElement( 'div' );
+
return $out;
}
/**
* @param $namespace Integer, default NS_MAIN
* @param $prefix String
- * @param $from String: list all pages from this name (default FALSE)
- * @param $hideredirects Bool: hide redirects (default FALSE)
+ * @param string $from list all pages from this name (default FALSE)
*/
- function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null, $hideredirects = false ) {
+ protected function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null ) {
global $wgContLang;
if ( $from === null ) {
$from = $prefix;
}
- $fromList = $this->getNamespaceKeyAndText($namespace, $from);
- $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
+ $fromList = $this->getNamespaceKeyAndText( $namespace, $from );
+ $prefixList = $this->getNamespaceKeyAndText( $namespace, $prefix );
$namespaces = $wgContLang->getNamespaces();
if ( !$prefixList || !$fromList ) {
@@ -169,7 +184,7 @@ class SpecialPrefixindex extends SpecialAllpages {
'page_title >= ' . $dbr->addQuotes( $fromKey ),
);
- if ( $hideredirects ) {
+ if ( $this->hideRedirects ) {
$conds['page_is_redirect'] = 0;
}
@@ -178,8 +193,8 @@ class SpecialPrefixindex extends SpecialAllpages {
$conds,
__METHOD__,
array(
- 'ORDER BY' => 'page_title',
- 'LIMIT' => $this->maxPerPage + 1,
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
'USE INDEX' => 'name_title',
)
);
@@ -187,34 +202,42 @@ class SpecialPrefixindex extends SpecialAllpages {
### @todo FIXME: Side link to previous
$n = 0;
- if( $res->numRows() > 0 ) {
+ if ( $res->numRows() > 0 ) {
$out = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-list-table' ) );
- while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
+ $prefixLength = strlen( $prefix );
+ while ( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$t = Title::makeTitle( $s->page_namespace, $s->page_title );
- if( $t ) {
- $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
+ if ( $t ) {
+ $displayed = $t->getText();
+ // Try not to generate unclickable links
+ if ( $this->stripPrefix && $prefixLength !== strlen( $displayed ) ) {
+ $displayed = substr( $displayed, $prefixLength );
+ }
+ $link = ( $s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
Linker::linkKnown(
$t,
- htmlspecialchars( $t->getText() ),
+ htmlspecialchars( $displayed ),
$s->page_is_redirect ? array( 'class' => 'mw-redirect' ) : array()
) .
- ($s->page_is_redirect ? '</div>' : '' );
+ ( $s->page_is_redirect ? '</div>' : '' );
} else {
$link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
}
- if( $n % 3 == 0 ) {
+ if ( $n % 3 == 0 ) {
$out .= '<tr>';
}
$out .= "<td>$link</td>";
$n++;
- if( $n % 3 == 0 ) {
+ if ( $n % 3 == 0 ) {
$out .= '</tr>';
}
}
- if( ($n % 3) != 0 ) {
+
+ if ( $n % 3 != 0 ) {
$out .= '</tr>';
}
+
$out .= Xml::closeElement( 'table' );
} else {
$out = '';
@@ -225,37 +248,45 @@ class SpecialPrefixindex extends SpecialAllpages {
if ( $this->including() ) {
$out2 = '';
} else {
- $nsForm = $this->namespacePrefixForm( $namespace, $prefix, $hideredirects );
+ $nsForm = $this->namespacePrefixForm( $namespace, $prefix );
$self = $this->getTitle();
- $out2 = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-nav-table' ) ) .
+ $out2 = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-nav-table' ) ) .
'<tr>
<td>' .
- $nsForm .
- '</td>
- <td id="mw-prefixindex-nav-form" class="mw-prefixindex-nav">';
+ $nsForm .
+ '</td>
+ <td id="mw-prefixindex-nav-form" class="mw-prefixindex-nav">';
- if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
+ if ( isset( $res ) && $res && ( $n == $this->maxPerPage ) &&
+ ( $s = $res->fetchObject() )
+ ) {
$query = array(
'from' => $s->page_title,
'prefix' => $prefix,
- 'hideredirects' => $hideredirects,
+ 'hideredirects' => $this->hideRedirects,
);
- if( $namespace || ($prefix == '')) {
+ if ( $namespace || $prefix == '' ) {
// Keep the namespace even if it's 0 for empty prefixes.
// This tells us we're not just a holdover from old links.
$query['namespace'] = $namespace;
}
+
$nextLink = Linker::linkKnown(
- $self,
- $this->msg( 'nextpage', str_replace( '_',' ', $s->page_title ) )->escaped(),
- array(),
- $query
- );
+ $self,
+ $this->msg( 'nextpage', str_replace( '_', ' ', $s->page_title ) )->escaped(),
+ array(),
+ $query
+ );
+
$out2 .= $nextLink;
- $footer = "\n" . Html::element( "hr" )
- . Html::rawElement( "div", array( "class" => "mw-prefixindex-nav" ), $nextLink );
+ $footer = "\n" . Html::element( 'hr' ) .
+ Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-prefixindex-nav' ),
+ $nextLink
+ );
}
$out2 .= "</td></tr>" .
Xml::closeElement( 'table' );
@@ -263,4 +294,8 @@ class SpecialPrefixindex extends SpecialAllpages {
$this->getOutput()->addHTML( $out2 . $out . $footer );
}
+
+ protected function getGroupName() {
+ return 'pages';
+ }
}
diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php
index 74ed5378..3de6ea24 100644
--- a/includes/specials/SpecialProtectedpages.php
+++ b/includes/specials/SpecialProtectedpages.php
@@ -29,7 +29,7 @@
class SpecialProtectedpages extends SpecialPage {
protected $IdLevel = 'level';
- protected $IdType = 'type';
+ protected $IdType = 'type';
public function __construct() {
parent::__construct( 'Protectedpages' );
@@ -40,7 +40,7 @@ class SpecialProtectedpages extends SpecialPage {
$this->outputHeader();
// Purge expired entries on one in every 10 queries
- if( !mt_rand( 0, 10 ) ) {
+ if ( !mt_rand( 0, 10 ) ) {
Title::purgeExpiredRestrictions();
}
@@ -49,19 +49,37 @@ class SpecialProtectedpages extends SpecialPage {
$level = $request->getVal( $this->IdLevel );
$sizetype = $request->getVal( 'sizetype' );
$size = $request->getIntOrNull( 'size' );
- $NS = $request->getIntOrNull( 'namespace' );
+ $ns = $request->getIntOrNull( 'namespace' );
$indefOnly = $request->getBool( 'indefonly' ) ? 1 : 0;
- $cascadeOnly = $request->getBool('cascadeonly') ? 1 : 0;
+ $cascadeOnly = $request->getBool( 'cascadeonly' ) ? 1 : 0;
- $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly, $cascadeOnly );
-
- $this->getOutput()->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly, $cascadeOnly ) );
+ $pager = new ProtectedPagesPager(
+ $this,
+ array(),
+ $type,
+ $level,
+ $ns,
+ $sizetype,
+ $size,
+ $indefOnly,
+ $cascadeOnly
+ );
- if( $pager->getNumRows() ) {
+ $this->getOutput()->addHTML( $this->showOptions(
+ $ns,
+ $type,
+ $level,
+ $sizetype,
+ $size,
+ $indefOnly,
+ $cascadeOnly
+ ) );
+
+ if ( $pager->getNumRows() ) {
$this->getOutput()->addHTML(
$pager->getNavigationBar() .
- '<ul>' . $pager->getBody() . '</ul>' .
- $pager->getNavigationBar()
+ '<ul>' . $pager->getBody() . '</ul>' .
+ $pager->getNavigationBar()
);
} else {
$this->getOutput()->addWikiMsg( 'protectedpagesempty' );
@@ -78,20 +96,39 @@ class SpecialProtectedpages extends SpecialPage {
static $infinity = null;
- if( is_null( $infinity ) ){
+ if ( is_null( $infinity ) ) {
$infinity = wfGetDB( DB_SLAVE )->getInfinity();
}
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+ if ( !$title ) {
+ wfProfileOut( __METHOD__ );
+
+ return Html::rawElement(
+ 'li',
+ array(),
+ Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $row->page_namespace,
+ $row->page_title
+ )
+ )
+ ) . "\n";
+ }
+
$link = Linker::link( $title );
- $description_items = array ();
+ $description_items = array();
+ // Messages: restriction-level-sysop, restriction-level-autoconfirmed
$protType = $this->msg( 'restriction-level-' . $row->pr_level )->escaped();
$description_items[] = $protType;
- if( $row->pr_cascade ) {
+ if ( $row->pr_cascade ) {
$description_items[] = $this->msg( 'protect-summary-cascade' )->text();
}
@@ -99,7 +136,7 @@ class SpecialProtectedpages extends SpecialPage {
$lang = $this->getLanguage();
$expiry = $lang->formatExpiry( $row->pr_expiry, TS_MW );
- if( $expiry != $infinity ) {
+ if ( $expiry != $infinity ) {
$user = $this->getUser();
$description_items[] = $this->msg(
'protect-expiring-local',
@@ -109,12 +146,13 @@ class SpecialProtectedpages extends SpecialPage {
)->escaped();
}
- if(!is_null($size = $row->page_len)) {
+ if ( !is_null( $size = $row->page_len ) ) {
$stxt = $lang->getDirMark() . ' ' . Linker::formatRevisionSize( $size );
}
- # Show a link to the change protection form for allowed users otherwise a link to the protection log
- if( $this->getUser()->isAllowed( 'protect' ) ) {
+ // Show a link to the change protection form for allowed users otherwise
+ // a link to the protection log
+ if ( $this->getUser()->isAllowed( 'protect' ) ) {
$changeProtection = Linker::linkKnown(
$title,
$this->msg( 'protect_change' )->escaped(),
@@ -134,29 +172,36 @@ class SpecialProtectedpages extends SpecialPage {
);
}
- $changeProtection = ' ' . $this->msg( 'parentheses' )->rawParams( $changeProtection )->escaped();
+ $changeProtection = ' ' . $this->msg( 'parentheses' )->rawParams( $changeProtection )
+ ->escaped();
wfProfileOut( __METHOD__ );
return Html::rawElement(
'li',
array(),
- $lang->specialList( $link . $stxt, $lang->commaList( $description_items ), false ) . $changeProtection ) . "\n";
+ $lang->specialList( $link . $stxt, $lang->commaList( $description_items ), false ) .
+ $changeProtection
+ ) . "\n";
}
/**
* @param $namespace Integer
- * @param $type String: restriction type
- * @param $level String: restriction level
- * @param $sizetype String: "min" or "max"
+ * @param string $type restriction type
+ * @param string $level restriction level
+ * @param string $sizetype "min" or "max"
* @param $size Integer
* @param $indefOnly Boolean: only indefinie protection
* @param $cascadeOnly Boolean: only cascading protection
* @return String: input form
*/
- protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly, $cascadeOnly ) {
+ protected function showOptions( $namespace, $type = 'edit', $level, $sizetype,
+ $size, $indefOnly, $cascadeOnly
+ ) {
global $wgScript;
+
$title = $this->getTitle();
+
return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), $this->msg( 'protectedpages' )->text() ) .
@@ -190,8 +235,8 @@ class SpecialProtectedpages extends SpecialPage {
'all' => '',
'label' => $this->msg( 'namespace' )->text()
), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
+ 'name' => 'namespace',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
)
)
@@ -199,31 +244,54 @@ class SpecialProtectedpages extends SpecialPage {
}
/**
+ * @param bool $indefOnly
* @return string Formatted HTML
*/
protected function getExpiryCheck( $indefOnly ) {
- return
- Xml::checkLabel( $this->msg( 'protectedpages-indef' )->text(), 'indefonly', 'indefonly', $indefOnly ) . "\n";
+ return Xml::checkLabel(
+ $this->msg( 'protectedpages-indef' )->text(),
+ 'indefonly',
+ 'indefonly',
+ $indefOnly
+ ) . "\n";
}
/**
+ * @param bool $cascadeOnly
* @return string Formatted HTML
*/
protected function getCascadeCheck( $cascadeOnly ) {
- return
- Xml::checkLabel( $this->msg( 'protectedpages-cascade' )->text(), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n";
+ return Xml::checkLabel(
+ $this->msg( 'protectedpages-cascade' )->text(),
+ 'cascadeonly',
+ 'cascadeonly',
+ $cascadeOnly
+ ) . "\n";
}
/**
+ * @param string $sizetype "min" or "max"
+ * @param mixed $size
* @return string Formatted HTML
*/
protected function getSizeLimit( $sizetype, $size ) {
$max = $sizetype === 'max';
- return
- Xml::radioLabel( $this->msg( 'minimum-size' )->text(), 'sizetype', 'min', 'wpmin', !$max ) .
+ return Xml::radioLabel(
+ $this->msg( 'minimum-size' )->text(),
+ 'sizetype',
+ 'min',
+ 'wpmin',
+ !$max
+ ) .
'&#160;' .
- Xml::radioLabel( $this->msg( 'maximum-size' )->text(), 'sizetype', 'max', 'wpmax', $max ) .
+ Xml::radioLabel(
+ $this->msg( 'maximum-size' )->text(),
+ 'sizetype',
+ 'max',
+ 'wpmax',
+ $max
+ ) .
'&#160;' .
Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) .
'&#160;' .
@@ -240,14 +308,15 @@ class SpecialProtectedpages extends SpecialPage {
$options = array();
// First pass to load the log names
- foreach( Title::getFilteredRestrictionTypes( true ) as $type ) {
+ foreach ( Title::getFilteredRestrictionTypes( true ) as $type ) {
+ // Messages: restriction-edit, restriction-move, restriction-create, restriction-upload
$text = $this->msg( "restriction-$type" )->text();
$m[$text] = $type;
}
// Third pass generates sorted XHTML content
- foreach( $m as $text => $type ) {
- $selected = ($type == $pr_type );
+ foreach ( $m as $text => $type ) {
+ $selected = ( $type == $pr_type );
$options[] = Xml::option( $text, $type, $selected ) . "\n";
}
@@ -266,21 +335,22 @@ class SpecialProtectedpages extends SpecialPage {
protected function getLevelMenu( $pr_level ) {
global $wgRestrictionLevels;
- $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); // Temporary array
+ // Temporary array
+ $m = array( $this->msg( 'restriction-level-all' )->text() => 0 );
$options = array();
// First pass to load the log names
- foreach( $wgRestrictionLevels as $type ) {
+ foreach ( $wgRestrictionLevels as $type ) {
// Messages used can be 'restriction-level-sysop' and 'restriction-level-autoconfirmed'
- if( $type !='' && $type !='*') {
+ if ( $type != '' && $type != '*' ) {
$text = $this->msg( "restriction-level-$type" )->text();
$m[$text] = $type;
}
}
// Third pass generates sorted XHTML content
- foreach( $m as $text => $type ) {
- $selected = ($type == $pr_level );
+ foreach ( $m as $text => $type ) {
+ $selected = ( $type == $pr_level );
$options[] = Xml::option( $text, $type, $selected );
}
@@ -290,6 +360,10 @@ class SpecialProtectedpages extends SpecialPage {
array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
implode( "\n", $options ) ) . "</span>";
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
/**
@@ -300,16 +374,16 @@ class ProtectedPagesPager extends AlphabeticPager {
public $mForm, $mConds;
private $type, $level, $namespace, $sizetype, $size, $indefonly;
- function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0,
- $indefonly = false, $cascadeonly = false )
- {
+ function __construct( $form, $conds = array(), $type, $level, $namespace,
+ $sizetype = '', $size = 0, $indefonly = false, $cascadeonly = false
+ ) {
$this->mForm = $form;
$this->mConds = $conds;
$this->type = ( $type ) ? $type : 'edit';
$this->level = $level;
$this->namespace = $namespace;
$this->sizetype = $sizetype;
- $this->size = intval($size);
+ $this->size = intval( $size );
$this->indefonly = (bool)$indefonly;
$this->cascadeonly = (bool)$cascadeonly;
parent::__construct( $form->getContext() );
@@ -322,6 +396,7 @@ class ProtectedPagesPager extends AlphabeticPager {
$lb->add( $row->page_namespace, $row->page_title );
}
$lb->execute();
+
return '';
}
@@ -332,31 +407,35 @@ class ProtectedPagesPager extends AlphabeticPager {
function getQueryInfo() {
$conds = $this->mConds;
$conds[] = '(pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ) .
- 'OR pr_expiry IS NULL)';
+ 'OR pr_expiry IS NULL)';
$conds[] = 'page_id=pr_page';
$conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type );
- if( $this->sizetype=='min' ) {
+ if ( $this->sizetype == 'min' ) {
$conds[] = 'page_len>=' . $this->size;
- } elseif( $this->sizetype=='max' ) {
+ } elseif ( $this->sizetype == 'max' ) {
$conds[] = 'page_len<=' . $this->size;
}
- if( $this->indefonly ) {
- $db = wfGetDB( DB_SLAVE );
- $conds[] = "pr_expiry = {$db->addQuotes( $db->getInfinity() )} OR pr_expiry IS NULL";
+ if ( $this->indefonly ) {
+ $infinity = $this->mDb->addQuotes( $this->mDb->getInfinity() );
+ $conds[] = "pr_expiry = $infinity OR pr_expiry IS NULL";
}
- if( $this->cascadeonly ) {
- $conds[] = "pr_cascade = '1'";
+ if ( $this->cascadeonly ) {
+ $conds[] = 'pr_cascade = 1';
}
- if( $this->level )
+ if ( $this->level ) {
$conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
- if( !is_null($this->namespace) )
+ }
+ if ( !is_null( $this->namespace ) ) {
$conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ }
+
return array(
'tables' => array( 'page_restrictions', 'page' ),
- 'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade',
+ 'fields' => array( 'pr_id', 'page_namespace', 'page_title', 'page_len',
+ 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade' ),
'conds' => $conds
);
}
diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php
index a80f0d0a..078e7b12 100644
--- a/includes/specials/SpecialProtectedtitles.php
+++ b/includes/specials/SpecialProtectedtitles.php
@@ -27,9 +27,8 @@
* @ingroup SpecialPage
*/
class SpecialProtectedtitles extends SpecialPage {
-
protected $IdLevel = 'level';
- protected $IdType = 'type';
+ protected $IdType = 'type';
public function __construct() {
parent::__construct( 'Protectedtitles' );
@@ -58,8 +57,8 @@ class SpecialProtectedtitles extends SpecialPage {
if ( $pager->getNumRows() ) {
$this->getOutput()->addHTML(
$pager->getNavigationBar() .
- '<ul>' . $pager->getBody() . '</ul>' .
- $pager->getNavigationBar()
+ '<ul>' . $pager->getBody() . '</ul>' .
+ $pager->getNavigationBar()
);
} else {
$this->getOutput()->addWikiMsg( 'protectedtitlesempty' );
@@ -69,6 +68,7 @@ class SpecialProtectedtitles extends SpecialPage {
/**
* Callback function to output a restriction
*
+ * @param object $row Database row
* @return string
*/
function formatRow( $row ) {
@@ -76,22 +76,40 @@ class SpecialProtectedtitles extends SpecialPage {
static $infinity = null;
- if( is_null( $infinity ) ){
+ if ( is_null( $infinity ) ) {
$infinity = wfGetDB( DB_SLAVE )->getInfinity();
}
$title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
- $link = Linker::link( $title );
-
- $description_items = array ();
+ if ( !$title ) {
+ wfProfileOut( __METHOD__ );
+
+ return Html::rawElement(
+ 'li',
+ array(),
+ Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $row->pt_namespace,
+ $row->pt_title
+ )
+ )
+ ) . "\n";
+ }
+ $link = Linker::link( $title );
+ $description_items = array();
+ // Messages: restriction-level-sysop, restriction-level-autoconfirmed
$protType = $this->msg( 'restriction-level-' . $row->pt_create_perm )->escaped();
-
$description_items[] = $protType;
-
$lang = $this->getLanguage();
- $expiry = strlen( $row->pt_expiry ) ? $lang->formatExpiry( $row->pt_expiry, TS_MW ) : $infinity;
- if( $expiry != $infinity ) {
+ $expiry = strlen( $row->pt_expiry ) ?
+ $lang->formatExpiry( $row->pt_expiry, TS_MW ) :
+ $infinity;
+
+ if ( $expiry != $infinity ) {
$user = $this->getUser();
$description_items[] = $this->msg(
'protect-expiring-local',
@@ -103,6 +121,7 @@ class SpecialProtectedtitles extends SpecialPage {
wfProfileOut( __METHOD__ );
+ // @todo i18n: This should use a comma separator instead of a hard coded comma, right?
return '<li>' . $lang->specialList( $link, implode( $description_items, ', ' ) ) . "</li>\n";
}
@@ -113,11 +132,12 @@ class SpecialProtectedtitles extends SpecialPage {
* @return string
* @private
*/
- function showOptions( $namespace, $type='edit', $level ) {
+ function showOptions( $namespace, $type = 'edit', $level ) {
global $wgScript;
$action = htmlspecialchars( $wgScript );
$title = $this->getTitle();
$special = htmlspecialchars( $title->getPrefixedDBkey() );
+
return "<form action=\"$action\" method=\"get\">\n" .
'<fieldset>' .
Xml::element( 'legend', array(), $this->msg( 'protectedtitles' )->text() ) .
@@ -142,46 +162,53 @@ class SpecialProtectedtitles extends SpecialPage {
'all' => '',
'label' => $this->msg( 'namespace' )->text()
), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
+ 'name' => 'namespace',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
)
);
}
/**
+ * @param string $pr_level Determines which option is selected as default
* @return string Formatted HTML
* @private
*/
function getLevelMenu( $pr_level ) {
global $wgRestrictionLevels;
- $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); // Temporary array
+ // Temporary array
+ $m = array( $this->msg( 'restriction-level-all' )->text() => 0 );
$options = array();
// First pass to load the log names
- foreach( $wgRestrictionLevels as $type ) {
- if ( $type !='' && $type !='*') {
+ foreach ( $wgRestrictionLevels as $type ) {
+ if ( $type != '' && $type != '*' ) {
+ // Messages: restriction-level-sysop, restriction-level-autoconfirmed
$text = $this->msg( "restriction-level-$type" )->text();
$m[$text] = $type;
}
}
+
// Is there only one level (aside from "all")?
- if( count($m) <= 2 ) {
+ if ( count( $m ) <= 2 ) {
return '';
}
// Third pass generates sorted XHTML content
- foreach( $m as $text => $type ) {
- $selected = ($type == $pr_level );
+ foreach ( $m as $text => $type ) {
+ $selected = ( $type == $pr_level );
$options[] = Xml::option( $text, $type, $selected );
}
- return
- Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . '&#160;' .
+ return Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . '&#160;' .
Xml::tags( 'select',
array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
implode( "\n", $options ) );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
/**
@@ -191,12 +218,14 @@ class SpecialProtectedtitles extends SpecialPage {
class ProtectedTitlesPager extends AlphabeticPager {
public $mForm, $mConds;
- function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
+ function __construct( $form, $conds = array(), $type, $level, $namespace,
+ $sizetype = '', $size = 0
+ ) {
$this->mForm = $form;
$this->mConds = $conds;
$this->level = $level;
$this->namespace = $namespace;
- $this->size = intval($size);
+ $this->size = intval( $size );
parent::__construct( $form->getContext() );
}
@@ -212,6 +241,7 @@ class ProtectedTitlesPager extends AlphabeticPager {
$lb->execute();
wfProfileOut( __METHOD__ );
+
return '';
}
@@ -232,13 +262,18 @@ class ProtectedTitlesPager extends AlphabeticPager {
function getQueryInfo() {
$conds = $this->mConds;
$conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
- if( $this->level )
+ if ( $this->level ) {
$conds['pt_create_perm'] = $this->level;
- if( !is_null($this->namespace) )
+ }
+
+ if ( !is_null( $this->namespace ) ) {
$conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ }
+
return array(
'tables' => 'protected_titles',
- 'fields' => 'pt_namespace,pt_title,pt_create_perm,pt_expiry,pt_timestamp',
+ 'fields' => array( 'pt_namespace', 'pt_title', 'pt_create_perm',
+ 'pt_expiry', 'pt_timestamp' ),
'conds' => $conds
);
}
@@ -247,4 +282,3 @@ class ProtectedTitlesPager extends AlphabeticPager {
return 'pt_timestamp';
}
}
-
diff --git a/includes/specials/SpecialRandomInCategory.php b/includes/specials/SpecialRandomInCategory.php
new file mode 100644
index 00000000..0e022bfa
--- /dev/null
+++ b/includes/specials/SpecialRandomInCategory.php
@@ -0,0 +1,291 @@
+<?php
+/**
+ * Implements Special:RandomInCategory
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Brian Wolff
+ */
+
+/**
+ * Special page to direct the user to a random page
+ *
+ * @note The method used here is rather biased. It is assumed that
+ * the use of this page will be people wanting to get a random page
+ * out of a maintenance category, to fix it up. The method used by
+ * this page should return different pages in an unpredictable fashion
+ * which is hoped to be sufficient, even if some pages are selected
+ * more often than others.
+ *
+ * A more unbiased method could be achieved by adding a cl_random field
+ * to the categorylinks table.
+ *
+ * The method used here is as follows:
+ * * Find the smallest and largest timestamp in the category
+ * * Pick a random timestamp in between
+ * * Pick an offset between 0 and 30
+ * * Get the offset'ed page that is newer than the timestamp selected
+ * The offset is meant to counter the fact the timestamps aren't usually
+ * uniformly distributed, so if things are very non-uniform at least we
+ * won't have the same page selected 99% of the time.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialRandomInCategory extends SpecialPage {
+ protected $extra = array(); // Extra SQL statements
+ protected $category = false; // Title object of category
+ protected $maxOffset = 30; // Max amount to fudge randomness by.
+ private $maxTimestamp = null;
+ private $minTimestamp = null;
+
+ public function __construct( $name = 'RandomInCategory' ) {
+ parent::__construct( $name );
+ }
+
+ /**
+ * Set which category to use.
+ * @param Title $cat
+ */
+ public function setCategory( Title $cat ) {
+ $this->category = $cat;
+ $this->maxTimestamp = null;
+ $this->minTimestamp = null;
+ }
+
+ public function execute( $par ) {
+ global $wgScript;
+
+ $cat = false;
+
+ $categoryStr = $this->getRequest()->getText( 'category', $par );
+
+ if ( $categoryStr ) {
+ $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
+ }
+
+ if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
+ // Someone searching for something like "Wikipedia:Foo"
+ $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
+ }
+
+ if ( $cat ) {
+ $this->setCategory( $cat );
+ }
+
+
+ if ( !$this->category && $categoryStr ) {
+ $this->setHeaders();
+ $this->getOutput()->addWikiMsg( 'randomincategory-invalidcategory',
+ wfEscapeWikiText( $categoryStr ) );
+
+ return;
+ } elseif ( !$this->category ) {
+ $this->setHeaders();
+ $input = Html::input( 'category' );
+ $submitText = $this->msg( 'randomincategory-selectcategory-submit' )->text();
+ $submit = Html::input( '', $submitText, 'submit' );
+
+ $msg = $this->msg( 'randomincategory-selectcategory' );
+ $form = Html::rawElement( 'form', array( 'action' => $wgScript ),
+ Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ $msg->rawParams( $input, $submit )->parse()
+ );
+ $this->getOutput()->addHtml( $form );
+
+ return;
+ }
+
+ $title = $this->getRandomTitle();
+
+ if ( is_null( $title ) ) {
+ $this->setHeaders();
+ $this->getOutput()->addWikiMsg( 'randomincategory-nopages',
+ $this->category->getText() );
+
+ return;
+ }
+
+ $query = $this->getRequest()->getValues();
+ unset( $query['title'] );
+ unset( $query['category'] );
+ $this->getOutput()->redirect( $title->getFullURL( $query ) );
+ }
+
+ /**
+ * Choose a random title.
+ * @return Title object (or null if nothing to choose from)
+ */
+ public function getRandomTitle() {
+ // Convert to float, since we do math with the random number.
+ $rand = (float)wfRandom();
+ $title = null;
+
+ // Given that timestamps are rather unevenly distributed, we also
+ // use an offset between 0 and 30 to make any biases less noticeable.
+ $offset = mt_rand( 0, $this->maxOffset );
+
+ if ( mt_rand( 0, 1 ) ) {
+ $up = true;
+ } else {
+ $up = false;
+ }
+
+ $row = $this->selectRandomPageFromDB( $rand, $offset, $up );
+
+ // Try again without the timestamp offset (wrap around the end)
+ if ( !$row ) {
+ $row = $this->selectRandomPageFromDB( false, $offset, $up );
+ }
+
+ // Maybe the category is really small and offset too high
+ if ( !$row ) {
+ $row = $this->selectRandomPageFromDB( $rand, 0, $up );
+ }
+
+ // Just get the first entry.
+ if ( !$row ) {
+ $row = $this->selectRandomPageFromDB( false, 0, true );
+ }
+
+ if ( $row ) {
+ return Title::makeTitle( $row->page_namespace, $row->page_title );
+ }
+
+ return null;
+ }
+
+ /**
+ * @param float $rand Random number between 0 and 1
+ * @param int $offset Extra offset to fudge randomness
+ * @param bool $up True to get the result above the random number, false for below
+ *
+ * @note The $up parameter is supposed to counteract what would happen if there
+ * was a large gap in the distribution of cl_timestamp values. This way instead
+ * of things to the right of the gap being favoured, both sides of the gap
+ * are favoured.
+ * @return Array Query information.
+ */
+ protected function getQueryInfo( $rand, $offset, $up ) {
+ $op = $up ? '>=' : '<=';
+ $dir = $up ? 'ASC' : 'DESC';
+ if ( !$this->category instanceof Title ) {
+ throw new MWException( 'No category set' );
+ }
+ $qi = array(
+ 'tables' => array( 'categorylinks', 'page' ),
+ 'fields' => array( 'page_title', 'page_namespace' ),
+ 'conds' => array_merge( array(
+ 'cl_to' => $this->category->getDBKey(),
+ ), $this->extra ),
+ 'options' => array(
+ 'ORDER BY' => 'cl_timestamp ' . $dir,
+ 'LIMIT' => 1,
+ 'OFFSET' => $offset
+ ),
+ 'join_conds' => array(
+ 'page' => array( 'INNER JOIN', 'cl_from = page_id' )
+ )
+ );
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $minClTime = $this->getTimestampOffset( $rand );
+ if ( $minClTime ) {
+ $qi['conds'][] = 'cl_timestamp ' . $op . ' ' .
+ $dbr->addQuotes( $dbr->timestamp( $minClTime ) );
+ }
+ return $qi;
+ }
+
+ /**
+ * @param float $rand Random number between 0 and 1
+ *
+ * @return int|bool A random (unix) timestamp from the range of the category or false on failure
+ */
+ protected function getTimestampOffset( $rand ) {
+ if ( $rand === false ) {
+ return false;
+ }
+ if ( !$this->minTimestamp || !$this->maxTimestamp ) {
+ try {
+ list( $this->minTimestamp, $this->maxTimestamp ) = $this->getMinAndMaxForCat( $this->category );
+ } catch ( MWException $e ) {
+ // Possibly no entries in category.
+ return false;
+ }
+ }
+
+ $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
+ return intval( $ts );
+ }
+
+ /**
+ * Get the lowest and highest timestamp for a category.
+ *
+ * @param Title $category
+ * @return Array The lowest and highest timestamp
+ * @throws MWException if category has no entries.
+ */
+ protected function getMinAndMaxForCat( Title $category ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->selectRow(
+ 'categorylinks',
+ array(
+ 'low' => 'MIN( cl_timestamp )',
+ 'high' => 'MAX( cl_timestamp )'
+ ),
+ array(
+ 'cl_to' => $this->category->getDBKey(),
+ ),
+ __METHOD__,
+ array(
+ 'LIMIT' => 1
+ )
+ );
+ if ( !$res ) {
+ throw new MWException( 'No entries in category' );
+ }
+ return array( wfTimestamp( TS_UNIX, $res->low ), wfTimestamp( TS_UNIX, $res->high ) );
+ }
+
+ /**
+ * @param float $rand A random number that is converted to a random timestamp
+ * @param int $offset A small offset to make the result seem more "random"
+ * @param bool $up Get the result above the random value
+ * @param String $fname The name of the calling method
+ * @return Array Info for the title selected.
+ */
+ private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $query = $this->getQueryInfo( $rand, $offset, $up );
+ $res = $dbr->select(
+ $query['tables'],
+ $query['fields'],
+ $query['conds'],
+ $fname,
+ $query['options'],
+ $query['join_conds']
+ );
+
+ return $res->fetchObject();
+ }
+
+ protected function getGroupName() {
+ return 'redirects';
+ }
+}
diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php
index 307088ed..c94d2b35 100644
--- a/includes/specials/SpecialRandompage.php
+++ b/includes/specials/SpecialRandompage.php
@@ -28,11 +28,11 @@
* @ingroup SpecialPage
*/
class RandomPage extends SpecialPage {
- private $namespaces; // namespaces to select pages from
+ private $namespaces; // namespaces to select pages from
protected $isRedir = false; // should the result be a redirect?
protected $extra = array(); // Extra SQL statements
- public function __construct( $name = 'Randompage' ){
+ public function __construct( $name = 'Randompage' ) {
$this->namespaces = MWNamespace::getContentNamespaces();
parent::__construct( $name );
}
@@ -41,15 +41,15 @@ class RandomPage extends SpecialPage {
return $this->namespaces;
}
- public function setNamespace ( $ns ) {
- if( !$ns || $ns < NS_MAIN ) {
+ public function setNamespace( $ns ) {
+ if ( !$ns || $ns < NS_MAIN ) {
$ns = NS_MAIN;
}
$this->namespaces = array( $ns );
}
// select redirects instead of normal pages?
- public function isRedirect(){
+ public function isRedirect() {
return $this->isRedir;
}
@@ -62,17 +62,19 @@ class RandomPage extends SpecialPage {
$title = $this->getRandomTitle();
- if( is_null( $title ) ) {
+ if ( is_null( $title ) ) {
$this->setHeaders();
+ // Message: randompage-nopages, randomredirect-nopages
$this->getOutput()->addWikiMsg( strtolower( $this->getName() ) . '-nopages',
$this->getNsList(), count( $this->namespaces ) );
+
return;
}
$redirectParam = $this->isRedirect() ? array( 'redirect' => 'no' ) : array();
$query = array_merge( $this->getRequest()->getValues(), $redirectParam );
unset( $query['title'] );
- $this->getOutput()->redirect( $title->getFullUrl( $query ) );
+ $this->getOutput()->redirect( $title->getFullURL( $query ) );
}
/**
@@ -83,13 +85,14 @@ class RandomPage extends SpecialPage {
private function getNsList() {
global $wgContLang;
$nsNames = array();
- foreach( $this->namespaces as $n ) {
- if( $n === NS_MAIN ) {
+ foreach ( $this->namespaces as $n ) {
+ if ( $n === NS_MAIN ) {
$nsNames[] = $this->msg( 'blanknamespace' )->plain();
} else {
$nsNames[] = $wgContLang->getNsText( $n );
}
}
+
return $wgContLang->commaList( $nsNames );
}
@@ -100,10 +103,14 @@ class RandomPage extends SpecialPage {
public function getRandomTitle() {
$randstr = wfRandom();
$title = null;
- if ( !wfRunHooks( 'SpecialRandomGetRandomTitle', array( &$randstr, &$this->isRedir, &$this->namespaces,
- &$this->extra, &$title ) ) ) {
+
+ 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
@@ -113,15 +120,15 @@ class RandomPage extends SpecialPage {
* any more bias than what the page_random scheme
* causes anyway. Trust me, I'm a mathematician. :)
*/
- if( !$row ) {
+ if ( !$row ) {
$row = $this->selectRandomPageFromDB( "0" );
}
- if( $row ) {
+ if ( $row ) {
return Title::makeTitleSafe( $row->page_namespace, $row->page_title );
- } else {
- return null;
}
+
+ return null;
}
protected function getQueryInfo( $randstr ) {
@@ -159,4 +166,8 @@ class RandomPage extends SpecialPage {
return $dbr->fetchObject( $res );
}
+
+ protected function getGroupName() {
+ return 'redirects';
+ }
}
diff --git a/includes/specials/SpecialRandomredirect.php b/includes/specials/SpecialRandomredirect.php
index 88c81b31..7c36a28a 100644
--- a/includes/specials/SpecialRandomredirect.php
+++ b/includes/specials/SpecialRandomredirect.php
@@ -28,9 +28,8 @@
* @ingroup SpecialPage
*/
class SpecialRandomredirect extends RandomPage {
- function __construct(){
+ function __construct() {
parent::__construct( 'Randomredirect' );
$this->isRedir = true;
}
-
}
diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php
index 2bd8b0a9..a42a2171 100644
--- a/includes/specials/SpecialRecentchanges.php
+++ b/includes/specials/SpecialRecentchanges.php
@@ -41,17 +41,18 @@ class SpecialRecentChanges extends IncludableSpecialPage {
*/
public function getDefaultOptions() {
$opts = new FormOptions();
+ $user = $this->getUser();
- $opts->add( 'days', (int)$this->getUser()->getOption( 'rcdays' ) );
- $opts->add( 'limit', (int)$this->getUser()->getOption( 'rclimit' ) );
+ $opts->add( 'days', $user->getIntOption( 'rcdays' ) );
+ $opts->add( 'limit', $user->getIntOption( 'rclimit' ) );
$opts->add( 'from', '' );
- $opts->add( 'hideminor', $this->getUser()->getBoolOption( 'hideminor' ) );
- $opts->add( 'hidebots', true );
- $opts->add( 'hideanons', false );
- $opts->add( 'hideliu', false );
- $opts->add( 'hidepatrolled', $this->getUser()->getBoolOption( 'hidepatrolled' ) );
- $opts->add( 'hidemyself', false );
+ $opts->add( 'hideminor', $user->getBoolOption( 'hideminor' ) );
+ $opts->add( 'hidebots', true );
+ $opts->add( 'hideanons', false );
+ $opts->add( 'hideliu', false );
+ $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) );
+ $opts->add( 'hidemyself', false );
$opts->add( 'namespace', '', FormOptions::INTNULL );
$opts->add( 'invert', false );
@@ -60,44 +61,47 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$opts->add( 'categories', '' );
$opts->add( 'categories_any', false );
$opts->add( 'tagfilter', '' );
+
return $opts;
}
/**
* Create a FormOptions object with options as specified by the user
*
- * @param $parameters array
+ * @param array $parameters
*
* @return FormOptions
*/
public function setup( $parameters ) {
$opts = $this->getDefaultOptions();
- foreach( $this->getCustomFilters() as $key => $params ) {
+ foreach ( $this->getCustomFilters() as $key => $params ) {
$opts->add( $key, $params['default'] );
}
$opts->fetchValuesFromRequest( $this->getRequest() );
// Give precedence to subpage syntax
- if( $parameters !== null ) {
+ if ( $parameters !== null ) {
$this->parseParameters( $parameters, $opts );
}
$opts->validateIntBounds( 'limit', 0, 5000 );
+
return $opts;
}
/**
* Get custom show/hide filters
*
- * @return Array Map of filter URL param names to properties (msg/default)
+ * @return array Map of filter URL param names to properties (msg/default)
*/
protected function getCustomFilters() {
if ( $this->customFilters === null ) {
$this->customFilters = array();
wfRunHooks( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters ) );
}
+
return $this->customFilters;
}
@@ -109,9 +113,9 @@ class SpecialRecentChanges extends IncludableSpecialPage {
public function feedSetup() {
global $wgFeedLimit;
$opts = $this->getDefaultOptions();
- # Feed is cached on limit,hideminor,namespace; other params would randomly not work
- $opts->fetchValuesFromRequest( $this->getRequest(), array( 'limit', 'hideminor', 'namespace' ) );
+ $opts->fetchValuesFromRequest( $this->getRequest() );
$opts->validateIntBounds( 'limit', 0, $wgFeedLimit );
+
return $opts;
}
@@ -127,14 +131,14 @@ class SpecialRecentChanges extends IncludableSpecialPage {
}
$this->rcOptions = $isFeed ? $this->feedSetup() : $this->setup( $this->rcSubpage );
}
+
return $this->rcOptions;
}
-
/**
* Main execution point
*
- * @param $subpage String
+ * @param string $subpage
*/
public function execute( $subpage ) {
$this->rcSubpage = $subpage;
@@ -144,36 +148,38 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$this->getOutput()->setSquidMaxage( 10 );
# Check if the client has a cached version
$lastmod = $this->checkLastModified( $feedFormat );
- if( $lastmod === false ) {
+ if ( $lastmod === false ) {
return;
}
$opts = $this->getOptions();
$this->setHeaders();
$this->outputHeader();
- $this->addRecentChangesJS();
+ $this->addModules();
// Fetch results, prepare a batch link existence check query
$conds = $this->buildMainQueryConds( $opts );
$rows = $this->doMainQuery( $conds, $opts );
- if( $rows === false ){
- if( !$this->including() ) {
+ if ( $rows === false ) {
+ if ( !$this->including() ) {
$this->doHeader( $opts );
}
+
return;
}
- if( !$feedFormat ) {
+ if ( !$feedFormat ) {
$batch = new LinkBatch;
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
$batch->add( NS_USER, $row->rc_user_text );
$batch->add( NS_USER_TALK, $row->rc_user_text );
$batch->add( $row->rc_namespace, $row->rc_title );
}
$batch->execute();
}
- if( $feedFormat ) {
+ if ( $feedFormat ) {
list( $changesFeed, $formatter ) = $this->getFeedObject( $feedFormat );
+ /** @var ChangesFeed $changesFeed */
$changesFeed->execute( $formatter, $rows, $lastmod, $opts );
} else {
$this->webOutput( $rows, $opts );
@@ -185,15 +191,17 @@ class SpecialRecentChanges extends IncludableSpecialPage {
/**
* Return an array with a ChangesFeed object and ChannelFeed object
*
- * @return Array
+ * @param string $feedFormat Feed's format (either 'rss' or 'atom')
+ * @return array
*/
- public function getFeedObject( $feedFormat ){
+ public function getFeedObject( $feedFormat ) {
$changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' );
$formatter = $changesFeed->getFeedObject(
$this->msg( 'recentchanges' )->inContentLanguage()->text(),
$this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(),
$this->getTitle()->getFullURL()
);
+
return array( $changesFeed, $formatter );
}
@@ -201,49 +209,49 @@ class SpecialRecentChanges extends IncludableSpecialPage {
* Process $par and put options found if $opts
* Mainly used when including the page
*
- * @param $par String
- * @param $opts FormOptions
+ * @param string $par
+ * @param FormOptions $opts
*/
public function parseParameters( $par, FormOptions $opts ) {
$bits = preg_split( '/\s*,\s*/', trim( $par ) );
- foreach( $bits as $bit ) {
- if( 'hidebots' === $bit ) {
+ foreach ( $bits as $bit ) {
+ if ( 'hidebots' === $bit ) {
$opts['hidebots'] = true;
}
- if( 'bots' === $bit ) {
+ if ( 'bots' === $bit ) {
$opts['hidebots'] = false;
}
- if( 'hideminor' === $bit ) {
+ if ( 'hideminor' === $bit ) {
$opts['hideminor'] = true;
}
- if( 'minor' === $bit ) {
+ if ( 'minor' === $bit ) {
$opts['hideminor'] = false;
}
- if( 'hideliu' === $bit ) {
+ if ( 'hideliu' === $bit ) {
$opts['hideliu'] = true;
}
- if( 'hidepatrolled' === $bit ) {
+ if ( 'hidepatrolled' === $bit ) {
$opts['hidepatrolled'] = true;
}
- if( 'hideanons' === $bit ) {
+ if ( 'hideanons' === $bit ) {
$opts['hideanons'] = true;
}
- if( 'hidemyself' === $bit ) {
+ if ( 'hidemyself' === $bit ) {
$opts['hidemyself'] = true;
}
- if( is_numeric( $bit ) ) {
- $opts['limit'] = $bit;
+ if ( is_numeric( $bit ) ) {
+ $opts['limit'] = $bit;
}
$m = array();
- if( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
$opts['limit'] = $m[1];
}
- if( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
+ if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
$opts['days'] = $m[1];
}
- if( preg_match( '/^namespace=(\d+)$/', $bit, $m ) ) {
+ if ( preg_match( '/^namespace=(\d+)$/', $bit, $m ) ) {
$opts['namespace'] = $m[1];
}
}
@@ -254,25 +262,26 @@ class SpecialRecentChanges extends IncludableSpecialPage {
* Don't use this if we are using the patrol feature, patrol changes don't
* update the timestamp
*
- * @param $feedFormat String
- * @return String or false
+ * @param string $feedFormat
+ * @return string|bool
*/
public function checkLastModified( $feedFormat ) {
$dbr = wfGetDB( DB_SLAVE );
$lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
- if( $feedFormat || !$this->getUser()->useRCPatrol() ) {
- if( $lastmod && $this->getOutput()->checkLastModified( $lastmod ) ) {
+ if ( $feedFormat || !$this->getUser()->useRCPatrol() ) {
+ if ( $lastmod && $this->getOutput()->checkLastModified( $lastmod ) ) {
# Client cache fresh and headers sent, nothing more to do.
return false;
}
}
+
return $lastmod;
}
/**
* Return an array of conditions depending of options set in $opts
*
- * @param $opts FormOptions
+ * @param FormOptions $opts
* @return array
*/
public function buildMainQueryConds( FormOptions $opts ) {
@@ -282,9 +291,9 @@ class SpecialRecentChanges extends IncludableSpecialPage {
# It makes no sense to hide both anons and logged-in users
# Where this occurs, force anons to be shown
$forcebot = false;
- if( $opts['hideanons'] && $opts['hideliu'] ){
+ if ( $opts['hideanons'] && $opts['hideliu'] ) {
# Check if the user wants to show bots only
- if( $opts['hidebots'] ){
+ if ( $opts['hidebots'] ) {
$opts['hideanons'] = false;
} else {
$forcebot = true;
@@ -294,12 +303,12 @@ class SpecialRecentChanges extends IncludableSpecialPage {
// Calculate cutoff
$cutoff_unixtime = time() - ( $opts['days'] * 86400 );
- $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
+ $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
$cutoff = $dbr->timestamp( $cutoff_unixtime );
- $fromValid = preg_match('/^[0-9]{14}$/', $opts['from']);
- if( $fromValid && $opts['from'] > wfTimestamp(TS_MW,$cutoff) ) {
- $cutoff = $dbr->timestamp($opts['from']);
+ $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
+ if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
+ $cutoff = $dbr->timestamp( $opts['from'] );
} else {
$opts->reset( 'from' );
}
@@ -310,27 +319,27 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$hideLoggedInUsers = $opts['hideliu'] && !$forcebot;
$hideAnonymousUsers = $opts['hideanons'] && !$forcebot;
- if( $opts['hideminor'] ) {
+ if ( $opts['hideminor'] ) {
$conds['rc_minor'] = 0;
}
- if( $opts['hidebots'] ) {
+ if ( $opts['hidebots'] ) {
$conds['rc_bot'] = 0;
}
- if( $hidePatrol ) {
+ if ( $hidePatrol ) {
$conds['rc_patrolled'] = 0;
}
- if( $forcebot ) {
+ if ( $forcebot ) {
$conds['rc_bot'] = 1;
}
- if( $hideLoggedInUsers ) {
+ if ( $hideLoggedInUsers ) {
$conds[] = 'rc_user = 0';
}
- if( $hideAnonymousUsers ) {
+ if ( $hideAnonymousUsers ) {
$conds[] = 'rc_user != 0';
}
- if( $opts['hidemyself'] ) {
- if( $this->getUser()->getId() ) {
+ if ( $opts['hidemyself'] ) {
+ if ( $this->getUser()->getId() ) {
$conds[] = 'rc_user != ' . $dbr->addQuotes( $this->getUser()->getId() );
} else {
$conds[] = 'rc_user_text != ' . $dbr->addQuotes( $this->getUser()->getName() );
@@ -338,13 +347,13 @@ class SpecialRecentChanges extends IncludableSpecialPage {
}
# Namespace filtering
- if( $opts['namespace'] !== '' ) {
+ if ( $opts['namespace'] !== '' ) {
$selectedNS = $dbr->addQuotes( $opts['namespace'] );
- $operator = $opts['invert'] ? '!=' : '=';
- $boolean = $opts['invert'] ? 'AND' : 'OR';
+ $operator = $opts['invert'] ? '!=' : '=';
+ $boolean = $opts['invert'] ? 'AND' : 'OR';
# namespace association (bug 2429)
- if( !$opts['associated'] ) {
+ if ( !$opts['associated'] ) {
$condition = "rc_namespace $operator $selectedNS";
} else {
# Also add the associated namespace
@@ -352,21 +361,22 @@ class SpecialRecentChanges extends IncludableSpecialPage {
MWNamespace::getAssociated( $opts['namespace'] )
);
$condition = "(rc_namespace $operator $selectedNS "
- . $boolean
- . " rc_namespace $operator $associatedNS)";
+ . $boolean
+ . " rc_namespace $operator $associatedNS)";
}
$conds[] = $condition;
}
+
return $conds;
}
/**
* Process the query
*
- * @param $conds Array
- * @param $opts FormOptions
- * @return bool|ResultWrapper result or false (for Recentchangeslinked only)
+ * @param array $conds
+ * @param FormOptions $opts
+ * @return bool|ResultWrapper Result or false (for Recentchangeslinked only)
*/
public function doMainQuery( $conds, $opts ) {
$tables = array( 'recentchanges' );
@@ -382,19 +392,22 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$invert = $opts['invert'];
$associated = $opts['associated'];
- $fields = array( $dbr->tableName( 'recentchanges' ) . '.*' ); // all rc columns
+ $fields = RecentChange::selectFields();
// JOIN on watchlist for users
- if ( $uid ) {
+ if ( $uid && $this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
$tables[] = 'watchlist';
$fields[] = 'wl_user';
$fields[] = 'wl_notificationtimestamp';
- $join_conds['watchlist'] = array('LEFT JOIN',
- "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace");
+ $join_conds['watchlist'] = array( 'LEFT JOIN', array(
+ 'wl_user' => $uid,
+ 'wl_title=rc_title',
+ 'wl_namespace=rc_namespace'
+ ) );
}
if ( $this->getUser()->isAllowed( 'rollback' ) ) {
$tables[] = 'page';
$fields[] = 'page_latest';
- $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id');
+ $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' );
}
// Tag stuff.
ChangeTags::modifyDisplayQuery(
@@ -407,86 +420,40 @@ class SpecialRecentChanges extends IncludableSpecialPage {
);
if ( !wfRunHooks( 'SpecialRecentChangesQuery',
- array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ) ) )
- {
+ array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ) )
+ ) {
return false;
}
- // Don't use the new_namespace_time timestamp index if:
- // (a) "All namespaces" selected
- // (b) We want pages in more than one namespace (inverted/associated)
- // (c) There is a tag to filter on (use tag index instead)
- // (d) UNION + sort/limit is not an option for the DBMS
- if( $namespace === ''
- || ( $invert || $associated )
- || $opts['tagfilter'] != ''
- || !$dbr->unionSupportsOrderAndLimit() )
- {
- $res = $dbr->select( $tables, $fields, $conds, __METHOD__,
- array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) +
- $query_options,
- $join_conds );
- // We have a new_namespace_time index! UNION over new=(0,1) and sort result set!
- } else {
- // New pages
- $sqlNew = $dbr->selectSQLText(
- $tables,
- $fields,
- array( 'rc_new' => 1 ) + $conds,
- __METHOD__,
- array(
- 'ORDER BY' => 'rc_timestamp DESC',
- 'LIMIT' => $limit,
- 'USE INDEX' => array( 'recentchanges' => 'new_name_timestamp' )
- ),
- $join_conds
- );
- // Old pages
- $sqlOld = $dbr->selectSQLText(
- $tables,
- $fields,
- array( 'rc_new' => 0 ) + $conds,
- __METHOD__,
- array(
- 'ORDER BY' => 'rc_timestamp DESC',
- 'LIMIT' => $limit,
- 'USE INDEX' => array( 'recentchanges' => 'new_name_timestamp' )
- ),
- $join_conds
- );
- # Join the two fast queries, and sort the result set
- $sql = $dbr->unionQueries( array( $sqlNew, $sqlOld ), false ) .
- ' ORDER BY rc_timestamp DESC';
- $sql = $dbr->limitResult( $sql, $limit, false );
- $res = $dbr->query( $sql, __METHOD__ );
- }
-
- return $res;
+ // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
+ // knowledge to use an index merge if it wants (it may use some other index though).
+ return $dbr->select(
+ $tables,
+ $fields,
+ $conds + array( 'rc_new' => array( 0, 1 ) ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) + $query_options,
+ $join_conds
+ );
}
/**
* Send output to the OutputPage object, only called if not used feeds
*
- * @param $rows Array of database rows
- * @param $opts FormOptions
+ * @param array $rows Database rows
+ * @param FormOptions $opts
*/
public function webOutput( $rows, $opts ) {
global $wgRCShowWatchingUsers, $wgShowUpdatedMarker, $wgAllowCategorizedRecentChanges;
- $limit = $opts['limit'];
-
- if( !$this->including() ) {
- // Output options box
- $this->doHeader( $opts );
- }
-
- // And now for the content
- $this->getOutput()->setFeedAppendQuery( $this->getFeedQuery() );
+ // Build the final data
- if( $wgAllowCategorizedRecentChanges ) {
+ if ( $wgAllowCategorizedRecentChanges ) {
$this->filterByCategories( $rows, $opts );
}
+ $limit = $opts['limit'];
+
$showWatcherCount = $wgRCShowWatchingUsers && $this->getUser()->getOption( 'shownumberswatching' );
$watcherCache = array();
@@ -495,23 +462,23 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$counter = 1;
$list = ChangesList::newFromContext( $this->getContext() );
- $s = $list->beginRecentChangesList();
- foreach( $rows as $obj ) {
- if( $limit == 0 ) {
+ $rclistOutput = $list->beginRecentChangesList();
+ foreach ( $rows as $obj ) {
+ if ( $limit == 0 ) {
break;
}
$rc = RecentChange::newFromRow( $obj );
$rc->counter = $counter++;
# Check if the page has been updated since the last visit
- if( $wgShowUpdatedMarker && !empty( $obj->wl_notificationtimestamp ) ) {
+ if ( $wgShowUpdatedMarker && !empty( $obj->wl_notificationtimestamp ) ) {
$rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
} else {
$rc->notificationtimestamp = false; // Default
}
# Check the number of users watching the page
$rc->numberofWatchingusers = 0; // Default
- if( $showWatcherCount && $obj->rc_namespace >= 0 ) {
- if( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
+ if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
+ if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
$watcherCache[$obj->rc_namespace][$obj->rc_title] =
$dbr->selectField(
'watchlist',
@@ -525,27 +492,66 @@ class SpecialRecentChanges extends IncludableSpecialPage {
}
$rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
}
- $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
- --$limit;
+
+ $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
+ if ( $changeLine !== false ) {
+ $rclistOutput .= $changeLine;
+ --$limit;
+ }
+ }
+ $rclistOutput .= $list->endRecentChangesList();
+
+ // Print things out
+
+ if ( !$this->including() ) {
+ // Output options box
+ $this->doHeader( $opts );
+ }
+
+ // And now for the content
+ $feedQuery = $this->getFeedQuery();
+ if ( $feedQuery !== '' ) {
+ $this->getOutput()->setFeedAppendQuery( $feedQuery );
+ } else {
+ $this->getOutput()->setFeedAppendQuery( false );
+ }
+
+ if ( $rows->numRows() === 0 ) {
+ $this->getOutput()->wrapWikiMsg(
+ "<div class='mw-changeslist-empty'>\n$1\n</div>", 'recentchanges-noresult'
+ );
+ } else {
+ $this->getOutput()->addHTML( $rclistOutput );
}
- $s .= $list->endRecentChangesList();
- $this->getOutput()->addHTML( $s );
}
/**
* Get the query string to append to feed link URLs.
- * This is overridden by RCL to add the target parameter
- * @return bool
+ *
+ * @return string
*/
public function getFeedQuery() {
- return false;
+ global $wgFeedLimit;
+
+ $this->getOptions()->validateIntBounds( 'limit', 0, $wgFeedLimit );
+ $options = $this->getOptions()->getChangedValues();
+
+ // wfArrayToCgi() omits options set to null or false
+ foreach ( $options as &$value ) {
+ if ( $value === false ) {
+ $value = '0';
+ }
+ }
+ unset( $value );
+
+ return wfArrayToCgi( $options );
}
/**
* Return the text to be displayed above the changes
*
- * @param $opts FormOptions
- * @return String: XHTML
+ * @param FormOptions $opts
+ * @return string XHTML
*/
public function doHeader( $opts ) {
global $wgScript;
@@ -554,10 +560,6 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$defaults = $opts->getAllValues();
$nondefaults = $opts->getChangedValues();
- $opts->consumeValues( array(
- 'namespace', 'invert', 'associated', 'tagfilter',
- 'categories', 'categories_any'
- ) );
$panel = array();
$panel[] = $this->optionsPanel( $defaults, $nondefaults );
@@ -569,24 +571,36 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$submit = ' ' . Xml::submitbutton( $this->msg( 'allpagessubmit' )->text() );
$out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) );
- foreach( $extraOpts as $name => $optionRow ) {
+ foreach ( $extraOpts as $name => $optionRow ) {
# Add submit button to the last row only
++$count;
$addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
$out .= Xml::openElement( 'tr' );
- if( is_array( $optionRow ) ) {
- $out .= Xml::tags( 'td', array( 'class' => 'mw-label mw-' . $name . '-label' ), $optionRow[0] );
- $out .= Xml::tags( 'td', array( 'class' => 'mw-input' ), $optionRow[1] . $addSubmit );
+ if ( is_array( $optionRow ) ) {
+ $out .= Xml::tags(
+ 'td',
+ array( 'class' => 'mw-label mw-' . $name . '-label' ),
+ $optionRow[0]
+ );
+ $out .= Xml::tags(
+ 'td',
+ array( 'class' => 'mw-input' ),
+ $optionRow[1] . $addSubmit
+ );
} else {
- $out .= Xml::tags( 'td', array( 'class' => 'mw-input', 'colspan' => 2 ), $optionRow . $addSubmit );
+ $out .= Xml::tags(
+ 'td',
+ array( 'class' => 'mw-input', 'colspan' => 2 ),
+ $optionRow . $addSubmit
+ );
}
$out .= Xml::closeElement( 'tr' );
}
$out .= Xml::closeElement( 'table' );
$unconsumed = $opts->getUnconsumedValues();
- foreach( $unconsumed as $key => $value ) {
+ foreach ( $unconsumed as $key => $value ) {
$out .= Html::hidden( $key, $value );
}
@@ -597,7 +611,11 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$panelString = implode( "\n", $panel );
$this->getOutput()->addHTML(
- Xml::fieldset( $this->msg( 'recentchanges-legend' )->text(), $panelString, array( 'class' => 'rcoptions' ) )
+ Xml::fieldset(
+ $this->msg( 'recentchanges-legend' )->text(),
+ $panelString,
+ array( 'class' => 'rcoptions' )
+ )
);
$this->setBottomText( $opts );
@@ -606,15 +624,19 @@ class SpecialRecentChanges extends IncludableSpecialPage {
/**
* Get options to be displayed in a form
*
- * @param $opts FormOptions
- * @return Array
+ * @param FormOptions $opts
+ * @return array
*/
function getExtraOptions( $opts ) {
+ $opts->consumeValues( array(
+ 'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
+ ) );
+
$extraOpts = array();
$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
global $wgAllowCategorizedRecentChanges;
- if( $wgAllowCategorizedRecentChanges ) {
+ if ( $wgAllowCategorizedRecentChanges ) {
$extraOpts['category'] = $this->categoryFilterForm( $opts );
}
@@ -623,14 +645,18 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$extraOpts['tagfilter'] = $tagFilter;
}
- wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
+ // Don't fire the hook for subclasses. (Or should we?)
+ if ( $this->getName() === 'Recentchanges' ) {
+ wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
+ }
+
return $extraOpts;
}
/**
* Send the text to be displayed above the options
*
- * @param $opts FormOptions
+ * @param FormOptions $opts Unused
*/
function setTopText( FormOptions $opts ) {
global $wgContLang;
@@ -649,19 +675,19 @@ class SpecialRecentChanges extends IncludableSpecialPage {
}
/**
- * Send the text to be displayed after the options, for use in
- * Recentchangeslinked
+ * Send the text to be displayed after the options, for use in subclasses.
*
- * @param $opts FormOptions
+ * @param FormOptions $opts
*/
- function setBottomText( FormOptions $opts ) {}
+ function setBottomText( FormOptions $opts ) {
+ }
/**
* Creates the choose namespace selection
*
* @todo Uses radio buttons (HASHAR)
- * @param $opts FormOptions
- * @return String
+ * @param FormOptions $opts
+ * @return string
*/
protected function namespaceFilterForm( FormOptions $opts ) {
$nsSelect = Html::namespaceSelector(
@@ -679,14 +705,15 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$opts['associated'],
array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
);
+
return array( $nsLabel, "$nsSelect $invert $associated" );
}
/**
* Create a input to filter changes by categories
*
- * @param $opts FormOptions
- * @return Array
+ * @param FormOptions $opts
+ * @return array
*/
protected function categoryFilterForm( FormOptions $opts ) {
list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
@@ -701,21 +728,21 @@ class SpecialRecentChanges extends IncludableSpecialPage {
/**
* Filter $rows by categories set in $opts
*
- * @param $rows Array of database rows
- * @param $opts FormOptions
+ * @param array $rows Database rows
+ * @param FormOptions $opts
*/
function filterByCategories( &$rows, FormOptions $opts ) {
- $categories = array_map( 'trim', explode( '|' , $opts['categories'] ) );
+ $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
- if( !count( $categories ) ) {
+ if ( !count( $categories ) ) {
return;
}
# Filter categories
$cats = array();
- foreach( $categories as $cat ) {
+ foreach ( $categories as $cat ) {
$cat = trim( $cat );
- if( $cat == '' ) {
+ if ( $cat == '' ) {
continue;
}
$cats[] = $cat;
@@ -725,16 +752,16 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$articles = array();
$a2r = array();
$rowsarr = array();
- foreach( $rows as $k => $r ) {
+ foreach ( $rows as $k => $r ) {
$nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
$id = $nt->getArticleID();
- if( $id == 0 ) {
+ if ( $id == 0 ) {
continue; # Page might have been deleted...
}
- if( !in_array( $id, $articles ) ) {
+ if ( !in_array( $id, $articles ) ) {
$articles[] = $id;
}
- if( !isset( $a2r[$id] ) ) {
+ if ( !isset( $a2r[$id] ) ) {
$a2r[$id] = array();
}
$a2r[$id][] = $k;
@@ -742,7 +769,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
}
# Shortcut?
- if( !count( $articles ) || !count( $cats ) ) {
+ if ( !count( $articles ) || !count( $cats ) ) {
return;
}
@@ -753,8 +780,8 @@ class SpecialRecentChanges extends IncludableSpecialPage {
# Filter
$newrows = array();
- foreach( $match as $id ) {
- foreach( $a2r[$id] as $rev ) {
+ foreach ( $match as $id ) {
+ foreach ( $a2r[$id] as $rev ) {
$k = $rev;
$newrows[$k] = $rowsarr[$k];
}
@@ -765,10 +792,10 @@ class SpecialRecentChanges extends IncludableSpecialPage {
/**
* Makes change an option link which carries all the other options
*
- * @param $title Title
- * @param $override Array: options to override
- * @param $options Array: current options
- * @param $active Boolean: whether to show the link in bold
+ * @param string $title Title
+ * @param array $override Options to override
+ * @param array $options Current options
+ * @param bool $active Whether to show the link in bold
* @return string
*/
function makeOptionsLink( $title, $override, $options, $active = false ) {
@@ -787,14 +814,15 @@ class SpecialRecentChanges extends IncludableSpecialPage {
if ( $active ) {
$text = '<strong>' . $text . '</strong>';
}
+
return Linker::linkKnown( $this->getTitle(), $text, array(), $params );
}
/**
* Creates the options panel.
*
- * @param $defaults Array
- * @param $nondefaults Array
+ * @param array $defaults
+ * @param array $nondefaults
* @return string
*/
function optionsPanel( $defaults, $nondefaults ) {
@@ -804,13 +832,13 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$note = '';
$msg = $this->msg( 'rclegend' );
- if( !$msg->isDisabled() ) {
+ if ( !$msg->isDisabled() ) {
$note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
}
$lang = $this->getLanguage();
$user = $this->getUser();
- if( $options['from'] ) {
+ if ( $options['from'] ) {
$note .= $this->msg( 'rcnotefrom' )->numParams( $options['limit'] )->params(
$lang->userTimeAndDate( $options['from'], $user ),
$lang->userDate( $options['from'], $user ),
@@ -818,37 +846,41 @@ class SpecialRecentChanges extends IncludableSpecialPage {
}
# Sort data for display and make sure it's unique after we've added user data.
- $wgRCLinkLimits[] = $options['limit'];
- $wgRCLinkDays[] = $options['days'];
- sort( $wgRCLinkLimits );
- sort( $wgRCLinkDays );
- $wgRCLinkLimits = array_unique( $wgRCLinkLimits );
- $wgRCLinkDays = array_unique( $wgRCLinkDays );
+ $linkLimits = $wgRCLinkLimits;
+ $linkLimits[] = $options['limit'];
+ sort( $linkLimits );
+ $linkLimits = array_unique( $linkLimits );
+
+ $linkDays = $wgRCLinkDays;
+ $linkDays[] = $options['days'];
+ sort( $linkDays );
+ $linkDays = array_unique( $linkDays );
// limit links
- foreach( $wgRCLinkLimits as $value ) {
+ $cl = array();
+ foreach ( $linkLimits as $value ) {
$cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
array( 'limit' => $value ), $nondefaults, $value == $options['limit'] );
}
$cl = $lang->pipeList( $cl );
// day links, reset 'from' to none
- foreach( $wgRCLinkDays as $value ) {
+ $dl = array();
+ foreach ( $linkDays as $value ) {
$dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] );
}
$dl = $lang->pipeList( $dl );
-
// show/hide links
$showhide = array( $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() );
$filters = array(
- 'hideminor' => 'rcshowhideminor',
- 'hidebots' => 'rcshowhidebots',
- 'hideanons' => 'rcshowhideanons',
- 'hideliu' => 'rcshowhideliu',
+ 'hideminor' => 'rcshowhideminor',
+ 'hidebots' => 'rcshowhidebots',
+ 'hideanons' => 'rcshowhideanons',
+ 'hideliu' => 'rcshowhideliu',
'hidepatrolled' => 'rcshowhidepatr',
- 'hidemyself' => 'rcshowhidemine'
+ 'hidemyself' => 'rcshowhidemine'
);
foreach ( $this->getCustomFilters() as $key => $params ) {
$filters[$key] = $params['msg'];
@@ -861,7 +893,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$links = array();
foreach ( $filters as $key => $msg ) {
$link = $this->makeOptionsLink( $showhide[1 - $options[$key]],
- array( $key => 1-$options[$key] ), $nondefaults );
+ array( $key => 1 - $options[$key] ), $nondefaults );
$links[] = $this->msg( $msg )->rawParams( $link )->escaped();
}
@@ -872,17 +904,23 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$now, array( 'from' => $timestamp ), $nondefaults
);
- $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) )->parse();
+ $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) )
+ ->parse();
$rclistfrom = $this->msg( 'rclistfrom' )->rawParams( $tl )->parse();
+
return "{$note}$rclinks<br />$rclistfrom";
}
/**
- * add javascript specific to the [[Special:RecentChanges]] page
+ * Add page-specific modules.
*/
- function addRecentChangesJS() {
+ protected function addModules() {
$this->getOutput()->addModules( array(
'mediawiki.special.recentchanges',
) );
}
+
+ protected function getGroupName() {
+ return 'changes';
+ }
}
diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php
index 862736d3..a8447046 100644
--- a/includes/specials/SpecialRecentchangeslinked.php
+++ b/includes/specials/SpecialRecentchangeslinked.php
@@ -29,7 +29,7 @@
class SpecialRecentchangeslinked extends SpecialRecentChanges {
var $rclTargetTitle;
- function __construct(){
+ function __construct() {
parent::__construct( 'Recentchangeslinked' );
}
@@ -37,7 +37,6 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
$opts = parent::getDefaultOptions();
$opts->add( 'target', '' );
$opts->add( 'showlinkedto', false );
- $opts->add( 'tagfilter', '' );
return $opts;
}
@@ -51,13 +50,13 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
return $opts;
}
- public function getFeedObject( $feedFormat ){
+ public function getFeedObject( $feedFormat ) {
$feed = new ChangesFeed( $feedFormat, false );
$feedObj = $feed->getFeedObject(
$this->msg( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() )
->inContentLanguage()->text(),
$this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(),
- $this->getTitle()->getFullUrl()
+ $this->getTitle()->getFullURL()
);
return array( $feed, $feedObj );
}
@@ -72,8 +71,8 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
}
$outputPage = $this->getOutput();
$title = Title::newFromURL( $target );
- if( !$title || $title->getInterwiki() != '' ){
- $outputPage->wrapWikiMsg( "<div class=\"errorbox\">\n$1\n</div><br style=\"clear: both\" />", 'allpagesbadtitle' );
+ if ( !$title || $title->getInterwiki() != '' ) {
+ $outputPage->wrapWikiMsg( "<div class=\"errorbox\">\n$1\n</div>", 'allpagesbadtitle' );
return false;
}
@@ -94,20 +93,24 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
$dbkey = $title->getDBkey();
$tables = array( 'recentchanges' );
- $select = array( $dbr->tableName( 'recentchanges' ) . '.*' );
+ $select = RecentChange::selectFields();
$join_conds = array();
$query_options = array();
// left join with watchlist table to highlight watched rows
$uid = $this->getUser()->getId();
- if( $uid ) {
+ if ( $uid && $this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
$tables[] = 'watchlist';
$select[] = 'wl_user';
- $join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" );
+ $join_conds['watchlist'] = array( 'LEFT JOIN', array(
+ 'wl_user' => $uid,
+ 'wl_title=rc_title',
+ 'wl_namespace=rc_namespace'
+ ));
}
if ( $this->getUser()->isAllowed( 'rollback' ) ) {
$tables[] = 'page';
- $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id');
+ $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' );
$select[] = 'page_latest';
}
ChangeTags::modifyDisplayQuery(
@@ -123,21 +126,21 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
return false;
}
- if( $ns == NS_CATEGORY && !$showlinkedto ) {
+ if ( $ns == NS_CATEGORY && !$showlinkedto ) {
// special handling for categories
- // XXX: should try to make this less klugy
+ // XXX: should try to make this less kludgy
$link_tables = array( 'categorylinks' );
$showlinkedto = true;
} else {
// for now, always join on these tables; really should be configurable as in whatlinkshere
$link_tables = array( 'pagelinks', 'templatelinks' );
// imagelinks only contains links to pages in NS_FILE
- if( $ns == NS_FILE || !$showlinkedto ) {
+ if ( $ns == NS_FILE || !$showlinkedto ) {
$link_tables[] = 'imagelinks';
}
}
- if( $id == 0 && !$showlinkedto ) {
+ if ( $id == 0 && !$showlinkedto ) {
return false; // nonexistent pages can't link to any pages
}
@@ -146,22 +149,22 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
$subsql = array(); // SELECT statements to combine with UNION
- foreach( $link_tables as $link_table ) {
+ foreach ( $link_tables as $link_table ) {
$pfx = $prefix[$link_table];
// imagelinks and categorylinks tables have no xx_namespace field, and have xx_to instead of xx_title
- if( $link_table == 'imagelinks' ) {
+ if ( $link_table == 'imagelinks' ) {
$link_ns = NS_FILE;
- } elseif( $link_table == 'categorylinks' ) {
+ } elseif ( $link_table == 'categorylinks' ) {
$link_ns = NS_CATEGORY;
} else {
$link_ns = 0;
}
- if( $showlinkedto ) {
+ if ( $showlinkedto ) {
// find changes to pages linking to this page
- if( $link_ns ) {
- if( $ns != $link_ns ) {
+ if ( $link_ns ) {
+ if ( $ns != $link_ns ) {
continue;
} // should never happen, but check anyway
$subconds = array( "{$pfx}_to" => $dbkey );
@@ -172,15 +175,15 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
} else {
// find changes to pages linked from this page
$subconds = array( "{$pfx}_from" => $id );
- if( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
+ if ( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
$subconds["rc_namespace"] = $link_ns;
$subjoin = "rc_title = {$pfx}_to";
} else {
- $subjoin = "rc_namespace = {$pfx}_namespace AND rc_title = {$pfx}_title";
+ $subjoin = array( "rc_namespace = {$pfx}_namespace", "rc_title = {$pfx}_title" );
}
}
- if( $dbr->unionSupportsOrderAndLimit()) {
+ if ( $dbr->unionSupportsOrderAndLimit() ) {
$order = array( 'ORDER BY' => 'rc_timestamp DESC' );
} else {
$order = array();
@@ -195,26 +198,27 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
$join_conds + array( $link_table => array( 'INNER JOIN', $subjoin ) )
);
- if( $dbr->unionSupportsOrderAndLimit())
+ if ( $dbr->unionSupportsOrderAndLimit() ) {
$query = $dbr->limitResult( $query, $limit );
+ }
$subsql[] = $query;
}
- if( count($subsql) == 0 ) {
+ if ( count( $subsql ) == 0 ) {
return false; // should never happen
}
- if( count($subsql) == 1 && $dbr->unionSupportsOrderAndLimit() ) {
+ if ( count( $subsql ) == 1 && $dbr->unionSupportsOrderAndLimit() ) {
$sql = $subsql[0];
} else {
// need to resort and relimit after union
- $sql = $dbr->unionQueries($subsql, false).' ORDER BY rc_timestamp DESC';
- $sql = $dbr->limitResult($sql, $limit, false);
+ $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 ) {
+ if ( $res->numRows() == 0 ) {
$this->mResultEmpty = true;
}
@@ -222,21 +226,21 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
}
/**
- * @param $opts FormOptions
+ * Get options to be displayed in a form
+ *
+ * @param FormOptions $opts
* @return array
*/
- function getExtraOptions( $opts ){
- $opts->consumeValues( array( 'showlinkedto', 'target', 'tagfilter' ) );
- $extraOpts = array();
- $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
+ function getExtraOptions( $opts ) {
+ $extraOpts = parent::getExtraOptions( $opts );
+
+ $opts->consumeValues( array( 'showlinkedto', 'target' ) );
+
$extraOpts['target'] = array( $this->msg( 'recentchangeslinked-page' )->escaped(),
- Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) .
- Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' .
+ Xml::input( 'target', 40, str_replace( '_', ' ', $opts['target'] ) ) .
+ Xml::check( 'showlinkedto', $opts['showlinkedto'], array( 'id' => 'showlinkedto' ) ) . ' ' .
Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) );
- $tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
- if ($tagFilter) {
- $extraOpts['tagfilter'] = $tagFilter;
- }
+
return $extraOpts;
}
@@ -257,23 +261,8 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
function setTopText( FormOptions $opts ) {
$target = $this->getTargetTitle();
- if( $target ) {
+ if ( $target ) {
$this->getOutput()->addBacklinkSubtitle( $target );
}
}
-
- public function getFeedQuery() {
- $target = $this->getTargetTitle();
- if( $target ) {
- return "target=" . urlencode( $target->getPrefixedDBkey() );
- } else {
- return false;
- }
- }
-
- function setBottomText( FormOptions $opts ) {
- if( isset( $this->mResultEmpty ) && $this->mResultEmpty ) {
- $this->getOutput()->addWikiMsg( 'recentchangeslinked-noresult' );
- }
- }
}
diff --git a/includes/specials/SpecialRedirect.php b/includes/specials/SpecialRedirect.php
new file mode 100644
index 00000000..f05dacbc
--- /dev/null
+++ b/includes/specials/SpecialRedirect.php
@@ -0,0 +1,235 @@
+<?php
+/**
+ * Implements Special:Redirect
+ *
+ * @section LICENSE
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page that redirects to: the user for a numeric user id,
+ * the file for a given filename, or the page for a given revision id.
+ *
+ * @ingroup SpecialPage
+ * @since 1.22
+ */
+class SpecialRedirect extends FormSpecialPage {
+
+ /**
+ * The type of the redirect (user/file/revision)
+ *
+ * @var string $mType
+ * @example 'user'
+ */
+ protected $mType;
+
+ /**
+ * The identifier/value for the redirect (which id, which file)
+ *
+ * @var string $mValue
+ * @example '42'
+ */
+ protected $mValue;
+
+ function __construct() {
+ parent::__construct( 'Redirect' );
+ $this->mType = null;
+ $this->mValue = null;
+ }
+
+ /**
+ * Set $mType and $mValue based on parsed value of $subpage.
+ */
+ function setParameter( $subpage ) {
+ // parse $subpage to pull out the parts
+ $parts = explode( '/', $subpage, 2 );
+ $this->mType = count( $parts ) > 0 ? $parts[0] : null;
+ $this->mValue = count( $parts ) > 1 ? $parts[1] : null;
+ }
+
+ /**
+ * Handle Special:Redirect/user/xxxx (by redirecting to User:YYYY)
+ *
+ * @return string|null url to redirect to, or null if $mValue is invalid.
+ */
+ function dispatchUser() {
+ if ( !ctype_digit( $this->mValue ) ) {
+ return null;
+ }
+ $user = User::newFromId( (int)$this->mValue );
+ $username = $user->getName(); // load User as side-effect
+ if ( $user->isAnon() ) {
+ return null;
+ }
+ $userpage = Title::makeTitle( NS_USER, $username );
+ return $userpage->getFullURL( '', false, PROTO_CURRENT );
+ }
+
+ /**
+ * Handle Special:Redirect/file/xxxx
+ *
+ * @return string|null url to redirect to, or null if $mValue is not found.
+ */
+ function dispatchFile() {
+ $title = Title::makeTitleSafe( NS_FILE, $this->mValue );
+
+ if ( ! $title instanceof Title ) {
+ return null;
+ }
+ $file = wfFindFile( $title );
+
+ if ( !$file || !$file->exists() ) {
+ return null;
+ }
+ // Default behavior: Use the direct link to the file.
+ $url = $file->getURL();
+ $request = $this->getRequest();
+ $width = $request->getInt( 'width', -1 );
+ $height = $request->getInt( 'height', -1 );
+
+ // If a width is requested...
+ if ( $width != -1 ) {
+ $mto = $file->transform( array( 'width' => $width, 'height' => $height ) );
+ // ... and we can
+ if ( $mto && !$mto->isError() ) {
+ // ... change the URL to point to a thumbnail.
+ $url = $mto->getURL();
+ }
+ }
+ return $url;
+ }
+
+ /**
+ * Handle Special:Redirect/revision/xxx
+ * (by redirecting to index.php?oldid=xxx)
+ *
+ * @return string|null url to redirect to, or null if $mValue is invalid.
+ */
+ function dispatchRevision() {
+ $oldid = $this->mValue;
+ if ( !ctype_digit( $oldid ) ) {
+ return null;
+ }
+ $oldid = (int)$oldid;
+ if ( $oldid === 0 ) {
+ return null;
+ }
+ return wfAppendQuery( wfScript( 'index' ), array(
+ 'oldid' => $oldid
+ ) );
+ }
+
+ /**
+ * Use appropriate dispatch* method to obtain a redirection URL,
+ * and either: redirect, set a 404 error code and error message,
+ * or do nothing (if $mValue wasn't set) allowing the form to be
+ * displayed.
+ *
+ * @return bool true if a redirect was successfully handled.
+ */
+ function dispatch() {
+ // the various namespaces supported by Special:Redirect
+ switch ( $this->mType ) {
+ case 'user':
+ $url = $this->dispatchUser();
+ break;
+ case 'file':
+ $url = $this->dispatchFile();
+ break;
+ case 'revision':
+ $url = $this->dispatchRevision();
+ break;
+ default:
+ $this->getOutput()->setStatusCode( 404 );
+ $url = null;
+ break;
+ }
+ if ( $url ) {
+ $this->getOutput()->redirect( $url );
+ return true;
+ }
+ if ( !is_null( $this->mValue ) ) {
+ $this->getOutput()->setStatusCode( 404 );
+ // Message: redirect-not-exists
+ $msg = $this->getMessagePrefix() . '-not-exists';
+ return Status::newFatal( $msg );
+ }
+ return false;
+ }
+
+ protected function getFormFields() {
+ $mp = $this->getMessagePrefix();
+ $ns = array(
+ // subpage => message
+ // Messages: redirect-user, redirect-revision, redirect-file
+ 'user' => $mp . '-user',
+ 'revision' => $mp . '-revision',
+ 'file' => $mp . '-file',
+ );
+ $a = array();
+ $a['type'] = array(
+ 'type' => 'select',
+ 'label-message' => $mp . '-lookup', // Message: redirect-lookup
+ 'options' => array(),
+ 'default' => current( array_keys( $ns ) ),
+ );
+ foreach ( $ns as $n => $m ) {
+ $m = $this->msg( $m )->text();
+ $a['type']['options'][$m] = $n;
+ }
+ $a['value'] = array(
+ 'type' => 'text',
+ 'label-message' => $mp . '-value' // Message: redirect-value
+ );
+ // set the defaults according to the parsed subpage path
+ if ( !empty( $this->mType ) ) {
+ $a['type']['default'] = $this->mType;
+ }
+ if ( !empty( $this->mValue ) ) {
+ $a['value']['default'] = $this->mValue;
+ }
+ return $a;
+ }
+
+ public function onSubmit( array $data ) {
+ if ( !empty( $data['type'] ) && !empty( $data['value'] ) ) {
+ $this->setParameter( $data['type'] . '/' . $data['value'] );
+ }
+ /* if this returns false, will show the form */
+ return $this->dispatch();
+ }
+
+ public function onSuccess() {
+ /* do nothing, we redirect in $this->dispatch if successful. */
+ }
+
+ protected function alterForm( HTMLForm $form ) {
+ /* display summary at top of page */
+ $this->outputHeader();
+ // tweak label on submit button
+ // Message: redirect-submit
+ $form->setSubmitTextMsg( $this->getMessagePrefix() . '-submit' );
+ /* submit form every time */
+ $form->setMethod( 'get' );
+ }
+
+ protected function getGroupName() {
+ return 'redirects';
+ }
+}
diff --git a/includes/specials/SpecialResetTokens.php b/includes/specials/SpecialResetTokens.php
new file mode 100644
index 00000000..ef2a45da
--- /dev/null
+++ b/includes/specials/SpecialResetTokens.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Implements Special:ResetTokens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Let users reset tokens like the watchlist token.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialResetTokens extends FormSpecialPage {
+ private $tokensList;
+
+ public function __construct() {
+ parent::__construct( 'ResetTokens' );
+ }
+
+ /**
+ * Returns the token information list for this page after running
+ * the hook and filtering out disabled preferences.
+ *
+ * @return array
+ */
+ protected function getTokensList() {
+ global $wgHiddenPrefs;
+
+ if ( !isset( $this->tokensList ) ) {
+ $tokens = array(
+ array( 'preference' => 'watchlisttoken', 'label-message' => 'resettokens-watchlist-token' ),
+ );
+ wfRunHooks( 'SpecialResetTokensTokens', array( &$tokens ) );
+
+ $tokens = array_filter( $tokens, function ( $tok ) use ( $wgHiddenPrefs ) {
+ return !in_array( $tok['preference'], $wgHiddenPrefs );
+ } );
+
+ $this->tokensList = $tokens;
+ }
+
+ return $this->tokensList;
+ }
+
+ public function execute( $par ) {
+ // This is a preferences page, so no user JS for y'all.
+ $this->getOutput()->disallowUserJs();
+
+ parent::execute( $par );
+
+ $this->getOutput()->addReturnTo( SpecialPage::getTitleFor( 'Preferences' ) );
+ }
+
+ public function onSuccess() {
+ $this->getOutput()->wrapWikiMsg(
+ "<div class='successbox'>\n$1\n</div>",
+ 'resettokens-done'
+ );
+ }
+
+ /**
+ * Display appropriate message if there's nothing to do.
+ * The submit button is also suppressed in this case (see alterForm()).
+ */
+ protected function getFormFields() {
+ $user = $this->getUser();
+ $tokens = $this->getTokensList();
+
+ if ( $tokens ) {
+ $tokensForForm = array();
+ foreach ( $tokens as $tok ) {
+ $label = $this->msg( 'resettokens-token-label' )
+ ->rawParams( $this->msg( $tok['label-message'] )->parse() )
+ ->params( $user->getTokenFromOption( $tok['preference'] ) )
+ ->escaped();
+ $tokensForForm[ $label ] = $tok['preference'];
+ }
+
+ $desc = array(
+ 'label-message' => 'resettokens-tokens',
+ 'type' => 'multiselect',
+ 'options' => $tokensForForm,
+ );
+ } else {
+ $desc = array(
+ 'label-message' => 'resettokens-no-tokens',
+ 'type' => 'info',
+ );
+ }
+
+ return array(
+ 'tokens' => $desc,
+ );
+ }
+
+ /**
+ * Suppress the submit button if there's nothing to do;
+ * provide additional message on it otherwise.
+ */
+ protected function alterForm( HTMLForm $form ) {
+ if ( $this->getTokensList() ) {
+ $form->setSubmitTextMsg( 'resettokens-resetbutton' );
+ } else {
+ $form->suppressDefaultSubmit();
+ }
+ }
+
+ public function onSubmit( array $formData ) {
+ if ( $formData['tokens'] ) {
+ $user = $this->getUser();
+ foreach ( $formData['tokens'] as $tokenPref ) {
+ $user->resetTokenFromOption( $tokenPref );
+ }
+ $user->saveSettings();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function getGroupName() {
+ return 'users';
+ }
+
+ public function isListed() {
+ return (bool)$this->getTokensList();
+ }
+}
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
index aba90cf8..825be6c4 100644
--- a/includes/specials/SpecialRevisiondelete.php
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -49,68 +49,43 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
/** Array of checkbox specs (message, name, deletion bits) */
var $checks;
- /** Information about the current type */
- var $typeInfo;
+ /** UI Labels about the current type */
+ var $typeLabels;
/** 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
+ * UI labels for each type.
*/
- static $allowedTypes = array(
+ static $UILabels = array(
'revision' => array(
'check-label' => 'revdelete-hide-text',
- 'deletion-bits' => Revision::DELETED_TEXT,
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
- 'list-class' => 'RevDel_RevisionList',
- 'permission' => 'deleterevision',
),
'archive' => array(
'check-label' => 'revdelete-hide-text',
- 'deletion-bits' => Revision::DELETED_TEXT,
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
- 'list-class' => 'RevDel_ArchiveList',
- 'permission' => 'deleterevision',
),
- 'oldimage'=> array(
+ 'oldimage' => array(
'check-label' => 'revdelete-hide-image',
- 'deletion-bits' => File::DELETED_FILE,
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
- 'list-class' => 'RevDel_FileList',
- 'permission' => 'deleterevision',
),
'filearchive' => array(
'check-label' => 'revdelete-hide-image',
- 'deletion-bits' => File::DELETED_FILE,
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
- 'list-class' => 'RevDel_ArchivedFileList',
- 'permission' => 'deleterevision',
),
'logging' => array(
'check-label' => 'revdelete-hide-name',
- 'deletion-bits' => LogPage::DELETED_ACTION,
'success' => 'logdelete-success',
'failure' => 'logdelete-failure',
- 'list-class' => 'RevDel_LogList',
- 'permission' => 'deletelogentry',
),
);
- /** 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', 'deletedhistory' );
}
@@ -133,7 +108,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$this->ids = explode( ',', $ids );
} else {
# Array input
- $this->ids = array_keys( $request->getArray('ids',array()) );
+ $this->ids = array_keys( $request->getArray( 'ids', array() ) );
}
// $this->ids = array_map( 'intval', $this->ids );
$this->ids = array_unique( array_filter( $this->ids ) );
@@ -147,24 +122,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
} else {
$this->typeName = $request->getVal( 'type' );
$this->targetObj = Title::newFromText( $request->getText( 'target' ) );
- if ( $this->targetObj->isSpecial( 'Log' ) ) {
- $result = wfGetDB( DB_SLAVE )->select( 'logging',
- 'log_type',
- array( 'log_id' => $this->ids ),
- __METHOD__,
- array( 'DISTINCT' )
- );
-
- $logTypes = array();
- foreach ( $result as $row ) {
- $logTypes[] = $row->log_type;
- }
-
- if ( count( $logTypes ) == 1 ) {
- // If there's only one type, the target can be set to include it.
- $this->targetObj = SpecialPage::getTitleFor( 'Log', $logTypes[0] );
- }
- }
}
# For reviewing deleted files...
@@ -175,28 +132,21 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
return;
}
- if ( isset( self::$deprecatedTypeMap[$this->typeName] ) ) {
- $this->typeName = self::$deprecatedTypeMap[$this->typeName];
- }
+ $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName );
# No targets?
- if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) {
+ if ( !$this->typeName || count( $this->ids ) == 0 ) {
throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
}
- $this->typeInfo = self::$allowedTypes[$this->typeName];
- $this->mIsAllowed = $user->isAllowed( $this->typeInfo['permission'] );
-
- # 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( $this->typeName == 'revision' ) {
- $rev = Revision::newFromId( $this->ids[0] );
- $this->targetObj = $rev ? $rev->getTitle() : $this->targetObj;
- }
+ $this->typeLabels = self::$UILabels[$this->typeName];
+ $this->mIsAllowed = $user->isAllowed( RevisionDeleter::getRestriction( $this->typeName ) );
+
+ # Allow the list type to adjust the passed target
+ $this->targetObj = RevisionDeleter::suggestTarget( $this->typeName, $this->targetObj, $this->ids );
$this->otherReason = $request->getVal( 'wpReason' );
# We need a target page!
- if( is_null($this->targetObj) ) {
+ if ( is_null( $this->targetObj ) ) {
$output->addWikiMsg( 'undelete-header' );
return;
}
@@ -205,17 +155,19 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
# Initialise checkboxes
$this->checks = array(
- array( $this->typeInfo['check-label'], 'wpHidePrimary', $this->typeInfo['deletion-bits'] ),
+ array( $this->typeLabels['check-label'], 'wpHidePrimary',
+ RevisionDeleter::getRevdelConstant( $this->typeName )
+ ),
array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER )
);
- if( $user->isAllowed('suppressrevision') ) {
+ if ( $user->isAllowed( 'suppressrevision' ) ) {
$this->checks[] = array( 'revdelete-hide-restricted',
'wpHideRestricted', Revision::DELETED_RESTRICTED );
}
# Either submit or create our form
- if( $this->mIsAllowed && $this->submitClicked ) {
+ if ( $this->mIsAllowed && $this->submitClicked ) {
$this->submit( $request );
} else {
$this->showForm();
@@ -228,9 +180,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
LogEventsList::showLogExtract( $output, 'delete',
$this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) );
# Show relevant lines from the suppression log
- if( $user->isAllowed( 'suppressionlog' ) ) {
+ if ( $user->isAllowed( 'suppressionlog' ) ) {
$suppressLogPage = new LogPage( 'suppress' );
- $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" );
+ $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" );
LogEventsList::showLogExtract( $output, 'suppress',
$this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) );
}
@@ -241,7 +193,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
*/
protected function showConvenienceLinks() {
# Give a link to the logs/hist for this page
- if( $this->targetObj ) {
+ if ( $this->targetObj ) {
$links = array();
$links[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Log' ),
@@ -258,7 +210,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
array( 'action' => 'history' )
);
# Link to deleted edits
- if( $this->getUser()->isAllowed('undelete') ) {
+ if ( $this->getUser()->isAllowed( 'undelete' ) ) {
$undelete = SpecialPage::getTitleFor( 'Undelete' );
$links[] = Linker::linkKnown(
$undelete,
@@ -301,8 +253,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
return;
}
$user = $this->getUser();
- if( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
- if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
+ if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
+ if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
throw new PermissionsError( 'suppressrevision' );
} else {
throw new PermissionsError( 'deletedtext' );
@@ -317,10 +269,11 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$this->getOutput()->addHTML(
Xml::openElement( 'form', array(
'method' => 'POST',
- 'action' => $this->getTitle()->getLocalUrl(
- 'target=' . urlencode( $this->targetObj->getPrefixedDBkey() ) .
- '&file=' . urlencode( $archiveName ) .
- '&token=' . urlencode( $user->getEditToken( $archiveName ) ) )
+ 'action' => $this->getTitle()->getLocalURL( array(
+ 'target' => $this->targetObj->getPrefixedDBkey(),
+ 'file' => $archiveName,
+ 'token' => $user->getEditToken( $archiveName ),
+ ) )
)
) .
Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) .
@@ -347,8 +300,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
*/
protected function getList() {
if ( is_null( $this->list ) ) {
- $class = $this->typeInfo['list-class'];
- $this->list = new $class( $this->getContext(), $this->targetObj, $this->ids );
+ $this->list = RevisionDeleter::createList(
+ $this->typeName, $this->getContext(), $this->targetObj, $this->ids
+ );
}
return $this->list;
}
@@ -361,7 +315,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$UserAllowed = true;
if ( $this->typeName == 'logging' ) {
- $this->getOutput()->addWikiMsg( 'logdelete-selected', $this->getLanguage()->formatNum( count($this->ids) ) );
+ $this->getOutput()->addWikiMsg( 'logdelete-selected', $this->getLanguage()->formatNum( count( $this->ids ) ) );
} else {
$this->getOutput()->addWikiMsg( 'revdelete-selected',
$this->targetObj->getPrefixedText(), count( $this->ids ) );
@@ -375,7 +329,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
for ( $list->reset(); $list->current(); $list->next() ) {
$item = $list->current();
if ( !$item->canView() ) {
- if( !$this->submitClicked ) {
+ if ( !$this->submitClicked ) {
throw new PermissionsError( 'suppressrevision' );
}
$UserAllowed = false;
@@ -384,7 +338,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$this->getOutput()->addHTML( $item->getHTML() );
}
- if( !$numRevisions ) {
+ if ( !$numRevisions ) {
throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
}
@@ -393,12 +347,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$this->addUsageText();
// Normal sysops can always see what they did, but can't always change it
- if( !$UserAllowed ) return;
+ if ( !$UserAllowed ) {
+ return;
+ }
// Show form if the user can submit
- if( $this->mIsAllowed ) {
+ if ( $this->mIsAllowed ) {
$out = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ),
+ 'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ),
'id' => 'mw-revdel-form-revisions' ) ) .
Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) .
$this->buildCheckBoxes() .
@@ -411,7 +367,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
Xml::listDropDown( 'wpRevDeleteReasonList',
$this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text(),
$this->msg( 'revdelete-reasonotherlist' )->inContentLanguage()->text(),
- '', 'wpReasonDropDown', 1
+ $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ), 'wpReasonDropDown', 1
) .
'</td>' .
"</tr><tr>\n" .
@@ -437,10 +393,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
} else {
$out = '';
}
- if( $this->mIsAllowed ) {
+ if ( $this->mIsAllowed ) {
$out .= Xml::closeElement( 'form' ) . "\n";
// Show link to edit the dropdown reasons
- if( $this->getUser()->isAllowed( 'editinterface' ) ) {
+ if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
$title = Title::makeTitle( NS_MEDIAWIKI, 'Revdelete-reason-dropdown' );
$link = Linker::link(
$title,
@@ -460,32 +416,33 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
*/
protected function addUsageText() {
$this->getOutput()->addWikiMsg( 'revdelete-text' );
- if( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
+ if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
$this->getOutput()->addWikiMsg( 'revdelete-suppress-text' );
}
- if( $this->mIsAllowed ) {
+ if ( $this->mIsAllowed ) {
$this->getOutput()->addWikiMsg( 'revdelete-confirm' );
}
}
/**
- * @return String: HTML
- */
+ * @return String: HTML
+ */
protected function buildCheckBoxes() {
$html = '<table>';
// If there is just one item, use checkboxes
$list = $this->getList();
- if( $list->length() == 1 ) {
+ if ( $list->length() == 1 ) {
$list->reset();
$bitfield = $list->current()->getBits(); // existing field
- if( $this->submitClicked ) {
+ if ( $this->submitClicked ) {
$bitfield = $this->extractBitfield( $this->extractBitParams(), $bitfield );
}
- foreach( $this->checks as $item ) {
+ foreach ( $this->checks as $item ) {
list( $message, $name, $field ) = $item;
$innerHTML = Xml::checkLabel( $this->msg( $message )->text(), $name, $name, $bitfield & $field );
- if( $field == Revision::DELETED_RESTRICTED )
+ if ( $field == Revision::DELETED_RESTRICTED ) {
$innerHTML = "<b>$innerHTML</b>";
+ }
$line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML );
$html .= "<tr>$line</tr>\n";
}
@@ -496,10 +453,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$html .= '<th class="mw-revdel-checkbox">' . $this->msg( 'revdelete-radio-unset' )->escaped() . '</th>';
$html .= '<th class="mw-revdel-checkbox">' . $this->msg( 'revdelete-radio-set' )->escaped() . '</th>';
$html .= "<th></th></tr>\n";
- foreach( $this->checks as $item ) {
+ foreach ( $this->checks as $item ) {
list( $message, $name, $field ) = $item;
// If there are several items, use third state by default...
- if( $this->submitClicked ) {
+ if ( $this->submitClicked ) {
$selected = $this->getRequest()->getInt( $name, 0 /* unchecked */ );
} else {
$selected = -1; // use existing field
@@ -508,7 +465,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>';
$line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>';
$label = $this->msg( $message )->escaped();
- if( $field == Revision::DELETED_RESTRICTED ) {
+ if ( $field == Revision::DELETED_RESTRICTED ) {
$label = "<b>$label</b>";
}
$line .= "<td>$label</td>";
@@ -522,26 +479,27 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
/**
* UI entry point for form submission.
+ * @throws PermissionsError
* @return bool
*/
protected function submit() {
# Check edit token on submission
- $token = $this->getRequest()->getVal('wpEditToken');
- if( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
+ $token = $this->getRequest()->getVal( 'wpEditToken' );
+ if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
$this->getOutput()->addWikiMsg( 'sessionfailure' );
return false;
}
$bitParams = $this->extractBitParams();
$listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); // from dropdown
$comment = $listReason;
- if( $comment != 'other' && $this->otherReason != '' ) {
+ if ( $comment != 'other' && $this->otherReason != '' ) {
// Entry from drop down menu + additional comment
$comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->otherReason;
- } elseif( $comment == 'other' ) {
+ } elseif ( $comment == 'other' ) {
$comment = $this->otherReason;
}
# Can the user set this field?
- if( $bitParams[Revision::DELETED_RESTRICTED]==1 && !$this->getUser()->isAllowed('suppressrevision') ) {
+ if ( $bitParams[Revision::DELETED_RESTRICTED] == 1 && !$this->getUser()->isAllowed( 'suppressrevision' ) ) {
throw new PermissionsError( 'suppressrevision' );
}
# If the save went through, go to success message...
@@ -561,7 +519,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
*/
protected function success() {
$this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
- $this->getOutput()->wrapWikiMsg( "<span class=\"success\">\n$1\n</span>", $this->typeInfo['success'] );
+ $this->getOutput()->wrapWikiMsg( "<span class=\"success\">\n$1\n</span>", $this->typeLabels['success'] );
$this->list->reloadFromMaster();
$this->showForm();
}
@@ -571,7 +529,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
*/
protected function failure( $status ) {
$this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
- $this->getOutput()->addWikiText( $status->getWikiText( $this->typeInfo['failure'] ) );
+ $this->getOutput()->addWikiText( $status->getWikiText( $this->typeLabels['failure'] ) );
$this->showForm();
}
@@ -582,15 +540,15 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
*/
protected function extractBitParams() {
$bitfield = array();
- foreach( $this->checks as $item ) {
- list( /* message */ , $name, $field ) = $item;
+ foreach ( $this->checks as $item ) {
+ list( /* message */, $name, $field ) = $item;
$val = $this->getRequest()->getInt( $name, 0 /* unchecked */ );
- if( $val < -1 || $val > 1) {
+ if ( $val < -1 || $val > 1 ) {
$val = -1; // -1 for existing value
}
$bitfield[$field] = $val;
}
- if( !isset($bitfield[Revision::DELETED_RESTRICTED]) ) {
+ if ( !isset( $bitfield[Revision::DELETED_RESTRICTED] ) ) {
$bitfield[Revision::DELETED_RESTRICTED] = 0;
}
return $bitfield;
@@ -598,21 +556,13 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
/**
* Put together a rev_deleted bitfield
- * @param $bitPars array extractBitParams() params
- * @param $oldfield int current bitfield
+ * @deprecated since 1.22, use RevisionDeleter::extractBitfield instead
+ * @param array $bitPars extractBitParams() params
+ * @param int $oldfield current bitfield
* @return array
*/
public static function extractBitfield( $bitPars, $oldfield ) {
- // Build the actual new rev_deleted bitfield
- $newBits = 0;
- foreach( $bitPars as $const => $val ) {
- if( $val == 1 ) {
- $newBits |= $const; // $const is the *_deleted const
- } elseif( $val == -1 ) {
- $newBits |= ($oldfield & $const); // use existing
- }
- }
- return $newBits;
+ return RevisionDeleter::extractBitfield( $bitPars, $oldfield );
}
/**
@@ -627,5 +577,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
array( 'value' => $bitfield, 'comment' => $reason )
);
}
-}
+ protected function getGroupName() {
+ return 'pagetools';
+ }
+}
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
index 5f5b6b4d..8609c740 100644
--- a/includes/specials/SpecialSearch.php
+++ b/includes/specials/SpecialSearch.php
@@ -42,6 +42,9 @@ class SpecialSearch extends SpecialPage {
/// Search engine
protected $searchEngine;
+ /// Search engine type, if not default
+ protected $searchEngineType;
+
/// For links
protected $extraParams = array();
@@ -78,7 +81,7 @@ class SpecialSearch extends SpecialPage {
/**
* Entry point
*
- * @param $par String or null
+ * @param string $par or null
*/
public function execute( $par ) {
$this->setHeaders();
@@ -98,6 +101,8 @@ class SpecialSearch extends SpecialPage {
$this->load();
+ $this->searchEngineType = $request->getVal( 'srbackend' );
+
if ( $request->getVal( 'fulltext' )
|| !is_null( $request->getVal( 'offset' ) )
|| !is_null( $request->getVal( 'searchx' ) ) )
@@ -137,8 +142,8 @@ class SpecialSearch extends SpecialPage {
if ( $profile === null ) {
// BC with old request format
$profile = 'advanced';
- foreach( $profiles as $key => $data ) {
- if ( $nslist === $data['namespaces'] && $key !== 'advanced') {
+ foreach ( $profiles as $key => $data ) {
+ if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
$profile = $key;
}
}
@@ -159,7 +164,7 @@ class SpecialSearch extends SpecialPage {
$default = $request->getBool( 'profile' ) ? 0 : 1;
$this->searchRedirects = $request->getBool( 'redirs', $default ) ? 1 : 0;
$this->didYouMeanHtml = ''; # html of did you mean... link
- $this->fulltext = $request->getVal('fulltext');
+ $this->fulltext = $request->getVal( 'fulltext' );
$this->profile = $profile;
}
@@ -173,7 +178,7 @@ class SpecialSearch extends SpecialPage {
# 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 ) ) {
+ if ( is_null( $t ) ) {
$this->showResults( $term );
return;
}
@@ -185,19 +190,19 @@ class SpecialSearch extends SpecialPage {
return;
}
- if( !is_null( $t ) ) {
+ if ( !is_null( $t ) ) {
$this->getOutput()->redirect( $t->getFullURL() );
return;
}
# No match, generate an edit URL
$t = Title::newFromText( $term );
- if( !is_null( $t ) ) {
+ if ( !is_null( $t ) ) {
global $wgGoToEdit;
wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
wfDebugLog( 'nogomatch', $t->getText(), false );
# If the feature is enabled, go straight to the edit page
- if( $wgGoToEdit ) {
+ if ( $wgGoToEdit ) {
$this->getOutput()->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) );
return;
}
@@ -218,7 +223,7 @@ class SpecialSearch extends SpecialPage {
$search->showRedirects = $this->searchRedirects; // BC
$search->setFeatureData( 'list-redirects', $this->searchRedirects );
$search->prefix = $this->mPrefix;
- $term = $search->transformSearchTerm($term);
+ $term = $search->transformSearchTerm( $term );
wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) );
@@ -250,21 +255,27 @@ class SpecialSearch extends SpecialPage {
$t = Title::newFromText( $term );
// fetch search results
- $rewritten = $search->replacePrefixes($term);
+ $rewritten = $search->replacePrefixes( $term );
$titleMatches = $search->searchTitle( $rewritten );
- if( !( $titleMatches instanceof SearchResultTooMany ) ) {
+ if ( !( $titleMatches instanceof SearchResultTooMany ) ) {
$textMatches = $search->searchText( $rewritten );
}
+ $textStatus = null;
+ if ( $textMatches instanceof Status ) {
+ $textStatus = $textMatches;
+ $textMatches = null;
+ }
+
// did you mean... suggestions
- if( $textMatches && $textMatches->hasSuggestion() ) {
+ if ( $textMatches && !$textStatus && $textMatches->hasSuggestion() ) {
$st = SpecialPage::getTitleFor( 'Search' );
- # mirror Go/Search behaviour of original request ..
+ # mirror Go/Search behavior of original request ..
$didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
- if( $this->fulltext != null ) {
+ if ( $this->fulltext != null ) {
$didYouMeanParams['fulltext'] = $this->fulltext;
}
@@ -275,7 +286,7 @@ class SpecialSearch extends SpecialPage {
$suggestionSnippet = $textMatches->getSuggestionSnippet();
- if( $suggestionSnippet == '' ) {
+ if ( $suggestionSnippet == '' ) {
$suggestionSnippet = null;
}
@@ -288,6 +299,13 @@ class SpecialSearch extends SpecialPage {
$this->didYouMeanHtml = '<div class="searchdidyoumean">' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>';
}
+
+ if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) {
+ # Hook requested termination
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
// start rendering the page
$out->addHtml(
Xml::openElement(
@@ -304,20 +322,20 @@ class SpecialSearch extends SpecialPage {
Xml::openElement( 'tr' ) .
Xml::openElement( 'td' ) . "\n" .
$this->shortDialog( $term ) .
- Xml::closeElement('td') .
- Xml::closeElement('tr') .
- Xml::closeElement('table')
+ Xml::closeElement( 'td' ) .
+ Xml::closeElement( 'tr' ) .
+ Xml::closeElement( 'table' )
);
// Sometimes the search engine knows there are too many hits
- if( $titleMatches instanceof SearchResultTooMany ) {
+ if ( $titleMatches instanceof SearchResultTooMany ) {
$out->wrapWikiMsg( "==$1==\n", 'toomanymatches' );
wfProfileOut( __METHOD__ );
return;
}
- $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':';
- if( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
+ $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
+ if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
$out->addHTML( $this->formHeader( $term, 0, 0 ) );
$out->addHtml( $this->getProfileForm( $this->profile, $term ) );
$out->addHTML( '</form>' );
@@ -340,21 +358,22 @@ class SpecialSearch extends SpecialPage {
// get total number of results if backend can calculate it
$totalRes = 0;
- if($titleMatches && !is_null( $titleMatches->getTotalHits() ) )
+ if ( $titleMatches && !is_null( $titleMatches->getTotalHits() ) ) {
$totalRes += $titleMatches->getTotalHits();
- if($textMatches && !is_null( $textMatches->getTotalHits() ))
+ }
+ if ( $textMatches && !is_null( $textMatches->getTotalHits() ) ) {
$totalRes += $textMatches->getTotalHits();
+ }
// show number of results and current offset
$out->addHTML( $this->formHeader( $term, $num, $totalRes ) );
$out->addHtml( $this->getProfileForm( $this->profile, $term ) );
-
$out->addHtml( Xml::closeElement( 'form' ) );
$out->addHtml( "<div class='searchresults'>" );
// prev/next links
- if( $num || $this->offset ) {
+ if ( $num || $this->offset ) {
// Show the create link ahead
$this->showCreateLink( $t );
$prevnext = $this->getLanguage()->viewPrevNext( $this->getTitle(), $this->offset, $this->limit,
@@ -368,42 +387,49 @@ class SpecialSearch extends SpecialPage {
}
$out->parserOptions()->setEditSection( false );
- if( $titleMatches ) {
- if( $numTitleMatches > 0 ) {
+ if ( $titleMatches ) {
+ if ( $numTitleMatches > 0 ) {
$out->wrapWikiMsg( "==$1==\n", 'titlematches' );
$out->addHTML( $this->showMatches( $titleMatches ) );
}
$titleMatches->free();
}
- if( $textMatches ) {
+ if ( $textMatches && !$textStatus ) {
// output appropriate heading
- if( $numTextMatches > 0 && $numTitleMatches > 0 ) {
+ if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
// if no title matches the heading is redundant
$out->wrapWikiMsg( "==$1==\n", 'textmatches' );
- } elseif( $totalRes == 0 ) {
+ } elseif ( $totalRes == 0 ) {
# Don't show the 'no text matches' if we received title matches
# $out->wrapWikiMsg( "==$1==\n", 'notextmatches' );
}
// show interwiki results if any
- if( $textMatches->hasInterwikiResults() ) {
+ if ( $textMatches->hasInterwikiResults() ) {
$out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
}
// show results
- if( $numTextMatches > 0 ) {
+ if ( $numTextMatches > 0 ) {
$out->addHTML( $this->showMatches( $textMatches ) );
}
$textMatches->free();
}
- if( $num === 0 ) {
- $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", array( 'search-nonefound', wfEscapeWikiText( $term ) ) );
- $this->showCreateLink( $t );
+ if ( $num === 0 ) {
+ if ( $textStatus ) {
+ $out->addHTML( '<div class="error">' .
+ htmlspecialchars( $textStatus->getWikiText( 'search-error' ) ) . '</div>' );
+ } else {
+ $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>",
+ array( 'search-nonefound', wfEscapeWikiText( $term ) ) );
+ $this->showCreateLink( $t );
+ }
}
$out->addHtml( "</div>" );
- if( $num || $this->offset ) {
+ if ( $num || $this->offset ) {
$out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
}
+ wfRunHooks( 'SpecialSearchResultsAppend', array( $this, $out, $term ) );
wfProfileOut( __METHOD__ );
}
@@ -414,16 +440,16 @@ class SpecialSearch extends SpecialPage {
// show direct page/create link if applicable
// Check DBkey !== '' in case of fragment link only.
- if( is_null( $t ) || $t->getDBkey() === '' ) {
+ if ( is_null( $t ) || $t->getDBkey() === '' ) {
// invalid title
// preserve the paragraph for margins etc...
$this->getOutput()->addHtml( '<p></p>' );
return;
}
- if( $t->isKnown() ) {
+ if ( $t->isKnown() ) {
$messageName = 'searchmenu-exists';
- } elseif( $t->userCan( 'create' ) ) {
+ } elseif ( $t->userCan( 'create', $this->getUser() ) ) {
$messageName = 'searchmenu-new';
} else {
$messageName = 'searchmenu-new-nocreate';
@@ -432,7 +458,7 @@ class SpecialSearch extends SpecialPage {
wfRunHooks( 'SpecialSearchCreateLink', array( $t, &$params ) );
// Extensions using the hook might still return an empty $messageName
- if( $messageName ) {
+ if ( $messageName ) {
$this->getOutput()->wrapWikiMsg( "<p class=\"mw-search-createlink\">\n$1</p>", $params );
} else {
// preserve the paragraph for margins etc...
@@ -445,11 +471,13 @@ class SpecialSearch extends SpecialPage {
*/
protected function setupPage( $term ) {
# Should advanced UI be used?
- $this->searchAdvanced = ($this->profile === 'advanced');
+ $this->searchAdvanced = ( $this->profile === 'advanced' );
$out = $this->getOutput();
- if( strval( $term ) !== '' ) {
+ if ( strval( $term ) !== '' ) {
$out->setPageTitle( $this->msg( 'searchresults' ) );
- $out->setHTMLTitle( $this->msg( 'pagetitle', $this->msg( 'searchresults-title', $term )->plain() ) );
+ $out->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams(
+ $this->msg( 'searchresults-title' )->rawParams( $term )->text()
+ ) );
}
// add javascript specific to special:search
$out->addModules( 'mediawiki.special.search' );
@@ -464,8 +492,8 @@ class SpecialSearch extends SpecialPage {
*/
protected function powerSearch( &$request ) {
$arr = array();
- foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
- if( $request->getCheck( 'ns' . $ns ) ) {
+ foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ if ( $request->getCheck( 'ns' . $ns ) ) {
$arr[] = $ns;
}
}
@@ -481,10 +509,10 @@ class SpecialSearch extends SpecialPage {
protected function powerSearchOptions() {
$opt = array();
$opt['redirs'] = $this->searchRedirects ? 1 : 0;
- if( $this->profile !== 'advanced' ) {
+ if ( $this->profile !== 'advanced' ) {
$opt['profile'] = $this->profile;
} else {
- foreach( $this->namespaces as $n ) {
+ foreach ( $this->namespaces as $n ) {
$opt['ns' . $n] = 1;
}
}
@@ -506,12 +534,12 @@ class SpecialSearch extends SpecialPage {
$out = "";
$infoLine = $matches->getInfo();
- if( !is_null($infoLine) ) {
+ if ( !is_null( $infoLine ) ) {
$out .= "\n<!-- {$infoLine} -->\n";
}
$out .= "<ul class='mw-search-results'>\n";
$result = $matches->next();
- while( $result ) {
+ while ( $result ) {
$out .= $this->showHit( $result, $terms );
$result = $matches->next();
}
@@ -527,24 +555,25 @@ class SpecialSearch extends SpecialPage {
* Format a single hit result
*
* @param $result SearchResult
- * @param $terms Array: terms to highlight
+ * @param array $terms terms to highlight
*
* @return string
*/
protected function showHit( $result, $terms ) {
wfProfileIn( __METHOD__ );
- if( $result->isBrokenTitle() ) {
+ if ( $result->isBrokenTitle() ) {
wfProfileOut( __METHOD__ );
return "<!-- Broken link in search result -->\n";
}
$t = $result->getTitle();
- $titleSnippet = $result->getTitleSnippet($terms);
+ $titleSnippet = $result->getTitleSnippet( $terms );
- if( $titleSnippet == '' )
+ if ( $titleSnippet == '' ) {
$titleSnippet = null;
+ }
$link_t = clone $t;
@@ -559,7 +588,7 @@ class SpecialSearch extends SpecialPage {
//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->userCan( 'read' ) ) {
+ if ( !$t->userCan( 'read', $this->getUser() ) ) {
wfProfileOut( __METHOD__ );
return "<li>{$link}</li>\n";
}
@@ -567,21 +596,22 @@ class SpecialSearch extends SpecialPage {
// 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() ) {
+ if ( $result->isMissingRevision() ) {
wfProfileOut( __METHOD__ );
return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
}
// format redirects / relevant sections
$redirectTitle = $result->getRedirectTitle();
- $redirectText = $result->getRedirectSnippet($terms);
+ $redirectText = $result->getRedirectSnippet( $terms );
$sectionTitle = $result->getSectionTitle();
- $sectionText = $result->getSectionSnippet($terms);
+ $sectionText = $result->getSectionSnippet( $terms );
$redirect = '';
- if( !is_null($redirectTitle) ) {
- if( $redirectText == '' )
+ if ( !is_null( $redirectTitle ) ) {
+ if ( $redirectText == '' ) {
$redirectText = null;
+ }
$redirect = "<span class='searchalttitle'>" .
$this->msg( 'search-redirect' )->rawParams(
@@ -591,9 +621,10 @@ class SpecialSearch extends SpecialPage {
$section = '';
- if( !is_null($sectionTitle) ) {
- if( $sectionText == '' )
+ if ( !is_null( $sectionTitle ) ) {
+ if ( $sectionText == '' ) {
$sectionText = null;
+ }
$section = "<span class='searchalttitle'>" .
$this->msg( 'search-section' )->rawParams(
@@ -602,12 +633,12 @@ class SpecialSearch extends SpecialPage {
}
// format text extract
- $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
+ $extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>";
$lang = $this->getLanguage();
// format score
- if( is_null( $result->getScore() ) ) {
+ if ( is_null( $result->getScore() ) ) {
// Search engine doesn't report scoring info
$score = '';
} else {
@@ -623,7 +654,7 @@ class SpecialSearch extends SpecialPage {
$size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
->numParams( $wordCount )->escaped();
- if( $t->getNamespace() == NS_CATEGORY ) {
+ if ( $t->getNamespace() == NS_CATEGORY ) {
$cat = Category::newFromTitle( $t );
$size = $this->msg( 'search-result-category-size' )
->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
@@ -634,7 +665,7 @@ class SpecialSearch extends SpecialPage {
// link to related articles if supported
$related = '';
- if( $result->hasRelated() ) {
+ if ( $result->hasRelated() ) {
$st = SpecialPage::getTitleFor( 'Search' );
$stParams = array_merge(
$this->powerSearchOptions(),
@@ -654,11 +685,11 @@ class SpecialSearch extends SpecialPage {
}
// Include a thumbnail for media files...
- if( $t->getNamespace() == NS_FILE ) {
+ if ( $t->getNamespace() == NS_FILE ) {
$img = wfFindFile( $t );
- if( $img ) {
+ if ( $img ) {
$thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
- if( $thumb ) {
+ if ( $thumb ) {
$desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
wfProfileOut( __METHOD__ );
// Float doesn't seem to interact well with the bullets.
@@ -667,7 +698,7 @@ class SpecialSearch extends SpecialPage {
return "<li>" .
'<table class="searchResultImage">' .
'<tr>' .
- '<td width="120" style="text-align: center; vertical-align: top;">' .
+ '<td style="width: 120px; text-align: center; vertical-align: top;">' .
$thumb->toHtml( array( 'desc-link' => true ) ) .
'</td>' .
'<td style="vertical-align: top;">' .
@@ -682,11 +713,21 @@ class SpecialSearch extends SpecialPage {
}
}
- wfProfileOut( __METHOD__ );
- return "<li><div class='mw-search-result-heading'>{$link} {$redirect} {$section}</div> {$extract}\n" .
- "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
- "</li>\n";
+ $html = null;
+
+ if ( wfRunHooks( 'ShowSearchHit', array(
+ $this, $result, $terms,
+ &$link, &$redirect, &$section, &$extract,
+ &$score, &$size, &$date, &$related,
+ &$html
+ ) ) ) {
+ $html = "<li><div class='mw-search-result-heading'>{$link} {$redirect} {$section}</div> {$extract}\n" .
+ "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
+ "</li>\n";
+ }
+ wfProfileOut( __METHOD__ );
+ return $html;
}
/**
@@ -702,22 +743,23 @@ class SpecialSearch extends SpecialPage {
wfProfileIn( __METHOD__ );
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
- $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
- $this->msg( 'search-interwiki-caption' )->text() . "</div>\n";
+ $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>" .
+ $this->msg( 'search-interwiki-caption' )->text() . "</div>\n";
$out .= "<ul class='mw-search-iwresults'>\n";
// work out custom project captions
$customCaptions = array();
$customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); // format per line <iwprefix>:<caption>
- foreach($customLines as $line) {
- $parts = explode(":",$line,2);
- if(count($parts) == 2) // validate line
+ foreach ( $customLines as $line ) {
+ $parts = explode( ":", $line, 2 );
+ if ( count( $parts ) == 2 ) { // validate line
$customCaptions[$parts[0]] = $parts[1];
+ }
}
$prev = null;
$result = $matches->next();
- while( $result ) {
+ while ( $result ) {
$out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
$prev = $result->getInterwikiPrefix();
$result = $matches->next();
@@ -738,24 +780,25 @@ class SpecialSearch extends SpecialPage {
* @param $lastInterwiki String
* @param $terms Array
* @param $query String
- * @param $customCaptions Array: iw prefix -> caption
+ * @param array $customCaptions iw prefix -> caption
*
* @return string
*/
- protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
+ protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions ) {
wfProfileIn( __METHOD__ );
- if( $result->isBrokenTitle() ) {
+ if ( $result->isBrokenTitle() ) {
wfProfileOut( __METHOD__ );
return "<!-- Broken link in search result -->\n";
}
$t = $result->getTitle();
- $titleSnippet = $result->getTitleSnippet($terms);
+ $titleSnippet = $result->getTitleSnippet( $terms );
- if( $titleSnippet == '' )
+ if ( $titleSnippet == '' ) {
$titleSnippet = null;
+ }
$link = Linker::linkKnown(
$t,
@@ -764,11 +807,12 @@ class SpecialSearch extends SpecialPage {
// format redirect if any
$redirectTitle = $result->getRedirectTitle();
- $redirectText = $result->getRedirectSnippet($terms);
+ $redirectText = $result->getRedirectSnippet( $terms );
$redirect = '';
- if( !is_null($redirectTitle) ) {
- if( $redirectText == '' )
+ if ( !is_null( $redirectTitle ) ) {
+ if ( $redirectText == '' ) {
$redirectText = null;
+ }
$redirect = "<span class='searchalttitle'>" .
$this->msg( 'search-redirect' )->rawParams(
@@ -778,8 +822,8 @@ class SpecialSearch extends SpecialPage {
$out = "";
// display project name
- if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) {
- if( array_key_exists($t->getInterwiki(),$customCaptions) ) {
+ if ( is_null( $lastInterwiki ) || $lastInterwiki != $t->getInterwiki() ) {
+ if ( array_key_exists( $t->getInterwiki(), $customCaptions ) ) {
// captions from 'search-interwiki-custom'
$caption = $customCaptions[$t->getInterwiki()];
} else {
@@ -789,7 +833,7 @@ class SpecialSearch extends SpecialPage {
$caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text();
}
// "more results" link (special page stuff could be localized, but we might not know target lang)
- $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
+ $searchTitle = Title::newFromText( $t->getInterwiki() . ":Special:Search" );
$searchLink = Linker::linkKnown(
$searchTitle,
$this->msg( 'search-interwiki-more' )->text(),
@@ -831,22 +875,26 @@ class SpecialSearch extends SpecialPage {
/**
* Generates the power search box at [[Special:Search]]
*
- * @param $term String: search term
+ * @param string $term search term
* @param $opts array
* @return String: HTML form
*/
protected function powerSearchBox( $term, $opts ) {
+ global $wgContLang;
+
// Groups namespaces into rows according to subject
$rows = array();
- foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) {
+ foreach ( SearchEngine::searchableNamespaces() as $namespace => $name ) {
$subject = MWNamespace::getSubject( $namespace );
- if( !array_key_exists( $subject, $rows ) ) {
+ if ( !array_key_exists( $subject, $rows ) ) {
$rows[$subject] = "";
}
- $name = str_replace( '_', ' ', $name );
- if( $name == '' ) {
+
+ $name = $wgContLang->getConverter()->convertNamespace( $namespace );
+ if ( $name == '' ) {
$name = $this->msg( 'blanknamespace' )->text();
}
+
$rows[$subject] .=
Xml::openElement(
'td', array( 'style' => 'white-space: nowrap' )
@@ -859,27 +907,30 @@ class SpecialSearch extends SpecialPage {
) .
Xml::closeElement( 'td' );
}
+
$rows = array_values( $rows );
$numRows = count( $rows );
// 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 ) {
+ for ( $i = 0; $i < $numRows; $i += 4 ) {
$namespaceTables .= Xml::openElement(
'table',
array( 'cellpadding' => 0, 'cellspacing' => 0 )
);
- for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
+
+ for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
$namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
}
+
$namespaceTables .= Xml::closeElement( 'table' );
}
$showSections = array( 'namespaceTables' => $namespaceTables );
// Show redirects check only if backend supports it
- if( $this->getSearchEngine()->supports( 'list-redirects' ) ) {
+ if ( $this->getSearchEngine()->supports( 'list-redirects' ) ) {
$showSections['redirects'] =
Xml::checkLabel( $this->msg( 'powersearch-redir' )->text(), 'redirs', 'redirs', $this->searchRedirects );
}
@@ -888,16 +939,15 @@ class SpecialSearch extends SpecialPage {
$hidden = '';
unset( $opts['redirs'] );
- foreach( $opts as $key => $value ) {
+ foreach ( $opts as $key => $value ) {
$hidden .= Html::hidden( $key, $value );
}
// Return final output
- return
- Xml::openElement(
+ return Xml::openElement(
'fieldset',
array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
) .
- Xml::element( 'legend', null, $this->msg('powersearch-legend' )->text() ) .
+ Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) .
Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
@@ -949,8 +999,10 @@ class SpecialSearch extends SpecialPage {
wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
- foreach( $profiles as &$data ) {
- if ( !is_array( $data['namespaces'] ) ) continue;
+ foreach ( $profiles as &$data ) {
+ if ( !is_array( $data['namespaces'] ) ) {
+ continue;
+ }
sort( $data['namespaces'] );
}
@@ -964,10 +1016,10 @@ class SpecialSearch extends SpecialPage {
* @return string
*/
protected function formHeader( $term, $resultsShown, $totalNum ) {
- $out = Xml::openElement('div', array( 'class' => 'mw-search-formheader' ) );
+ $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) );
$bareterm = $term;
- if( $this->startsWithImage( $term ) ) {
+ if ( $this->startsWithImage( $term ) ) {
// Deletes prefixes
$bareterm = substr( $term, strpos( $term, ':' ) + 1 );
}
@@ -1001,11 +1053,11 @@ class SpecialSearch extends SpecialPage {
);
}
$out .= Xml::closeElement( 'ul' );
- $out .= Xml::closeElement('div') ;
+ $out .= Xml::closeElement( 'div' );
// Results-info
if ( $resultsShown > 0 ) {
- if ( $totalNum > 0 ){
+ if ( $totalNum > 0 ) {
$top = $this->msg( 'showingresultsheader' )
->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
->params( wfEscapeWikiText( $term ) )
@@ -1026,7 +1078,7 @@ class SpecialSearch extends SpecialPage {
}
$out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
- $out .= Xml::closeElement('div');
+ $out .= Xml::closeElement( 'div' );
return $out;
}
@@ -1053,15 +1105,15 @@ class SpecialSearch extends SpecialPage {
* Make a search link with some target namespaces
*
* @param $term String
- * @param $namespaces Array ignored
- * @param $label String: link's text
- * @param $tooltip String: link's tooltip
- * @param $params Array: query string parameters
+ * @param array $namespaces ignored
+ * @param string $label link's text
+ * @param string $tooltip link's tooltip
+ * @param array $params query string parameters
* @return String: HTML fragment
*/
protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) {
$opt = $params;
- foreach( $namespaces as $n ) {
+ foreach ( $namespaces as $n ) {
$opt['ns' . $n] = 1;
}
$opt['redirs'] = $this->searchRedirects;
@@ -1078,7 +1130,8 @@ class SpecialSearch extends SpecialPage {
'a',
array(
'href' => $this->getTitle()->getLocalURL( $stParams ),
- 'title' => $tooltip),
+ 'title' => $tooltip
+ ),
$label
);
}
@@ -1086,14 +1139,14 @@ class SpecialSearch extends SpecialPage {
/**
* Check if query starts with image: prefix
*
- * @param $term String: the string to check
+ * @param string $term the string to check
* @return Boolean
*/
protected function startsWithImage( $term ) {
global $wgContLang;
$p = explode( ':', $term );
- if( count( $p ) > 1 ) {
+ if ( count( $p ) > 1 ) {
return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
}
return false;
@@ -1102,7 +1155,7 @@ class SpecialSearch extends SpecialPage {
/**
* Check if query starts with all: prefix
*
- * @param $term String: the string to check
+ * @param string $term the string to check
* @return Boolean
*/
protected function startsWithAll( $term ) {
@@ -1110,8 +1163,8 @@ class SpecialSearch extends SpecialPage {
$allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text();
$p = explode( ':', $term );
- if( count( $p ) > 1 ) {
- return $p[0] == $allkeyword;
+ if ( count( $p ) > 1 ) {
+ return $p[0] == $allkeyword;
}
return false;
}
@@ -1123,7 +1176,8 @@ class SpecialSearch extends SpecialPage {
*/
public function getSearchEngine() {
if ( $this->searchEngine === null ) {
- $this->searchEngine = SearchEngine::create();
+ $this->searchEngine = $this->searchEngineType ?
+ SearchEngine::create( $this->searchEngineType ) : SearchEngine::create();
}
return $this->searchEngine;
}
@@ -1141,4 +1195,7 @@ class SpecialSearch extends SpecialPage {
$this->extraParams[$key] = $value;
}
+ protected function getGroupName() {
+ return 'pages';
+ }
}
diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php
index 5a4e8f03..9b50875a 100644
--- a/includes/specials/SpecialShortpages.php
+++ b/includes/specials/SpecialShortpages.php
@@ -38,15 +38,15 @@ class ShortPagesPage extends QueryPage {
}
function getQueryInfo() {
- return array (
- 'tables' => array ( 'page' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
+ return array(
+ 'tables' => array( 'page' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'page_len' ),
- 'conds' => array ( 'page_namespace' =>
+ 'conds' => array( 'page_namespace' =>
MWNamespace::getContentNamespaces(),
'page_is_redirect' => 0 ),
- 'options' => array ( 'USE INDEX' => 'page_redirect_namespace_len' )
+ 'options' => array( 'USE INDEX' => 'page_redirect_namespace_len' )
);
}
@@ -56,8 +56,7 @@ class ShortPagesPage extends QueryPage {
/**
* @param $db DatabaseBase
- * @param $res
- * @return void
+ * @param ResultWrapper $res
*/
function preprocessResults( $db, $res ) {
# There's no point doing a batch check if we aren't caching results;
@@ -79,6 +78,11 @@ class ShortPagesPage extends QueryPage {
return false;
}
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
$dm = $this->getLanguage()->getDirMark();
@@ -110,4 +114,8 @@ class ShortPagesPage extends QueryPage {
? "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]"
: "<del>${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]</del>";
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php
index e973ddc8..47c89d0d 100644
--- a/includes/specials/SpecialSpecialpages.php
+++ b/includes/specials/SpecialSpecialpages.php
@@ -53,32 +53,37 @@ class SpecialSpecialpages extends UnlistedSpecialPage {
$pages = SpecialPageFactory::getUsablePages( $this->getUser() );
- if( !count( $pages ) ) {
+ if ( !count( $pages ) ) {
# Yeah, that was pointless. Thanks for coming.
return false;
}
/** Put them into a sortable array */
$groups = array();
+ /** @var SpecialPage $page */
foreach ( $pages as $page ) {
if ( $page->isListed() ) {
- $group = SpecialPageFactory::getGroup( $page );
- if( !isset( $groups[$group] ) ) {
+ $group = $page->getFinalGroupName();
+ if ( !isset( $groups[$group] ) ) {
$groups[$group] = array();
}
- $groups[$group][$page->getDescription()] = array( $page->getTitle(), $page->isRestricted(), $page->isExpensive() );
+ $groups[$group][$page->getDescription()] = array(
+ $page->getTitle(),
+ $page->isRestricted(),
+ $page->isCached()
+ );
}
}
/** Sort */
if ( $wgSortSpecialPages ) {
- foreach( $groups as $group => $sortedPages ) {
+ foreach ( $groups as $group => $sortedPages ) {
ksort( $groups[$group] );
}
}
/** Always move "other" to end */
- if( array_key_exists( 'other', $groups ) ) {
+ if ( array_key_exists( 'other', $groups ) ) {
$other = $groups['other'];
unset( $groups['other'] );
$groups['other'] = $other;
@@ -88,43 +93,42 @@ class SpecialSpecialpages extends UnlistedSpecialPage {
}
private function outputPageList( $groups ) {
- global $wgMiserMode;
$out = $this->getOutput();
$includesRestrictedPages = false;
$includesCachedPages = false;
foreach ( $groups as $group => $sortedPages ) {
- $middle = ceil( count( $sortedPages )/2 );
$total = count( $sortedPages );
+ $middle = ceil( $total / 2 );
$count = 0;
$out->wrapWikiMsg( "<h2 class=\"mw-specialpagesgroup\" id=\"mw-specialpagesgroup-$group\">$1</h2>\n", "specialpages-group-$group" );
$out->addHTML(
- Html::openElement( 'table', array( 'style' => 'width:100%;', 'class' => 'mw-specialpages-table' ) ) ."\n" .
+ Html::openElement( 'table', array( 'style' => 'width:100%;', 'class' => 'mw-specialpages-table' ) ) . "\n" .
Html::openElement( 'tr' ) . "\n" .
Html::openElement( 'td', array( 'style' => 'width:30%;vertical-align:top' ) ) . "\n" .
Html::openElement( 'ul' ) . "\n"
);
- foreach( $sortedPages as $desc => $specialpage ) {
- list( $title, $restricted, $expensive) = $specialpage;
+ foreach ( $sortedPages as $desc => $specialpage ) {
+ list( $title, $restricted, $cached ) = $specialpage;
$pageClasses = array();
- if ( $expensive && $wgMiserMode ){
+ if ( $cached ) {
$includesCachedPages = true;
$pageClasses[] = 'mw-specialpagecached';
}
- if( $restricted ) {
+ if ( $restricted ) {
$includesRestrictedPages = true;
$pageClasses[] = 'mw-specialpagerestricted';
}
- $link = Linker::linkKnown( $title , htmlspecialchars( $desc ) );
+ $link = Linker::linkKnown( $title, htmlspecialchars( $desc ) );
$out->addHTML( Html::rawElement( 'li', array( 'class' => implode( ' ', $pageClasses ) ), $link ) . "\n" );
# Split up the larger groups
$count++;
- if( $total > 3 && $count == $middle ) {
+ if ( $total > 3 && $count == $middle ) {
$out->addHTML(
Html::closeElement( 'ul' ) . Html::closeElement( 'td' ) .
Html::element( 'td', array( 'style' => 'width:10%' ), '' ) .
diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php
index 46881ec4..f26d1a60 100644
--- a/includes/specials/SpecialStatistics.php
+++ b/includes/specials/SpecialStatistics.php
@@ -53,18 +53,18 @@ class SpecialStatistics extends SpecialPage {
# Staticic - views
$viewsStats = '';
- if( !$wgDisableCounters ) {
+ if ( !$wgDisableCounters ) {
$viewsStats = $this->getViewsStats();
}
# Set active user count
- if( !$wgMiserMode ) {
+ if ( !$wgMiserMode ) {
$key = wfMemcKey( 'sitestats', 'activeusers-updated' );
// Re-calculate the count if the last tally is old...
- if( !$wgMemc->get($key) ) {
+ if ( !$wgMemc->get( $key ) ) {
$dbw = wfGetDB( DB_MASTER );
SiteStatsUpdate::cacheUpdate( $dbw );
- $wgMemc->set( $key, '1', 24*3600 ); // don't update for 1 day
+ $wgMemc->set( $key, '1', 24 * 3600 ); // don't update for 1 day
}
}
@@ -84,13 +84,13 @@ class SpecialStatistics extends SpecialPage {
$text .= $viewsStats;
# Statistic - popular pages
- if( !$wgDisableCounters && !$wgMiserMode ) {
+ if ( !$wgDisableCounters && !$wgMiserMode ) {
$text .= $this->getMostViewedPages();
}
# Statistic - other
$extraStats = array();
- if( wfRunHooks( 'SpecialStatsAddExtra', array( &$extraStats ) ) ) {
+ if ( wfRunHooks( 'SpecialStatsAddExtra', array( &$extraStats ) ) ) {
$text .= $this->getOtherStats( $extraStats );
}
@@ -111,15 +111,15 @@ class SpecialStatistics extends SpecialPage {
* @param $number Float: a statistical number
* @param $trExtraParams Array: params to table row, see Html::elememt
* @param $descMsg String: message key
- * @param $descMsgParam Array: message params
+ * @param array|string $descMsgParam Message parameters
* @return string table row in HTML format
*/
private function formatRow( $text, $number, $trExtraParams = array(), $descMsg = '', $descMsgParam = '' ) {
- if( $descMsg ) {
+ if ( $descMsg ) {
$msg = $this->msg( $descMsg, $descMsgParam );
if ( $msg->exists() ) {
$descriptionText = $this->msg( 'parentheses' )->rawParams( $msg->parse() )->escaped();
- $text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc'),
+ $text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc' ),
" $descriptionText" );
}
}
@@ -185,7 +185,7 @@ class SpecialStatistics extends SpecialPage {
private function getGroupStats() {
global $wgGroupPermissions, $wgImplicitGroups;
$text = '';
- foreach( $wgGroupPermissions as $group => $permissions ) {
+ foreach ( $wgGroupPermissions as $group => $permissions ) {
# Skip generic * and implicit groups
if ( in_array( $group, $wgImplicitGroups ) || $group == '*' ) {
continue;
@@ -217,12 +217,12 @@ class SpecialStatistics extends SpecialPage {
# Add a class when a usergroup contains no members to allow hiding these rows
$classZero = '';
$countUsers = SiteStats::numberingroup( $groupname );
- if( $countUsers == 0 ) {
+ if ( $countUsers == 0 ) {
$classZero = ' statistics-group-zero';
}
$text .= $this->formatRow( $grouppage . ' ' . $grouplink,
$this->getLanguage()->formatNum( $countUsers ),
- array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . $classZero ) );
+ array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . $classZero ) );
}
return $text;
}
@@ -233,11 +233,11 @@ class SpecialStatistics extends SpecialPage {
Xml::closeElement( 'tr' ) .
$this->formatRow( $this->msg( 'statistics-views-total' )->parse(),
$this->getLanguage()->formatNum( $this->views ),
- array ( 'class' => 'mw-statistics-views-total' ), 'statistics-views-total-desc' ) .
+ array( 'class' => 'mw-statistics-views-total' ), 'statistics-views-total-desc' ) .
$this->formatRow( $this->msg( 'statistics-views-peredit' )->parse(),
$this->getLanguage()->formatNum( sprintf( '%.2f', $this->edits ?
$this->views / $this->edits : 0 ) ),
- array ( 'class' => 'mw-statistics-views-peredit' ) );
+ array( 'class' => 'mw-statistics-views-peredit' ) );
}
private function getMostViewedPages() {
@@ -260,13 +260,13 @@ class SpecialStatistics extends SpecialPage {
'LIMIT' => 10,
)
);
- if( $res->numRows() > 0 ) {
+ if ( $res->numRows() > 0 ) {
$text .= Xml::openElement( 'tr' );
$text .= Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-mostpopular' )->parse() );
$text .= Xml::closeElement( 'tr' );
foreach ( $res as $row ) {
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
- if( $title instanceof Title ) {
+ if ( $title instanceof Title ) {
$text .= $this->formatRow( Linker::link( $title ),
$this->getLanguage()->formatNum( $row->page_counter ) );
@@ -277,21 +277,59 @@ class SpecialStatistics extends SpecialPage {
return $text;
}
- private function getOtherStats( $stats ) {
- if ( !count( $stats ) )
- return '';
+ /**
+ * Conversion of external statistics into an internal representation
+ * Following a ([<header-message>][<item-message>] = number) pattern
+ *
+ * @param array $stats
+ * @return string
+ */
+ private function getOtherStats( array $stats ) {
+ $return = '';
- $return = Xml::openElement( 'tr' ) .
- Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-hooks' )->parse() ) .
- Xml::closeElement( 'tr' );
+ foreach ( $stats as $header => $items ) {
+ // Identify the structure used
+ if ( is_array( $items ) ) {
+
+ // Ignore headers that are recursively set as legacy header
+ if ( $header !== 'statistics-header-hooks' ) {
+ $return .= $this->formatRowHeader( $header );
+ }
+
+ // Collect all items that belong to the same header
+ foreach ( $items as $key => $value ) {
+ $name = $this->msg( $key )->parse();
+ $number = htmlspecialchars( $value );
- foreach( $stats as $name => $number ) {
- $name = htmlspecialchars( $name );
- $number = htmlspecialchars( $number );
+ $return .= $this->formatRow( $name, $this->getLanguage()->formatNum( $number ), array( 'class' => 'mw-statistics-hook', 'id' => 'mw-' . $key ) );
+ }
+ } else {
+ // Create the legacy header only once
+ if ( $return === '' ) {
+ $return .= $this->formatRowHeader( 'statistics-header-hooks' );
+ }
- $return .= $this->formatRow( $name, $this->getLanguage()->formatNum( $number ), array( 'class' => 'mw-statistics-hook' ) );
+ // Recursively remap the legacy structure
+ $return .= $this->getOtherStats( array( 'statistics-header-hooks' => array( $header => $items ) ) );
+ }
}
return $return;
}
+
+ /**
+ * Format row header
+ *
+ * @param string $header
+ * @return string
+ */
+ private function formatRowHeader( $header ) {
+ return Xml::openElement( 'tr' ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( $header )->parse() ) .
+ Xml::closeElement( 'tr' );
+ }
+
+ protected function getGroupName() {
+ return 'wiki';
+ }
}
diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php
index 4036ebb2..077e7cbc 100644
--- a/includes/specials/SpecialTags.php
+++ b/includes/specials/SpecialTags.php
@@ -27,6 +27,10 @@
* @ingroup SpecialPage
*/
class SpecialTags extends SpecialPage {
+ /**
+ * @var array List of defined tags
+ */
+ public $definedTags;
function __construct() {
parent::__construct( 'Tags' );
@@ -44,52 +48,71 @@ class SpecialTags extends SpecialPage {
$html = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
+ Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() )
);
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'change_tag', array( 'ct_tag', 'hitcount' => 'count(*)' ),
- array(), __METHOD__, array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ) );
- foreach ( $res as $row ) {
- $html .= $this->doTagRow( $row->ct_tag, $row->hitcount );
- }
+ // Used in #doTagRow()
+ $this->definedTags = array_fill_keys( ChangeTags::listDefinedTags(), true );
- foreach( ChangeTags::listDefinedTags() as $tag ) {
- $html .= $this->doTagRow( $tag, 0 );
+ foreach ( ChangeTags::tagUsageStatistics() as $tag => $hitcount ) {
+ $html .= $this->doTagRow( $tag, $hitcount );
}
- $out->addHTML( Xml::tags( 'table', array( 'class' => 'wikitable mw-tags-table' ), $html ) );
+ $out->addHTML( Xml::tags(
+ 'table',
+ array( 'class' => 'wikitable sortable mw-tags-table' ),
+ $html
+ ) );
}
function doTagRow( $tag, $hitcount ) {
- static $doneTags = array();
-
- if ( in_array( $tag, $doneTags ) ) {
- return '';
- }
-
+ $user = $this->getUser();
$newRow = '';
$newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
$disp = ChangeTags::tagDescription( $tag );
- $disp .= ' ';
- $editLink = Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), $this->msg( 'tags-edit' )->escaped() );
- $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
+ if ( $user->isAllowed( 'editinterface' ) ) {
+ $disp .= ' ';
+ $editLink = Linker::link(
+ Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ),
+ $this->msg( 'tags-edit' )->escaped()
+ );
+ $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
+ }
$newRow .= Xml::tags( 'td', null, $disp );
$msg = $this->msg( "tag-$tag-description" );
$desc = !$msg->exists() ? '' : $msg->parse();
- $desc .= ' ';
- $editDescLink = Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), $this->msg( 'tags-edit' )->escaped() );
- $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
+ if ( $user->isAllowed( 'editinterface' ) ) {
+ $desc .= ' ';
+ $editDescLink = Linker::link(
+ Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ),
+ $this->msg( 'tags-edit' )->escaped()
+ );
+ $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
+ }
$newRow .= Xml::tags( 'td', null, $desc );
- $hitcount = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped();
- $hitcount = Linker::link( SpecialPage::getTitleFor( 'Recentchanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) );
- $newRow .= Xml::tags( 'td', null, $hitcount );
+ $active = isset( $this->definedTags[$tag] ) ? 'tags-active-yes' : 'tags-active-no';
+ $active = $this->msg( $active )->escaped();
+ $newRow .= Xml::tags( 'td', null, $active );
+
+ $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped();
+ $hitcountLink = Linker::link(
+ SpecialPage::getTitleFor( 'Recentchanges' ),
+ $hitcountLabel,
+ array(),
+ array( 'tagfilter' => $tag )
+ );
- $doneTags[] = $tag;
+ // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
+ $newRow .= Xml::tags( 'td', array( 'data-sort-value' => $hitcount ), $hitcountLink );
return Xml::tags( 'tr', null, $newRow ) . "\n";
}
+
+ protected function getGroupName() {
+ return 'changes';
+ }
}
diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php
index fb2005b5..ca93b6d1 100644
--- a/includes/specials/SpecialUnblock.php
+++ b/includes/specials/SpecialUnblock.php
@@ -32,11 +32,11 @@ class SpecialUnblock extends SpecialPage {
protected $type;
protected $block;
- public function __construct(){
+ public function __construct() {
parent::__construct( 'Unblock', 'block' );
}
- public function execute( $par ){
+ public function execute( $par ) {
$this->checkPermissions();
$this->checkReadOnly();
@@ -56,8 +56,8 @@ class SpecialUnblock extends SpecialPage {
$form->setSubmitTextMsg( 'ipusubmit' );
$form->addPreText( $this->msg( 'unblockiptext' )->parseAsBlock() );
- if( $form->show() ){
- switch( $this->type ){
+ if ( $form->show() ) {
+ switch ( $this->type ) {
case Block::TYPE_USER:
case Block::TYPE_IP:
$out->addWikiMsg( 'unblocked', wfEscapeWikiText( $this->target ) );
@@ -73,7 +73,7 @@ class SpecialUnblock extends SpecialPage {
}
}
- protected function getFields(){
+ protected function getFields() {
$fields = array(
'Target' => array(
'type' => 'text',
@@ -92,21 +92,21 @@ class SpecialUnblock extends SpecialPage {
)
);
- if( $this->block instanceof Block ){
+ if ( $this->block instanceof Block ) {
list( $target, $type ) = $this->block->getTargetAndType();
# Autoblocks are logged as "autoblock #123 because the IP was recently used by
# User:Foo, and we've just got any block, auto or not, that applies to a target
# the user has specified. Someone could be fishing to connect IPs to autoblocks,
# so don't show any distinction between unblocked IPs and autoblocked IPs
- if( $type == Block::TYPE_AUTO && $this->type == Block::TYPE_IP ){
+ if ( $type == Block::TYPE_AUTO && $this->type == Block::TYPE_IP ) {
$fields['Target']['default'] = $this->target;
unset( $fields['Name'] );
} else {
$fields['Target']['default'] = $target;
$fields['Target']['type'] = 'hidden';
- switch( $type ){
+ switch ( $type ) {
case Block::TYPE_USER:
case Block::TYPE_IP:
$fields['Name']['default'] = Linker::link(
@@ -138,6 +138,8 @@ class SpecialUnblock extends SpecialPage {
/**
* Submit callback for an HTMLForm object
+ * @param array $data
+ * @param HTMLForm $form
* @return Array( Array(message key, parameters)
*/
public static function processUIUnblock( array $data, HTMLForm $form ) {
@@ -149,14 +151,15 @@ class SpecialUnblock extends SpecialPage {
*
* @param $data Array
* @param $context IContextSource
+ * @throws ErrorPageError
* @return Array( Array(message key, parameters) ) on failure, True on success
*/
- public static function processUnblock( array $data, IContextSource $context ){
+ public static function processUnblock( array $data, IContextSource $context ) {
$performer = $context->getUser();
$target = $data['Target'];
$block = Block::newFromTarget( $data['Target'] );
- if( !$block instanceof Block ){
+ if ( !$block instanceof Block ) {
return array( array( 'ipb_cant_unblock', $target ) );
}
@@ -171,14 +174,14 @@ class SpecialUnblock extends SpecialPage {
# If the specified IP is a single address, and the block is a range block, don't
# unblock the whole range.
list( $target, $type ) = SpecialBlock::getTargetAndType( $target );
- if( $block->getType() == Block::TYPE_RANGE && $type == Block::TYPE_IP ) {
- $range = $block->getTarget();
- return array( array( 'ipb_blocked_as_range', $target, $range ) );
+ if ( $block->getType() == Block::TYPE_RANGE && $type == Block::TYPE_IP ) {
+ $range = $block->getTarget();
+ return array( array( 'ipb_blocked_as_range', $target, $range ) );
}
# If the name was hidden and the blocking user cannot hide
# names, then don't allow any block removals...
- if( !$performer->isAllowed( 'hideuser' ) && $block->mHideName ) {
+ if ( !$performer->isAllowed( 'hideuser' ) && $block->mHideName ) {
return array( 'unblock-hideuser' );
}
@@ -188,7 +191,7 @@ class SpecialUnblock extends SpecialPage {
}
# Unset _deleted fields as needed
- if( $block->mHideName ) {
+ if ( $block->mHideName ) {
# Something is deeply FUBAR if this is not a User object, but who knows?
$id = $block->getTarget() instanceof User
? $block->getTarget()->getID()
@@ -208,8 +211,12 @@ class SpecialUnblock extends SpecialPage {
# Make log entry
$log = new LogPage( 'block' );
- $log->addEntry( 'unblock', $page, $data['Reason'] );
+ $log->addEntry( 'unblock', $page, $data['Reason'], array(), $performer );
return true;
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialUncategorizedcategories.php b/includes/specials/SpecialUncategorizedcategories.php
index 70d98df9..1cc40a9e 100644
--- a/includes/specials/SpecialUncategorizedcategories.php
+++ b/includes/specials/SpecialUncategorizedcategories.php
@@ -31,4 +31,17 @@ class UncategorizedCategoriesPage extends UncategorizedPagesPage {
parent::__construct( $name );
$this->requestedNamespace = NS_CATEGORY;
}
+
+ /**
+ * Formats the result
+ * @param Skin $skin The current skin
+ * @param object $result The query result
+ * @return string The category link
+ */
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_CATEGORY, $result->title );
+ $text = $title->getText();
+
+ return Linker::linkKnown( $title, htmlspecialchars( $text ) );
+ }
}
diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php
index 5865bf62..3bfcedec 100644
--- a/includes/specials/SpecialUncategorizedimages.php
+++ b/includes/specials/SpecialUncategorizedimages.php
@@ -47,7 +47,7 @@ class UncategorizedImagesPage extends ImageQueryPage {
}
function getQueryInfo() {
- return array (
+ return array(
'tables' => array( 'page', 'categorylinks' ),
'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
@@ -60,4 +60,7 @@ class UncategorizedImagesPage extends ImageQueryPage {
);
}
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialUncategorizedpages.php b/includes/specials/SpecialUncategorizedpages.php
index 1226a6ca..8bc9e489 100644
--- a/includes/specials/SpecialUncategorizedpages.php
+++ b/includes/specials/SpecialUncategorizedpages.php
@@ -41,20 +41,23 @@ class UncategorizedPagesPage extends PageQueryPage {
function isExpensive() {
return true;
}
- function isSyndicated() { return false; }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'page', 'categorylinks' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
+ return array(
+ 'tables' => array( 'page', 'categorylinks' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'page_title' ),
// default for page_namespace is all content namespaces (if requestedNamespace is false)
// otherwise, page_namespace is requestedNamespace
- 'conds' => array ( 'cl_from IS NULL',
- 'page_namespace' => ( $this->requestedNamespace!==false ? $this->requestedNamespace : MWNamespace::getContentNamespaces() ),
+ 'conds' => array( 'cl_from IS NULL',
+ 'page_namespace' => ( $this->requestedNamespace !== false ? $this->requestedNamespace : MWNamespace::getContentNamespaces() ),
'page_is_redirect' => 0 ),
- 'join_conds' => array ( 'categorylinks' => array (
+ 'join_conds' => array( 'categorylinks' => array(
'LEFT JOIN', 'cl_from = page_id' ) )
);
}
@@ -62,8 +65,13 @@ class UncategorizedPagesPage extends PageQueryPage {
function getOrderFields() {
// For some crazy reason ordering by a constant
// causes a filesort
- if( $this->requestedNamespace === false && count( MWNamespace::getContentNamespaces() ) > 1 )
+ if ( $this->requestedNamespace === false && count( MWNamespace::getContentNamespaces() ) > 1 ) {
return array( 'page_namespace', 'page_title' );
+ }
return array( 'page_title' );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php
index d8e0b97c..d4aed113 100644
--- a/includes/specials/SpecialUndelete.php
+++ b/includes/specials/SpecialUndelete.php
@@ -27,15 +27,23 @@
* @ingroup SpecialPage
*/
class PageArchive {
-
/**
* @var Title
*/
protected $title;
- var $fileStatus;
+
+ /**
+ * @var Status
+ */
+ protected $fileStatus;
+
+ /**
+ * @var Status
+ */
+ protected $revisionStatus;
function __construct( $title ) {
- if( is_null( $title ) ) {
+ if ( is_null( $title ) ) {
throw new MWException( __METHOD__ . ' given a null title.' );
}
$this->title = $title;
@@ -58,14 +66,14 @@ class PageArchive {
* given title prefix.
* Returns result wrapper with (ar_namespace, ar_title, count) fields.
*
- * @param $prefix String: title prefix
+ * @param string $prefix Title prefix
* @return ResultWrapper
*/
public static function listPagesByPrefix( $prefix ) {
$dbr = wfGetDB( DB_SLAVE );
$title = Title::newFromText( $prefix );
- if( $title ) {
+ if ( $title ) {
$ns = $title->getNamespace();
$prefix = $title->getDBkey();
} else {
@@ -73,36 +81,36 @@ class PageArchive {
// @todo handle bare namespace names cleanly?
$ns = 0;
}
+
$conds = array(
'ar_namespace' => $ns,
'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
);
+
return self::listPages( $dbr, $conds );
}
/**
- * @param $dbr DatabaseBase
- * @param $condition
+ * @param DatabaseBase $dbr
+ * @param string|array $condition
* @return bool|ResultWrapper
*/
protected static function listPages( $dbr, $condition ) {
- return $dbr->resultObject(
- $dbr->select(
- array( 'archive' ),
- array(
- 'ar_namespace',
- 'ar_title',
- 'count' => 'COUNT(*)'
- ),
- $condition,
- __METHOD__,
- array(
- 'GROUP BY' => array( 'ar_namespace', 'ar_title' ),
- 'ORDER BY' => array( 'ar_namespace', 'ar_title' ),
- 'LIMIT' => 100,
- )
+ return $dbr->resultObject( $dbr->select(
+ array( 'archive' ),
+ array(
+ 'ar_namespace',
+ 'ar_title',
+ 'count' => 'COUNT(*)'
+ ),
+ $condition,
+ __METHOD__,
+ array(
+ 'GROUP BY' => array( 'ar_namespace', 'ar_title' ),
+ 'ORDER BY' => array( 'ar_namespace', 'ar_title' ),
+ 'LIMIT' => 100,
)
- );
+ ) );
}
/**
@@ -112,18 +120,28 @@ class PageArchive {
* @return ResultWrapper
*/
function listRevisions() {
+ global $wgContentHandlerUseDB;
+
$dbr = wfGetDB( DB_SLAVE );
+
+ $fields = array(
+ 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
+ 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+
$res = $dbr->select( 'archive',
- array(
- 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
- 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
- ),
+ $fields,
array( 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ),
+ 'ar_title' => $this->title->getDBkey() ),
__METHOD__,
array( 'ORDER BY' => 'ar_timestamp DESC' ) );
- $ret = $dbr->resultObject( $res );
- return $ret;
+
+ return $dbr->resultObject( $res );
}
/**
@@ -135,70 +153,66 @@ class PageArchive {
* @todo Does this belong in Image for fuller encapsulation?
*/
function listFiles() {
- if( $this->title->getNamespace() == NS_FILE ) {
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'filearchive',
- array(
- 'fa_id',
- 'fa_name',
- 'fa_archive_name',
- 'fa_storage_key',
- 'fa_storage_group',
- 'fa_size',
- 'fa_width',
- 'fa_height',
- 'fa_bits',
- 'fa_metadata',
- 'fa_media_type',
- 'fa_major_mime',
- 'fa_minor_mime',
- 'fa_description',
- 'fa_user',
- 'fa_user_text',
- 'fa_timestamp',
- 'fa_deleted' ),
- array( 'fa_name' => $this->title->getDBkey() ),
- __METHOD__,
- array( 'ORDER BY' => 'fa_timestamp DESC' ) );
- $ret = $dbr->resultObject( $res );
- return $ret;
+ if ( $this->title->getNamespace() != NS_FILE ) {
+ return null;
}
- return null;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select(
+ 'filearchive',
+ ArchivedFile::selectFields(),
+ array( 'fa_name' => $this->title->getDBkey() ),
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_timestamp DESC' )
+ );
+
+ return $dbr->resultObject( $res );
}
/**
* Return a Revision object containing data for the deleted revision.
* Note that the result *may* or *may not* have a null page ID.
*
- * @param $timestamp String
- * @return Revision
+ * @param string $timestamp
+ * @return Revision|null
*/
function getRevision( $timestamp ) {
+ global $wgContentHandlerUseDB;
+
$dbr = wfGetDB( DB_SLAVE );
+
+ $fields = array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_sha1',
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+
$row = $dbr->selectRow( 'archive',
- array(
- 'ar_rev_id',
- 'ar_text',
- 'ar_comment',
- 'ar_user',
- 'ar_user_text',
- 'ar_timestamp',
- 'ar_minor_edit',
- 'ar_flags',
- 'ar_text_id',
- 'ar_deleted',
- 'ar_len',
- 'ar_sha1',
- ),
+ $fields,
array( 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
__METHOD__ );
- if( $row ) {
- return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) );
- } else {
- return null;
+
+ if ( $row ) {
+ return Revision::newFromArchiveRow( $row, array( 'title' => $this->title ) );
}
+
+ return null;
}
/**
@@ -208,8 +222,8 @@ class PageArchive {
* May produce unexpected results in case of history merges or other
* unusual time issues.
*
- * @param $timestamp String
- * @return Revision or null
+ * @param string $timestamp
+ * @return Revision|null Null when there is no previous revision
*/
function getPreviousRevision( $timestamp ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -218,9 +232,9 @@ class PageArchive {
$row = $dbr->selectRow( 'archive',
'ar_timestamp',
array( 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- 'ar_timestamp < ' .
- $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
__METHOD__,
array(
'ORDER BY' => 'ar_timestamp DESC',
@@ -234,7 +248,7 @@ class PageArchive {
'page_title' => $this->title->getDBkey(),
'page_id = rev_page',
'rev_timestamp < ' .
- $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
__METHOD__,
array(
'ORDER BY' => 'rev_timestamp DESC',
@@ -242,38 +256,39 @@ class PageArchive {
$prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
$prevLiveId = $row ? intval( $row->rev_id ) : null;
- if( $prevLive && $prevLive > $prevDeleted ) {
+ if ( $prevLive && $prevLive > $prevDeleted ) {
// Most prior revision was live
return Revision::newFromId( $prevLiveId );
- } elseif( $prevDeleted ) {
+ } elseif ( $prevDeleted ) {
// Most prior revision was deleted
return $this->getRevision( $prevDeleted );
- } else {
- // No prior revision on this page.
- return null;
}
+
+ // No prior revision on this page.
+ return null;
}
/**
* Get the text from an archive row containing ar_text, ar_flags and ar_text_id
*
- * @param $row Object: database row
- * @return Revision
+ * @param object $row Database row
+ * @return string
*/
function getTextFromRow( $row ) {
- if( is_null( $row->ar_text_id ) ) {
+ if ( is_null( $row->ar_text_id ) ) {
// An old row from MediaWiki 1.4 or previous.
// Text is embedded in this row in classic compression format.
return Revision::getRevisionText( $row, 'ar_' );
- } else {
- // New-style: keyed to the text storage backend.
- $dbr = wfGetDB( DB_SLAVE );
- $text = $dbr->selectRow( 'text',
- array( 'old_text', 'old_flags' ),
- array( 'old_id' => $row->ar_text_id ),
- __METHOD__ );
- return Revision::getRevisionText( $text );
}
+
+ // New-style: keyed to the text storage backend.
+ $dbr = wfGetDB( DB_SLAVE );
+ $text = $dbr->selectRow( 'text',
+ array( 'old_text', 'old_flags' ),
+ array( 'old_id' => $row->ar_text_id ),
+ __METHOD__ );
+
+ return Revision::getRevisionText( $text );
}
/**
@@ -282,35 +297,37 @@ class PageArchive {
*
* If there are no archived revisions for the page, returns NULL.
*
- * @return String
+ * @return string|null
*/
function getLastRevisionText() {
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'archive',
array( 'ar_text', 'ar_flags', 'ar_text_id' ),
array( 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ),
+ 'ar_title' => $this->title->getDBkey() ),
__METHOD__,
array( 'ORDER BY' => 'ar_timestamp DESC' ) );
- if( $row ) {
+
+ if ( $row ) {
return $this->getTextFromRow( $row );
- } else {
- return null;
}
+
+ return null;
}
/**
* Quick check if any archived revisions are present for the page.
*
- * @return Boolean
+ * @return boolean
*/
function isDeleted() {
$dbr = wfGetDB( DB_SLAVE );
$n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
array( 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ),
+ 'ar_title' => $this->title->getDBkey() ),
__METHOD__
);
+
return ( $n > 0 );
}
@@ -319,18 +336,16 @@ class PageArchive {
* Once restored, the items will be removed from the archive tables.
* The deletion log will be updated with an undeletion notice.
*
- * @param $timestamps Array: pass an empty array to restore all revisions, otherwise list the ones to undelete.
- * @param $comment String
- * @param $fileVersions Array
- * @param $unsuppress Boolean
- * @param $user User doing the action, or null to use $wgUser
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ * @param bool $unsuppress
+ * @param User $user User performing the action, or null to use $wgUser
*
* @return array(number of file revisions restored, number of image revisions restored, log message)
* on success, false on failure
*/
function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false, User $user = null ) {
- global $wgUser;
-
// If both the set of text revisions and file revisions are empty,
// restore everything. Otherwise, just restore the requested items.
$restoreAll = empty( $timestamps ) && empty( $fileVersions );
@@ -338,10 +353,10 @@ class PageArchive {
$restoreText = $restoreAll || !empty( $timestamps );
$restoreFiles = $restoreAll || !empty( $fileVersions );
- if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
+ if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
$img = wfLocalFile( $this->title );
$this->fileStatus = $img->restore( $fileVersions, $unsuppress );
- if ( !$this->fileStatus->isOk() ) {
+ if ( !$this->fileStatus->isOK() ) {
return false;
}
$filesRestored = $this->fileStatus->successCount;
@@ -349,24 +364,26 @@ class PageArchive {
$filesRestored = 0;
}
- if( $restoreText ) {
- $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
- if( $textRestored === false ) { // It must be one of UNDELETE_*
+ if ( $restoreText ) {
+ $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
+ if ( !$this->revisionStatus->isOK() ) {
return false;
}
+
+ $textRestored = $this->revisionStatus->getValue();
} else {
$textRestored = 0;
}
// Touch the log!
- if( $textRestored && $filesRestored ) {
+ if ( $textRestored && $filesRestored ) {
$reason = wfMessage( 'undeletedrevisions-files' )
->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
- } elseif( $textRestored ) {
+ } elseif ( $textRestored ) {
$reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
->inContentLanguage()->text();
- } elseif( $filesRestored ) {
+ } elseif ( $filesRestored ) {
$reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
->inContentLanguage()->text();
} else {
@@ -374,11 +391,12 @@ class PageArchive {
return false;
}
- if( trim( $comment ) != '' ) {
+ if ( trim( $comment ) != '' ) {
$reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
}
if ( $user === null ) {
+ global $wgUser;
$user = $wgUser;
}
@@ -386,6 +404,9 @@ class PageArchive {
$logEntry->setPerformer( $user );
$logEntry->setTarget( $this->title );
$logEntry->setComment( $reason );
+
+ wfRunHooks( 'ArticleUndeleteLogEntry', array( $this, &$logEntry, $user ) );
+
$logid = $logEntry->insert();
$logEntry->publish( $logid );
@@ -397,18 +418,20 @@ class PageArchive {
* to the cur/old tables. If the page currently exists, all revisions will
* be stuffed into old, otherwise the most recent will go into cur.
*
- * @param $timestamps Array: pass an empty array to restore all revisions, otherwise list the ones to undelete.
- * @param $comment String
- * @param $unsuppress Boolean: remove all ar_deleted/fa_deleted restrictions of seletected revs
- *
- * @return Mixed: number of revisions restored or false on failure
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
+ * @param string $comment
+ * @throws ReadOnlyError
+ * @return Status Object containing the number of revisions restored on success
*/
private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
+ global $wgContentHandlerUseDB;
+
if ( wfReadOnly() ) {
- return false;
+ throw new ReadOnlyError();
}
- $restoreAll = empty( $timestamps );
+ $restoreAll = empty( $timestamps );
$dbw = wfGetDB( DB_MASTER );
# Does this page already exist? We'll have to update it...
@@ -420,24 +443,30 @@ class PageArchive {
$page = $dbw->selectRow( 'page',
array( 'page_id', 'page_latest' ),
array( 'page_namespace' => $this->title->getNamespace(),
- 'page_title' => $this->title->getDBkey() ),
+ 'page_title' => $this->title->getDBkey() ),
__METHOD__,
array( 'FOR UPDATE' ) // lock page
);
- if( $page ) {
+
+ if ( $page ) {
$makepage = false;
# Page already exists. Import the history, and if necessary
# we'll update the latest revision field in the record.
- $newid = 0;
- $pageId = $page->page_id;
- $previousRevId = $page->page_latest;
+
+ $previousRevId = $page->page_latest;
+
# Get the time span of this page
$previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
array( 'rev_id' => $previousRevId ),
__METHOD__ );
- if( $previousTimestamp === false ) {
- wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
- return 0;
+
+ if ( $previousTimestamp === false ) {
+ wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
+
+ $status = Status::newGood( 0 );
+ $status->warning( 'undeleterevision-missing' );
+
+ return $status;
}
} else {
# Have to create a new article...
@@ -446,7 +475,7 @@ class PageArchive {
$previousTimestamp = 0;
}
- if( $restoreAll ) {
+ if ( $restoreAll ) {
$oldones = '1 = 1'; # All revisions...
} else {
$oldts = implode( ',',
@@ -457,58 +486,89 @@ class PageArchive {
$oldones = "ar_timestamp IN ( {$oldts} )";
}
+ $fields = array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_page_id',
+ 'ar_len',
+ 'ar_sha1'
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+
/**
* Select each archived revision...
*/
$result = $dbw->select( 'archive',
- /* fields */ array(
- 'ar_rev_id',
- 'ar_text',
- 'ar_comment',
- 'ar_user',
- 'ar_user_text',
- 'ar_timestamp',
- 'ar_minor_edit',
- 'ar_flags',
- 'ar_text_id',
- 'ar_deleted',
- 'ar_page_id',
- 'ar_len',
- 'ar_sha1' ),
+ $fields,
/* WHERE */ array(
'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
+ 'ar_title' => $this->title->getDBkey(),
$oldones ),
__METHOD__,
/* options */ array( 'ORDER BY' => 'ar_timestamp' )
);
$ret = $dbw->resultObject( $result );
$rev_count = $dbw->numRows( $result );
- if( !$rev_count ) {
+
+ if ( !$rev_count ) {
wfDebug( __METHOD__ . ": no revisions to restore\n" );
- return false; // ???
+
+ $status = Status::newGood( 0 );
+ $status->warning( "undelete-no-results" );
+ return $status;
}
$ret->seek( $rev_count - 1 ); // move to last
$row = $ret->fetchObject(); // get newest archived rev
$ret->seek( 0 ); // move back
- if( $makepage ) {
+ // grab the content to check consistency with global state before restoring the page.
+ $revision = Revision::newFromArchiveRow( $row,
+ array(
+ 'title' => $article->getTitle(), // used to derive default content model
+ )
+ );
+ $user = User::newFromName( $revision->getRawUserText(), false );
+ $content = $revision->getContent( Revision::RAW );
+
+ //NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+ $status = $content->prepareSave( $article, 0, -1, $user );
+
+ if ( !$status->isOK() ) {
+ return $status;
+ }
+
+ 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!
+ if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
+ return Status::newFatal( "undeleterevdel" );
}
// Safe to insert now...
- $newid = $article->insertOn( $dbw );
+ $newid = $article->insertOn( $dbw );
$pageId = $newid;
} else {
// Check if a deleted revision will become the current revision...
- if( $row->ar_timestamp > $previousTimestamp ) {
+ 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!
+ if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
+ return Status::newFatal( "undeleterevdel" );
}
}
+
+ $newid = false;
+ $pageId = $article->getId();
}
$revision = null;
@@ -516,10 +576,10 @@ class PageArchive {
foreach ( $ret as $row ) {
// Check for key dupes due to shitty archive integrity.
- if( $row->ar_rev_id ) {
+ if ( $row->ar_rev_id ) {
$exists = $dbw->selectField( 'revision', '1',
array( 'rev_id' => $row->ar_rev_id ), __METHOD__ );
- if( $exists ) {
+ if ( $exists ) {
continue; // don't throw DB errors
}
}
@@ -528,6 +588,7 @@ class PageArchive {
$revision = Revision::newFromArchiveRow( $row,
array(
'page' => $pageId,
+ 'title' => $this->title,
'deleted' => $unsuppress ? 0 : $row->ar_deleted
) );
@@ -546,7 +607,7 @@ class PageArchive {
// Was anything restored at all?
if ( $restored == 0 ) {
- return 0;
+ return Status::newGood( 0 );
}
$created = (bool)$newid;
@@ -561,18 +622,27 @@ class PageArchive {
wfRunHooks( 'ArticleUndelete', array( &$this->title, $created, $comment ) );
- if( $this->title->getNamespace() == NS_FILE ) {
+ if ( $this->title->getNamespace() == NS_FILE ) {
$update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
$update->doUpdate();
}
- return $restored;
+ return Status::newGood( $restored );
}
/**
* @return Status
*/
- function getFileStatus() { return $this->fileStatus; }
+ function getFileStatus() {
+ return $this->fileStatus;
+ }
+
+ /**
+ * @return Status
+ */
+ function getRevisionStatus() {
+ return $this->revisionStatus;
+ }
}
/**
@@ -604,10 +674,13 @@ class SpecialUndelete extends SpecialPage {
} else {
$this->mTarget = $request->getVal( 'target' );
}
+
$this->mTargetObj = null;
+
if ( $this->mTarget !== null && $this->mTarget !== '' ) {
$this->mTargetObj = Title::newFromURL( $this->mTarget );
}
+
$this->mSearchPrefix = $request->getText( 'prefix' );
$time = $request->getVal( 'timestamp' );
$this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
@@ -638,16 +711,16 @@ class SpecialUndelete extends SpecialPage {
$this->mRestore = false;
}
- if( $this->mRestore || $this->mInvert ) {
+ if ( $this->mRestore || $this->mInvert ) {
$timestamps = array();
$this->mFileVersions = array();
- foreach( $request->getValues() as $key => $val ) {
+ foreach ( $request->getValues() as $key => $val ) {
$matches = array();
- if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
+ if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
array_push( $timestamps, $matches[1] );
}
- if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
+ if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
$this->mFileVersions[] = intval( $matches[1] );
}
}
@@ -687,13 +760,13 @@ class SpecialUndelete extends SpecialPage {
if ( $this->mTimestamp !== '' ) {
$this->showRevision( $this->mTimestamp );
- } elseif ( $this->mFilename !== null ) {
+ } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
$file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
// Check if user is allowed to see this file
if ( !$file->exists() ) {
$out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
} elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
- if( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
+ if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
throw new PermissionsError( 'suppressrevision' );
} else {
throw new PermissionsError( 'deletedtext' );
@@ -716,22 +789,27 @@ class SpecialUndelete extends SpecialPage {
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'undelete-search-title' ) );
$out->addHTML(
- Xml::openElement( 'form', array(
- 'method' => 'get',
- 'action' => $wgScript ) ) .
- Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) .
- Html::hidden( 'title',
- $this->getTitle()->getPrefixedDbKey() ) .
- Xml::inputLabel( $this->msg( 'undelete-search-prefix' )->text(),
- 'prefix', 'prefix', 20,
- $this->mSearchPrefix ) . ' ' .
- Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' )
+ Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
+ Html::rawElement(
+ 'label',
+ array( 'for' => 'prefix' ),
+ $this->msg( 'undelete-search-prefix' )->parse()
+ ) .
+ Xml::input(
+ 'prefix',
+ 20,
+ $this->mSearchPrefix,
+ array( 'id' => 'prefix', 'autofocus' => true )
+ ) . ' ' .
+ Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
);
# List undeletable articles
- if( $this->mSearchPrefix ) {
+ if ( $this->mSearchPrefix ) {
$result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
$this->showList( $result );
}
@@ -740,15 +818,15 @@ class SpecialUndelete extends SpecialPage {
/**
* Generic list of deleted pages
*
- * @param $result ResultWrapper
+ * @param ResultWrapper $result
* @return bool
*/
private function showList( $result ) {
$out = $this->getOutput();
- if( $result->numRows() == 0 ) {
+ if ( $result->numRows() == 0 ) {
$out->addWikiMsg( 'undelete-no-results' );
- return;
+ return false;
}
$out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
@@ -766,8 +844,15 @@ class SpecialUndelete extends SpecialPage {
);
} else {
// The title is no longer valid, show as text
- $item = Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
- Linker::getInvalidTitleDescription( $this->getContext(), $row->ar_namespace, $row->ar_title ) );
+ $item = Html::element(
+ 'span',
+ array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription(
+ $this->getContext(),
+ $row->ar_namespace,
+ $row->ar_title
+ )
+ );
}
$revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
$out->addHTML( "<li>{$item} ({$revs})</li>\n" );
@@ -779,42 +864,50 @@ class SpecialUndelete extends SpecialPage {
}
private function showRevision( $timestamp ) {
- if( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
- return 0;
+ if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
+ return;
}
$archive = new PageArchive( $this->mTargetObj );
- wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) );
+ if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) {
+ return;
+ }
$rev = $archive->getRevision( $timestamp );
$out = $this->getOutput();
$user = $this->getUser();
- if( !$rev ) {
+ if ( !$rev ) {
$out->addWikiMsg( 'undeleterevision-missing' );
return;
}
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- if( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
- $out->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ $out->wrapWikiMsg(
+ "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+ 'rev-deleted-text-permission'
+ );
return;
- } else {
- $out->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
- $out->addHTML( '<br />' );
- // and we are allowed to see...
}
+
+ $out->wrapWikiMsg(
+ "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+ 'rev-deleted-text-view'
+ );
+ $out->addHTML( '<br />' );
+ // and we are allowed to see...
}
- if( $this->mDiff ) {
+ if ( $this->mDiff ) {
$previousRev = $archive->getPreviousRevision( $timestamp );
- if( $previousRev ) {
+ if ( $previousRev ) {
$this->showDiff( $previousRev, $rev );
- if( $this->mDiffOnly ) {
+ if ( $this->mDiffOnly ) {
return;
- } else {
- $out->addHTML( '<hr />' );
}
+
+ $out->addHTML( '<hr />' );
} else {
$out->addWikiMsg( 'undelete-nodiff' );
}
@@ -834,7 +927,11 @@ class SpecialUndelete extends SpecialPage {
$t = $lang->userTime( $timestamp, $user );
$userLink = Linker::revUserTools( $rev );
- if( $this->mPreview ) {
+ $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
+
+ $isText = ( $content instanceof TextContent );
+
+ if ( $this->mPreview || $isText ) {
$openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
} else {
$openDiv = '<div id="mw-undelete-revision">';
@@ -851,92 +948,120 @@ class SpecialUndelete extends SpecialPage {
$out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
$time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
- wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
- if( $this->mPreview ) {
+ if ( !wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) {
+ return;
+ }
+
+ if ( $this->mPreview || !$isText ) {
+ // NOTE: non-text content has no source view, so always use rendered preview
+
// Hide [edit]s
$popts = $out->parserOptions();
$popts->setEditSection( false );
- $out->parserOptions( $popts );
- $out->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER, $user ), $this->mTargetObj, true );
+
+ $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
+ $out->addParserOutput( $pout );
}
- $out->addHTML(
- Xml::element( 'textarea', array(
+ if ( $isText ) {
+ // source view for textual content
+ $sourceView = Xml::element(
+ 'textarea',
+ array(
'readonly' => 'readonly',
- 'cols' => intval( $user->getOption( 'cols' ) ),
- 'rows' => intval( $user->getOption( 'rows' ) ) ),
- $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) .
- Xml::openElement( 'div' ) .
- Xml::openElement( 'form', array(
- 'method' => 'post',
- 'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
- Xml::element( 'input', array(
- 'type' => 'hidden',
- 'name' => 'target',
- 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
- Xml::element( 'input', array(
- 'type' => 'hidden',
- 'name' => 'timestamp',
- 'value' => $timestamp ) ) .
- Xml::element( 'input', array(
- 'type' => 'hidden',
- 'name' => 'wpEditToken',
- 'value' => $user->getEditToken() ) ) .
- Xml::element( 'input', array(
+ 'cols' => $user->getIntOption( 'cols' ),
+ 'rows' => $user->getIntOption( 'rows' )
+ ),
+ $content->getNativeData() . "\n"
+ );
+
+ $previewButton = Xml::element( 'input', array(
'type' => 'submit',
'name' => 'preview',
- 'value' => $this->msg( 'showpreview' )->text() ) ) .
- Xml::element( 'input', array(
- 'name' => 'diff',
- 'type' => 'submit',
- 'value' => $this->msg( 'showdiff' )->text() ) ) .
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'div' ) );
+ 'value' => $this->msg( 'showpreview' )->text()
+ ) );
+ } else {
+ $sourceView = '';
+ $previewButton = '';
+ }
+
+ $diffButton = Xml::element( 'input', array(
+ 'name' => 'diff',
+ 'type' => 'submit',
+ 'value' => $this->msg( 'showdiff' )->text() ) );
+
+ $out->addHTML(
+ $sourceView .
+ Xml::openElement( 'div', array(
+ 'style' => 'clear: both' ) ) .
+ Xml::openElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
+ Xml::element( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'target',
+ 'value' => $this->mTargetObj->getPrefixedDBkey() ) ) .
+ Xml::element( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'timestamp',
+ 'value' => $timestamp ) ) .
+ Xml::element( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'wpEditToken',
+ 'value' => $user->getEditToken() ) ) .
+ $previewButton .
+ $diffButton .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'div' )
+ );
}
/**
* Build a diff display between this and the previous either deleted
* or non-deleted edit.
*
- * @param $previousRev Revision
- * @param $currentRev Revision
- * @return String: HTML
+ * @param Revision $previousRev
+ * @param Revision $currentRev
+ * @return string HTML
*/
function showDiff( $previousRev, $currentRev ) {
- $diffEngine = new DifferenceEngine( $this->getContext() );
+ $diffContext = clone $this->getContext();
+ $diffContext->setTitle( $currentRev->getTitle() );
+ $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
+
+ $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
$diffEngine->showDiffStyle();
- $this->getOutput()->addHTML(
- "<div>" .
- "<table width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+ $this->getOutput()->addHTML( "<div>" .
+ "<table style='width: 98%;' cellpadding='0' cellspacing='4' class='diff'>" .
"<col class='diff-marker' />" .
"<col class='diff-content' />" .
"<col class='diff-marker' />" .
"<col class='diff-content' />" .
"<tr>" .
- "<td colspan='2' width='50%' style='text-align: center' class='diff-otitle'>" .
- $this->diffHeader( $previousRev, 'o' ) .
- "</td>\n" .
- "<td colspan='2' width='50%' style='text-align: center' class='diff-ntitle'>" .
- $this->diffHeader( $currentRev, 'n' ) .
- "</td>\n" .
+ "<td colspan='2' style='width: 50%; text-align: center' class='diff-otitle'>" .
+ $this->diffHeader( $previousRev, 'o' ) .
+ "</td>\n" .
+ "<td colspan='2' style='width: 50%; text-align: center' class='diff-ntitle'>" .
+ $this->diffHeader( $currentRev, 'n' ) .
+ "</td>\n" .
"</tr>" .
- $diffEngine->generateDiffBody(
- $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
- $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
+ $diffEngine->generateContentDiffBody(
+ $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
+ $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) .
"</table>" .
"</div>\n"
);
}
/**
- * @param $rev Revision
- * @param $prefix
+ * @param Revision $rev
+ * @param string $prefix
* @return string
*/
private function diffHeader( $rev, $prefix ) {
$isDeleted = !( $rev->getId() && $rev->getTitle() );
- if( $isDeleted ) {
+ if ( $isDeleted ) {
/// @todo FIXME: $rev->getTitle() is null for deleted revs...?
$targetPage = $this->getTitle();
$targetQuery = array(
@@ -948,30 +1073,34 @@ class SpecialUndelete extends SpecialPage {
$targetPage = $rev->getTitle();
$targetQuery = array( 'oldid' => $rev->getId() );
}
+
// Add show/hide deletion links if available
$user = $this->getUser();
$lang = $this->getLanguage();
$rdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
- if ( $rdel ) $rdel = " $rdel";
- return
- '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
- Linker::link(
- $targetPage,
- $this->msg(
- 'revisionasof',
- $lang->userTimeAndDate( $rev->getTimestamp(), $user ),
- $lang->userDate( $rev->getTimestamp(), $user ),
- $lang->userTime( $rev->getTimestamp(), $user )
- )->escaped(),
- array(),
- $targetQuery
- ) .
+
+ if ( $rdel ) {
+ $rdel = " $rdel";
+ }
+
+ return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
+ Linker::link(
+ $targetPage,
+ $this->msg(
+ 'revisionasof',
+ $lang->userTimeAndDate( $rev->getTimestamp(), $user ),
+ $lang->userDate( $rev->getTimestamp(), $user ),
+ $lang->userTime( $rev->getTimestamp(), $user )
+ )->escaped(),
+ array(),
+ $targetQuery
+ ) .
'</strong></div>' .
- '<div id="mw-diff-'.$prefix.'title2">' .
- Linker::revUserTools( $rev ) . '<br />' .
+ '<div id="mw-diff-' . $prefix . 'title2">' .
+ Linker::revUserTools( $rev ) . '<br />' .
'</div>' .
- '<div id="mw-diff-'.$prefix.'title3">' .
- Linker::revComment( $rev ) . $rdel . '<br />' .
+ '<div id="mw-diff-' . $prefix . 'title3">' .
+ Linker::revComment( $rev ) . $rdel . '<br />' .
'</div>';
}
@@ -989,15 +1118,16 @@ class SpecialUndelete extends SpecialPage {
$lang->userTime( $file->getTimestamp(), $user ) );
$out->addHTML(
Xml::openElement( 'form', array(
- 'method' => 'POST',
- 'action' => $this->getTitle()->getLocalURL(
- 'target=' . urlencode( $this->mTarget ) .
- '&file=' . urlencode( $key ) .
- '&token=' . urlencode( $user->getEditToken( $key ) ) )
+ 'method' => 'POST',
+ 'action' => $this->getTitle()->getLocalURL( array(
+ 'target' => $this->mTarget,
+ 'file' => $key,
+ 'token' => $user->getEditToken( $key ),
+ ) ),
)
) .
- Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
- '</form>'
+ Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
+ '</form>'
);
}
@@ -1023,7 +1153,7 @@ class SpecialUndelete extends SpecialPage {
private function showHistory() {
$out = $this->getOutput();
- if( $this->mAllowed ) {
+ if ( $this->mAllowed ) {
$out->addModules( 'mediawiki.special.undelete' );
}
$out->wrapWikiMsg(
@@ -1057,7 +1187,7 @@ class SpecialUndelete extends SpecialPage {
$haveFiles = $files && $files->numRows() > 0;
# Batch existence check on user and talk pages
- if( $haveRevisions ) {
+ if ( $haveRevisions ) {
$batch = new LinkBatch();
foreach ( $revisions as $row ) {
$batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
@@ -1066,7 +1196,7 @@ class SpecialUndelete extends SpecialPage {
$batch->execute();
$revisions->seek( 0 );
}
- if( $haveFiles ) {
+ if ( $haveFiles ) {
$batch = new LinkBatch();
foreach ( $files as $row ) {
$batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
@@ -1079,7 +1209,10 @@ class SpecialUndelete extends SpecialPage {
if ( $this->mAllowed ) {
$action = $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) );
# Start the form here
- $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
+ $top = Xml::openElement(
+ 'form',
+ array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' )
+ );
$out->addHTML( $top );
}
@@ -1089,59 +1222,60 @@ class SpecialUndelete extends SpecialPage {
LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
# Show relevant lines from the suppression log:
$suppressLogPage = new LogPage( 'suppress' );
- if( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
+ if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
$out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
}
- if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
+ if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
# Format the user-visible controls (comment field, submission button)
# in a nice little table
- if( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
+ if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
$unsuppressBox =
"<tr>
<td>&#160;</td>
<td class='mw-input'>" .
- Xml::checkLabel( $this->msg( 'revdelete-unsuppress' )->text(),
- 'wpUnsuppress', 'mw-undelete-unsuppress', $this->mUnsuppress ).
+ Xml::checkLabel( $this->msg( 'revdelete-unsuppress' )->text(),
+ 'wpUnsuppress', 'mw-undelete-unsuppress', $this->mUnsuppress ) .
"</td>
</tr>";
} else {
$unsuppressBox = '';
}
+
$table =
Xml::fieldset( $this->msg( 'undelete-fieldset-title' )->text() ) .
- Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
"<tr>
<td colspan='2' class='mw-undelete-extrahelp'>" .
- $this->msg( 'undeleteextrahelp' )->parseAsBlock() .
- "</td>
- </tr>
- <tr>
- <td class='mw-label'>" .
- Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
- "</td>
- </tr>
- <tr>
- <td>&#160;</td>
- <td class='mw-submit'>" .
- Xml::submitButton( $this->msg( 'undeletebtn' )->text(), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . ' ' .
- Xml::submitButton( $this->msg( 'undeleteinvert' )->text(), array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) ) .
- "</td>
- </tr>" .
+ $this->msg( 'undeleteextrahelp' )->parseAsBlock() .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment', 'autofocus' => true ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&#160;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( $this->msg( 'undeletebtn' )->text(), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . ' ' .
+ Xml::submitButton( $this->msg( 'undeleteinvert' )->text(), array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) ) .
+ "</td>
+ </tr>" .
$unsuppressBox .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'fieldset' );
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
$out->addHTML( $table );
}
$out->addHTML( Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n" );
- if( $haveRevisions ) {
+ if ( $haveRevisions ) {
# The page's stored (deleted) history:
$out->addHTML( '<ul>' );
$remaining = $revisions->numRows();
@@ -1157,7 +1291,7 @@ class SpecialUndelete extends SpecialPage {
$out->addWikiMsg( 'nohistory' );
}
- if( $haveFiles ) {
+ if ( $haveFiles ) {
$out->addHTML( Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n" );
$out->addHTML( '<ul>' );
foreach ( $files as $row ) {
@@ -1169,7 +1303,7 @@ class SpecialUndelete extends SpecialPage {
if ( $this->mAllowed ) {
# Slip in the hidden controls here
- $misc = Html::hidden( 'target', $this->mTarget );
+ $misc = Html::hidden( 'target', $this->mTarget );
$misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
$misc .= Xml::closeElement( 'form' );
$out->addHTML( $misc );
@@ -1180,13 +1314,16 @@ class SpecialUndelete extends SpecialPage {
private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
$rev = Revision::newFromArchiveRow( $row,
- array( 'page' => $this->mTargetObj->getArticleID() ) );
+ array(
+ 'title' => $this->mTargetObj
+ ) );
+
$revTextSize = '';
$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
// Build checkboxen...
- if( $this->mAllowed ) {
- if( $this->mInvert ) {
- if( in_array( $ts, $this->mTargetTimestamp ) ) {
+ if ( $this->mAllowed ) {
+ if ( $this->mInvert ) {
+ if ( in_array( $ts, $this->mTargetTimestamp ) ) {
$checkBox = Xml::check( "ts$ts" );
} else {
$checkBox = Xml::check( "ts$ts", true );
@@ -1197,15 +1334,16 @@ class SpecialUndelete extends SpecialPage {
} else {
$checkBox = '';
}
- $user = $this->getUser();
+
// Build page & diff links...
- if( $this->mCanView ) {
+ $user = $this->getUser();
+ if ( $this->mCanView ) {
$titleObj = $this->getTitle();
# Last link
- if( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+ if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
$pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
$last = $this->msg( 'diff' )->escaped();
- } elseif( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
+ } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
$pageLink = $this->getPageLink( $rev, $titleObj, $ts );
$last = Linker::linkKnown(
$titleObj,
@@ -1225,28 +1363,35 @@ class SpecialUndelete extends SpecialPage {
$pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
$last = $this->msg( 'diff' )->escaped();
}
+
// User links
$userLink = Linker::revUserTools( $rev );
+
// Revision text size
$size = $row->ar_len;
- if( !is_null( $size ) ) {
+ if ( !is_null( $size ) ) {
$revTextSize = Linker::formatRevisionSize( $size );
}
+
// Edit summary
$comment = Linker::revComment( $rev );
+
// Revision delete links
$revdlink = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
- $revisionRow = $this->msg( 'undelete-revisionrow' )->rawParams( $checkBox, $revdlink, $last, $pageLink , $userLink, $revTextSize, $comment )->escaped();
+ $revisionRow = $this->msg( 'undelete-revisionrow' )
+ ->rawParams( $checkBox, $revdlink, $last, $pageLink, $userLink, $revTextSize, $comment )
+ ->escaped();
+
return "<li>$revisionRow</li>";
}
private function formatFileRow( $row ) {
$file = ArchivedFile::newFromRow( $row );
-
$ts = wfTimestamp( TS_MW, $row->fa_timestamp );
$user = $this->getUser();
- if( $this->mAllowed && $row->fa_storage_key ) {
+
+ if ( $this->mAllowed && $row->fa_storage_key ) {
$checkBox = Xml::check( 'fileid' . $row->fa_id );
$key = urlencode( $row->fa_storage_key );
$pageLink = $this->getFileLink( $file, $this->getTitle(), $ts, $key );
@@ -1256,15 +1401,18 @@ class SpecialUndelete extends SpecialPage {
}
$userLink = $this->getFileUser( $file );
$data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
- $bytes = $this->msg( 'parentheses' )->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )->plain();
+ $bytes = $this->msg( 'parentheses' )
+ ->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
+ ->plain();
$data = htmlspecialchars( $data . ' ' . $bytes );
$comment = $this->getFileComment( $file );
// Add show/hide deletion links if available
$canHide = $user->isAllowed( 'deleterevision' );
- if( $canHide || ( $file->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
- if( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
- $revdlink = Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ if ( $canHide || ( $file->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
+ if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
+ // Revision was hidden from sysops
+ $revdlink = Linker::revDeleteLinkDisabled( $canHide );
} else {
$query = array(
'type' => 'filearchive',
@@ -1284,104 +1432,114 @@ class SpecialUndelete extends SpecialPage {
/**
* Fetch revision text link if it's available to all users
*
- * @param $rev Revision
- * @param $titleObj Title
- * @param $ts string Timestamp
+ * @param Revision $rev
+ * @param Title $titleObj
+ * @param string $ts Timestamp
* @return string
*/
function getPageLink( $rev, $titleObj, $ts ) {
$user = $this->getUser();
$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
- if( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
return '<span class="history-deleted">' . $time . '</span>';
- } else {
- $link = Linker::linkKnown(
- $titleObj,
- htmlspecialchars( $time ),
- array(),
- array(
- 'target' => $this->mTargetObj->getPrefixedText(),
- 'timestamp' => $ts
- )
- );
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $link = '<span class="history-deleted">' . $link . '</span>';
- }
- return $link;
}
+
+ $link = Linker::linkKnown(
+ $titleObj,
+ htmlspecialchars( $time ),
+ array(),
+ array(
+ 'target' => $this->mTargetObj->getPrefixedText(),
+ 'timestamp' => $ts
+ )
+ );
+
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ }
+
+ return $link;
}
/**
* Fetch image view link if it's available to all users
*
- * @param $file File
- * @param $titleObj Title
- * @param $ts string A timestamp
- * @param $key String: a storage key
+ * @param File|ArchivedFile $file
+ * @param Title $titleObj
+ * @param string $ts A timestamp
+ * @param string $key a storage key
*
- * @return String: HTML fragment
+ * @return string HTML fragment
*/
function getFileLink( $file, $titleObj, $ts, $key ) {
$user = $this->getUser();
$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
- if( !$file->userCan( File::DELETED_FILE, $user ) ) {
+ if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
return '<span class="history-deleted">' . $time . '</span>';
- } else {
- $link = Linker::linkKnown(
- $titleObj,
- htmlspecialchars( $time ),
- array(),
- array(
- 'target' => $this->mTargetObj->getPrefixedText(),
- 'file' => $key,
- 'token' => $user->getEditToken( $key )
- )
- );
- if( $file->isDeleted( File::DELETED_FILE ) ) {
- $link = '<span class="history-deleted">' . $link . '</span>';
- }
- return $link;
}
+
+ $link = Linker::linkKnown(
+ $titleObj,
+ htmlspecialchars( $time ),
+ array(),
+ array(
+ 'target' => $this->mTargetObj->getPrefixedText(),
+ 'file' => $key,
+ 'token' => $user->getEditToken( $key )
+ )
+ );
+
+ if ( $file->isDeleted( File::DELETED_FILE ) ) {
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ }
+
+ return $link;
}
/**
* Fetch file's user id if it's available to this user
*
- * @param $file File
- * @return String: HTML fragment
+ * @param File|ArchivedFile $file
+ * @return string HTML fragment
*/
function getFileUser( $file ) {
- if( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
- return '<span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
- } else {
- $link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
- Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
- if( $file->isDeleted( File::DELETED_USER ) ) {
- $link = '<span class="history-deleted">' . $link . '</span>';
- }
- return $link;
+ if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
+ return '<span class="history-deleted">' .
+ $this->msg( 'rev-deleted-user' )->escaped() .
+ '</span>';
}
+
+ $link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
+ Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
+
+ if ( $file->isDeleted( File::DELETED_USER ) ) {
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ }
+
+ return $link;
}
/**
* Fetch file upload comment if it's available to this user
*
- * @param $file File
- * @return String: HTML fragment
+ * @param File|ArchivedFile $file
+ * @return string HTML fragment
*/
function getFileComment( $file ) {
- if( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
+ if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
return '<span class="history-deleted"><span class="comment">' .
$this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>';
- } else {
- $link = Linker::commentBlock( $file->getRawDescription() );
- if( $file->isDeleted( File::DELETED_COMMENT ) ) {
- $link = '<span class="history-deleted">' . $link . '</span>';
- }
- return $link;
}
+
+ $link = Linker::commentBlock( $file->getRawDescription() );
+
+ if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ }
+
+ return $link;
}
function undelete() {
@@ -1406,7 +1564,7 @@ class SpecialUndelete extends SpecialPage {
$this->getUser()
);
- if( is_array( $ok ) ) {
+ if ( is_array( $ok ) ) {
if ( $ok[1] ) { // Undeleted file count
wfRunHooks( 'FileUndeleteComplete', array(
$this->mTargetObj, $this->mFileVersions,
@@ -1417,14 +1575,22 @@ class SpecialUndelete extends SpecialPage {
$out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
} else {
$out->setPageTitle( $this->msg( 'undelete-error' ) );
- $out->addWikiMsg( 'cannotundelete' );
- $out->addWikiMsg( 'undeleterevdel' );
}
- // Show file deletion warnings and errors
+ // Show revision undeletion warnings and errors
+ $status = $archive->getRevisionStatus();
+ if ( $status && !$status->isGood() ) {
+ $out->addWikiText( '<div class="error">' . $status->getWikiText( 'cannotundelete', 'cannotundelete' ) . '</div>' );
+ }
+
+ // Show file undeletion warnings and errors
$status = $archive->getFileStatus();
- if( $status && !$status->isGood() ) {
+ if ( $status && !$status->isGood() ) {
$out->addWikiText( '<div class="error">' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '</div>' );
}
}
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php
index 2e772540..35141d80 100644
--- a/includes/specials/SpecialUnlockdb.php
+++ b/includes/specials/SpecialUnlockdb.php
@@ -84,4 +84,8 @@ class SpecialUnlockdb extends FormSpecialPage {
$out->addSubtitle( $this->msg( 'unlockdbsuccesssub' ) );
$out->addWikiMsg( 'unlockdbsuccesstext' );
}
+
+ protected function getGroupName() {
+ return 'wiki';
+ }
}
diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php
index 1bd38e17..b686a5b5 100644
--- a/includes/specials/SpecialUnusedcategories.php
+++ b/includes/specials/SpecialUnusedcategories.php
@@ -26,7 +26,9 @@
*/
class UnusedCategoriesPage extends QueryPage {
- function isExpensive() { return true; }
+ function isExpensive() {
+ return true;
+ }
function __construct( $name = 'Unusedcategories' ) {
parent::__construct( $name );
@@ -37,15 +39,15 @@ class UnusedCategoriesPage extends QueryPage {
}
function getQueryInfo() {
- return array (
- 'tables' => array ( 'page', 'categorylinks' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
+ return array(
+ 'tables' => array( 'page', 'categorylinks' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'page_title' ),
- 'conds' => array ( 'cl_from IS NULL',
+ 'conds' => array( 'cl_from IS NULL',
'page_namespace' => NS_CATEGORY,
'page_is_redirect' => 0 ),
- 'join_conds' => array ( 'categorylinks' => array (
+ 'join_conds' => array( 'categorylinks' => array(
'LEFT JOIN', 'cl_to = page_title' ) )
);
}
@@ -58,8 +60,17 @@ class UnusedCategoriesPage extends QueryPage {
return false;
}
+ /**
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string
+ */
function formatResult( $skin, $result ) {
$title = Title::makeTitle( NS_CATEGORY, $result->title );
return Linker::link( $title, htmlspecialchars( $title->getText() ) );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php
index cdab557e..d332db75 100644
--- a/includes/specials/SpecialUnusedimages.php
+++ b/includes/specials/SpecialUnusedimages.php
@@ -45,28 +45,28 @@ class UnusedimagesPage extends ImageQueryPage {
function getQueryInfo() {
global $wgCountCategorizedImagesAsUsed;
- $retval = array (
- 'tables' => array ( 'image', 'imagelinks' ),
- 'fields' => array ( 'namespace' => NS_FILE,
+ $retval = array(
+ 'tables' => array( 'image', 'imagelinks' ),
+ 'fields' => array( 'namespace' => NS_FILE,
'title' => 'img_name',
'value' => 'img_timestamp',
'img_user', 'img_user_text',
'img_description' ),
- 'conds' => array ( 'il_to IS NULL' ),
- 'join_conds' => array ( 'imagelinks' => array (
+ 'conds' => array( 'il_to IS NULL' ),
+ 'join_conds' => array( 'imagelinks' => array(
'LEFT JOIN', 'il_to = img_name' ) )
);
if ( $wgCountCategorizedImagesAsUsed ) {
// Order is significant
- $retval['tables'] = array ( 'image', 'page', 'categorylinks',
+ $retval['tables'] = array( 'image', 'page', 'categorylinks',
'imagelinks' );
$retval['conds']['page_namespace'] = NS_FILE;
$retval['conds'][] = 'cl_from IS NULL';
$retval['conds'][] = 'img_name = page_title';
- $retval['join_conds']['categorylinks'] = array (
+ $retval['join_conds']['categorylinks'] = array(
'LEFT JOIN', 'cl_from = page_id' );
- $retval['join_conds']['imagelinks'] = array (
+ $retval['join_conds']['imagelinks'] = array(
'LEFT JOIN', 'il_to = page_title' );
}
return $retval;
@@ -80,4 +80,7 @@ class UnusedimagesPage extends ImageQueryPage {
return $this->msg( 'unusedimagestext' )->parseAsBlock();
}
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php
index 06077d1f..1dc9f420 100644
--- a/includes/specials/SpecialUnusedtemplates.php
+++ b/includes/specials/SpecialUnusedtemplates.php
@@ -35,28 +35,36 @@ class UnusedtemplatesPage extends QueryPage {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
- function sortDescending() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function sortDescending() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'page', 'templatelinks' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
+ return array(
+ 'tables' => array( 'page', 'templatelinks' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'page_title' ),
- 'conds' => array ( 'page_namespace' => NS_TEMPLATE,
+ 'conds' => array( 'page_namespace' => NS_TEMPLATE,
'tl_from IS NULL',
'page_is_redirect' => 0 ),
- 'join_conds' => array ( 'templatelinks' => array (
- 'LEFT JOIN', array ( 'tl_title = page_title',
+ 'join_conds' => array( 'templatelinks' => array(
+ 'LEFT JOIN', array( 'tl_title = page_title',
'tl_namespace = page_namespace' ) ) )
);
}
/**
- * @param $skin Skin
- * @param $result
+ * @param Skin $skin
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
@@ -77,4 +85,8 @@ class UnusedtemplatesPage extends QueryPage {
function getPageHeader() {
return $this->msg( 'unusedtemplatestext' )->parseAsBlock();
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php
index e5a79413..954e3ffe 100644
--- a/includes/specials/SpecialUnwatchedpages.php
+++ b/includes/specials/SpecialUnwatchedpages.php
@@ -35,34 +35,41 @@ class UnwatchedpagesPage extends QueryPage {
parent::__construct( $name, 'unwatchedpages' );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
function getQueryInfo() {
- return array (
- 'tables' => array ( 'page', 'watchlist' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
+ return array(
+ 'tables' => array( 'page', 'watchlist' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'page_namespace' ),
- 'conds' => array ( 'wl_title IS NULL',
+ 'conds' => array( 'wl_title IS NULL',
'page_is_redirect' => 0,
"page_namespace != '" . NS_MEDIAWIKI .
"'" ),
- 'join_conds' => array ( 'watchlist' => array (
- 'LEFT JOIN', array ( 'wl_title = page_title',
+ 'join_conds' => array( 'watchlist' => array(
+ 'LEFT JOIN', array( 'wl_title = page_title',
'wl_namespace = page_namespace' ) ) )
);
}
- function sortDescending() { return false; }
+ function sortDescending() {
+ return false;
+ }
function getOrderFields() {
return array( 'page_namespace', 'page_title' );
}
/**
- * @param $skin Skin
- * @param $result
+ * @param Skin $skin
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
@@ -87,4 +94,8 @@ class UnwatchedpagesPage extends QueryPage {
return $this->getLanguage()->specialList( $plink, $wlink );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
index 43ea345b..09facf4f 100644
--- a/includes/specials/SpecialUpload.php
+++ b/includes/specials/SpecialUpload.php
@@ -39,7 +39,7 @@ class SpecialUpload extends SpecialPage {
}
/** Misc variables **/
- public $mRequest; // The WebRequest or FauxRequest this form is supposed to handle
+ public $mRequest; // The WebRequest or FauxRequest this form is supposed to handle
public $mSourceType;
/**
@@ -54,7 +54,7 @@ class SpecialUpload extends SpecialPage {
public $mUploadClicked;
/** User input variables from the "description" section **/
- public $mDesiredDestName; // The requested target file name
+ public $mDesiredDestName; // The requested target file name
public $mComment;
public $mLicense;
@@ -66,10 +66,10 @@ class SpecialUpload extends SpecialPage {
/** Hidden variables **/
public $mDestWarningAck;
- public $mForReUpload; // The user followed an "overwrite this file" link
- public $mCancelUpload; // The user clicked "Cancel and return to upload form" button
+ public $mForReUpload; // The user followed an "overwrite this file" link
+ public $mCancelUpload; // The user clicked "Cancel and return to upload form" button
public $mTokenOk;
- public $mUploadSuccessful = false; // Subclasses can use this to determine whether a file was uploaded
+ public $mUploadSuccessful = false; // Subclasses can use this to determine whether a file was uploaded
/** Text injection points for hooks not using HTMLForm **/
public $uploadFormTextTop;
@@ -82,32 +82,37 @@ class SpecialUpload extends SpecialPage {
*/
protected function loadRequest() {
$this->mRequest = $request = $this->getRequest();
- $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
- $this->mUpload = UploadBase::createFromRequest( $request );
- $this->mUploadClicked = $request->wasPosted()
+ $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
+ $this->mUpload = UploadBase::createFromRequest( $request );
+ $this->mUploadClicked = $request->wasPosted()
&& ( $request->getCheck( 'wpUpload' )
|| $request->getCheck( 'wpUploadIgnoreWarning' ) );
// 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->getText( 'wpDestFile' );
+ if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
$this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
}
- $this->mComment = $request->getText( 'wpUploadDescription' );
- $this->mLicense = $request->getText( 'wpLicense' );
+ $this->mLicense = $request->getText( 'wpLicense' );
-
- $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
- $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
+ $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
+ $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
|| $request->getCheck( 'wpUploadIgnoreWarning' );
- $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isLoggedIn();
- $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
- $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
+ $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isLoggedIn();
+ $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
+ $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
+
+ $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
+ $commentDefault = '';
+ $commentMsg = wfMessage( 'upload-default-description' )->inContentLanguage();
+ if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
+ $commentDefault = $commentMsg->plain();
+ }
+ $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
- $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
- $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
- || $request->getCheck( 'wpReUpload' ); // b/w compat
+ $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' );
@@ -137,19 +142,19 @@ class SpecialUpload extends SpecialPage {
$this->outputHeader();
# Check uploading enabled
- if( !UploadBase::isEnabled() ) {
+ if ( !UploadBase::isEnabled() ) {
throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
}
# Check permissions
$user = $this->getUser();
$permissionRequired = UploadBase::isAllowed( $user );
- if( $permissionRequired !== true ) {
+ if ( $permissionRequired !== true ) {
throw new PermissionsError( $permissionRequired );
}
# Check blocks
- if( $user->isBlocked() ) {
+ if ( $user->isBlocked() ) {
throw new UserBlockedError( $user->getBlock() );
}
@@ -174,7 +179,7 @@ class SpecialUpload extends SpecialPage {
$this->processUpload();
} else {
# Backwards compatibility hook
- if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) {
+ if ( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) {
wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
return;
}
@@ -209,13 +214,15 @@ class SpecialUpload extends SpecialPage {
/**
* Get an UploadForm instance with title and text properly set.
*
- * @param $message String: HTML string to add to the form
- * @param $sessionKey String: session key in case this is a stashed upload
+ * @param string $message HTML string to add to the form
+ * @param string $sessionKey session key in case this is a stashed upload
* @param $hideIgnoreWarning Boolean: whether to hide "ignore warning" check box
* @return UploadForm
*/
protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
# Initialize form
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle() ); // Remove subpage
$form = new UploadForm( array(
'watch' => $this->getWatchCheck(),
'forreupload' => $this->mForReUpload,
@@ -227,11 +234,10 @@ class SpecialUpload extends SpecialPage {
'texttop' => $this->uploadFormTextTop,
'textaftersummary' => $this->uploadFormTextAfterSummary,
'destfile' => $this->mDesiredDestName,
- ), $this->getContext() );
- $form->setTitle( $this->getTitle() );
+ ), $context );
# Check the token, but only if necessary
- if(
+ if (
!$this->mTokenOk && !$this->mCancelUpload &&
( $this->mUpload && $this->mUploadClicked )
) {
@@ -246,9 +252,9 @@ class SpecialUpload extends SpecialPage {
LogEventsList::showLogExtract( $delNotice, array( 'delete', 'move' ),
$desiredTitleObj,
'', array( 'lim' => 10,
- 'conds' => array( "log_action != 'revision'" ),
- 'showIfEmpty' => false,
- 'msgKey' => array( 'upload-recreate-warning' ) )
+ 'conds' => array( "log_action != 'revision'" ),
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'upload-recreate-warning' ) )
);
}
$form->addPreText( $delNotice );
@@ -277,7 +283,7 @@ class SpecialUpload extends SpecialPage {
$title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
$user = $this->getUser();
// Show a subtitle link to deleted revisions (to sysops et al only)
- if( $title instanceof Title ) {
+ if ( $title instanceof Title ) {
$count = $title->isDeleted();
if ( $count > 0 && $user->isAllowed( 'deletedhistory' ) ) {
$restorelink = Linker::linkKnown(
@@ -300,7 +306,7 @@ class SpecialUpload extends SpecialPage {
* essentially means that UploadBase::VERIFICATION_ERROR and
* UploadBase::EMPTY_FILE should not be passed here.
*
- * @param $message String: HTML message to be passed to mainUploadForm
+ * @param string $message HTML message to be passed to mainUploadForm
*/
protected function showRecoverableUploadError( $message ) {
$sessionKey = $this->mUpload->stashSession();
@@ -312,12 +318,12 @@ class SpecialUpload extends SpecialPage {
$this->showUploadForm( $form );
}
/**
- * Stashes the upload, shows the main form, but adds an "continue anyway button".
+ * Stashes the upload, shows the main form, but adds a "continue anyway button".
* Also checks whether there are actually warnings to display.
*
* @param $warnings Array
* @return boolean true if warnings were displayed, false if there are no
- * warnings and the should continue processing like there was no warning
+ * warnings and it should continue processing
*/
protected function showUploadWarning( $warnings ) {
# If there are no warnings, or warnings we can ignore, return early.
@@ -335,12 +341,15 @@ class SpecialUpload extends SpecialPage {
$warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
. '<ul class="warning">';
- foreach( $warnings as $warning => $args ) {
- if( $warning == 'exists' ) {
+ foreach ( $warnings as $warning => $args ) {
+ if ( $warning == 'badfilename' ) {
+ $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText();
+ }
+ if ( $warning == 'exists' ) {
$msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
- } elseif( $warning == 'duplicate' ) {
- $msg = self::getDupeWarning( $args );
- } elseif( $warning == 'duplicate-archive' ) {
+ } elseif ( $warning == 'duplicate' ) {
+ $msg = $this->getDupeWarning( $args );
+ } elseif ( $warning == 'duplicate-archive' ) {
$msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
. "</li>\n";
@@ -371,7 +380,7 @@ class SpecialUpload extends SpecialPage {
/**
* Show the upload form with error message, but do not stash the file.
*
- * @param $message string HTML string
+ * @param string $message HTML string
*/
protected function showUploadError( $message ) {
$message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" .
@@ -386,12 +395,12 @@ class SpecialUpload extends SpecialPage {
protected function processUpload() {
// Fetch the file if required
$status = $this->mUpload->fetchFile();
- if( !$status->isOK() ) {
+ if ( !$status->isOK() ) {
$this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
return;
}
- if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) {
+ if ( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) {
wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" );
// This code path is deprecated. If you want to break upload processing
// do so by hooking into the appropriate hooks in UploadBase::verifyUpload
@@ -410,7 +419,7 @@ class SpecialUpload extends SpecialPage {
// Verify permissions for this title
$permErrors = $this->mUpload->verifyTitlePermissions( $this->getUser() );
- if( $permErrors !== true ) {
+ if ( $permErrors !== true ) {
$code = array_shift( $permErrors[0] );
$this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() );
return;
@@ -419,15 +428,15 @@ class SpecialUpload extends SpecialPage {
$this->mLocalFile = $this->mUpload->getLocalFile();
// Check warnings if necessary
- if( !$this->mIgnoreWarning ) {
+ if ( !$this->mIgnoreWarning ) {
$warnings = $this->mUpload->checkWarnings();
- if( $this->showUploadWarning( $warnings ) ) {
+ if ( $this->showUploadWarning( $warnings ) ) {
return;
}
}
// Get the page text if this is not a reupload
- if( !$this->mForReUpload ) {
+ if ( !$this->mForReUpload ) {
$pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
$this->mCopyrightStatus, $this->mCopyrightSource );
} else {
@@ -455,15 +464,14 @@ class SpecialUpload extends SpecialPage {
*/
public static function getInitialPageText( $comment = '', $license = '', $copyStatus = '', $source = '' ) {
global $wgUseCopyrightUpload, $wgForceUIMsgAsContentMsg;
- $wgForceUIMsgAsContentMsg = (array) $wgForceUIMsgAsContentMsg;
$msg = array();
/* These messages are transcluded into the actual text of the description page.
* Thus, forcing them as content messages makes the upload to produce an int: template
* instead of hardcoding it there in the uploader language.
*/
- foreach( array( 'license-header', 'filedesc', 'filestatus', 'filesource' ) as $msgName ) {
- if ( in_array( $msgName, $wgForceUIMsgAsContentMsg ) ) {
+ foreach ( array( 'license-header', 'filedesc', 'filestatus', 'filesource' ) as $msgName ) {
+ if ( in_array( $msgName, (array)$wgForceUIMsgAsContentMsg ) ) {
$msg[$msgName] = "{{int:$msgName}}";
} else {
$msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
@@ -473,17 +481,17 @@ class SpecialUpload extends SpecialPage {
if ( $wgUseCopyrightUpload ) {
$licensetxt = '';
if ( $license != '' ) {
- $licensetxt = '== ' . $msg[ 'license-header' ] . " ==\n" . '{{' . $license . '}}' . "\n";
+ $licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
}
- $pageText = '== ' . $msg[ 'filedesc' ] . " ==\n" . $comment . "\n" .
- '== ' . $msg[ 'filestatus' ] . " ==\n" . $copyStatus . "\n" .
+ $pageText = '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n" .
+ '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n" .
"$licensetxt" .
- '== ' . $msg[ 'filesource' ] . " ==\n" . $source;
+ '== ' . $msg['filesource'] . " ==\n" . $source;
} else {
if ( $license != '' ) {
- $filedesc = $comment == '' ? '' : '== ' . $msg[ 'filedesc' ] . " ==\n" . $comment . "\n";
+ $filedesc = $comment == '' ? '' : '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n";
$pageText = $filedesc .
- '== ' . $msg[ 'license-header' ] . " ==\n" . '{{' . $license . '}}' . "\n";
+ '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
} else {
$pageText = $comment;
}
@@ -504,13 +512,13 @@ class SpecialUpload extends SpecialPage {
* @return Bool|String
*/
protected function getWatchCheck() {
- if( $this->getUser()->getOption( 'watchdefault' ) ) {
+ if ( $this->getUser()->getOption( 'watchdefault' ) ) {
// Watch all edits!
return true;
}
$local = wfLocalFile( $this->mDesiredDestName );
- if( $local && $local->exists() ) {
+ if ( $local && $local->exists() ) {
// We're uploading a new version of an existing file.
// No creation, so don't watch it if we're not already.
return $this->getUser()->isWatched( $local->getTitle() );
@@ -520,16 +528,16 @@ class SpecialUpload extends SpecialPage {
}
}
-
/**
* Provides output to the user for a result of UploadBase::verifyUpload
*
- * @param $details Array: result of UploadBase::verifyUpload
+ * @param array $details result of UploadBase::verifyUpload
+ * @throws MWException
*/
protected function processVerificationError( $details ) {
global $wgFileExtensions;
- switch( $details['status'] ) {
+ switch ( $details['status'] ) {
/** Statuses that only require name changing **/
case UploadBase::MIN_LENGTH_PARTNAME:
@@ -563,8 +571,9 @@ class SpecialUpload extends SpecialPage {
} else {
$msg->params( $details['finalExt'] );
}
- $msg->params( $this->getLanguage()->commaList( $wgFileExtensions ),
- count( $wgFileExtensions ) );
+ $extensions = array_unique( $wgFileExtensions );
+ $msg->params( $this->getLanguage()->commaList( $extensions ),
+ count( $extensions ) );
// Add PLURAL support for the first parameter. This results
// in a bit unlogical parameter sequence, but does not break
@@ -622,7 +631,7 @@ class SpecialUpload extends SpecialPage {
* Formats a result of UploadBase::getExistsWarning as HTML
* This check is static and can be done pre-upload via AJAX
*
- * @param $exists Array: the result of UploadBase::getExistsWarning
+ * @param array $exists the result of UploadBase::getExistsWarning
* @return String: empty string if there is no warning or an HTML fragment
*/
public static function getExistsWarning( $exists ) {
@@ -634,10 +643,10 @@ class SpecialUpload extends SpecialPage {
$filename = $file->getTitle()->getPrefixedText();
$warning = '';
- if( $exists['warning'] == 'exists' ) {
+ if ( $exists['warning'] == 'exists' ) {
// Exact match
$warning = wfMessage( 'fileexists', $filename )->parse();
- } elseif( $exists['warning'] == 'page-exists' ) {
+ } elseif ( $exists['warning'] == 'page-exists' ) {
// Page exists but file does not
$warning = wfMessage( 'filepageexists', $filename )->parse();
} elseif ( $exists['warning'] == 'exists-normalized' ) {
@@ -645,7 +654,7 @@ class SpecialUpload extends SpecialPage {
$exists['normalizedFile']->getTitle()->getPrefixedText() )->parse();
} elseif ( $exists['warning'] == 'thumb' ) {
// Swapped argument order compared with other messages for backwards compatibility
- $warning = wfMessage( 'fileexists-thumbnail-yes',
+ $warning = wfMessage( 'fileexists-thumbnail-yes',
$exists['thumbFile']->getTitle()->getPrefixedText(), $filename )->parse();
} elseif ( $exists['warning'] == 'thumb-name' ) {
// Image w/o '180px-' does not exists, but we do not like these filenames
@@ -673,42 +682,19 @@ class SpecialUpload extends SpecialPage {
}
/**
- * Get a list of warnings
- *
- * @param $filename String: local filename, e.g. 'file exists', 'non-descriptive filename'
- * @return Array: list of warning messages
- */
- 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 );
- }
- $s = '&#160;';
- if ( $file ) {
- $exists = UploadBase::getExistsWarning( $file );
- $warning = self::getExistsWarning( $exists );
- if ( $warning !== '' ) {
- $s = "<div>$warning</div>";
- }
- }
- return $s;
- }
-
- /**
* Construct a warning and a gallery from an array of duplicate files.
* @param $dupes array
* @return string
*/
- public static function getDupeWarning( $dupes ) {
+ public function getDupeWarning( $dupes ) {
if ( !$dupes ) {
return '';
}
- $gallery = new ImageGallery;
+ $gallery = ImageGalleryBase::factory();
+ $gallery->setContext( $this->getContext() );
$gallery->setShowBytes( false );
- foreach( $dupes as $file ) {
+ foreach ( $dupes as $file ) {
$gallery->add( $file->getTitle() );
}
return '<li>' .
@@ -716,6 +702,9 @@ class SpecialUpload extends SpecialPage {
$gallery->toHtml() . "</li>\n";
}
+ protected function getGroupName() {
+ return 'media';
+ }
}
/**
@@ -789,6 +778,8 @@ class UploadForm extends HTMLForm {
* @return Array: descriptor array
*/
protected function getSourceSection() {
+ global $wgCopyUploadsFromSpecialUpload;
+
if ( $this->mSessionKey ) {
return array(
'SessionKey' => array(
@@ -802,7 +793,9 @@ class UploadForm extends HTMLForm {
);
}
- $canUploadByUrl = UploadFromUrl::isEnabled() && UploadFromUrl::isAllowed( $this->getUser() );
+ $canUploadByUrl = UploadFromUrl::isEnabled()
+ && UploadFromUrl::isAllowed( $this->getUser() )
+ && $wgCopyUploadsFromSpecialUpload;
$radio = $canUploadByUrl;
$selectedSourceType = strtolower( $this->getRequest()->getText( 'wpSourceType', 'File' ) );
@@ -835,10 +828,13 @@ class UploadForm extends HTMLForm {
'upload-type' => 'File',
'radio' => &$radio,
'help' => $this->msg( 'upload-maxfilesize',
- $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] )
- )->parse() . ' ' . $this->msg( 'upload_source_file' )->escaped(),
+ $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) )
+ ->parse() .
+ $this->msg( 'word-separator' )->escaped() .
+ $this->msg( 'upload_source_file' )->escaped(),
'checked' => $selectedSourceType == 'file',
);
+
if ( $canUploadByUrl ) {
$this->mMaxUploadSize['url'] = UploadBase::getMaxUploadSize( 'url' );
$descriptor['UploadFileURL'] = array(
@@ -849,8 +845,10 @@ class UploadForm extends HTMLForm {
'upload-type' => 'url',
'radio' => &$radio,
'help' => $this->msg( 'upload-maxfilesize',
- $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] )
- )->parse() . ' ' . $this->msg( 'upload_source_url' )->escaped(),
+ $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] ) )
+ ->parse() .
+ $this->msg( 'word-separator' )->escaped() .
+ $this->msg( 'upload_source_url' )->escaped(),
'checked' => $selectedSourceType == 'url',
);
}
@@ -876,21 +874,21 @@ class UploadForm extends HTMLForm {
global $wgCheckFileExtensions, $wgStrictFileExtensions,
$wgFileExtensions, $wgFileBlacklist;
- if( $wgCheckFileExtensions ) {
- if( $wgStrictFileExtensions ) {
+ if ( $wgCheckFileExtensions ) {
+ if ( $wgStrictFileExtensions ) {
# Everything not permitted is banned
$extensionsList =
'<div id="mw-upload-permitted">' .
- $this->msg( 'upload-permitted', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) )->parseAsBlock() .
+ $this->msg( 'upload-permitted', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileExtensions ) ) )->parseAsBlock() .
"</div>\n";
} else {
# We have to list both preferred and prohibited
$extensionsList =
'<div id="mw-upload-preferred">' .
- $this->msg( 'upload-preferred', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) )->parseAsBlock() .
+ $this->msg( 'upload-preferred', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileExtensions ) ) )->parseAsBlock() .
"</div>\n" .
'<div id="mw-upload-prohibited">' .
- $this->msg( 'upload-prohibited', $this->getContext()->getLanguage()->commaList( $wgFileBlacklist ) )->parseAsBlock() .
+ $this->msg( 'upload-prohibited', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileBlacklist ) ) )->parseAsBlock() .
"</div>\n";
}
} else {
@@ -946,7 +944,7 @@ class UploadForm extends HTMLForm {
? 'filereuploadsummary'
: 'fileuploadsummary',
'default' => $this->mComment,
- 'cols' => intval( $this->getUser()->getOption( 'cols' ) ),
+ 'cols' => $this->getUser()->getIntOption( 'cols' ),
'rows' => 8,
)
);
@@ -1077,7 +1075,6 @@ class UploadForm extends HTMLForm {
$out = $this->getOutput();
$out->addJsConfigVars( $scriptVars );
-
$out->addModules( array(
'mediawiki.action.edit', // For <charinsert> support
'mediawiki.legacy.upload', // Old form stuff...
@@ -1106,7 +1103,7 @@ class UploadSourceField extends HTMLTextField {
* @return string
*/
function getLabelHtml( $cellAttributes = array() ) {
- $id = "wpSourceType{$this->mParams['upload-type']}";
+ $id = $this->mParams['id'];
$label = Html::rawElement( 'label', array( 'for' => $id ), $this->mLabel );
if ( !empty( $this->mParams['radio'] ) ) {
@@ -1134,4 +1131,3 @@ class UploadSourceField extends HTMLTextField {
: 60;
}
}
-
diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php
index 1a00d731..1373df1a 100644
--- a/includes/specials/SpecialUploadStash.php
+++ b/includes/specials/SpecialUploadStash.php
@@ -57,7 +57,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
/**
* Execute page -- can output a file directly or show a listing of them.
*
- * @param $subPage String: subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
+ * @param string $subPage subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
* @return Boolean: success
*/
public function execute( $subPage ) {
@@ -73,7 +73,8 @@ class SpecialUploadStash extends UnlistedSpecialPage {
* If file available in stash, cats it out to the client as a simple HTTP response.
* n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward.
*
- * @param $key String: the key of a particular requested file
+ * @param string $key the key of a particular requested file
+ * @throws HttpError
* @return bool
*/
public function showUpload( $key ) {
@@ -87,19 +88,19 @@ class SpecialUploadStash extends UnlistedSpecialPage {
} else {
return $this->outputLocalFile( $params['file'] );
}
- } catch( UploadStashFileNotFoundException $e ) {
+ } catch ( UploadStashFileNotFoundException $e ) {
$code = 404;
$message = $e->getMessage();
- } catch( UploadStashZeroLengthFileException $e ) {
+ } catch ( UploadStashZeroLengthFileException $e ) {
$code = 500;
$message = $e->getMessage();
- } catch( UploadStashBadPathException $e ) {
+ } catch ( UploadStashBadPathException $e ) {
$code = 500;
$message = $e->getMessage();
- } catch( SpecialUploadStashTooLargeException $e ) {
+ } catch ( SpecialUploadStashTooLargeException $e ) {
$code = 500;
$message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes. ' . $e->getMessage();
- } catch( Exception $e ) {
+ } catch ( Exception $e ) {
$code = 500;
$message = $e->getMessage();
}
@@ -113,6 +114,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
* application the transform parameters
*
* @param string $key
+ * @throws UploadStashBadPathException
* @return array
*/
private function parseKey( $key ) {
@@ -132,8 +134,13 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
$handler = $file->getHandler();
- $params = $handler->parseParamString( $paramString );
- return array( 'file' => $file, 'type' => $type, 'params' => $params );
+ if ( $handler ) {
+ $params = $handler->parseParamString( $paramString );
+ return array( 'file' => $file, 'type' => $type, 'params' => $params );
+ } else {
+ throw new UploadStashBadPathException( 'No handler found for ' .
+ "mime {$file->getMimeType()} of file {$file->getPath()}" );
+ }
}
return array( 'file' => $file, 'type' => $type );
@@ -164,10 +171,11 @@ class SpecialUploadStash extends UnlistedSpecialPage {
/**
* Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT.
- * @param $file: File object
- * @param $params: scaling parameters ( e.g. array( width => '50' ) );
- * @param $flags: scaling flags ( see File:: constants )
+ * @param $file File
+ * @param array $params Scaling parameters ( e.g. array( width => '50' ) );
+ * @param int $flags Scaling flags ( see File:: constants )
* @throws MWException
+ * @throws UploadStashFileNotFoundException
* @return boolean success
*/
private function outputLocallyScaledThumb( $file, $params, $flags ) {
@@ -189,7 +197,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
// now we should construct a File, so we can get mime and other such info in a standard way
// n.b. mimetype may be different from original (ogx original -> jpeg thumb)
- $thumbFile = new UnregisteredLocalFile( false,
+ $thumbFile = new UnregisteredLocalFile( false,
$this->stash->repo, $thumbnailImage->getStoragePath(), false );
if ( !$thumbFile ) {
throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
@@ -219,7 +227,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
global $wgUploadStashScalerBaseUrl;
$scalerBaseUrl = $wgUploadStashScalerBaseUrl;
- if( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
+ if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
// this is apparently a protocol-relative URL, which makes no sense in this context,
// since this is used for communication that's internal to the application.
// default to http.
@@ -258,6 +266,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
* Side effect: writes HTTP response to STDOUT.
*
* @param $file File object with a local path (e.g. UnregisteredLocalFile, LocalFile. Oddly these don't share an ancestor!)
+ * @throws SpecialUploadStashTooLargeException
* @return bool
*/
private function outputLocalFile( File $file ) {
@@ -273,8 +282,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
/**
* Output HTTP response of raw content
* Side effect: writes HTTP response to STDOUT.
- * @param $content String content
- * @param $contentType String mime type
+ * @param string $content content
+ * @param string $contentType mime type
+ * @throws SpecialUploadStashTooLargeException
* @return bool
*/
private function outputContents( $content, $contentType ) {
@@ -291,13 +301,15 @@ class SpecialUploadStash extends UnlistedSpecialPage {
* Output headers for streaming
* XXX unsure about encoding as binary; if we received from HTTP perhaps we should use that encoding, concatted with semicolon to mimeType as it usually is.
* Side effect: preps PHP to write headers to STDOUT.
- * @param String $contentType : string suitable for content-type header
- * @param String $size: length in bytes
+ * @param string $contentType : string suitable for content-type header
+ * @param string $size: length in bytes
*/
private static function outputFileHeaders( $contentType, $size ) {
header( "Content-Type: $contentType", true );
header( 'Content-Transfer-Encoding: binary', true );
header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
+ // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache
+ header( 'Cache-Control: private' );
header( "Content-Length: $size", true );
}
@@ -306,6 +318,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
* Note the stash has to be recreated since this is being called in a static context.
* This works, because there really is only one stash per logged-in user, despite appearances.
*
+ * @param array $formData
* @return Status
*/
public static function tryClearStashedUploads( $formData ) {
@@ -322,14 +335,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
/**
* Default action when we don't have a subpage -- just show links to the uploads we have,
* Also show a button to clear stashed files
- * @param $status [optional] Status: the result of processRequest
* @return bool
*/
- private function showUploads( $status = null ) {
- if ( $status === null ) {
- $status = Status::newGood();
- }
-
+ private function showUploads() {
// sets the title, etc.
$this->setHeaders();
$this->outputHeader();
@@ -337,15 +345,16 @@ class SpecialUploadStash extends UnlistedSpecialPage {
// create the form, which will also be used to execute a callback to process incoming form data
// this design is extremely dubious, but supposedly HTMLForm is our standard now?
+ $context = new DerivativeContext( $this->getContext() );
+ $context->setTitle( $this->getTitle() ); // Remove subpage
$form = new HTMLForm( array(
'Clear' => array(
'type' => 'hidden',
'default' => true,
'name' => 'clear',
)
- ), $this->getContext(), 'clearStashedUploads' );
- $form->setSubmitCallback( array( __CLASS__ , 'tryClearStashedUploads' ) );
- $form->setTitle( $this->getTitle() );
+ ), $context, 'clearStashedUploads' );
+ $form->setSubmitCallback( array( __CLASS__, 'tryClearStashedUploads' ) );
$form->setSubmitTextMsg( 'uploadstash-clear' );
$form->prepareForm();
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
index 58da77da..5ac3e654 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -48,18 +48,21 @@ class LoginForm extends SpecialPage {
var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
var $mSkipCookieCheck, $mReturnToQuery, $mToken, $mStickHTTPS;
var $mType, $mReason, $mRealName;
- var $mAbortLoginErrorMsg = 'login-abort-generic';
+ var $mAbortLoginErrorMsg = null;
private $mLoaded = false;
+ private $mSecureLoginUrl;
/**
- * @var ExternalUser
+ * @ var WebRequest
*/
- private $mExtUser = null;
+ private $mOverrideRequest = null;
/**
- * @ var WebRequest
+ * Effective request; set at the beginning of load
+ *
+ * @var WebRequest $mRequest
*/
- private $mOverrideRequest = null;
+ private $mRequest = null;
/**
* @param WebRequest $request
@@ -86,6 +89,7 @@ class LoginForm extends SpecialPage {
} else {
$request = $this->mOverrideRequest;
}
+ $this->mRequest = $request;
$this->mType = $request->getText( 'type' );
$this->mUsername = $request->getText( 'wpName' );
@@ -95,31 +99,32 @@ class LoginForm extends SpecialPage {
$this->mReason = $request->getText( 'wpReason' );
$this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
$this->mPosted = $request->wasPosted();
- $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
$this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
&& $wgEnableEmail;
+ $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ) && !$this->mCreateaccountMail;
$this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
$this->mAction = $request->getVal( 'action' );
$this->mRemember = $request->getCheck( 'wpRemember' );
- $this->mStickHTTPS = $request->getCheck( 'wpStickHTTPS' );
+ $this->mFromHTTP = $request->getBool( 'fromhttp', false );
+ $this->mStickHTTPS = ( !$this->mFromHTTP && $request->detectProtocol() === 'https' ) || $request->getBool( 'wpForceHttps', false );
$this->mLanguage = $request->getText( 'uselang' );
$this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
$this->mToken = ( $this->mType == 'signup' ) ? $request->getVal( 'wpCreateaccountToken' ) : $request->getVal( 'wpLoginToken' );
$this->mReturnTo = $request->getVal( 'returnto', '' );
$this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
- if( $wgEnableEmail ) {
+ if ( $wgEnableEmail ) {
$this->mEmail = $request->getText( 'wpEmail' );
} else {
$this->mEmail = '';
}
- if( !in_array( 'realname', $wgHiddenPrefs ) ) {
+ if ( !in_array( 'realname', $wgHiddenPrefs ) ) {
$this->mRealName = $request->getText( 'wpRealName' );
} else {
$this->mRealName = '';
}
- if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ if ( !$wgAuth->validDomain( $this->mDomain ) ) {
$this->mDomain = $wgAuth->getDomain();
}
$wgAuth->setDomain( $this->mDomain );
@@ -128,7 +133,7 @@ class LoginForm extends SpecialPage {
# 2. Do not return to PasswordReset after a successful password change
# but goto Wiki start page (Main_Page) instead ( bug 33997 )
$returnToTitle = Title::newFromText( $this->mReturnTo );
- if( is_object( $returnToTitle ) && (
+ if ( is_object( $returnToTitle ) && (
$returnToTitle->isSpecial( 'Userlogout' )
|| $returnToTitle->isSpecial( 'PasswordReset' ) ) ) {
$this->mReturnTo = '';
@@ -137,27 +142,61 @@ class LoginForm extends SpecialPage {
}
function getDescription() {
- return $this->msg( $this->getUser()->isAllowed( 'createaccount' ) ?
- 'userlogin' : 'userloginnocreate' )->text();
+ if ( $this->mType === 'signup' ) {
+ return $this->msg( 'createaccount' )->text();
+ } else {
+ return $this->msg( 'login' )->text();
+ }
}
- public function execute( $par ) {
+ /*
+ * @param $subPage string|null
+ */
+ public function execute( $subPage ) {
if ( session_id() == '' ) {
wfSetupSession();
}
$this->load();
- $this->setHeaders();
- if ( $par == 'signup' ) { # Check for [[Special:Userlogin/signup]]
+ // Check for [[Special:Userlogin/signup]]. This affects form display and
+ // page title.
+ if ( $subPage == 'signup' ) {
$this->mType = 'signup';
}
+ $this->setHeaders();
+
+ // If logging in and not on HTTPS, either redirect to it or offer a link.
+ global $wgSecureLogin;
+ if ( WebRequest::detectProtocol() !== 'https' ) {
+ $title = $this->getFullTitle();
+ $query = array(
+ 'returnto' => $this->mReturnTo,
+ 'returntoquery' => $this->mReturnToQuery,
+ 'title' => null,
+ ) + $this->mRequest->getQueryValues();
+ $url = $title->getFullURL( $query, false, PROTO_HTTPS );
+ if ( $wgSecureLogin && wfCanIPUseHTTPS( $this->getRequest()->getIP() ) ) {
+ $url = wfAppendQuery( $url, 'fromhttp=1' );
+ $this->getOutput()->redirect( $url );
+ // Since we only do this redir to change proto, always vary
+ $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
+ return;
+ } else {
+ // A wiki without HTTPS login support should set $wgServer to
+ // http://somehost, in which case the secure URL generated
+ // above won't actually start with https://
+ if ( substr( $url, 0, 8 ) === 'https://' ) {
+ $this->mSecureLoginUrl = $url;
+ }
+ }
+ }
if ( !is_null( $this->mCookieCheck ) ) {
$this->onCookieRedirectCheck( $this->mCookieCheck );
return;
- } elseif( $this->mPosted ) {
- if( $this->mCreateaccount ) {
+ } elseif ( $this->mPosted ) {
+ if ( $this->mCreateaccount ) {
$this->addNewAccount();
return;
} elseif ( $this->mCreateaccountMail ) {
@@ -180,24 +219,27 @@ class LoginForm extends SpecialPage {
return;
}
- $u = $this->addNewaccountInternal();
-
- if ( $u == null ) {
+ $status = $this->addNewaccountInternal();
+ if ( !$status->isGood() ) {
+ $error = $status->getMessage();
+ $this->mainLoginForm( $error->toString() );
return;
}
+ $u = $status->getValue();
+
// Wipe the initial password and mail a temporary one
$u->setPassword( null );
$u->saveSettings();
$result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
wfRunHooks( 'AddNewAccount', array( $u, true ) );
- $u->addNewUserLogEntry( true, $this->mReason );
+ $u->addNewUserLogEntry( 'byemail', $this->mReason );
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'accmailtitle' ) );
- if( !$result->isGood() ) {
+ if ( !$result->isGood() ) {
$this->mainLoginForm( $this->msg( 'mailerror', $result->getWikiText() )->text() );
} else {
$out->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
@@ -210,26 +252,42 @@ class LoginForm extends SpecialPage {
* @return bool
*/
function addNewAccount() {
- global $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector;
+ global $wgContLang, $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector;
# Create the account and abort if there's a problem doing so
- $u = $this->addNewAccountInternal();
- if( $u == null ) {
+ $status = $this->addNewAccountInternal();
+ if ( !$status->isGood() ) {
+ $error = $status->getMessage();
+ $this->mainLoginForm( $error->toString() );
return false;
}
- # If we showed up language selection links, and one was in use, be
- # smart (and sensible) and save that language as the user's preference
- if( $wgLoginLanguageSelector && $this->mLanguage ) {
- $u->setOption( 'language', $this->mLanguage );
+ $u = $status->getValue();
+
+ # Only save preferences if the user is not creating an account for someone else.
+ if ( $this->getUser()->isAnon() ) {
+ # If we showed up language selection links, and one was in use, be
+ # smart (and sensible) and save that language as the user's preference
+ if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+ $u->setOption( 'language', $this->mLanguage );
+ } else {
+
+ # Otherwise the user's language preference defaults to $wgContLang,
+ # but it may be better to set it to their preferred $wgContLang variant,
+ # based on browser preferences or URL parameters.
+ $u->setOption( 'language', $wgContLang->getPreferredVariant() );
+ }
+ if ( $wgContLang->hasVariants() ) {
+ $u->setOption( 'variant', $wgContLang->getPreferredVariant() );
+ }
}
$out = $this->getOutput();
# Send out an email authentication message if needed
- if( $wgEmailAuthentication && Sanitizer::validateEmail( $u->getEmail() ) ) {
+ if ( $wgEmailAuthentication && Sanitizer::validateEmail( $u->getEmail() ) ) {
$status = $u->sendConfirmationMail();
- if( $status->isGood() ) {
+ if ( $status->isGood() ) {
$out->addWikiMsg( 'confirmemail_oncreate' );
} else {
$out->addWikiText( $status->getWikiText( 'confirmemail_sendfailed' ) );
@@ -242,7 +300,7 @@ class LoginForm extends SpecialPage {
# If not logged in, assume the new account as the current one and set
# session cookies then show a "welcome" message or a "need cookies"
# message as needed
- if( $this->getUser()->isAnon() ) {
+ if ( $this->getUser()->isAnon() ) {
$u->setCookies();
$wgUser = $u;
// This should set it for OutputPage and the Skin
@@ -250,8 +308,8 @@ class LoginForm extends SpecialPage {
// wrong.
$this->getContext()->setUser( $u );
wfRunHooks( 'AddNewAccount', array( $u, false ) );
- $u->addNewUserLogEntry();
- if( $this->hasSessionCookie() ) {
+ $u->addNewUserLogEntry( 'create' );
+ if ( $this->hasSessionCookie() ) {
$this->successfulCreation();
} else {
$this->cookieRedirectCheck( 'new' );
@@ -262,23 +320,24 @@ class LoginForm extends SpecialPage {
$out->addWikiMsg( 'accountcreatedtext', $u->getName() );
$out->addReturnTo( $this->getTitle() );
wfRunHooks( 'AddNewAccount', array( $u, false ) );
- $u->addNewUserLogEntry( false, $this->mReason );
+ $u->addNewUserLogEntry( 'create2', $this->mReason );
}
return true;
}
/**
+ * Make a new user account using the loaded data.
* @private
- * @return bool|User
+ * @throws PermissionsError|ReadOnlyError
+ * @return Status
*/
- function addNewAccountInternal() {
+ public function addNewAccountInternal() {
global $wgAuth, $wgMemc, $wgAccountCreationThrottle,
$wgMinimalPasswordLength, $wgEmailConfirmToEdit;
// If the user passes an invalid domain, something is fishy
- if( !$wgAuth->validDomain( $this->mDomain ) ) {
- $this->mainLoginForm( $this->msg( 'wrongpassword' )->text() );
- return false;
+ if ( !$wgAuth->validDomain( $this->mDomain ) ) {
+ return Status::newFatal( 'wrongpassword' );
}
// If we are not allowing users to login locally, we should be checking
@@ -286,11 +345,15 @@ class LoginForm extends SpecialPage {
// cation server before they create an account (otherwise, they can
// create a local account and login as any domain user). We only need
// to check this for domains that aren't local.
- if( 'local' != $this->mDomain && $this->mDomain != '' ) {
- if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mUsername )
- || !$wgAuth->authenticate( $this->mUsername, $this->mPassword ) ) ) {
- $this->mainLoginForm( $this->msg( 'wrongpassword' )->text() );
- return false;
+ if ( 'local' != $this->mDomain && $this->mDomain != '' ) {
+ if (
+ !$wgAuth->canCreateAccounts() &&
+ (
+ !$wgAuth->userExists( $this->mUsername ) ||
+ !$wgAuth->authenticate( $this->mUsername, $this->mPassword )
+ )
+ ) {
+ return Status::newFatal( 'wrongpassword' );
}
}
@@ -301,28 +364,28 @@ class LoginForm extends SpecialPage {
# Request forgery checks.
if ( !self::getCreateaccountToken() ) {
self::setCreateaccountToken();
- $this->mainLoginForm( $this->msg( 'nocookiesfornew' )->parse() );
- return false;
+ return Status::newFatal( 'nocookiesfornew' );
}
# The user didn't pass a createaccount token
if ( !$this->mToken ) {
- $this->mainLoginForm( $this->msg( 'sessionfailure' )->text() );
- return false;
+ return Status::newFatal( 'sessionfailure' );
}
# Validate the createaccount token
if ( $this->mToken !== self::getCreateaccountToken() ) {
- $this->mainLoginForm( $this->msg( 'sessionfailure' )->text() );
- return false;
+ return Status::newFatal( 'sessionfailure' );
}
# Check permissions
$currentUser = $this->getUser();
+ $creationBlock = $currentUser->isBlockedFromCreateAccount();
if ( !$currentUser->isAllowed( 'createaccount' ) ) {
throw new PermissionsError( 'createaccount' );
- } elseif ( $currentUser->isBlockedFromCreateAccount() ) {
- $this->userBlockedMessage( $currentUser->isBlockedFromCreateAccount() );
+ } elseif ( $creationBlock instanceof Block ) {
+ // Throws an ErrorPageError.
+ $this->userBlockedMessage( $creationBlock );
+ // This should never be reached.
return false;
}
@@ -334,58 +397,45 @@ class LoginForm extends SpecialPage {
$ip = $this->getRequest()->getIP();
if ( $currentUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
- $this->mainLoginForm( $this->msg( 'sorbs_create_account_reason' )->text() . ' ' . $this->msg( 'parentheses', $ip )->escaped() );
- return false;
+ return Status::newFatal( 'sorbs_create_account_reason' );
}
# Now create a dummy user ($u) and check if it is valid
$name = trim( $this->mUsername );
$u = User::newFromName( $name, 'creatable' );
if ( !is_object( $u ) ) {
- $this->mainLoginForm( $this->msg( 'noname' )->text() );
- return false;
+ return Status::newFatal( 'noname' );
+ } elseif ( 0 != $u->idForName() ) {
+ return Status::newFatal( 'userexists' );
}
- if ( 0 != $u->idForName() ) {
- $this->mainLoginForm( $this->msg( 'userexists' )->text() );
- return false;
- }
-
- if ( 0 != strcmp( $this->mPassword, $this->mRetype ) ) {
- $this->mainLoginForm( $this->msg( 'badretype' )->text() );
- return false;
- }
+ if ( $this->mCreateaccountMail ) {
+ # do not force a password for account creation by email
+ # set invalid password, it will be replaced later by a random generated password
+ $this->mPassword = null;
+ } else {
+ if ( $this->mPassword !== $this->mRetype ) {
+ return Status::newFatal( 'badretype' );
+ }
- # check for minimal password length
- $valid = $u->getPasswordValidity( $this->mPassword );
- if ( $valid !== true ) {
- if ( !$this->mCreateaccountMail ) {
- if ( is_array( $valid ) ) {
- $message = array_shift( $valid );
- $params = $valid;
- } else {
- $message = $valid;
- $params = array( $wgMinimalPasswordLength );
+ # check for minimal password length
+ $valid = $u->getPasswordValidity( $this->mPassword );
+ if ( $valid !== true ) {
+ if ( !is_array( $valid ) ) {
+ $valid = array( $valid, $wgMinimalPasswordLength );
}
- $this->mainLoginForm( $this->msg( $message, $params )->text() );
- return false;
- } else {
- # do not force a password for account creation by email
- # set invalid password, it will be replaced later by a random generated password
- $this->mPassword = null;
+ return call_user_func_array( 'Status::newFatal', $valid );
}
}
# if you need a confirmed email address to edit, then obviously you
# need an email address.
- if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) {
- $this->mainLoginForm( $this->msg( 'noemailtitle' )->text() );
- return false;
+ if ( $wgEmailConfirmToEdit && strval( $this->mEmail ) === '' ) {
+ return Status::newFatal( 'noemailtitle' );
}
- if( !empty( $this->mEmail ) && !Sanitizer::validateEmail( $this->mEmail ) ) {
- $this->mainLoginForm( $this->msg( 'invalidemailaddress' )->text() );
- return false;
+ if ( strval( $this->mEmail ) !== '' && !Sanitizer::validateEmail( $this->mEmail ) ) {
+ return Status::newFatal( 'invalidemailaddress' );
}
# Set some additional data so the AbortNewAccount hook can be used for
@@ -394,11 +444,12 @@ class LoginForm extends SpecialPage {
$u->setRealName( $this->mRealName );
$abortError = '';
- if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
+ if ( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
// Hook point to add extra creation throttles and blocks
wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
- $this->mainLoginForm( $abortError );
- return false;
+ $abortError = new RawMessage( $abortError );
+ $abortError->text();
+ return Status::newFatal( $abortError );
}
// Hook point to check for exempt from account creation throttle
@@ -412,16 +463,14 @@ class LoginForm extends SpecialPage {
$wgMemc->set( $key, 0, 86400 );
}
if ( $value >= $wgAccountCreationThrottle ) {
- $this->throttleHit( $wgAccountCreationThrottle );
- return false;
+ return Status::newFatal( 'acct_creation_throttle_hit', $wgAccountCreationThrottle );
}
$wgMemc->incr( $key );
}
}
- if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
- $this->mainLoginForm( $this->msg( 'externaldberror' )->text() );
- return false;
+ if ( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
+ return Status::newFatal( 'externaldberror' );
}
self::clearCreateaccountToken();
@@ -434,13 +483,16 @@ class LoginForm extends SpecialPage {
*
* @param $u User object.
* @param $autocreate boolean -- true if this is an autocreation via auth plugin
- * @return User object.
+ * @return Status object, with the User object in the value member on success
* @private
*/
function initUser( $u, $autocreate ) {
global $wgAuth;
- $u->addToDatabase();
+ $status = $u->addToDatabase();
+ if ( !$status->isOK() ) {
+ return $status;
+ }
if ( $wgAuth->allowPasswordChange() ) {
$u->setPassword( $this->mPassword );
@@ -452,22 +504,13 @@ class LoginForm extends SpecialPage {
$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();
# Update user count
- $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
- $ssUpdate->doUpdate();
+ DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
- return $u;
+ return Status::newGood( $u );
}
/**
@@ -523,17 +566,13 @@ class LoginForm extends SpecialPage {
return self::SUCCESS;
}
- $this->mExtUser = ExternalUser::newFromName( $this->mUsername );
-
- # TODO: Allow some magic here for invalid external names, e.g., let the
- # user choose a different wiki name.
$u = User::newFromName( $this->mUsername );
- if( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) {
+ if ( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) {
return self::ILLEGAL;
}
$isAutoCreated = false;
- if ( 0 == $u->getID() ) {
+ if ( $u->getID() == 0 ) {
$status = $this->attemptAutoCreate( $u );
if ( $status !== self::SUCCESS ) {
return $status;
@@ -541,27 +580,20 @@ class LoginForm extends SpecialPage {
$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();
}
// Give general extensions, such as a captcha, a chance to abort logins
$abort = self::ABORTED;
- if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$this->mAbortLoginErrorMsg ) ) ) {
+ $msg = null;
+ if ( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$msg ) ) ) {
+ $this->mAbortLoginErrorMsg = $msg;
return $abort;
}
global $wgBlockDisablesLogin;
if ( !$u->checkPassword( $this->mPassword ) ) {
- if( $u->checkTemporaryPassword( $this->mPassword ) ) {
+ if ( $u->checkTemporaryPassword( $this->mPassword ) ) {
// The e-mailed temporary password should not be used for actu-
// al logins; that's a very sloppy habit, and insecure if an
// attacker has a few seconds to click "search" on someone's o-
@@ -578,7 +610,7 @@ class LoginForm extends SpecialPage {
// As a side-effect, we can authenticate the user's e-mail ad-
// dress if it's not already done, since the temporary password
// was sent via e-mail.
- if( !$u->isEmailConfirmed() ) {
+ if ( !$u->isEmailConfirmed() ) {
$u->confirmEmail();
$u->saveSettings();
}
@@ -588,7 +620,7 @@ class LoginForm extends SpecialPage {
// 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
@@ -620,7 +652,7 @@ class LoginForm extends SpecialPage {
/**
* Increment the login attempt throttle hit count for the (username,current IP)
* tuple unless the throttle was already reached.
- * @param $username string The user name
+ * @param string $username The user name
* @return Bool|Integer The integer hit count or True if it is already at the limit
*/
public static function incLoginThrottle( $username ) {
@@ -648,7 +680,7 @@ class LoginForm extends SpecialPage {
/**
* Clear the login attempt throttle hit count for the (username,current IP) tuple.
- * @param $username string The user name
+ * @param string $username The user name
* @return void
*/
public static function clearLoginThrottle( $username ) {
@@ -668,44 +700,26 @@ class LoginForm extends SpecialPage {
* @return integer Status code
*/
function attemptAutoCreate( $user ) {
- global $wgAuth, $wgAutocreatePolicy;
+ global $wgAuth;
if ( $this->getUser()->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 ( $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;
- }
+ 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;
}
$abortError = '';
- if( !wfRunHooks( 'AbortAutoAccount', array( $user, &$abortError ) ) ) {
+ if ( !wfRunHooks( 'AbortAutoAccount', array( $user, &$abortError ) ) ) {
// Hook point to add extra creation throttles and blocks
wfDebug( "LoginForm::attemptAutoCreate: a hook blocked creation: $abortError\n" );
$this->mAbortLoginErrorMsg = $abortError;
@@ -713,24 +727,40 @@ class LoginForm extends SpecialPage {
}
wfDebug( __METHOD__ . ": creating account\n" );
- $this->initUser( $user, true );
+ $status = $this->initUser( $user, true );
+
+ if ( !$status->isOK() ) {
+ $errors = $status->getErrorsByType( 'error' );
+ $this->mAbortLoginErrorMsg = $errors[0]['message'];
+ return self::ABORTED;
+ }
+
return self::SUCCESS;
}
function processLogin() {
- global $wgMemc, $wgLang;
+ global $wgMemc, $wgLang, $wgSecureLogin, $wgPasswordAttemptThrottle;
switch ( $this->authenticateUserData() ) {
case self::SUCCESS:
# We've verified now, update the real record
$user = $this->getUser();
- if( (bool)$this->mRemember != (bool)$user->getOption( 'rememberpassword' ) ) {
+ if ( (bool)$this->mRemember != $user->getBoolOption( 'rememberpassword' ) ) {
$user->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
$user->saveSettings();
} else {
$user->invalidateCache();
}
- $user->setCookies();
+
+ if ( $user->requiresHTTPS() ) {
+ $this->mStickHTTPS = true;
+ }
+
+ if ( $wgSecureLogin && !$this->mStickHTTPS ) {
+ $user->setCookies( null, false );
+ } else {
+ $user->setCookies();
+ }
self::clearLoginToken();
// Reset the throttle
@@ -738,7 +768,7 @@ class LoginForm extends SpecialPage {
$key = wfMemcKey( 'password-throttle', $request->getIP(), md5( $this->mUsername ) );
$wgMemc->delete( $key );
- if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
+ if ( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
/* Replace the language object to provide user interface in
* correct language immediately on this first page load.
*/
@@ -755,56 +785,73 @@ class LoginForm extends SpecialPage {
break;
case self::NEED_TOKEN:
- $this->mainLoginForm( $this->msg( 'nocookiesforlogin' )->parse() );
+ $error = $this->mAbortLoginErrorMsg ?: 'nocookiesforlogin';
+ $this->mainLoginForm( $this->msg( $error )->parse() );
break;
case self::WRONG_TOKEN:
- $this->mainLoginForm( $this->msg( 'sessionfailure' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'sessionfailure';
+ $this->mainLoginForm( $this->msg( $error )->text() );
break;
case self::NO_NAME:
case self::ILLEGAL:
- $this->mainLoginForm( $this->msg( 'noname' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'noname';
+ $this->mainLoginForm( $this->msg( $error )->text() );
break;
case self::WRONG_PLUGIN_PASS:
- $this->mainLoginForm( $this->msg( 'wrongpassword' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword';
+ $this->mainLoginForm( $this->msg( $error )->text() );
break;
case self::NOT_EXISTS:
- if( $this->getUser()->isAllowed( 'createaccount' ) ) {
- $this->mainLoginForm( $this->msg( 'nosuchuser',
+ if ( $this->getUser()->isAllowed( 'createaccount' ) ) {
+ $error = $this->mAbortLoginErrorMsg ?: 'nosuchuser';
+ $this->mainLoginForm( $this->msg( $error,
wfEscapeWikiText( $this->mUsername ) )->parse() );
} else {
- $this->mainLoginForm( $this->msg( 'nosuchusershort',
+ $error = $this->mAbortLoginErrorMsg ?: 'nosuchusershort';
+ $this->mainLoginForm( $this->msg( $error,
wfEscapeWikiText( $this->mUsername ) )->text() );
}
break;
case self::WRONG_PASS:
- $this->mainLoginForm( $this->msg( 'wrongpassword' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword';
+ $this->mainLoginForm( $this->msg( $error )->text() );
break;
case self::EMPTY_PASS:
- $this->mainLoginForm( $this->msg( 'wrongpasswordempty' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'wrongpasswordempty';
+ $this->mainLoginForm( $this->msg( $error )->text() );
break;
case self::RESET_PASS:
- $this->resetLoginForm( $this->msg( 'resetpass_announce' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'resetpass_announce';
+ $this->resetLoginForm( $this->msg( $error )->text() );
break;
case self::CREATE_BLOCKED:
- $this->userBlockedMessage( $this->getUser()->mBlock );
+ $this->userBlockedMessage( $this->getUser()->isBlockedFromCreateAccount() );
break;
case self::THROTTLED:
- $this->mainLoginForm( $this->msg( 'login-throttled' )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'login-throttled';
+ $this->mainLoginForm( $this->msg( $error )
+ ->params ( $this->getLanguage()->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) )
+ ->text()
+ );
break;
case self::USER_BLOCKED:
- $this->mainLoginForm( $this->msg( 'login-userblocked',
- $this->mUsername )->escaped() );
+ $error = $this->mAbortLoginErrorMsg ?: 'login-userblocked';
+ $this->mainLoginForm( $this->msg( $error, $this->mUsername )->escaped() );
break;
case self::ABORTED:
- $this->mainLoginForm( $this->msg( $this->mAbortLoginErrorMsg )->text() );
+ $error = $this->mAbortLoginErrorMsg ?: 'login-abort-generic';
+ $this->mainLoginForm( $this->msg( $error )->text() );
break;
default:
throw new MWException( 'Unhandled case value' );
}
}
+ /**
+ * @param $error string
+ */
function resetLoginForm( $error ) {
- $this->getOutput()->addHTML( Xml::element('p', array( 'class' => 'error' ), $error ) );
+ $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' => 'error' ), $error ) );
$reset = new SpecialChangePassword();
$reset->setContext( $this->getContext() );
$reset->execute( null );
@@ -813,18 +860,18 @@ class LoginForm extends SpecialPage {
/**
* @param $u User object
* @param $throttle Boolean
- * @param $emailTitle String: message name of email title
- * @param $emailText String: message name of email text
+ * @param string $emailTitle message name of email title
+ * @param string $emailText message name of email text
* @return Status object
*/
function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
- global $wgServer, $wgScript, $wgNewPasswordExpiry;
+ global $wgCanonicalServer, $wgScript, $wgNewPasswordExpiry;
if ( $u->getEmail() == '' ) {
return Status::newFatal( 'noemail', $u->getName() );
}
$ip = $this->getRequest()->getIP();
- if( !$ip ) {
+ if ( !$ip ) {
return Status::newFatal( 'badipaddress' );
}
@@ -835,14 +882,13 @@ class LoginForm extends SpecialPage {
$u->setNewpassword( $np, $throttle );
$u->saveSettings();
$userLanguage = $u->getOption( 'language' );
- $m = $this->msg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript,
+ $m = $this->msg( $emailText, $ip, $u->getName(), $np, '<' . $wgCanonicalServer . $wgScript . '>',
round( $wgNewPasswordExpiry / 86400 ) )->inLanguage( $userLanguage )->text();
$result = $u->sendMail( $this->msg( $emailTitle )->inLanguage( $userLanguage )->text(), $m );
return $result;
}
-
/**
* Run any hooks registered for logins, then HTTP redirect to
* $this->mReturnTo (or Main Page if that's undefined). Formerly we had a
@@ -859,8 +905,9 @@ class LoginForm extends SpecialPage {
$injected_html = '';
wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) );
- if( $injected_html !== '' ) {
- $this->displaySuccessfulLogin( 'loginsuccess', $injected_html );
+ if ( $injected_html !== '' ) {
+ $this->displaySuccessfulAction( $this->msg( 'loginsuccesstitle' ),
+ 'loginsuccess', $injected_html );
} else {
$this->executeReturnTo( 'successredirect' );
}
@@ -876,7 +923,7 @@ class LoginForm extends SpecialPage {
# Run any hooks; display injected HTML
$currentUser = $this->getUser();
$injected_html = '';
- $welcome_creation_msg = 'welcomecreation';
+ $welcome_creation_msg = 'welcomecreation-msg';
wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) );
@@ -887,18 +934,21 @@ class LoginForm extends SpecialPage {
*/
wfRunHooks( 'BeforeWelcomeCreation', array( &$welcome_creation_msg, &$injected_html ) );
- $this->displaySuccessfulLogin( $welcome_creation_msg, $injected_html );
+ $this->displaySuccessfulAction( $this->msg( 'welcomeuser', $this->getUser()->getName() ),
+ $welcome_creation_msg, $injected_html );
}
/**
- * Display a "login successful" page.
+ * Display an "successful action" page.
+ *
+ * @param string|Message $title page's title
* @param $msgname string
* @param $injected_html string
*/
- private function displaySuccessfulLogin( $msgname, $injected_html ) {
+ private function displaySuccessfulAction( $title, $msgname, $injected_html ) {
$out = $this->getOutput();
- $out->setPageTitle( $this->msg( 'loginsuccesstitle' ) );
- if( $msgname ){
+ $out->setPageTitle( $title );
+ if ( $msgname ) {
$out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
}
@@ -913,6 +963,7 @@ class LoginForm extends SpecialPage {
* User::isBlockedFromCreateAccount(), which gets this block, ignores the 'hardblock'
* setting on blocks (bug 13611).
* @param $block Block the block causing this error
+ * @throws ErrorPageError
*/
function userBlockedMessage( Block $block ) {
# Let's be nice about this, it's likely that this feature will be used
@@ -922,23 +973,38 @@ class LoginForm extends SpecialPage {
# haven't bothered to log out before trying to create an account to
# evade it, but we'll leave that to their guilty conscience to figure
# out.
-
- $out = $this->getOutput();
- $out->setPageTitle( $this->msg( 'cantcreateaccounttitle' ) );
-
- $block_reason = $block->mReason;
- if ( strval( $block_reason ) === '' ) {
- $block_reason = $this->msg( 'blockednoreason' )->text();
- }
-
- $out->addWikiMsg(
+ throw new ErrorPageError(
+ 'cantcreateaccounttitle',
'cantcreateaccount-text',
- $block->getTarget(),
- $block_reason,
- $block->getByName()
+ array(
+ $block->getTarget(),
+ $block->mReason ? $block->mReason : $this->msg( 'blockednoreason' )->text(),
+ $block->getByName()
+ )
);
+ }
- $this->executeReturnTo( 'error' );
+ /**
+ * Add a "return to" link or redirect to it.
+ * Extensions can use this to reuse the "return to" logic after
+ * inject steps (such as redirection) into the login process.
+ *
+ * @param $type string, one of the following:
+ * - error: display a return to link ignoring $wgRedirectOnLogin
+ * - success: display a return to link using $wgRedirectOnLogin if needed
+ * - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
+ * @param string $returnTo
+ * @param array|string $returnToQuery
+ * @param bool $stickHTTPs Keep redirect link on HTTPs
+ * @since 1.22
+ */
+ public function showReturnToPage(
+ $type, $returnTo = '', $returnToQuery = '', $stickHTTPs = false
+ ) {
+ $this->mReturnTo = $returnTo;
+ $this->mReturnToQuery = $returnToQuery;
+ $this->mStickHTTPS = $stickHTTPs;
+ $this->executeReturnTo( $type );
}
/**
@@ -965,14 +1031,22 @@ class LoginForm extends SpecialPage {
$returnToTitle = Title::newMainPage();
}
+ if ( $wgSecureLogin && !$this->mStickHTTPS ) {
+ $options = array( 'http' );
+ $proto = PROTO_HTTP;
+ } elseif ( $wgSecureLogin ) {
+ $options = array( 'https' );
+ $proto = PROTO_HTTPS;
+ } else {
+ $options = array();
+ $proto = PROTO_RELATIVE;
+ }
+
if ( $type == 'successredirect' ) {
- $redirectUrl = $returnToTitle->getFullURL( $returnToQuery );
- if( $wgSecureLogin && !$this->mStickHTTPS ) {
- $redirectUrl = preg_replace( '/^https:/', 'http:', $redirectUrl );
- }
+ $redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto );
$this->getOutput()->redirect( $redirectUrl );
} else {
- $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery );
+ $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options );
}
}
@@ -987,6 +1061,7 @@ class LoginForm extends SpecialPage {
$titleObj = $this->getTitle();
$user = $this->getUser();
+ $out = $this->getOutput();
if ( $this->mType == 'signup' ) {
// Block signup here if in readonly. Keeps user from
@@ -1003,7 +1078,8 @@ class LoginForm extends SpecialPage {
}
}
- if ( $this->mUsername == '' ) {
+ // Pre-fill username (if not creating an account, bug 44775).
+ if ( $this->mUsername == '' && $this->mType != 'signup' ) {
if ( $user->isLoggedIn() ) {
$this->mUsername = $user->getName();
} else {
@@ -1013,14 +1089,33 @@ class LoginForm extends SpecialPage {
if ( $this->mType == 'signup' ) {
$template = new UsercreateTemplate();
+
+ $out->addModuleStyles( array(
+ 'mediawiki.ui',
+ 'mediawiki.special.createaccount'
+ ) );
+ // XXX hack pending RL or JS parse() support for complex content messages
+ // https://bugzilla.wikimedia.org/show_bug.cgi?id=25349
+ $out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp',
+ $this->msg( 'createacct-imgcaptcha-help' )->parse() );
+ $out->addModules( array(
+ 'mediawiki.special.createaccount.js'
+ ) );
+ // Must match number of benefits defined in messages
+ $template->set( 'benefitCount', 3 );
+
$q = 'action=submitlogin&type=signup';
$linkq = 'type=login';
- $linkmsg = 'gotaccount';
} else {
$template = new UserloginTemplate();
+
+ $out->addModuleStyles( array(
+ 'mediawiki.ui',
+ 'mediawiki.special.userlogin'
+ ) );
+
$q = 'action=submitlogin&type=login';
$linkq = 'type=signup';
- $linkmsg = 'nologin';
}
if ( $this->mReturnTo !== '' ) {
@@ -1033,16 +1128,14 @@ class LoginForm extends SpecialPage {
$linkq .= $returnto;
}
- # Don't show a "create account" link if the user can't
- if( $this->showCreateOrLoginLink( $user ) ) {
+ # Don't show a "create account" link if the user can't.
+ if ( $this->showCreateOrLoginLink( $user ) ) {
# Pass any language selection on to the mode switch link
- if( $wgLoginLanguageSelector && $this->mLanguage ) {
+ if ( $wgLoginLanguageSelector && $this->mLanguage ) {
$linkq .= '&uselang=' . $this->mLanguage;
}
- $link = Html::element( 'a', array( 'href' => $titleObj->getLocalURL( $linkq ) ),
- $this->msg( $linkmsg . 'link' )->text() ); # Calling either 'gotaccountlink' or 'nologinlink'
-
- $template->set( 'link', $this->msg( $linkmsg )->rawParams( $link )->parse() );
+ // Supply URL, login template creates the button.
+ $template->set( 'createOrLoginHref', $titleObj->getLocalURL( $linkq ) );
} else {
$template->set( 'link', '' );
}
@@ -1052,9 +1145,11 @@ class LoginForm extends SpecialPage {
: is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) );
$template->set( 'header', '' );
+ $template->set( 'skin', $this->getSkin() );
$template->set( 'name', $this->mUsername );
$template->set( 'password', $this->mPassword );
$template->set( 'retype', $this->mRetype );
+ $template->set( 'createemailset', $this->mCreateaccountMail );
$template->set( 'email', $this->mEmail );
$template->set( 'realname', $this->mRealName );
$template->set( 'domain', $this->mDomain );
@@ -1074,7 +1169,9 @@ class LoginForm extends SpecialPage {
$template->set( 'usereason', $user->isLoggedIn() );
$template->set( 'remember', $user->getOption( 'rememberpassword' ) || $this->mRemember );
$template->set( 'cansecurelogin', ( $wgSecureLogin === true ) );
- $template->set( 'stickHTTPS', $this->mStickHTTPS );
+ $template->set( 'stickhttps', (int)$this->mStickHTTPS );
+ $template->set( 'loggedin', $user->isLoggedIn() );
+ $template->set( 'loggedinuser', $user->getName() );
if ( $this->mType == 'signup' ) {
if ( !self::getCreateaccountToken() ) {
@@ -1089,15 +1186,16 @@ class LoginForm extends SpecialPage {
}
# Prepare language selection links as needed
- if( $wgLoginLanguageSelector ) {
+ if ( $wgLoginLanguageSelector ) {
$template->set( 'languages', $this->makeLanguageSelector() );
- if( $this->mLanguage ) {
+ if ( $this->mLanguage ) {
$template->set( 'uselang', $this->mLanguage );
}
}
+ $template->set( 'secureLoginUrl', $this->mSecureLoginUrl );
// Use loginend-https for HTTPS requests if it's not blank, loginend otherwise
- // Ditto for signupend
+ // Ditto for signupend. New forms use neither.
$usingHTTPS = WebRequest::detectProtocol() == 'https';
$loginendHTTPS = $this->msg( 'loginend-https' );
$signupendHTTPS = $this->msg( 'signupend-https' );
@@ -1120,22 +1218,21 @@ class LoginForm extends SpecialPage {
wfRunHooks( 'UserLoginForm', array( &$template ) );
}
- $out = $this->getOutput();
$out->disallowUserJs(); // just in case...
$out->addTemplate( $template );
}
/**
- * @private
+ * Whether the login/create account form should display a link to the
+ * other form (in addition to whatever the skin provides).
*
* @param $user User
- *
- * @return Boolean
+ * @return bool
*/
- function showCreateOrLoginLink( &$user ) {
- if( $this->mType == 'signup' ) {
+ private function showCreateOrLoginLink( &$user ) {
+ if ( $this->mType == 'signup' ) {
return true;
- } elseif( $user->isAllowed( 'createaccount' ) ) {
+ } elseif ( $user->isAllowed( 'createaccount' ) ) {
return true;
} else {
return false;
@@ -1213,17 +1310,12 @@ class LoginForm extends SpecialPage {
* Renew the user's session id, using strong entropy
*/
private function renewSessionId() {
- if ( wfCheckEntropy() ) {
- session_regenerate_id( false );
- } else {
- //If we don't trust PHP's entropy, we have to replace the session manually
- $tmp = $_SESSION;
- session_unset();
- session_write_close();
- session_id( MWCryptRand::generateHex( 32 ) );
- session_start();
- $_SESSION = $tmp;
+ global $wgSecureLogin, $wgCookieSecure;
+ if ( $wgSecureLogin && !$this->mStickHTTPS ) {
+ $wgCookieSecure = false;
}
+
+ wfResetSessionID();
}
/**
@@ -1260,13 +1352,6 @@ class LoginForm extends SpecialPage {
}
/**
- * @private
- */
- function throttleHit( $limit ) {
- $this->mainLoginForm( $this->msg( 'acct_creation_throttle_hit' )->numParams( $limit )->parse() );
- }
-
- /**
* Produce a bar of links which allow the user to select another language
* during login/registration but retain "returnto"
*
@@ -1274,10 +1359,10 @@ class LoginForm extends SpecialPage {
*/
function makeLanguageSelector() {
$msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
- if( !$msg->isBlank() ) {
+ if ( !$msg->isBlank() ) {
$langs = explode( "\n", $msg->text() );
$links = array();
- foreach( $langs as $lang ) {
+ foreach ( $langs as $lang ) {
$lang = trim( $lang, '* ' );
$parts = explode( '|', $lang );
if ( count( $parts ) >= 2 ) {
@@ -1295,20 +1380,20 @@ class LoginForm extends SpecialPage {
* Create a language selector link for a particular language
* Links back to this page preserving type and returnto
*
- * @param $text Link text
- * @param $lang Language code
+ * @param string $text Link text
+ * @param string $lang Language code
* @return string
*/
function makeLanguageSelectorLink( $text, $lang ) {
- if( $this->getLanguage()->getCode() == $lang ) {
+ if ( $this->getLanguage()->getCode() == $lang ) {
// no link for currently used language
return htmlspecialchars( $text );
}
$query = array( 'uselang' => $lang );
- if( $this->mType == 'signup' ) {
+ if ( $this->mType == 'signup' ) {
$query['type'] = 'signup';
}
- if( $this->mReturnTo !== '' ) {
+ if ( $this->mReturnTo !== '' ) {
$query['returnto'] = $this->mReturnTo;
$query['returntoquery'] = $this->mReturnToQuery;
}
@@ -1324,4 +1409,8 @@ class LoginForm extends SpecialPage {
$query
);
}
+
+ protected function getGroupName() {
+ return 'login';
+ }
}
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
index ab2bf0ac..d957e875 100644
--- a/includes/specials/SpecialUserlogout.php
+++ b/includes/specials/SpecialUserlogout.php
@@ -49,8 +49,11 @@ class SpecialUserlogout extends UnlistedSpecialPage {
$oldName = $user->getName();
$user->logout();
+ $loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
+ $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+
$out = $this->getOutput();
- $out->addWikiMsg( 'logouttext' );
+ $out->addWikiMsg( 'logouttext', $loginURL );
// Hook.
$injected_html = '';
@@ -59,4 +62,8 @@ class SpecialUserlogout extends UnlistedSpecialPage {
$out->returnToMain();
}
+
+ protected function getGroupName() {
+ return 'login';
+ }
}
diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php
index 9f5a48a5..4501736f 100644
--- a/includes/specials/SpecialUserrights.php
+++ b/includes/specials/SpecialUserrights.php
@@ -45,6 +45,11 @@ class UserrightsPage extends SpecialPage {
return $this->userCanChangeRights( $user, false );
}
+ /**
+ * @param User $user
+ * @param bool $checkIfSelf
+ * @return bool
+ */
public function userCanChangeRights( $user, $checkIfSelf = true ) {
$available = $this->changeableGroups();
if ( $user->getId() == 0 ) {
@@ -54,7 +59,7 @@ class UserrightsPage extends SpecialPage {
|| !empty( $available['remove'] )
|| ( ( $this->isself || !$checkIfSelf ) &&
( !empty( $available['add-self'] )
- || !empty( $available['remove-self'] ) ) );
+ || !empty( $available['remove-self'] ) ) );
}
/**
@@ -62,6 +67,7 @@ class UserrightsPage extends SpecialPage {
* Depending on the submit button used, call a form or a save function.
*
* @param $par Mixed: string if any subpage provided, else null
+ * @throws UserBlockedError|PermissionsError
*/
public function execute( $par ) {
// If the visitor doesn't have permissions to assign or remove
@@ -74,13 +80,13 @@ class UserrightsPage extends SpecialPage {
* (e.g. they don't have the userrights permission), then don't
* allow them to use Special:UserRights.
*/
- if( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) {
+ if ( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) {
throw new UserBlockedError( $user->getBlock() );
}
$request = $this->getRequest();
- if( $par !== null ) {
+ if ( $par !== null ) {
$this->mTarget = $par;
} else {
$this->mTarget = $request->getVal( 'user' );
@@ -94,15 +100,27 @@ class UserrightsPage extends SpecialPage {
* edit their own groups, automatically set them as the
* target.
*/
- if ( !count( $available['add'] ) && !count( $available['remove'] ) )
+ if ( !count( $available['add'] ) && !count( $available['remove'] ) ) {
$this->mTarget = $user->getName();
+ }
}
if ( User::getCanonicalName( $this->mTarget ) == $user->getName() ) {
$this->isself = true;
}
- if( !$this->userCanChangeRights( $user, true ) ) {
+ if ( !$this->userCanChangeRights( $user, true ) ) {
+ if ( $this->isself && $request->getCheck( 'success' ) ) {
+ // bug 48609: if the user just removed its own rights, this would
+ // leads it in a "permissions error" page. In that case, show a
+ // message that it can't anymore use this page instead of an error
+ $this->setHeaders();
+ $out = $this->getOutput();
+ $out->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", 'userrights-removed-self' );
+ $out->returnToMain();
+ return;
+ }
+
// @todo FIXME: There may be intermediate groups we can mention.
$msg = $user->isAnon() ? 'userrights-nologin' : 'userrights-notallowed';
throw new PermissionsError( null, array( array( $msg ) ) );
@@ -121,50 +139,54 @@ class UserrightsPage extends SpecialPage {
$this->switchForm();
}
- if( $request->wasPosted() ) {
+ if (
+ $request->wasPosted() &&
+ $request->getCheck( 'saveusergroups' ) &&
+ $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
+ ) {
// save settings
- if( $request->getCheck( 'saveusergroups' ) ) {
- $reason = $request->getVal( 'user-reason' );
- $tok = $request->getVal( 'wpEditToken' );
- if( $user->matchEditToken( $tok, $this->mTarget ) ) {
- $this->saveUserGroups(
- $this->mTarget,
- $reason
- );
-
- $out->redirect( $this->getSuccessURL() );
- return;
- }
+ $status = $this->fetchUser( $this->mTarget );
+ if ( !$status->isOK() ) {
+ $this->getOutput()->addWikiText( $status->getWikiText() );
+ return;
+ }
+
+ $targetUser = $status->value;
+
+ if ( $request->getVal( 'conflictcheck-originalgroups' ) !== implode( ',', $targetUser->getGroups() ) ) {
+ $out->addWikiMsg( 'userrights-conflict' );
+ } else {
+ $this->saveUserGroups(
+ $this->mTarget,
+ $request->getVal( 'user-reason' ),
+ $targetUser
+ );
+
+ $out->redirect( $this->getSuccessURL() );
+ return;
}
}
// show some more forms
- if( $this->mTarget !== null ) {
+ if ( $this->mTarget !== null ) {
$this->editUserGroupsForm( $this->mTarget );
}
}
function getSuccessURL() {
- return $this->getTitle( $this->mTarget )->getFullURL();
+ return $this->getTitle( $this->mTarget )->getFullURL( array( 'success' => 1 ) );
}
/**
* Save user groups changes in the database.
* Data comes from the editUserGroupsForm() form function
*
- * @param $username String: username to apply changes to.
- * @param $reason String: reason for group change
+ * @param string $username username to apply changes to.
+ * @param string $reason reason for group change
+ * @param User|UserRightsProxy $user Target user object.
* @return null
*/
- function saveUserGroups( $username, $reason = '' ) {
- $status = $this->fetchUser( $username );
- if( !$status->isOK() ) {
- $this->getOutput()->addWikiText( $status->getWikiText() );
- return;
- } else {
- $user = $status->value;
- }
-
+ function saveUserGroups( $username, $reason, $user ) {
$allgroups = $this->getAllGroups();
$addgroup = array();
$removegroup = array();
@@ -188,12 +210,14 @@ class UserrightsPage extends SpecialPage {
* 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
+ * @param array $add of groups to add
+ * @param array $remove of groups to remove
+ * @param string $reason reason for group change
* @return Array: Tuple of added, then removed groups
*/
function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
+ global $wgAuth;
+
// Validate input set...
$isself = ( $user->getName() == $this->getUser()->getName() );
$groups = $user->getGroups();
@@ -212,15 +236,15 @@ class UserrightsPage extends SpecialPage {
$newGroups = $oldGroups;
// remove then add groups
- if( $remove ) {
+ if ( $remove ) {
$newGroups = array_diff( $newGroups, $remove );
- foreach( $remove as $group ) {
+ foreach ( $remove as $group ) {
$user->removeGroup( $group );
}
}
- if( $add ) {
+ if ( $add ) {
$newGroups = array_merge( $newGroups, $add );
- foreach( $add as $group ) {
+ foreach ( $add as $group ) {
$user->addGroup( $group );
}
}
@@ -229,40 +253,42 @@ class UserrightsPage extends SpecialPage {
// Ensure that caches are cleared
$user->invalidateCache();
+ // update groups in external authentication database
+ $wgAuth->updateExternalDBGroups( $user, $add, $remove );
+
wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
wfRunHooks( 'UserRights', array( &$user, $add, $remove ) );
- if( $newGroups != $oldGroups ) {
+ if ( $newGroups != $oldGroups ) {
$this->addLogEntry( $user, $oldGroups, $newGroups, $reason );
}
return array( $add, $remove );
}
-
/**
* Add a rights log entry for an action.
*/
function addLogEntry( $user, $oldGroups, $newGroups, $reason ) {
- $log = new LogPage( 'rights' );
-
- $log->addEntry( 'rights',
- $user->getUserPage(),
- $reason,
- array(
- $this->makeGroupNameListForLog( $oldGroups ),
- $this->makeGroupNameListForLog( $newGroups )
- )
- );
+ $logEntry = new ManualLogEntry( 'rights', 'rights' );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setTarget( $user->getUserPage() );
+ $logEntry->setComment( $reason );
+ $logEntry->setParameters( array(
+ '4::oldgroups' => $oldGroups,
+ '5::newgroups' => $newGroups,
+ ) );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
}
/**
* Edit user groups membership
- * @param $username String: name of the user.
+ * @param string $username name of the user.
*/
function editUserGroupsForm( $username ) {
$status = $this->fetchUser( $username );
- if( !$status->isOK() ) {
+ if ( !$status->isOK() ) {
$this->getOutput()->addWikiText( $status->getWikiText() );
return;
} else {
@@ -283,63 +309,64 @@ class UserrightsPage extends SpecialPage {
* return a user (or proxy) object for manipulating it.
*
* Side effects: error output for invalid access
+ * @param string $username
* @return Status object
*/
public function fetchUser( $username ) {
global $wgUserrightsInterwikiDelimiter;
$parts = explode( $wgUserrightsInterwikiDelimiter, $username );
- if( count( $parts ) < 2 ) {
+ if ( count( $parts ) < 2 ) {
$name = trim( $username );
$database = '';
} else {
list( $name, $database ) = array_map( 'trim', $parts );
- if( $database == wfWikiID() ) {
+ if ( $database == wfWikiID() ) {
$database = '';
} else {
- if( !$this->getUser()->isAllowed( 'userrights-interwiki' ) ) {
+ if ( !$this->getUser()->isAllowed( 'userrights-interwiki' ) ) {
return Status::newFatal( 'userrights-no-interwiki' );
}
- if( !UserRightsProxy::validDatabase( $database ) ) {
+ if ( !UserRightsProxy::validDatabase( $database ) ) {
return Status::newFatal( 'userrights-nodatabase', $database );
}
}
}
- if( $name === '' ) {
+ if ( $name === '' ) {
return Status::newFatal( 'nouserspecified' );
}
- if( $name[0] == '#' ) {
+ if ( $name[0] == '#' ) {
// Numeric ID can be specified...
// We'll do a lookup for the name internally.
$id = intval( substr( $name, 1 ) );
- if( $database == '' ) {
+ if ( $database == '' ) {
$name = User::whoIs( $id );
} else {
$name = UserRightsProxy::whoIs( $database, $id );
}
- if( !$name ) {
+ if ( !$name ) {
return Status::newFatal( 'noname' );
}
} else {
$name = User::getCanonicalName( $name );
- if( $name === false ) {
+ if ( $name === false ) {
// invalid name
return Status::newFatal( 'nosuchusershort', $username );
}
}
- if( $database == '' ) {
+ if ( $database == '' ) {
$user = User::newFromName( $name );
} else {
$user = UserRightsProxy::newFromName( $database, $name );
}
- if( !$user || $user->isAnon() ) {
+ if ( !$user || $user->isAnon() ) {
return Status::newFatal( 'nosuchusershort', $username );
}
@@ -347,15 +374,24 @@ class UserrightsPage extends SpecialPage {
}
function makeGroupNameList( $ids ) {
- if( empty( $ids ) ) {
+ if ( empty( $ids ) ) {
return $this->msg( 'rightsnone' )->inContentLanguage()->text();
} else {
return implode( ', ', $ids );
}
}
+ /**
+ * Make a list of group names to be stored as parameter for log entries
+ *
+ * @deprecated in 1.21; use LogFormatter instead.
+ * @param $ids array
+ * @return string
+ */
function makeGroupNameListForLog( $ids ) {
- if( empty( $ids ) ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
+ if ( empty( $ids ) ) {
return '';
} else {
return $this->makeGroupNameList( $ids );
@@ -369,9 +405,9 @@ class UserrightsPage extends SpecialPage {
global $wgScript;
$this->getOutput()->addHTML(
Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
- Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
- Xml::inputLabel( $this->msg( 'userrights-user-editname' )->text(), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ) ) . ' ' .
+ Xml::inputLabel( $this->msg( 'userrights-user-editname' )->text(), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ), array( 'autofocus' => true ) ) . ' ' .
Xml::submitButton( $this->msg( 'editusergroup' )->text() ) .
Html::closeElement( 'fieldset' ) .
Html::closeElement( 'form' ) . "\n"
@@ -383,7 +419,7 @@ class UserrightsPage extends SpecialPage {
* form will be able to manipulate based on the current user's system
* permissions.
*
- * @param $groups Array: list of groups the given user is in
+ * @param array $groups list of groups the given user is in
* @return Array: Tuple of addable, then removable groups
*/
protected function splitGroups( $groups ) {
@@ -409,27 +445,41 @@ class UserrightsPage extends SpecialPage {
*/
protected function showEditUserGroupsForm( $user, $groups ) {
$list = array();
- foreach( $groups as $group ) {
+ $membersList = array();
+ foreach ( $groups as $group ) {
$list[] = self::buildGroupLink( $group );
+ $membersList[] = self::buildGroupMemberLink( $group );
}
- $autolist = array();
+ $autoList = array();
+ $autoMembersList = array();
if ( $user instanceof User ) {
- foreach( Autopromote::getAutopromoteGroups( $user ) as $group ) {
- $autolist[] = self::buildGroupLink( $group );
+ foreach ( Autopromote::getAutopromoteGroups( $user ) as $group ) {
+ $autoList[] = self::buildGroupLink( $group );
+ $autoMembersList[] = self::buildGroupMemberLink( $group );
}
}
+ $language = $this->getLanguage();
+ $displayedList = $this->msg( 'userrights-groupsmember-type',
+ $language->listToText( $list ),
+ $language->listToText( $membersList )
+ )->plain();
+ $displayedAutolist = $this->msg( 'userrights-groupsmember-type',
+ $language->listToText( $autoList ),
+ $language->listToText( $autoMembersList )
+ )->plain();
+
$grouplist = '';
$count = count( $list );
- if( $count > 0 ) {
+ if ( $count > 0 ) {
$grouplist = $this->msg( 'userrights-groupsmember', $count, $user->getName() )->parse();
- $grouplist = '<p>' . $grouplist . ' ' . $this->getLanguage()->listToText( $list ) . "</p>\n";
+ $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
}
- $count = count( $autolist );
- if( $count > 0 ) {
+ $count = count( $autoList );
+ if ( $count > 0 ) {
$autogrouplistintro = $this->msg( 'userrights-groupsmember-auto', $count, $user->getName() )->parse();
- $grouplist .= '<p>' . $autogrouplistintro . ' ' . $this->getLanguage()->listToText( $autolist ) . "</p>\n";
+ $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
}
$userToolLinks = Linker::userToolLinks(
@@ -443,6 +493,7 @@ class UserrightsPage extends SpecialPage {
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
Html::hidden( 'user', $this->mTarget ) .
Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
+ Html::hidden( 'conflictcheck-originalgroups', implode( ',', $user->getGroups() ) ) . // Conflict detection
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), $this->msg( 'userrights-editusergroup', $user->getName() )->text() ) .
$this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() .
@@ -479,10 +530,17 @@ class UserrightsPage extends SpecialPage {
* @return string
*/
private static function buildGroupLink( $group ) {
- static $cache = array();
- if( !isset( $cache[$group] ) )
- $cache[$group] = User::makeGroupLinkHtml( $group, htmlspecialchars( User::getGroupName( $group ) ) );
- return $cache[$group];
+ return User::makeGroupLinkHtml( $group, User::getGroupName( $group ) );
+ }
+
+ /**
+ * Format a link to a group member description page
+ *
+ * @param $group string
+ * @return string
+ */
+ private static function buildGroupMemberLink( $group ) {
+ return User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
}
/**
@@ -497,7 +555,7 @@ class UserrightsPage extends SpecialPage {
* Adds a table with checkboxes where you can select what groups to add/remove
*
* @todo Just pass the username string?
- * @param $usergroups Array: groups the user belongs to
+ * @param array $usergroups groups the user belongs to
* @param $user User a user object
* @return string XHTML table element with checkboxes
*/
@@ -505,17 +563,17 @@ class UserrightsPage extends SpecialPage {
$allgroups = $this->getAllGroups();
$ret = '';
- # Put all column info into an associative array so that extensions can
- # more easily manage it.
+ // 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?
+ // Should the checkbox be disabled?
$disabled = !(
( $set && $this->canRemove( $group ) ) ||
( !$set && $this->canAdd( $group ) ) );
- # Do we need to point out that this action is irreversible?
+ // Do we need to point out that this action is irreversible?
$irreversible = !$disabled && (
( $set && !$this->canAdd( $group ) ) ||
( !$set && !$this->canRemove( $group ) ) );
@@ -526,27 +584,30 @@ class UserrightsPage extends SpecialPage {
'irreversible' => $irreversible
);
- if( $disabled ) {
+ if ( $disabled ) {
$columns['unchangeable'][$group] = $checkbox;
} else {
$columns['changeable'][$group] = $checkbox;
}
}
- # Build the HTML table
- $ret .= Xml::openElement( 'table', array( 'class' => 'mw-userrights-groups' ) ) .
+ // Build the HTML table
+ $ret .= Xml::openElement( 'table', array( 'class' => 'mw-userrights-groups' ) ) .
"<tr>\n";
- foreach( $columns as $name => $column ) {
- if( $column === array() )
+ foreach ( $columns as $name => $column ) {
+ if ( $column === array() ) {
continue;
+ }
+ // Messages: userrights-changeable-col, userrights-unchangeable-col
$ret .= Xml::element( 'th', null, $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text() );
}
- $ret.= "</tr>\n<tr>\n";
- foreach( $columns as $column ) {
- if( $column === array() )
+ $ret .= "</tr>\n<tr>\n";
+ foreach ( $columns as $column ) {
+ if ( $column === array() ) {
continue;
+ }
$ret .= "\t<td style='vertical-align:top;'>\n";
- foreach( $column as $group => $checkbox ) {
+ foreach ( $column as $group => $checkbox ) {
$attr = $checkbox['disabled'] ? array( 'disabled' => 'disabled' ) : array();
$member = User::getGroupMember( $group, $user->getName() );
@@ -574,14 +635,13 @@ class UserrightsPage extends SpecialPage {
* @return bool Can we remove the group?
*/
private function canRemove( $group ) {
- // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
- // PHP.
+ // $this->changeableGroups()['remove'] doesn't work, of course. Thanks, PHP.
$groups = $this->changeableGroups();
return in_array( $group, $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] ) );
}
/**
- * @param $group string: the name of the group to check
+ * @param string $group the name of the group to check
* @return bool Can we add the group?
*/
private function canAdd( $group ) {
@@ -592,7 +652,7 @@ class UserrightsPage extends SpecialPage {
/**
* Returns $this->getUser()->changeableGroups()
*
- * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) )
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ), 'add-self' => array( addablegroups to self ), 'remove-self' => array( removable groups from self ) )
*/
function changeableGroups() {
return $this->getUser()->changeableGroups();
@@ -609,4 +669,8 @@ class UserrightsPage extends SpecialPage {
$output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
}
+
+ protected function getGroupName() {
+ return 'users';
+ }
}
diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php
index 4e5b6bf5..5ba785f5 100644
--- a/includes/specials/SpecialVersion.php
+++ b/includes/specials/SpecialVersion.php
@@ -40,7 +40,7 @@ class SpecialVersion extends SpecialPage {
'https://svn.wikimedia.org/svnroot/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki',
);
- public function __construct(){
+ public function __construct() {
parent::__construct( 'Version' );
}
@@ -48,29 +48,41 @@ class SpecialVersion extends SpecialPage {
* main()
*/
public function execute( $par ) {
- global $wgSpecialVersionShowHooks;
+ global $wgSpecialVersionShowHooks, $IP;
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
$out->allowClickjacking();
- $text =
- $this->getMediaWikiCredits() .
- $this->softwareInformation() .
- $this->getEntryPointInfo() .
- $this->getExtensionCredits();
- if ( $wgSpecialVersionShowHooks ) {
- $text .= $this->getWgHooks();
- }
+ if ( $par !== 'Credits' ) {
+ $text =
+ $this->getMediaWikiCredits() .
+ $this->softwareInformation() .
+ $this->getEntryPointInfo() .
+ $this->getExtensionCredits();
+ if ( $wgSpecialVersionShowHooks ) {
+ $text .= $this->getWgHooks();
+ }
- $out->addWikiText( $text );
- $out->addHTML( $this->IPInfo() );
+ $out->addWikiText( $text );
+ $out->addHTML( $this->IPInfo() );
- if ( $this->getRequest()->getVal( 'easteregg' ) ) {
- if ( $this->showEasterEgg() ) {
+ if ( $this->getRequest()->getVal( 'easteregg' ) ) {
// TODO: put something interesting here
}
+ } else {
+ // Credits sub page
+
+ // Header
+ $out->addHTML( wfMessage( 'version-credits-summary' )->parseAsBlock() );
+
+ $wikiText = file_get_contents( $IP . '/CREDITS' );
+
+ // Take everything from the first section onwards, to remove the (not localized) header
+ $wikiText = substr( $wikiText, strpos( $wikiText, '==' ) );
+
+ $out->addWikiText( $wikiText );
}
}
@@ -100,6 +112,14 @@ class SpecialVersion extends SpecialPage {
public static function getCopyrightAndAuthorList() {
global $wgLang;
+ if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
+ $othersLink = '[//www.mediawiki.org/wiki/Special:Version/Credits ' . wfMessage( 'version-poweredby-others' )->text() . ']';
+ } else {
+ $othersLink = '[[Special:Version/Credits|' . wfMessage( 'version-poweredby-others' )->text() . ']]';
+ }
+
+ $translatorsLink = '[//translatewiki.net/wiki/Translating:MediaWiki/Credits ' . wfMessage( 'version-poweredby-translators' )->text() . ']';
+
$authorList = array(
'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
@@ -108,13 +128,11 @@ class SpecialVersion extends SpecialPage {
'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
- 'Timo Tijhof',
- '[{{SERVER}}{{SCRIPTPATH}}/CREDITS ' .
- wfMessage( 'version-poweredby-others' )->text() .
- ']'
+ 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', $othersLink,
+ $translatorsLink
);
- return wfMessage( 'version-poweredby-credits', date( 'Y' ),
+ return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
$wgLang->listToText( $authorList ) )->text();
}
@@ -131,20 +149,20 @@ class SpecialVersion extends SpecialPage {
// wikimarkup can be used.
$software = array();
$software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
- $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")";
+ $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . PHP_SAPI . ")";
$software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
// Allow a hook to add/remove items.
wfRunHooks( 'SoftwareInfo', array( &$software ) );
$out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMessage( 'version-software' )->text() ) .
- Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) .
+ Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) .
"<tr>
<th>" . wfMessage( 'version-software-product' )->text() . "</th>
<th>" . wfMessage( 'version-software-version' )->text() . "</th>
</tr>\n";
- foreach( $software as $name => $version ) {
+ foreach ( $software as $name => $version ) {
$out .= "<tr>
<td>" . $name . "</td>
<td dir=\"ltr\">" . $version . "</td>
@@ -203,11 +221,11 @@ class SpecialVersion extends SpecialPage {
wfProfileIn( __METHOD__ );
$gitVersion = self::getVersionLinkedGit();
- if( $gitVersion ) {
+ if ( $gitVersion ) {
$v = $gitVersion;
} else {
$svnVersion = self::getVersionLinkedSvn();
- if( $svnVersion ) {
+ if ( $svnVersion ) {
$v = $svnVersion;
} else {
$v = $wgVersion; // fallback
@@ -222,10 +240,10 @@ class SpecialVersion extends SpecialPage {
* @return string wgVersion + a link to subversion revision of svn BASE
*/
private static function getVersionLinkedSvn() {
- global $wgVersion, $IP;
+ global $IP;
$info = self::getSvnInfo( $IP );
- if( !isset( $info['checkout-rev'] ) ) {
+ if ( !isset( $info['checkout-rev'] ) ) {
return false;
}
@@ -236,32 +254,54 @@ class SpecialVersion extends SpecialPage {
)->text();
if ( isset( $info['viewvc-url'] ) ) {
- $version = "$wgVersion [{$info['viewvc-url']} $linkText]";
+ $version = "[{$info['viewvc-url']} $linkText]";
} else {
- $version = "$wgVersion $linkText";
+ $version = $linkText;
}
- return $version;
+ return self::getwgVersionLinked() . " $version";
+ }
+
+ /**
+ * @return string
+ */
+ private static function getwgVersionLinked() {
+ global $wgVersion;
+ $versionUrl = "";
+ if ( wfRunHooks( 'SpecialVersionVersionUrl', array( $wgVersion, &$versionUrl ) ) ) {
+ $versionParts = array();
+ preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
+ $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
+ }
+ return "[$versionUrl $wgVersion]";
}
/**
- * @return bool|string wgVersion + HEAD sha1 stripped to the first 7 chars. False on failure
+ * @since 1.22 Returns the HEAD date in addition to the sha1 and link
+ * @return bool|string wgVersion + HEAD sha1 stripped to the first 7 chars with link and date, or false on failure
*/
private static function getVersionLinkedGit() {
- global $wgVersion, $IP;
+ global $IP, $wgLang;
$gitInfo = new GitInfo( $IP );
$headSHA1 = $gitInfo->getHeadSHA1();
- if( !$headSHA1 ) {
+ if ( !$headSHA1 ) {
return false;
}
$shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
- $viewerUrl = $gitInfo->getHeadViewUrl();
- if ( $viewerUrl !== false ) {
- $shortSHA1 = "[$viewerUrl $shortSHA1]";
+
+ $gitHeadUrl = $gitInfo->getHeadViewUrl();
+ if ( $gitHeadUrl !== false ) {
+ $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
}
- return "$wgVersion $shortSHA1";
+
+ $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
+ if ( $gitHeadCommitDate ) {
+ $shortSHA1 .= "<br/>" . $wgLang->timeanddate( $gitHeadCommitDate, true );
+ }
+
+ return self::getwgVersionLinked() . " $shortSHA1";
}
/**
@@ -353,11 +393,6 @@ class SpecialVersion extends SpecialPage {
// We want the 'other' type to be last in the list.
$out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
- if ( count( $wgExtensionFunctions ) ) {
- $out .= $this->openExtType( $this->msg( 'version-extension-functions' )->text(), 'extension-functions' );
- $out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
- }
-
$tags = $wgParser->getTags();
$cnt = count( $tags );
@@ -366,11 +401,11 @@ class SpecialVersion extends SpecialPage {
$tags[$i] = "&lt;{$tags[$i]}&gt;";
}
$out .= $this->openExtType( $this->msg( 'version-parser-extensiontags' )->text(), 'parser-tags' );
- $out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n";
+ $out .= '<tr><td colspan="4">' . $this->listToText( $tags ) . "</td></tr>\n";
}
$fhooks = $wgParser->getFunctionHooks();
- if( count( $fhooks ) ) {
+ if ( count( $fhooks ) ) {
$out .= $this->openExtType( $this->msg( 'version-parser-function-hooks' )->text(), 'parser-function-hooks' );
$out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n";
}
@@ -415,7 +450,7 @@ class SpecialVersion extends SpecialPage {
* @return int
*/
function compare( $a, $b ) {
- if( $a['name'] === $b['name'] ) {
+ if ( $a['name'] === $b['name'] ) {
return 0;
} else {
return $this->getLanguage()->lc( $a['name'] ) > $this->getLanguage()->lc( $b['name'] )
@@ -432,6 +467,8 @@ class SpecialVersion extends SpecialPage {
* @return string
*/
function getCreditsForExtension( array $extension ) {
+ global $wgLang;
+
$name = isset( $extension['name'] ) ? $extension['name'] : '[no name]';
$vcsText = false;
@@ -445,6 +482,10 @@ class SpecialVersion extends SpecialPage {
if ( $gitViewerUrl !== false ) {
$vcsText = "[$gitViewerUrl $vcsText]";
}
+ $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
+ if ( $gitHeadCommitDate ) {
+ $vcsText .= "<br/>" . $wgLang->timeanddate( $gitHeadCommitDate, true );
+ }
} else {
$svnInfo = self::getSvnInfo( dirname( $extension['path'] ) );
# Make subversion text/link.
@@ -472,13 +513,13 @@ class SpecialVersion extends SpecialPage {
}
# Make description text.
- $description = isset ( $extension['description'] ) ? $extension['description'] : '';
+ $description = isset( $extension['description'] ) ? $extension['description'] : '';
- if( isset ( $extension['descriptionmsg'] ) ) {
+ if ( isset( $extension['descriptionmsg'] ) ) {
# Look for a localized description.
$descriptionMsg = $extension['descriptionmsg'];
- if( is_array( $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
@@ -497,7 +538,7 @@ class SpecialVersion extends SpecialPage {
<td colspan=\"2\"><em>$mainLink $versionText</em></td>";
}
- $author = isset ( $extension['author'] ) ? $extension['author'] : array();
+ $author = isset( $extension['author'] ) ? $extension['author'] : array();
$extDescAuthor = "<td>$description</td>
<td>" . $this->listAuthors( $author, false ) . "</td>
</tr>\n";
@@ -533,21 +574,22 @@ class SpecialVersion extends SpecialPage {
$ret .= Xml::closeElement( 'table' );
return $ret;
- } else
+ } else {
return '';
+ }
}
private function openExtType( $text, $name = null ) {
$opt = array( 'colspan' => 4 );
$out = '';
- if( $this->firstExtOpened ) {
+ if ( $this->firstExtOpened ) {
// Insert a spacing line
$out .= '<tr class="sv-space">' . Html::element( 'td', $opt ) . "</tr>\n";
}
$this->firstExtOpened = true;
- if( $name ) {
+ if ( $name ) {
$opt['id'] = "sv-$name";
}
@@ -562,9 +604,8 @@ class SpecialVersion extends SpecialPage {
* @return String: HTML fragment
*/
private function IPInfo() {
- $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
- return "<!-- visited from $ip -->\n" .
- "<span style='display:none'>visited from $ip</span>";
+ $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
+ return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
}
/**
@@ -575,9 +616,11 @@ class SpecialVersion extends SpecialPage {
*/
function listAuthors( $authors ) {
$list = array();
- foreach( (array)$authors as $item ) {
- if( $item == '...' ) {
+ foreach ( (array)$authors as $item ) {
+ if ( $item == '...' ) {
$list[] = $this->msg( 'version-poweredby-others' )->text();
+ } elseif ( substr( $item, -5 ) == ' ...]' ) {
+ $list[] = substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]";
} else {
$list[] = $item;
}
@@ -588,7 +631,7 @@ class SpecialVersion extends SpecialPage {
/**
* Convert an array of items into a list for display.
*
- * @param $list Array of elements to display
+ * @param array $list of elements to display
* @param $sort Boolean: whether to sort the items in $list
*
* @return String
@@ -618,16 +661,16 @@ class SpecialVersion extends SpecialPage {
* @return Mixed
*/
public static function arrayToString( $list ) {
- if( is_array( $list ) && count( $list ) == 1 ) {
+ if ( is_array( $list ) && count( $list ) == 1 ) {
$list = $list[0];
}
- if( is_object( $list ) ) {
+ if ( is_object( $list ) ) {
$class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
return $class;
} elseif ( !is_array( $list ) ) {
return $list;
} else {
- if( is_object( $list[0] ) ) {
+ if ( is_object( $list[0] ) ) {
$class = get_class( $list[0] );
} else {
$class = $list[0];
@@ -656,7 +699,7 @@ class SpecialVersion extends SpecialPage {
// http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
$entries = $dir . '/.svn/entries';
- if( !file_exists( $entries ) ) {
+ if ( !file_exists( $entries ) ) {
return false;
}
@@ -666,9 +709,9 @@ class SpecialVersion extends SpecialPage {
}
// check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
- if( preg_match( '/^<\?xml/', $lines[0] ) ) {
+ if ( preg_match( '/^<\?xml/', $lines[0] ) ) {
// subversion is release <= 1.3
- if( !function_exists( 'simplexml_load_file' ) ) {
+ if ( !function_exists( 'simplexml_load_file' ) ) {
// We could fall back to expat... YUCK
return false;
}
@@ -678,11 +721,11 @@ class SpecialVersion extends SpecialPage {
$xml = simplexml_load_file( $entries );
wfRestoreWarnings();
- if( $xml ) {
- foreach( $xml->entry as $entry ) {
- if( $xml->entry[0]['name'] == '' ) {
+ if ( $xml ) {
+ foreach ( $xml->entry as $entry ) {
+ if ( $xml->entry[0]['name'] == '' ) {
// The directory entry should always have a revision marker.
- if( $entry['revision'] ) {
+ if ( $entry['revision'] ) {
return array( 'checkout-rev' => intval( $entry['revision'] ) );
}
}
@@ -722,7 +765,7 @@ class SpecialVersion extends SpecialPage {
/**
* Retrieve the revision number of a Subversion working directory.
*
- * @param $dir String: directory of the svn checkout
+ * @param string $dir directory of the svn checkout
*
* @return Integer: revision number as int
*/
@@ -739,7 +782,7 @@ class SpecialVersion extends SpecialPage {
}
/**
- * @param $dir String: directory of the git checkout
+ * @param string $dir directory of the git checkout
* @return bool|String sha1 of commit HEAD points to
*/
public static function getGitHeadSha1( $dir ) {
@@ -753,19 +796,32 @@ class SpecialVersion extends SpecialPage {
*/
public function getEntryPointInfo() {
global $wgArticlePath, $wgScriptPath;
+ $scriptPath = $wgScriptPath ? $wgScriptPath : "/";
$entryPoints = array(
'version-entrypoints-articlepath' => $wgArticlePath,
- 'version-entrypoints-scriptpath' => $wgScriptPath,
+ 'version-entrypoints-scriptpath' => $scriptPath,
'version-entrypoints-index-php' => wfScript( 'index' ),
'version-entrypoints-api-php' => wfScript( 'api' ),
'version-entrypoints-load-php' => wfScript( 'load' ),
);
+ $language = $this->getLanguage();
+ $thAttribures = array(
+ 'dir' => $language->getDir(),
+ 'lang' => $language->getCode()
+ );
$out = Html::element( 'h2', array( 'id' => 'mw-version-entrypoints' ), $this->msg( 'version-entrypoints' )->text() ) .
- Html::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'mw-version-entrypoints-table' ) ) .
+ Html::openElement( 'table',
+ array(
+ 'class' => 'wikitable plainlinks',
+ 'id' => 'mw-version-entrypoints-table',
+ 'dir' => 'ltr',
+ 'lang' => 'en'
+ )
+ ) .
Html::openElement( 'tr' ) .
- Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-entrypoint' )->text() ) .
- Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-url' )->text() ) .
+ Html::element( 'th', $thAttribures, $this->msg( 'version-entrypoints-header-entrypoint' )->text() ) .
+ Html::element( 'th', $thAttribures, $this->msg( 'version-entrypoints-header-url' )->text() ) .
Html::closeElement( 'tr' );
foreach ( $entryPoints as $message => $value ) {
@@ -782,108 +838,8 @@ class SpecialVersion extends SpecialPage {
return $out;
}
- function showEasterEgg() {
- $rx = $rp = $xe = '';
- $alpha = array("", "kbQW", "\$\n()");
- $beta = implode( "', '", $alpha);
- $juliet = 'echo $delta + strrev($foxtrot) - $alfa + $wgVersion . base64_decode($bravo) * $charlie';
- for ( $i = 1; $i <= 4; $i++ ) {
- $rx .= '([^j]*)J';
- $rp .= "+(\\$i)";
- }
-
- $rx = "/$rx/Sei";
- $O = substr("$alpha')", 1);
- for ( $i = 1; $i <= strlen( $rx ) / 3; $i++ ) {
- $rx[$i-1] = strtolower( $rx[$i-1] );
- }
- $ry = ".*?(.((.)(.))).{1,3}(.)(.{1,$i})(\\4.\\3)(.).*";
- $ry = "/$ry/Sei";
- $O = substr("$beta')", 1);
- preg_match_all('/(?<=\$)[[:alnum:]]*/',substr($juliet, 0, $i<<1), $charlie);
- foreach( $charlie[0] as $bravo ) {
- $$bravo =& $xe;
- }
- $xe = 'xe=<<<mo/./hfromowoxv=<<<m
-쵍潅旅𞗎왎캎𐺆ߨ趥䲀쫥𒯡𚦄𚬀Ꝍ螃䤎꤯溃𔱢櫅褡䞠⽬✡栠迤⾏𐵥쾃𜜧줏袏浣।궇䬃꼁꿤𘐧
-𞛁윥桯䦎䵎Ꞅ𚠣涁쭀讀撠蝠讄伣𞫡枮ⵇ𚥣𐡃𐭏沢𞜄𞴏𞻧⠤쳯蒣䮎𒵬컡豣ۅ𐯥⦇𐫁漅蛁꼤从楆
-⥀䡦𚭅沢⠬輁䲯좡梇䟇伄육较촅䥃要𞝄迯쟠꺃ⶥ栆궀撠満ꐣ𞦇좧𐠅𞫠𐠧𚮣讇輤亀➏欣첡쮧⽬
-氀쮧跧𐫥䪀⬬⾅𞼀ⵏ괬ত櫤䭀楦𚫃𐣂괥챣𐥇楀귧읠죯쒡ۅ𐾤䳄䤄𞽀괬躏譇䮄搥𚬁䯄津䶮⾅𐫅
-𐴂௧쮯궣輥ߡ亀𞪀氀诤𐯢⿅諃⫤𞦁䮣⦬죄椎貧𞛄ඇ쿇亏跤⦌术থۏ仆䛇枡䪄𐵇곁謠𞿯ⶏⶃ䞣
-궥螏蝁ꤣ⟬极涇𞴧伢𞼯ଅ𚣡즡⡌浣䯇쿃ⳇ궏ས⢃曦⦥蛧갠컡楧𘬧袏⦏⢠䳠챤⽧𚠧⬣⼀潧⭅椤
-𞟯軁종쵃䬆𞮀𞮅꤇𞣅溎楯곡⢡꾥첥쫧Ⱨ균檏辀䭮⡄𐞯쿁䱤𐠠柅迠웏𚟯⾅豠𐡀𐡅䱀轡⾯쥃⥁溆
-䢣䞮柄ꠌⶡ𞒯𐳣𞳅蛤椏𞯀✠귬ຄ𐷡𞜠䶃𞭀毥𞡯桥ꐥ❣쳀𞾧⡧𖥢꽧죄ത𖴧ޥ歠ແ위䯎撯쬁䮣浅
-쾇泮𐢁켄𞧧𞦏䦯꾯迡𞐯曎䢦쿣杦궯⡀䤦䷢𐭢쟁쯯⧤蟯䡏氇𒭯𔜧𞢣𞱏蝤𒬧궧ߢ𐭆䛃찃쭣沠𚬀𞿏
-䴃𐣣䣎𐺃ꥅ轃⣄蟧⦡𒛧蟃毣洇䞎Ҡ潄仆𐲃𞧥철䢤俎譯泠쮄␥栏쾯ⳏ짡𞾯⥡𚠬߂𚥯ކ澥䲀ⵀ𞻃
-ⵡ𚦣𒯣✬𐟯𞥥輄䱀굡榏❡첄⦄ꡥⶣ𞡤⺁𞞡ݣ𐢅𒷤⤡꿄蝡𞱁ⴄ贁𒛬氃𞞇𞶡ޅ짣߁𞱃𐫄ۥ𞰣𐱅欤
-梢蝡柧䥏仏撣𐳣𞠅좇𞐣蒣䰤྅𚪏࿂ಇ濤䞦쮅𚬁𚭧𚬬𒴯𐵣𚥌沮潁좤澅𐻯杣棦ꤤ洯𐳃𚭀콅궧쭠𔥢
-𞱠桎䝆겡쭄𞵁겯䥂ⶀ𐥂𚧬⽬䠇쳄❬Ⰼ𞵀䐦⿌웃𒿠첏𐛡浣涆𒯌⢤অ䭎𚜧갣𞾏䴮⡃꤯죠䰀쬯༄䫏
-𐱂ꢅ䬦賧𐯡유辇➥佃仮귣젏𒴯⭅ꢡ각컄⒤⻠讁涅䠃跥袏佄𞝄𐳇泄껧𚮡𞱏棇满གྷ𐻯輤괅𚠬❥겠
-𒐧䣂ꤏ襃𞼧𜰧伎襡웅𞳧걯䳣𚟡켁쭄洠컥❅⿏亂𚯧𚯯쯅𞮅⢁𐠦𒮠𚯣𞞥诤꣏辀𖥢椯겇毣濢𞝣𚢀➠
-䮮浥겁沣졣䜦泇歏𐾄搯曯柆ۇۇ䞀泆𐾧武𚭠況꽌𐧢ꝅ軀⬠쾣𞡀榧𞣏𚦤Ⱡ䠦Ⲥ𞰯𞻥쿇䬄貃柅涢
-갏⼁𐿧ݏౠ𐿣褀涡𘼧𞮏༅𞵡𐥆䮄𐮥➇ꝣݥ䡏䯎梢𚟇輇ꤠ䫣䵀ण漂𞬯⢡軀𚭅𐯆௦𚠤襁쫇⾡濧沤
-䜇伢ۇ汧첏䤎잤䛯Ⰱ俇𞵃ꢧ殂궏榮ޣ𞼧涂氏𞬇滦즤蜀⠥𐺏쐣⾏껬콇漯Ꝡ柦櫇읁梠仇장滦⟠꿯
-쮁搥櫢𐫣ꠏ𒮬椥𐛤誅栮朥迣⺄ඇ𞣣⿏䬂쾏⫠⒧✏궇襤⡁𞯇濃𚣠Ⱐ𚫤歯䛠𒛥𞫇쮠𞟤컃𞢯⬣濡䦣
-衏貣柂𞳁森챏ಇ고𚫠蟄䤏젯𒮡⫯楀䞄䳣쮅궤轧껯𞥤𐪃𞶡潇ބ𚥣𐵇浣𐬀蝤⽧쐣쾇➣𞝀𐡦䮠䤣𐠄
-Ꝡ𐾁蠤𞛡𞵀䬦覯搦⥯쥏梂걯𐾧ⵁ೦챁𚣌躄轡𐯣𞻥䢦𐝂財䲧𐦁䬎첁棏␣౦잧棆젥襁젃䤏⢏榀ⵁ
-螅赡𒿯ⶣ赧꾤𚬅濁𒛏涆𐴂ॡ䳦ߢ赁䯇䢃ꠌ泄柠泡찇𐛢𞰏䪂𐝢櫇𚰧漥𐣄𞜤𐥁⟤淣ഡ䳮த谀ཡ𞾧
-➁血꽧蟧辧게⻣𚣣쳏ഡ䠄杮𞣠죃汦諤య毠蝅𐦄謄殯𞱄䳀ⳏ𞶁쟇ආ𐻢잏𐿡䳃ۂ𞭥䝇䦇⥌켏쥯춏
-𖽢𐳃𒷡𚫥𚟇𐿧𚦧𐝢䥦𚯀棇潡⥄歡찁朆⻠䤆𖤧漢𜐧ꡅ⽄쾠𐥣衏𚥠𐥆䤣অ𞛇䤣𐡡𐢏䞦𖐧ߣ裏𚫁𐵤
-ཅۄ춁䲃欆귬𐺀诀滁𞫇𐯇䝃𞧡챃첥𞭤꺏쫅𞫡䱮𞼤અ𒭤견Ф𐫁𐾧佣𖱢澢쿏𞛧⽅侮榅𐾄य쥏蜏䣣
-𚥌𐫏쵥𚥡➤跡殃䰣䯤𞳥읤ⴏ굄𚬧⥇줡걬০켃𜼧𚧯첣䜂𞵇𚟀찃궀谀Ɽ伎䢮𒛄𚦀ꤥ⾣𐭁沅䬇䧠𐱇
-沀濡ठ𞰄쟠𐺅ꐣ𐴂躄佇⦇毄计賀䢎澡𒮌䲄𒠧캀䟣𐷧褀𞻅蠤൯棏蜃𞮤澄❧⾥撦⽬ⶥ𐪄ய𔼧ބ躄
-䬎챯𚫇⽯𐾠𞛠𚛧䬎Ꞅ굥𐢂𚠣⠥䝧朄𞧥࿏웥꽬གྷ浅⦁❬𐺆侢栦⧠𞛯궠ඦ𚭧趤谥此𐲂𐬃軠𚪅𐞦𞷤
-蛄俧袥补榏읠⤁⠀豇俢쮯꤇➏𐴁ⶤ涮찣𒮇읁榠跣𜤧⦅ໃಆ𞛯䵣谠𞰅ꢯ⡧淯柤궡✠䮎괯𒮣❅朎
-⥅웣䯮첀𚫣꒤𐣠쭏洀蛡楆𚮣ൡ䮮ү氠𐜏濆䜢䷯潣歃䷯𞣡웁쭄椥䟂➅𒯣𒯤ૡༀ䭧ܣ죅𐯠ए軯䧣
-Ⱔ䐢⬥檂䠮⫤䛠꜡䛆讠𚭄✠꿏欣蠡𐵆켏豣譄𞣇춣𒭯𐻢䠃䰠撦朅䮄榦溃貀𒯅䶇⾁𞬧澡𐻦䲮榀𞯧
-𐪄䢆侄𞾏朦꜇𐮢ཏ𐯣췧꺁𞱃枠櫧桠괬枇ꜯ곇𐰂𘜧𐦄컡濦汥줠𞲡輀𞫃𐠣쥇⣃𞴏䳂⟤漇쯣껃𐾀衃
-𚮄쯇𒼧𐝄浥洄楠৯춥蒧⾯𐫆༂ꤌ毮䤆⺄༠०袀䢂죃ⴣ𐿯梇溄毦𞼄螄櫤쳃栅満걌毠𞞏ⱌ𚮡꒧䢆
-ꥁ泎𞭅仧궀辯諯웅𞳇津趃অ꿏伏𐵤캁⠃𐦂𐶀ꝣ䛂贤济杧𐝁撠䱤殥歡躇楄꒧꽧𞽧䡣쵧𒯃𐱆ꜯ위
-ཀ谠諃𐬃軅␥𞰇贠撣߅꽤⠥ಡ𐝀궥윁𞳁Ⰴܯ즡歎𞷥ⵅഏ蝁𞟇구ꝧ܅䱦껡䛦߅蒯俧콣𚭅梧䛠ꡇ
-ݧ𚮏웥Т⬠䬦榀𐢂貤𞰅𚭠謣䱦⒡췧𐥀濇⧣⤀좯殧𞬣줤⣀楏楎굏ݤ滁ۇ𘐧𚯯䒯Ⰰ𞼤ҡ䰦𚣠椯❏
-趯𐣯豀쵅춀⳥䷠읡ۯ⺄ۅ䶏춤枂櫅ۅ𞥅䱃䭣𒳯汮澃𞢃谥ⵤ구𚣄콡曤𞣏ই߂읅蠠𜰧䞦ꞇⲏ𚮌諧
-趯첏䬎𐡏李겠⥇𞻥曢汥𞳡浆欠躅𐦁𞲯谡𞦏袧襃棧𚦁𞡡蟀侠𒛏찇챠쪇洠܀쯤䝇螏𞿣蜏俄𞦡⼀ལ
-谥촯䲦⥁ඤ𞛡𐐧⤃궅༡褡䭏毆濆⧡蛣Ф𞵇蠏ݤ賯꜁溅⡡ߡ𞥧䮄榆䵄求謥𐐧Ꞁ쯏⧡貇䛇䐢撦袥
-쮇䫀𞜄দ굯𞦁⻤襇줅⬅ہఠ⻀𔠧쒠䫆𐡅梄梯輤䥣읏⤄ⶡ诃䮢譡𞻠ߤ枤櫥𐢥伦袠ꢃ쳀裣𞼅䰄𞻡
-𒯇槥淠䯃ඏ⒯𚫣𚠯𞠣𚛄椦泮汣赃潥𚫇ദ𞛤𞿣䰏쮡𖭢蝏毁䶂䦧档䪂𞾃쟀𚪄𞞃𞳥𞼀𐿯졇웄䳎汀𐫣
-漠𚫄ꐡଥ认꽡𐱏𐭏𚼧⦄梎આ枀䠦楇쒤ꞃꤡⴅꞅ𞯁අҡ𞞤氣즤裀𞜅𐵥櫁𐵀༦𐳃쳣𐡯桧𞿠权굁죁
-짤𖤧蟃澀𒭏𞲯ߏ⣣⬁Ⱔ졥𚦌潆ꐡ⽤웁浥𞞃𐫄棆갤濧⼣겅쬄൧젣此潆⻯䜃꤯궠쮥𘬧曀⿅譅槣䞂
-䝎ꡏ𚟣䰀梥⾬ܡ𞿇𞠥𐮠𞺃䢮આ䧮쮃誅櫆𚪃죯诠䵀䯀跥𐾣⻥䤆Ⰰ꜄棧枃⻇థ誃𚛁࿇贄𞡣欎⽡𞱁
-𞲄⬏杇𐠅𐱃𞢤➁𐵤𐢄꒥즏亀쭁𚭡漆𞮇첁𐢦殎쮁滠𐠥榯𐮧𒵬⡀䮆䣠준讥𞼃䶇⪅껃泃𖱢楀갠複撮
-✡𐭢ແ𞮧𞛥쫃⽤規䥇沁轁𐡅ಢ䧮椁⬇𐤁𞡯杅武楥歎䟄溇䯢𒵬𐢣迃䪎䳤满ଅⱇ쭀ಥ𞥄䥆⧥𚞧좃
-유栤༡𐰃俇Ⰵ殇蠄⽏⾠܇𒮄澄𚦅⡤䪎榮Я견濂賣쮠仠䝮䶢𞦏𐫆ݏ襅褥찯𞤤ݥ象侯쵇궥𞠃윀웧
-𖰧殀蛡⫥亃觯潥蠀补ⴄ觧𐡇𐾆ꐯ䡣췡潏⻯⾁諏య꿧䱠𚭯찥ꞅ⪃콄즯쳣覧𞰄Ⲅ𞿣𚬧𞵤쐯⬃ඤ겤
-ⵃ蟥𞟧谣轇䛂𐮄佀߁氣𒯧榡𒷬桇䷯觠椄챥ꠌ蒯꜌䭤➡侦䣤𚦬䲀쥁⒤𐦄Ꝭ䢮𐣅ꡌ歡䝯䢣괯𚮣⥀
-줣०𚭀殣𚬥𒮇⟄趥좠洦ꢬ装䠆𒝠曧➁𒿧椃䠀𞡅𖼧䳇ງ줄ধ𞳁Ⱜ覠ꝃ殣𚯤涡䳠귥𐯁⫤覯𞲡𞼄༦
-䢦쥥줤ꡤড젃ಧꢥ諤𔭢ඥ𒛌枅𖜧줄躀ఏ䦎𞯄졯譄➇仄䰏蛏촡䞣춅涧⡄滀ଢ䮇每𘠧𚯧侇澀ꐡ杣
-𒷧槧߅䶠윥귡귧⤯𚪃𐷢ཆ裁毧𐥣𐯥⬤蝧첀⭁𞻡潤𞟃䝎池𞦀殤Ҡ𞵏䝯ཁ쟧𒰧氢귡𚛧𒿯ꥄ⭌䜇ۥ
-ꝡ𞯯棄⣏ꤥ০𐯠𒷤𞦣쮁𞰠𚧡桧𐐧ⴤꠡ軅𞟃衄䠦ߤ܅ⲃଢ蛄溎椀𞠀䛃𞡣𞟣澅𚭬䧤⡇贤⫌쪄ށ朣
-⻏켅𐽢⼡𐲀잠௧𞬥𞥀౧䦤ས誇漎譠迄䦂䳇𞣡正𐵤계楧ޅ✬𞿯棅𞳧𞛤𞜀쭯𞮀诠𐥀枢䥮䭆楆컧ଆ
-𞶇➬అ䤦誃𐠅𐿤䟀洀⡤𚟣滤𞥇𞾣즀𐠁⼃䰎溄꽅웇✡𐾥䲀⡏ܣ讣𞿥⼤覄𚯇䡇అ蝀⥌侧껄Ꝭ流贀
-漁쒤첧죏곡⣃趃賄撠।읠ⶌ𚣅⾥춧𞞠쒡쿀𞦠䵯毁涠𞫀⣡ꡄ䢀満棃䡯𐛣୯䳯ⵡୡ䥃❇⠅䣆杧𐳃
-귧覀𞼠漎𞴁𞤡ཇ䰦𞲣❃歆콣꿇朏𞢄𞵠Ꝍ𞡅賡𞧠曏꼃𞻯꼬ಇ𞴯资榎쮯輤ॡ䜎⦌𞶅𐠏𚧧⡃쳁𐵅࿀
-𞒧𞝤쯣껧쪃𞣠椃쐡⟤߇웅䱧䛣𞷧𐳤𚬠쮀䠏𞭇꽣𞿇⠣쟣𞢅ദ洅촥컇𚦁쵡ꞅ䠆𐥇⒥涯䐢ⴅ𒭡쮤꺅
-𞥇컠ⳁ漃𐲃윇诤겣𞥄伣䜠⻇𞡀修꜡𞻣䳎❄켇꽡𘼧쭄洂𞟏꜠𐮦Ⰳ쵅𐬂梀櫯䜯꜡䛣༏杇⪀캄𞰠⼌
-条𐳄没ⳅ➏𒮀첡❬侯캅检𞡧棡𞬄𞥧𞒠𞶄䥧𐳃𞻧𐝁ཧ謏𐫇𚯅讄枥𚞬첡쾀欎육웠𐭤୯濧譁챤䶢껤
-𞯤쒤𐾂辧𞮡𚭏褡⼣𞼃䳃␠𞝁豁ߡ櫦𒮬极𞱥ⶠઇꝠ𐭤𞝇沣棁柄𐳂䠯楅곅⼣⥃ༀ螡ߥ柤褣曠沧꒬
-𐴃䵂䲇蠀𐿧䲇ඦ𒯇⺁커謁𚣣𚫃컁漢䠀调ⲃ䢢ބ辅毡갯𚮁䤣椦𞲯१𞞠輯𘜧𐯣𐳅⽄𞽤𚧤𚬡䴆𞷠ଦ
-䱠䒮諃ఏ𐠡桦𞟇𚭧谁𞻤𐡁쥡浣𞼇譀⫌쮥ꢅ컁曅ꥅ𞟅ଏ찀汅𐷦ೡ谠𞦥䬀𞴡䢠쳀⡏𐵃ߠߠඅ겧淤
-쥣每譄꼠𒮣쫁쭥讥ॡ쿇𐾡ஆ伃⫠汇䜢衯楥济俏极𚣣撮쬅蜏⧤蛥쮁⥃𚯣것ஃ줠䣇迅泆𞟯𞰥⤯𐧣
-𚥯萠泎ଡ蠄涣త⾏⻌䝧ༀ榮ү𐳃歂浅𞬄ꡥ첤⬇유𐶃讏欤俤잧⡌𞭥ⱁ춥氤𐠧修流쫤䵆𞠃܀웣𞶏
-곧萡ꠀ걁𞟠认쮀𐽢谥잡𞼣佮𞺏軡⾁쮯ߡ⧯쟡䰆⽀굇촤认䵄輥𞦤𞲇䡮侢朆쬣搢⽃濃𞾄⣧𞶥柁༢
-⼅𞦀ॠ軀浯ܡ𒯡컡谤ඤ曢⧠짠컠𚠯꿡𐺀𒬧곌濂ণ웧⾡栅䞠괬ܤ䦄伏曀了ཡ榧䭦𒭯⛃衧濠𚐧읥
-쵁𐛣⪅蜤𞤁装고𒯬쳅⻁ݣ䳆ৠ䐦𐮡ऄ⫏𐶁쿧䜎𐿣젡귧棥櫁쿣泯俣佦⾥朦潏ꢤ𞫣ꙧ𞂎𐺆ڦՈ췥
-췧䙭䶍澥𞜅쨯쵥Ⱕ쵥䗌쵍潅旅暬Ոⵤ旆𞗎줭젠ৡ쮠┢𚴧𐵣潧𞾥𜔧𞑢贮𞽅跣쓄䔭𞷥⽇𞾅𞴥ꔥ䓭
-₎챍澥엇𞗎곭贇Ԇ쬡쩯䘠䯃𐯤湁𚚭Ո꽤엇𞗎ꔭ₎谥𐗇䗌쳭䙭䟍◎쳭䙍侭쾇쵤蓄䕍췥췧䓭◎쳭
-䒭𞗎ߏ䓭亭è청𞻥䙭侭䷤擏䕍췤⽇䐍䕍ⵤ摆位ཧ𞗅暬è춍찤ⲥ䙭䔭𚚭è谥𐗇䗌첍䙭䟍◎䕍𐗄
-엎ߏ◎첍⒬䓭亭è效𐱅궤◄虬䶭侄䗌꾄쓅䕍췥췧╂旄◌첍𞗂旌藂꾄쓅䕍ⵤ檦첍𞗂旌暬è𞂆效
-꽤엇虬䕍𐱅궤⚤è챍澥엇𞗎춍찤ⲥ₎𞂆찭𞽇䙭侭쾇൧蓇䕍꽤엇暬೨藅䗌ⳇ查䗌찭𞽇䓭䙭𞙮䔭
-枅ද𞝅➥赏𒶯ⵯඏ춥쟅ⵅ쟥𐵥螥ⴅ춯䟏췯淯䴏ꗍ旌₆效ꡁ𚦀桁⪣꼭𚠥𞽇𚩭𞘌ⱅ𞷥𐣇졣쓀暬è
-줭젠ৡ쮠┢𚴧꽠𜔧𞑢跮쵅䭀𞡀䗌è斈쳮𞴤侭ට𞩎𐵍潅暅汤津𞐥࿄𞴥ⶎ澥𞜅쑏𐗍肌惨澈漥𞾇쵤
-趤굄𞓅䶍澥𞜅쨯𞰅Ⱕ쵥䗌찭𞽇䓭䓭䐍è惨𐩍Э薎è擨₎𞗆
-mowoxf=<<<moDzk=hgs8GbPbqrcbvagDdJkbe zk=zk>0kssss?zk-0k10000:zk kbe zk=DDzk<<3&0kssssJ|Dzk>>13JJ^3658 kbe zk=pueDzk&0kssJ.pueDzk>>8JJ?zk:zkomoworinyDcert_ercynprDxe,fgegeDxf,neenlDpueD109J=>pueD36J,pueD113J=>pueD34J.pueD92J. 0 .pueD34JJJ,fgegeDxv,neenlDpueD13J=>snyfr,pueD10J=>snyfrJJJJwo';
-
- $haystack = preg_replace($ry, "$1$2$5$1_$7$89$i$5$6$8$O", $juliet);
- return preg_replace( $rx, $rp, $haystack );
+ protected function getGroupName() {
+ return 'wiki';
}
+
}
diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php
index 0b1fb251..d2ffdb94 100644
--- a/includes/specials/SpecialWantedcategories.php
+++ b/includes/specials/SpecialWantedcategories.php
@@ -35,22 +35,22 @@ class WantedCategoriesPage extends WantedQueryPage {
}
function getQueryInfo() {
- return array (
- 'tables' => array ( 'categorylinks', 'page' ),
- 'fields' => array ( 'namespace' => NS_CATEGORY,
+ return array(
+ 'tables' => array( 'categorylinks', 'page' ),
+ 'fields' => array( 'namespace' => NS_CATEGORY,
'title' => 'cl_to',
'value' => 'COUNT(*)' ),
- 'conds' => array ( 'page_title IS NULL' ),
- 'options' => array ( 'GROUP BY' => 'cl_to' ),
- 'join_conds' => array ( 'page' => array ( 'LEFT JOIN',
- array ( 'page_title = cl_to',
+ 'conds' => array( 'page_title IS NULL' ),
+ 'options' => array( 'GROUP BY' => 'cl_to' ),
+ 'join_conds' => array( 'page' => array( 'LEFT JOIN',
+ array( 'page_title = cl_to',
'page_namespace' => NS_CATEGORY ) ) )
);
}
/**
- * @param $skin Skin
- * @param $result
+ * @param Skin $skin
+ * @param object $result Result row
* @return string
*/
function formatResult( $skin, $result ) {
@@ -72,4 +72,8 @@ class WantedCategoriesPage extends WantedQueryPage {
$nlinks = $this->msg( 'nmembers' )->numParams( $result->value )->escaped();
return $this->getLanguage()->specialList( $plink, $nlinks );
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php
index f52f7bb9..b5c1fdbe 100644
--- a/includes/specials/SpecialWantedfiles.php
+++ b/includes/specials/SpecialWantedfiles.php
@@ -42,7 +42,7 @@ class WantedFilesPage extends WantedQueryPage {
$catMessage = $this->msg( 'broken-file-category' )
->title( Title::newFromText( "Wanted Files", NS_MAIN ) )
->inContentLanguage();
-
+
if ( !$catMessage->isDisabled() ) {
$category = Title::makeTitleSafe( NS_CATEGORY, $catMessage->text() );
} else {
@@ -73,18 +73,22 @@ class WantedFilesPage extends WantedQueryPage {
}
function getQueryInfo() {
- return array (
- 'tables' => array ( 'imagelinks', 'image' ),
- 'fields' => array ( 'namespace' => NS_FILE,
+ return array(
+ 'tables' => array( 'imagelinks', 'image' ),
+ 'fields' => array( 'namespace' => NS_FILE,
'title' => 'il_to',
'value' => 'COUNT(*)' ),
- 'conds' => array ( 'img_name IS NULL' ),
- 'options' => array ( 'GROUP BY' => 'il_to' ),
- 'join_conds' => array ( 'image' =>
- array ( 'LEFT JOIN',
- array ( 'il_to = img_name' )
+ 'conds' => array( 'img_name IS NULL' ),
+ 'options' => array( 'GROUP BY' => 'il_to' ),
+ 'join_conds' => array( 'image' =>
+ array( 'LEFT JOIN',
+ array( 'il_to = img_name' )
)
)
);
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php
index 7673305d..acec4ea4 100644
--- a/includes/specials/SpecialWantedpages.php
+++ b/includes/specials/SpecialWantedpages.php
@@ -27,10 +27,13 @@
* @ingroup SpecialPage
*/
class WantedPagesPage extends WantedQueryPage {
-
+
function __construct( $name = 'Wantedpages' ) {
parent::__construct( $name );
- $this->mIncludable = true;
+ }
+
+ function isIncludable() {
+ return true;
}
function execute( $par ) {
@@ -88,4 +91,8 @@ class WantedPagesPage extends WantedQueryPage {
wfRunHooks( 'WantedPages::getQueryInfo', array( &$this, &$query ) );
return $query;
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php
index f3e33698..d13fa031 100644
--- a/includes/specials/SpecialWantedtemplates.php
+++ b/includes/specials/SpecialWantedtemplates.php
@@ -38,18 +38,22 @@ class WantedTemplatesPage extends WantedQueryPage {
}
function getQueryInfo() {
- return array (
- 'tables' => array ( 'templatelinks', 'page' ),
- 'fields' => array ( 'namespace' => 'tl_namespace',
+ return array(
+ 'tables' => array( 'templatelinks', 'page' ),
+ 'fields' => array( 'namespace' => 'tl_namespace',
'title' => 'tl_title',
'value' => 'COUNT(*)' ),
- 'conds' => array ( 'page_title IS NULL',
+ 'conds' => array( 'page_title IS NULL',
'tl_namespace' => NS_TEMPLATE ),
- 'options' => array (
+ 'options' => array(
'GROUP BY' => array( 'tl_namespace', 'tl_title' ) ),
- 'join_conds' => array ( 'page' => array ( 'LEFT JOIN',
- array ( 'page_namespace = tl_namespace',
+ 'join_conds' => array( 'page' => array( 'LEFT JOIN',
+ array( 'page_namespace = tl_namespace',
'page_title = tl_title' ) ) )
);
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php
index 5dfc1133..55dc6efd 100644
--- a/includes/specials/SpecialWatchlist.php
+++ b/includes/specials/SpecialWatchlist.php
@@ -26,8 +26,8 @@ class SpecialWatchlist extends SpecialPage {
/**
* Constructor
*/
- public function __construct( $page = 'Watchlist' ){
- parent::__construct( $page );
+ public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
+ parent::__construct( $page, $restriction );
}
/**
@@ -41,7 +41,7 @@ class SpecialWatchlist extends SpecialPage {
$output = $this->getOutput();
# Anons don't get a watchlist
- if( $user->isAnon() ) {
+ if ( $user->isAnon() ) {
$output->setPageTitle( $this->msg( 'watchnologin' ) );
$output->setRobotPolicy( 'noindex,nofollow' );
$llink = Linker::linkKnown(
@@ -54,17 +54,16 @@ class SpecialWatchlist extends SpecialPage {
return;
}
+ // Check permissions
+ $this->checkPermissions();
+
// Add feed links
- $wlToken = $user->getOption( 'watchlisttoken' );
- if ( !$wlToken ) {
- $wlToken = MWCryptRand::generateHex( 40 );
- $user->setOption( 'watchlisttoken', $wlToken );
- $user->saveSettings();
+ $wlToken = $user->getTokenFromOption( 'watchlisttoken' );
+ if ( $wlToken ) {
+ $this->addFeedLinks( array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
+ 'wlowner' => $user->getName(), 'wltoken' => $wlToken ) );
}
- $this->addFeedLinks( array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
- 'wlowner' => $user->getName(), 'wltoken' => $wlToken ) );
-
$this->setHeaders();
$this->outputHeader();
@@ -74,9 +73,9 @@ class SpecialWatchlist extends SpecialPage {
$request = $this->getRequest();
$mode = SpecialEditWatchlist::getMode( $request, $par );
- if( $mode !== false ) {
+ if ( $mode !== false ) {
# TODO: localise?
- switch( $mode ){
+ switch ( $mode ) {
case SpecialEditWatchlist::EDIT_CLEAR:
$mode = 'clear';
break;
@@ -87,56 +86,51 @@ class SpecialWatchlist extends SpecialPage {
$mode = null;
}
$title = SpecialPage::getTitleFor( 'EditWatchlist', $mode );
- $output->redirect( $title->getLocalUrl() );
+ $output->redirect( $title->getLocalURL() );
return;
}
- $nitems = $this->countItems();
+ $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+
+ $nitems = $this->countItems( $dbr );
if ( $nitems == 0 ) {
$output->addWikiMsg( 'nowatchlist' );
return;
}
- // @TODO: use FormOptions!
+ // @todo use FormOptions!
$defaults = array(
- /* float */ 'days' => floatval( $user->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+ /* float */ 'days' => floatval( $user->getOption( 'watchlistdays' ) ),
/* bool */ 'hideMinor' => (int)$user->getBoolOption( 'watchlisthideminor' ),
- /* bool */ 'hideBots' => (int)$user->getBoolOption( 'watchlisthidebots' ),
+ /* bool */ 'hideBots' => (int)$user->getBoolOption( 'watchlisthidebots' ),
/* bool */ 'hideAnons' => (int)$user->getBoolOption( 'watchlisthideanons' ),
- /* bool */ 'hideLiu' => (int)$user->getBoolOption( 'watchlisthideliu' ),
+ /* bool */ 'hideLiu' => (int)$user->getBoolOption( 'watchlisthideliu' ),
/* bool */ 'hidePatrolled' => (int)$user->getBoolOption( 'watchlisthidepatrolled' ),
- /* bool */ 'hideOwn' => (int)$user->getBoolOption( 'watchlisthideown' ),
- /* ? */ 'namespace' => 'all',
- /* ? */ 'invert' => false,
+ /* bool */ 'hideOwn' => (int)$user->getBoolOption( 'watchlisthideown' ),
+ /* bool */ 'extended' => (int)$user->getBoolOption( 'extendwatchlist' ),
+ /* ? */ 'namespace' => '', //means all
+ /* ? */ 'invert' => false,
/* bool */ 'associated' => false,
);
$this->customFilters = array();
wfRunHooks( 'SpecialWatchlistFilters', array( $this, &$this->customFilters ) );
- foreach( $this->customFilters as $key => $params ) {
- $defaults[$key] = $params['msg'];
+ foreach ( $this->customFilters as $key => $params ) {
+ $defaults[$key] = $params['default'];
}
# Extract variables from the request, falling back to user preferences or
# other default values if these don't exist
- $prefs['days'] = floatval( $user->getOption( 'watchlistdays' ) );
- $prefs['hideminor'] = $user->getBoolOption( 'watchlisthideminor' );
- $prefs['hidebots'] = $user->getBoolOption( 'watchlisthidebots' );
- $prefs['hideanons'] = $user->getBoolOption( 'watchlisthideanons' );
- $prefs['hideliu'] = $user->getBoolOption( 'watchlisthideliu' );
- $prefs['hideown' ] = $user->getBoolOption( 'watchlisthideown' );
- $prefs['hidepatrolled' ] = $user->getBoolOption( 'watchlisthidepatrolled' );
-
- # Get query variables
$values = array();
- $values['days'] = $request->getVal( 'days', $prefs['days'] );
- $values['hideMinor'] = (int)$request->getBool( 'hideMinor', $prefs['hideminor'] );
- $values['hideBots'] = (int)$request->getBool( 'hideBots' , $prefs['hidebots'] );
- $values['hideAnons'] = (int)$request->getBool( 'hideAnons', $prefs['hideanons'] );
- $values['hideLiu'] = (int)$request->getBool( 'hideLiu' , $prefs['hideliu'] );
- $values['hideOwn'] = (int)$request->getBool( 'hideOwn' , $prefs['hideown'] );
- $values['hidePatrolled'] = (int)$request->getBool( 'hidePatrolled', $prefs['hidepatrolled'] );
- foreach( $this->customFilters as $key => $params ) {
- $values[$key] = (int)$request->getBool( $key );
+ $values['days'] = floatval( $request->getVal( 'days', $defaults['days'] ) );
+ $values['hideMinor'] = (int)$request->getBool( 'hideMinor', $defaults['hideMinor'] );
+ $values['hideBots'] = (int)$request->getBool( 'hideBots', $defaults['hideBots'] );
+ $values['hideAnons'] = (int)$request->getBool( 'hideAnons', $defaults['hideAnons'] );
+ $values['hideLiu'] = (int)$request->getBool( 'hideLiu', $defaults['hideLiu'] );
+ $values['hideOwn'] = (int)$request->getBool( 'hideOwn', $defaults['hideOwn'] );
+ $values['hidePatrolled'] = (int)$request->getBool( 'hidePatrolled', $defaults['hidePatrolled'] );
+ $values['extended'] = (int)$request->getBool( 'extended', $defaults['extended'] );
+ foreach ( $this->customFilters as $key => $params ) {
+ $values[$key] = (int)$request->getBool( $key, $defaults[$key] );
}
# Get namespace value, if supplied, and prepare a WHERE fragment
@@ -164,64 +158,41 @@ class SpecialWatchlist extends SpecialPage {
$values['invert'] = $invert;
$values['associated'] = $associated;
- if( is_null( $values['days'] ) || !is_numeric( $values['days'] ) ) {
- $big = 1000; /* The magical big */
- if( $nitems > $big ) {
- # Set default cutoff shorter
- $values['days'] = $defaults['days'] = (12.0 / 24.0); # 12 hours...
- } else {
- $values['days'] = $defaults['days']; # default cutoff for shortlisters
- }
- } else {
- $values['days'] = floatval( $values['days'] );
- }
-
// Dump everything here
$nondefaults = array();
foreach ( $defaults as $name => $defValue ) {
wfAppendToArrayIfNotDefault( $name, $values[$name], $defaults, $nondefaults );
}
- if( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) &&
+ if ( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) &&
$request->wasPosted() )
{
$user->clearAllNotifications();
- $output->redirect( $this->getTitle()->getFullUrl( $nondefaults ) );
+ $output->redirect( $this->getTitle()->getFullURL( $nondefaults ) );
return;
}
- $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
-
# Possible where conditions
$conds = array();
- if( $values['days'] > 0 ) {
- $conds[] = "rc_timestamp > '".$dbr->timestamp( time() - intval( $values['days'] * 86400 ) )."'";
+ if ( $values['days'] > 0 ) {
+ $conds[] = 'rc_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( time() - intval( $values['days'] * 86400 ) ) );
}
- # If the watchlist is relatively short, it's simplest to zip
- # down its entirety and then sort the results.
-
- # If it's relatively long, it may be worth our while to zip
- # through the time-sorted page list checking for watched items.
-
- # Up estimate of watched items by 15% to compensate for talk pages...
-
-
# Toggles
- if( $values['hideOwn'] ) {
+ if ( $values['hideOwn'] ) {
$conds[] = 'rc_user != ' . $user->getId();
}
- if( $values['hideBots'] ) {
+ if ( $values['hideBots'] ) {
$conds[] = 'rc_bot = 0';
}
- if( $values['hideMinor'] ) {
+ if ( $values['hideMinor'] ) {
$conds[] = 'rc_minor = 0';
}
- if( $values['hideLiu'] ) {
+ if ( $values['hideLiu'] ) {
$conds[] = 'rc_user = 0';
}
- if( $values['hideAnons'] ) {
+ if ( $values['hideAnons'] ) {
$conds[] = 'rc_user != 0';
}
if ( $user->useRCPatrol() && $values['hidePatrolled'] ) {
@@ -232,47 +203,75 @@ class SpecialWatchlist extends SpecialPage {
}
# Toggle watchlist content (all recent edits or just the latest)
- if( $user->getOption( 'extendwatchlist' ) ) {
- $limitWatchlist = intval( $user->getOption( 'wllimit' ) );
+ if ( $values['extended'] ) {
+ $limitWatchlist = $user->getIntOption( 'wllimit' );
$usePage = false;
} else {
# Top log Ids for a page are not stored
- $conds[] = 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG;
+ $nonRevisionTypes = array( RC_LOG );
+ wfRunHooks( 'SpecialWatchlistGetNonRevisionTypes', array( &$nonRevisionTypes ) );
+ if ( $nonRevisionTypes ) {
+ if ( count( $nonRevisionTypes ) === 1 ) {
+ // if only one use an equality instead of IN condition
+ $nonRevisionTypes = reset( $nonRevisionTypes );
+ }
+ $conds[] = $dbr->makeList(
+ array(
+ 'rc_this_oldid=page_latest',
+ 'rc_type' => $nonRevisionTypes,
+ ),
+ LIST_OR
+ );
+ }
$limitWatchlist = 0;
$usePage = true;
}
# Show a message about slave lag, if applicable
$lag = wfGetLB()->safeGetLag( $dbr );
- if( $lag > 0 ) {
+ if ( $lag > 0 ) {
$output->showLagWarning( $lag );
}
- # Create output form
- $form = Xml::fieldset( $this->msg( 'watchlist-options' )->text(), false, array( 'id' => 'mw-watchlist-options' ) );
+ # Create output
+ $form = '';
# Show watchlist header
- $form .= $this->msg( 'watchlist-details' )->numParams( $nitems )->parse();
-
- if( $user->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
- $form .= $this->msg( 'wlheader-enotif' )->parseAsBlock() . "\n";
+ $form .= "<p>";
+ $form .= $this->msg( 'watchlist-details' )->numParams( $nitems )->parse() . "\n";
+ if ( $wgEnotifWatchlist && $user->getOption( 'enotifwatchlistpages' ) ) {
+ $form .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
+ }
+ if ( $wgShowUpdatedMarker ) {
+ $form .= $this->msg( 'wlheader-showupdated' )->parse() . "\n";
}
- if( $wgShowUpdatedMarker ) {
+ $form .= "</p>";
+
+ if ( $wgShowUpdatedMarker ) {
$form .= Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->getTitle()->getLocalUrl(),
- 'id' => 'mw-watchlist-resetbutton' ) ) .
- $this->msg( 'wlheader-showupdated' )->parse() . ' ' .
- Xml::submitButton( $this->msg( 'enotif_reset' )->text(), array( 'name' => 'dummy' ) ) .
- Html::hidden( 'reset', 'all' );
- foreach ( $nondefaults as $key => $value ) {
- $form .= Html::hidden( $key, $value );
- }
- $form .= Xml::closeElement( 'form' );
- }
- $form .= '<hr />';
+ 'action' => $this->getTitle()->getLocalURL(),
+ 'id' => 'mw-watchlist-resetbutton' ) ) . "\n" .
+ Xml::submitButton( $this->msg( 'enotif_reset' )->text(), array( 'name' => 'dummy' ) ) . "\n" .
+ Html::hidden( 'reset', 'all' ) . "\n";
+ foreach ( $nondefaults as $key => $value ) {
+ $form .= Html::hidden( $key, $value ) . "\n";
+ }
+ $form .= Xml::closeElement( 'form' ) . "\n";
+ }
+
+ $form .= Xml::openElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $this->getTitle()->getLocalURL(),
+ 'id' => 'mw-watchlist-form'
+ ) );
+ $form .= Xml::fieldset(
+ $this->msg( 'watchlist-options' )->text(),
+ false,
+ array( 'id' => 'mw-watchlist-options' )
+ );
$tables = array( 'recentchanges', 'watchlist' );
- $fields = array( $dbr->tableName( 'recentchanges' ) . '.*' );
+ $fields = RecentChange::selectFields();
$join_conds = array(
'watchlist' => array(
'INNER JOIN',
@@ -284,24 +283,40 @@ class SpecialWatchlist extends SpecialPage {
),
);
$options = array( 'ORDER BY' => 'rc_timestamp DESC' );
- if( $wgShowUpdatedMarker ) {
+ if ( $wgShowUpdatedMarker ) {
$fields[] = 'wl_notificationtimestamp';
}
- if( $limitWatchlist ) {
+ if ( $limitWatchlist ) {
$options['LIMIT'] = $limitWatchlist;
}
- $rollbacker = $user->isAllowed('rollback');
+ $rollbacker = $user->isAllowed( 'rollback' );
if ( $usePage || $rollbacker ) {
$tables[] = 'page';
- $join_conds['page'] = array('LEFT JOIN','rc_cur_id=page_id');
+ $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' );
if ( $rollbacker ) {
$fields[] = 'page_latest';
}
}
+ // Log entries with DELETED_ACTION must not show up unless the user has
+ // the necessary rights.
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
+ $bitmask = LogPage::DELETED_ACTION;
+ } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+ $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
+ } else {
+ $bitmask = 0;
+ }
+ if ( $bitmask ) {
+ $conds[] = $dbr->makeList( array(
+ 'rc_type != ' . RC_LOG,
+ $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
+ ), LIST_OR );
+ }
+
ChangeTags::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $options, '' );
- wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
+ wfRunHooks( 'SpecialWatchlistQuery', array( &$conds, &$tables, &$join_conds, &$fields, $values ) );
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
$numRows = $res->numRows();
@@ -310,21 +325,21 @@ class SpecialWatchlist extends SpecialPage {
$lang = $this->getLanguage();
$wlInfo = '';
- if( $values['days'] > 0 ) {
+ if ( $values['days'] > 0 ) {
$timestamp = wfTimestampNow();
$wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $values['days'] * 24 ) )->params(
- $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user ) )->parse() . '<br />';
+ $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user ) )->parse() . "<br />\n";
}
- $cutofflinks = "\n" . $this->cutoffLinks( $values['days'], $nondefaults ) . "<br />\n";
+ $cutofflinks = $this->cutoffLinks( $values['days'], $nondefaults ) . "<br />\n";
# Spit out some control panel links
$filters = array(
- 'hideMinor' => 'rcshowhideminor',
- 'hideBots' => 'rcshowhidebots',
- 'hideAnons' => 'rcshowhideanons',
- 'hideLiu' => 'rcshowhideliu',
- 'hideOwn' => 'rcshowhidemine',
+ 'hideMinor' => 'rcshowhideminor',
+ 'hideBots' => 'rcshowhidebots',
+ 'hideAnons' => 'rcshowhideanons',
+ 'hideLiu' => 'rcshowhideliu',
+ 'hideOwn' => 'rcshowhidemine',
'hidePatrolled' => 'rcshowhidepatr'
);
foreach ( $this->customFilters as $key => $params ) {
@@ -336,24 +351,28 @@ class SpecialWatchlist extends SpecialPage {
}
$links = array();
- foreach( $filters as $name => $msg ) {
+ foreach ( $filters as $name => $msg ) {
$links[] = $this->showHideLink( $nondefaults, $msg, $name, $values[$name] );
}
+ $hiddenFields = $nondefaults;
+ unset( $hiddenFields['namespace'] );
+ unset( $hiddenFields['invert'] );
+ unset( $hiddenFields['associated'] );
+
# Namespace filter and put the whole form together.
$form .= $wlInfo;
$form .= $cutofflinks;
- $form .= $lang->pipeList( $links );
- $form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl(), 'id' => 'mw-watchlist-form-namespaceselector' ) );
- $form .= '<hr /><p>';
+ $form .= $lang->pipeList( $links ) . "\n";
+ $form .= "<hr />\n<p>";
$form .= Html::namespaceSelector(
array(
'selected' => $nameSpace,
'all' => '',
'label' => $this->msg( 'namespace' )->text()
), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
+ 'name' => 'namespace',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
)
) . '&#160;';
@@ -371,20 +390,19 @@ class SpecialWatchlist extends SpecialPage {
$associated,
array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
) . '&#160;';
- $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . '</p>';
- $form .= Html::hidden( 'days', $values['days'] );
- foreach ( $filters as $key => $msg ) {
- if ( $values[$key] ) {
- $form .= Html::hidden( $key, 1 );
- }
+ $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "</p>\n";
+ foreach ( $hiddenFields as $key => $value ) {
+ $form .= Html::hidden( $key, $value ) . "\n";
}
- $form .= Xml::closeElement( 'form' );
- $form .= Xml::closeElement( 'fieldset' );
+ $form .= Xml::closeElement( 'fieldset' ) . "\n";
+ $form .= Xml::closeElement( 'form' ) . "\n";
$output->addHTML( $form );
# If there's nothing to show, stop here
- if( $numRows == 0 ) {
- $output->addWikiMsg( 'watchnochange' );
+ if ( $numRows == 0 ) {
+ $output->wrapWikiMsg(
+ "<div class='mw-changeslist-empty'>\n$1\n</div>", 'recentchanges-noresult'
+ );
return;
}
@@ -432,7 +450,10 @@ class SpecialWatchlist extends SpecialPage {
$rc->numberofWatchingusers = 0;
}
- $s .= $list->recentChangesLine( $rc, $updated, $counter );
+ $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
+ if ( $changeLine !== false ) {
+ $s .= $changeLine;
+ }
}
$s .= $list->endRecentChangesList();
@@ -441,7 +462,7 @@ class SpecialWatchlist extends SpecialPage {
protected function showHideLink( $options, $message, $name, $value ) {
$label = $this->msg( $value ? 'show' : 'hide' )->escaped();
- $options[$name] = 1 - (int) $value;
+ $options[$name] = 1 - (int)$value;
return $this->msg( $message )->rawParams( Linker::linkKnown( $this->getTitle(), $label, array(), $options ) )->escaped();
}
@@ -472,17 +493,19 @@ class SpecialWatchlist extends SpecialPage {
/**
* Returns html
*
+ * @param int $days This gets overwritten, so is not used
+ * @param array $options Query parameters for URL
* @return string
*/
protected function cutoffLinks( $days, $options = array() ) {
$hours = array( 1, 2, 6, 12 );
$days = array( 1, 3, 7 );
$i = 0;
- foreach( $hours as $h ) {
+ foreach ( $hours as $h ) {
$hours[$i++] = $this->hoursLink( $h, $options );
}
$i = 0;
- foreach( $days as $d ) {
+ foreach ( $days as $d ) {
$days[$i++] = $this->daysLink( $d, $options );
}
return $this->msg( 'wlshowlast' )->rawParams(
@@ -494,11 +517,10 @@ class SpecialWatchlist extends SpecialPage {
/**
* Count the number of items on a user's watchlist
*
+ * @param DatabaseBase $dbr A database connection
* @return Integer
*/
- protected function countItems() {
- $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
-
+ protected function countItems( $dbr ) {
# Fetch the raw count
$res = $dbr->select( 'watchlist', array( 'count' => 'COUNT(*)' ),
array( 'wl_user' => $this->getUser()->getId() ), __METHOD__ );
@@ -507,4 +529,8 @@ class SpecialWatchlist extends SpecialPage {
return floor( $count / 2 );
}
+
+ protected function getGroupName() {
+ return 'changes';
+ }
}
diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php
index f1356493..05c7dd5f 100644
--- a/includes/specials/SpecialWhatlinkshere.php
+++ b/includes/specials/SpecialWhatlinkshere.php
@@ -69,7 +69,7 @@ class SpecialWhatLinksHere extends SpecialPage {
$opts->validateIntBounds( 'limit', 0, 5000 );
// Give precedence to subpage syntax
- if ( isset($par) ) {
+ if ( isset( $par ) ) {
$opts->setValue( 'target', $par );
}
@@ -77,7 +77,7 @@ class SpecialWhatLinksHere extends SpecialPage {
$this->opts = $opts;
$this->target = Title::newFromURL( $opts->getValue( 'target' ) );
- if( !$this->target ) {
+ if ( !$this->target ) {
$out->addHTML( $this->whatlinkshereForm() );
return;
}
@@ -94,11 +94,11 @@ class SpecialWhatLinksHere extends SpecialPage {
}
/**
- * @param $level int Recursion level
- * @param $target Title Target title
- * @param $limit int Number of entries to display
- * @param $from Title Display from this article ID
- * @param $back Title Display from this article ID at backwards scrolling
+ * @param int $level Recursion level
+ * @param Title $target Target title
+ * @param int $limit Number of entries to display
+ * @param int $from Display from this article ID (default: 0)
+ * @param int $back Display from this article ID at backwards scrolling (default: 0)
*/
function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
global $wgMaxRedirectLinksRetrieved;
@@ -111,7 +111,7 @@ class SpecialWhatLinksHere extends SpecialPage {
$hidetrans = $this->opts->getValue( 'hidetrans' );
$hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
- $fetchlinks = (!$hidelinks || !$hideredirs);
+ $fetchlinks = ( !$hidelinks || !$hideredirs );
// Make the query
$plConds = array(
@@ -119,9 +119,9 @@ class SpecialWhatLinksHere extends SpecialPage {
'pl_namespace' => $target->getNamespace(),
'pl_title' => $target->getDBkey(),
);
- if( $hideredirs ) {
+ if ( $hideredirs ) {
$plConds['rd_from'] = null;
- } elseif( $hidelinks ) {
+ } elseif ( $hidelinks ) {
$plConds[] = 'rd_from is NOT NULL';
}
@@ -137,7 +137,7 @@ class SpecialWhatLinksHere extends SpecialPage {
);
$namespace = $this->opts->getValue( 'namespace' );
- if ( is_int($namespace) ) {
+ if ( is_int( $namespace ) ) {
$plConds['page_namespace'] = $namespace;
$tlConds['page_namespace'] = $namespace;
$ilConds['page_namespace'] = $namespace;
@@ -166,36 +166,40 @@ class SpecialWhatLinksHere extends SpecialPage {
'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
)));
- if( $fetchlinks ) {
+ if ( $fetchlinks ) {
$options['ORDER BY'] = 'pl_from';
$plRes = $dbr->select( array( 'pagelinks', 'page', 'redirect' ), $fields,
$plConds, __METHOD__, $options,
- $joinConds);
+ $joinConds
+ );
}
- if( !$hidetrans ) {
+ if ( !$hidetrans ) {
$options['ORDER BY'] = 'tl_from';
$tlRes = $dbr->select( array( 'templatelinks', 'page', 'redirect' ), $fields,
$tlConds, __METHOD__, $options,
- $joinConds);
+ $joinConds
+ );
}
- if( !$hideimages ) {
+ if ( !$hideimages ) {
$options['ORDER BY'] = 'il_from';
$ilRes = $dbr->select( array( 'imagelinks', 'page', 'redirect' ), $fields,
$ilConds, __METHOD__, $options,
- $joinConds);
+ $joinConds
+ );
}
- if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
+ if ( ( !$fetchlinks || !$plRes->numRows() ) && ( $hidetrans || !$tlRes->numRows() ) && ( $hideimages || !$ilRes->numRows() ) ) {
if ( 0 == $level ) {
$out->addHTML( $this->whatlinkshereForm() );
// Show filters only if there are links
- if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
+ if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) {
$out->addHTML( $this->getFilterPanel() );
+ }
- $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
+ $errMsg = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
$out->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
}
return;
@@ -204,21 +208,21 @@ class SpecialWhatLinksHere extends SpecialPage {
// Read the rows into an array and remove duplicates
// templatelinks comes second so that the templatelinks row overwrites the
// pagelinks row, so we get (inclusion) rather than nothing
- if( $fetchlinks ) {
+ if ( $fetchlinks ) {
foreach ( $plRes as $row ) {
$row->is_template = 0;
$row->is_image = 0;
$rows[$row->page_id] = $row;
}
}
- if( !$hidetrans ) {
+ if ( !$hidetrans ) {
foreach ( $tlRes as $row ) {
$row->is_template = 1;
$row->is_image = 0;
$rows[$row->page_id] = $row;
}
}
- if( !$hideimages ) {
+ if ( !$hideimages ) {
foreach ( $ilRes as $row ) {
$row->is_template = 0;
$row->is_image = 1;
@@ -269,7 +273,7 @@ class SpecialWhatLinksHere extends SpecialPage {
$out->addHTML( $this->listEnd() );
- if( $level == 0 ) {
+ if ( $level == 0 ) {
$out->addHTML( $prevnext );
}
}
@@ -292,7 +296,7 @@ class SpecialWhatLinksHere extends SpecialPage {
}
}
- if( $row->rd_from ) {
+ if ( $row->rd_from ) {
$query = array( 'redirect' => 'no' );
} else {
$query = array();
@@ -308,12 +312,15 @@ class SpecialWhatLinksHere extends SpecialPage {
// Display properties (redirect or template)
$propsText = '';
$props = array();
- if ( $row->rd_from )
+ if ( $row->rd_from ) {
$props[] = $msgcache['isredirect'];
- if ( $row->is_template )
+ }
+ if ( $row->is_template ) {
$props[] = $msgcache['istemplate'];
- if( $row->is_image )
+ }
+ if ( $row->is_image ) {
$props[] = $msgcache['isimage'];
+ }
if ( count( $props ) ) {
$propsText = $this->msg( 'parentheses' )->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
@@ -334,8 +341,9 @@ class SpecialWhatLinksHere extends SpecialPage {
protected function wlhLink( Title $target, $text ) {
static $title = null;
- if ( $title === null )
+ if ( $title === null ) {
$title = $this->getTitle();
+ }
return Linker::linkKnown(
$title,
@@ -360,7 +368,7 @@ class SpecialWhatLinksHere extends SpecialPage {
$next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
$changed = $this->opts->getChangedValues();
- unset($changed['target']); // Already in the request title
+ unset( $changed['target'] ); // Already in the request title
if ( 0 != $prevId ) {
$overrides = array( 'from' => $this->opts->getValue( 'back' ) );
@@ -419,8 +427,8 @@ class SpecialWhatLinksHere extends SpecialPage {
'all' => '',
'label' => $this->msg( 'namespace' )->text()
), array(
- 'name' => 'namespace',
- 'id' => 'namespace',
+ 'name' => 'namespace',
+ 'id' => 'namespace',
'class' => 'namespaceselector',
)
);
@@ -446,22 +454,27 @@ class SpecialWhatLinksHere extends SpecialPage {
$hide = $this->msg( 'hide' )->escaped();
$changed = $this->opts->getChangedValues();
- unset($changed['target']); // Already in the request title
+ unset( $changed['target'] ); // Already in the request title
$links = array();
$types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
- if( $this->target->getNamespace() == NS_FILE )
+ 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 found by grep
- foreach( $types as $type ) {
+ foreach ( $types as $type ) {
$chosen = $this->opts->getValue( $type );
$msg = $chosen ? $show : $hide;
$overrides = array( $type => !$chosen );
- $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
+ $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
$this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
}
return Xml::fieldset( $this->msg( 'whatlinkshere-filters' )->text(), $this->getLanguage()->pipeList( $links ) );
}
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
}
diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php
index 2988b04f..9d23499f 100644
--- a/includes/specials/SpecialWithoutinterwiki.php
+++ b/includes/specials/SpecialWithoutinterwiki.php
@@ -44,21 +44,21 @@ class WithoutInterwikiPage extends PageQueryPage {
global $wgScript;
# Do not show useless input form if special page is cached
- if( $this->isCached() ) {
+ if ( $this->isCached() ) {
return '';
}
$prefix = $this->prefix;
$t = $this->getTitle();
- return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, $this->msg( 'withoutinterwiki-legend' )->text() ) .
- Html::hidden( 'title', $t->getPrefixedText() ) .
- Xml::inputLabel( $this->msg( 'allpagesprefix' )->text(), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
- Xml::submitButton( $this->msg( 'withoutinterwiki-submit' )->text() ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' );
+ return Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . "\n" .
+ Html::openElement( 'fieldset' ) . "\n" .
+ Html::element( 'legend', null, $this->msg( 'withoutinterwiki-legend' )->text() ) . "\n" .
+ Html::hidden( 'title', $t->getPrefixedText() ) . "\n" .
+ Xml::inputLabel( $this->msg( 'allpagesprefix' )->text(), 'prefix', 'wiprefix', 20, $prefix ) . "\n" .
+ Xml::submitButton( $this->msg( 'withoutinterwiki-submit' )->text() ) . "\n" .
+ Html::closeElement( 'fieldset' ) . "\n" .
+ Html::closeElement( 'form' );
}
function sortDescending() {
@@ -78,15 +78,15 @@ class WithoutInterwikiPage extends PageQueryPage {
}
function getQueryInfo() {
- $query = array (
- 'tables' => array ( 'page', 'langlinks' ),
- 'fields' => array ( 'namespace' => 'page_namespace',
+ $query = array(
+ 'tables' => array( 'page', 'langlinks' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
'title' => 'page_title',
'value' => 'page_title' ),
- 'conds' => array ( 'll_title IS NULL',
+ 'conds' => array( 'll_title IS NULL',
'page_namespace' => MWNamespace::getContentNamespaces(),
'page_is_redirect' => 0 ),
- 'join_conds' => array ( 'langlinks' => array (
+ 'join_conds' => array( 'langlinks' => array(
'LEFT JOIN', 'll_from = page_id' ) )
);
if ( $this->prefix ) {
@@ -95,4 +95,8 @@ class WithoutInterwikiPage extends PageQueryPage {
}
return $query;
}
+
+ protected function getGroupName() {
+ return 'maintenance';
+ }
}
diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php
index bf5c487a..0006df40 100644
--- a/includes/templates/NoLocalSettings.php
+++ b/includes/templates/NoLocalSettings.php
@@ -21,6 +21,10 @@
* @ingroup Templates
*/
+if ( !defined( 'MEDIAWIKI' ) ) {
+ die( "NoLocalSettings.php is not a valid MediaWiki entry point\n" );
+}
+
if ( !isset( $wgVersion ) ) {
$wgVersion = 'VERSION';
}
@@ -48,12 +52,12 @@ if ( !function_exists( 'session_name' ) ) {
$installerStarted = ( $success && isset( $_SESSION['installData'] ) );
}
?>
-<!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'>
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
<head>
+ <meta charset="UTF-8" />
<title>MediaWiki <?php echo htmlspecialchars( $wgVersion ) ?></title>
- <meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
- <style type='text/css' media='screen'>
+ <style media='screen'>
html, body {
color: #000;
background-color: #fff;
@@ -75,9 +79,9 @@ if ( !function_exists( 'session_name' ) ) {
<p>
<?php
if ( $installerStarted ) {
- echo( "Please <a href=\"" . htmlspecialchars( $path ) . "mw-config/index." . htmlspecialchars( $ext ) . "\"> complete the installation</a> and download LocalSettings.php." );
+ echo "Please <a href=\"" . htmlspecialchars( $path ) . "mw-config/index." . htmlspecialchars( $ext ) . "\"> complete the installation</a> and download LocalSettings.php.";
} else {
- echo( "Please <a href=\"" . htmlspecialchars( $path ) . "mw-config/index." . htmlspecialchars( $ext ) . "\"> set up the wiki</a> first." );
+ echo "Please <a href=\"" . htmlspecialchars( $path ) . "mw-config/index." . htmlspecialchars( $ext ) . "\"> set up the wiki</a> first.";
}
?>
</p>
diff --git a/includes/templates/Usercreate.php b/includes/templates/Usercreate.php
index 98727f17..fcd492cb 100644
--- a/includes/templates/Usercreate.php
+++ b/includes/templates/Usercreate.php
@@ -1,6 +1,6 @@
<?php
/**
- * Html form for account creation.
+ * Html form for account creation (since 1.22 with VForm appearance).
*
* 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
@@ -21,16 +21,12 @@
* @ingroup Templates
*/
-/**
- * @defgroup Templates Templates
- */
+class UsercreateTemplate extends BaseTemplate {
-if( !defined( 'MEDIAWIKI' ) ) die( -1 );
-
-/**
- * @ingroup Templates
- */
-class UsercreateTemplate extends QuickTemplate {
+ /**
+ * Extensions (AntiSpoof and TitleBlacklist) call this in response to
+ * UserCreateForm hook to add checkboxes to the create account form.
+ */
function addInputItem( $name, $value, $type, $msg, $helptext = false ) {
$this->data['extraInput'][] = array(
'name' => $name,
@@ -40,214 +36,255 @@ class UsercreateTemplate extends QuickTemplate {
'helptext' => $helptext,
);
}
-
+
function execute() {
- if( $this->data['message'] ) {
+ global $wgCookieExpiration;
+ $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
?>
- <div class="<?php $this->text('messagetype') ?>box">
- <?php if ( $this->data['messagetype'] == 'error' ) { ?>
- <strong><?php $this->msg( 'loginerror' )?></strong><br />
- <?php } ?>
- <?php $this->html('message') ?>
- </div>
- <div class="visualClear"></div>
-<?php } ?>
+<div class="mw-ui-container">
+ <?php if ( $this->haveData( 'languages' ) ) { ?>
+ <div id="languagelinks">
+ <p><?php $this->html( 'languages' ); ?></p>
+ </div>
+ <?php }
+ if ( !wfMessage( 'signupstart' )->isDisabled() ) { ?>
+ <div id="signupstart"><?php $this->msgWiki( 'signupstart' ); ?></div>
+ <?php } ?>
+ <div id="userloginForm">
+ <h2 class="createaccount-join">
+ <?php $this->msg( $this->data['loggedin'] ? 'createacct-another-join' : 'createacct-join' ); ?>
+ </h2>
+ <form name="userlogin2" id="userlogin2" class="mw-ui-vform" method="post" action="<?php $this->text( 'action' ); ?>">
+ <section class="mw-form-header">
+ <?php $this->html( 'header' ); /* extensions such as ConfirmEdit add form HTML here */ ?>
+ </section>
+ <?php if ( $this->data['message'] ) { ?>
+ <div class="<?php $this->text( 'messagetype' ); ?>box">
+ <?php if ( $this->data['messagetype'] == 'error' ) { ?>
+ <strong><?php $this->msg( 'createacct-error' ); ?></strong>
+ <br />
+ <?php } ?>
+ <?php $this->html( 'message' ); ?>
+ </div>
+ <?php } ?>
-<div id="signupstart"><?php $this->msgWiki( 'signupstart' ); ?></div>
-<div id="userlogin">
+ <div>
+ <label for='wpName2'>
+ <?php $this->msg( 'userlogin-yourname' ); ?>
-<form name="userlogin2" id="userlogin2" method="post" action="<?php $this->text('action') ?>">
- <h2><?php $this->msg('createaccount') ?></h2>
- <p id="userloginlink"><?php $this->html('link') ?></p>
- <?php $this->html('header'); /* pre-table point for form plugins... */ ?>
- <?php if( $this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
- <table>
- <tr>
- <td class="mw-label"><label for='wpName2'><?php $this->msg('yourname') ?></label></td>
- <td class="mw-input">
+ <span class="mw-ui-flush-right"><?php echo $this->getMsg( 'createacct-helpusername' )->parse(); ?></span>
+ </label>
<?php
- echo Html::input( 'wpName', $this->data['name'], 'text', array(
- 'class' => 'loginText',
- 'id' => 'wpName2',
- 'tabindex' => '1',
- 'size' => '20',
- 'required',
- 'autofocus'
- ) ); ?>
- </td>
- </tr>
- <tr>
- <td class="mw-label"><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td>
- <td class="mw-input">
-<?php
- echo Html::input( 'wpPassword', null, 'password', array(
- 'class' => 'loginPassword',
- 'id' => 'wpPassword2',
- 'tabindex' => '2',
- 'size' => '20'
- ) + User::passwordChangeInputAttribs() ); ?>
- </td>
- </tr>
- <?php if( $this->data['usedomain'] ) {
- $doms = "";
- foreach( $this->data['domainnames'] as $dom ) {
- $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
- }
- ?>
- <tr>
- <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td>
- <td class="mw-input">
- <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
- tabindex="3">
- <?php echo $doms ?>
- </select>
- </td>
- </tr>
- <?php } ?>
- <tr>
- <td class="mw-label"><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td>
- <td class="mw-input">
+ echo Html::input( 'wpName', $this->data['name'], 'text', array(
+ 'class' => 'mw-input loginText',
+ 'id' => 'wpName2',
+ 'tabindex' => '1',
+ 'size' => '20',
+ 'required',
+ 'placeholder' => $this->getMsg( $this->data['loggedin'] ?
+ 'createacct-another-username-ph' : 'userlogin-yourname-ph' )->text(),
+ ) );
+ ?>
+ </div>
+
+ <div>
+ <?php if ( $this->data['createemail'] ) { ?>
+ <label class="mw-ui-checkbox-label">
+ <input name="wpCreateaccountMail" type="checkbox" value="1" id="wpCreateaccountMail" tabindex="2"
+ <?php if ( $this->data['createemailset'] ) {
+ echo 'checked="checked"';
+ } ?>
+ >
+ <?php $this->msg( 'createaccountmail' ); ?>
+ </label>
+ <?php } ?>
+ </div>
+
+ <div class="mw-row-password">
+ <label for='wpPassword2'><?php $this->msg( 'userlogin-yourpassword' ); ?></label>
+ <?php
+ echo Html::input( 'wpPassword', null, 'password', array(
+ 'class' => 'mw-input loginPassword',
+ 'id' => 'wpPassword2',
+ 'tabindex' => '3',
+ 'size' => '20',
+ 'required',
+ 'placeholder' => $this->getMsg( 'createacct-yourpassword-ph' )->text()
+ ) + User::passwordChangeInputAttribs() );
+ ?>
+ </div>
+
+ <?php
+ if ( $this->data['usedomain'] ) {
+ $doms = "";
+ foreach ( $this->data['domainnames'] as $dom ) {
+ $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
+ }
+ ?>
+ <div id="mw-user-domain-section">
+ <label for="wpDomain"><?php $this->msg( 'yourdomainname' ); ?></label>
+ <div class="mw-input">
+ <select name="wpDomain" value="<?php $this->text( 'domain' ); ?>" tabindex="4">
+ <?php echo $doms ?>
+ </select>
+ </div>
+ </div>
+ <?php } ?>
+
+ <div class="mw-row-password">
+ <label for='wpRetype'><?php $this->msg( 'createacct-yourpasswordagain' ); ?></label>
<?php
- echo Html::input( 'wpRetype', null, 'password', array(
- 'class' => 'loginPassword',
- 'id' => 'wpRetype',
- 'tabindex' => '4',
- 'size' => '20'
- ) + User::passwordChangeInputAttribs() ); ?>
- </td>
- </tr>
- <tr>
- <?php if( $this->data['useemail'] ) { ?>
- <td class="mw-label"><label for='wpEmail'><?php $this->msg('youremail') ?></label></td>
- <td class="mw-input">
+ echo Html::input( 'wpRetype', null, 'password', array(
+ 'class' => 'mw-input loginPassword',
+ 'id' => 'wpRetype',
+ 'tabindex' => '5',
+ 'size' => '20',
+ 'required',
+ 'placeholder' => $this->getMsg( 'createacct-yourpasswordagain-ph' )->text()
+ ) + User::passwordChangeInputAttribs() );
+ ?>
+ </div>
+
+ <div>
+ <?php if ( $this->data['useemail'] ) { ?>
+ <label for='wpEmail'>
+ <?php
+ $this->msg( $this->data['emailrequired'] ?
+ 'createacct-emailrequired' :
+ 'createacct-emailoptional'
+ );
+ ?>
+ </label>
<?php
- echo Html::input( 'wpEmail', $this->data['email'], 'email', array(
- 'class' => 'loginText',
- 'id' => 'wpEmail',
- 'tabindex' => '5',
- 'size' => '20'
- ) ); ?>
+ echo Html::input( 'wpEmail', $this->data['email'], 'email', array(
+ 'class' => 'mw-input loginText',
+ 'id' => 'wpEmail',
+ 'tabindex' => '6',
+ 'size' => '20',
+ 'required' => $this->data['emailrequired'],
+ 'placeholder' => $this->getMsg( $this->data['loggedin'] ?
+ 'createacct-another-email-ph' : 'createacct-email-ph' )->text()
+ ) );
+ ?>
+ <?php } ?>
+ </div>
+
+ <?php if ( $this->data['userealname'] ) { ?>
+ <div>
+ <label for='wpRealName'><?php $this->msg( 'createacct-realname' ); ?></label>
+ <input type='text' class='mw-input loginText' name="wpRealName" id="wpRealName"
+ tabindex="7"
+ value="<?php $this->text( 'realname' ); ?>" size='20' />
<div class="prefsectiontip">
- <?php // duplicated in Preferences.php profilePreferences()
- if( $this->data['emailrequired'] ) {
- $this->msgWiki('prefs-help-email-required');
- } else {
- $this->msgWiki('prefs-help-email');
- }
- if( $this->data['emailothers'] ) {
- $this->msgWiki('prefs-help-email-others');
- } ?>
+ <?php $this->msgWiki( $this->data['loggedin'] ? 'createacct-another-realname-tip' : 'prefs-help-realname' ); ?>
</div>
- </td>
- <?php } ?>
- <?php if( $this->data['userealname'] ) { ?>
- </tr>
- <tr>
- <td class="mw-label"><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td>
- <td class="mw-input">
- <input type='text' class='loginText' name="wpRealName" id="wpRealName"
- tabindex="6"
- value="<?php $this->text('realname') ?>" size='20' />
- <div class="prefsectiontip">
- <?php $this->msgWiki('prefs-help-realname'); ?>
- </div>
- </td>
+ </div>
<?php } ?>
- <?php if( $this->data['usereason'] ) { ?>
- </tr>
- <tr>
- <td class="mw-label"><label for='wpReason'><?php $this->msg('createaccountreason') ?></label></td>
- <td class="mw-input">
- <input type='text' class='loginText' name="wpReason" id="wpReason"
- tabindex="7"
- value="<?php $this->text('reason') ?>" size='20' />
- </td>
+
+ <?php if ( $this->data['usereason'] ) { ?>
+ <div>
+ <label for='wpReason'><?php $this->msg( 'createacct-reason' ); ?></label>
+ <?php echo Html::input( 'wpReason', $this->data['reason'], 'text', array(
+ 'class' => 'mw-input loginText',
+ 'id' => 'wpReason',
+ 'tabindex' => '8',
+ 'size' => '20',
+ 'placeholder' => $this->getMsg( 'createacct-reason-ph' )->text()
+ ) ); ?>
+ </div>
<?php } ?>
- </tr>
- <?php if( $this->data['canremember'] ) { ?>
- <tr>
- <td></td>
- <td class="mw-input">
- <?php
- global $wgCookieExpiration;
- $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
- echo Xml::checkLabel(
- wfMessage( 'remembermypassword' )->numParams( $expirationDays )->text(),
- 'wpRemember',
- 'wpRemember',
- $this->data['remember'],
- array( 'tabindex' => '8' )
- )
- ?>
- </td>
- </tr>
-<?php }
- $tabIndex = 9;
- if ( isset( $this->data['extraInput'] ) && is_array( $this->data['extraInput'] ) ) {
- foreach ( $this->data['extraInput'] as $inputItem ) { ?>
- <tr>
- <?php
- if ( !empty( $inputItem['msg'] ) && $inputItem['type'] != 'checkbox' ) {
- ?><td class="mw-label"><label for="<?php
- echo htmlspecialchars( $inputItem['name'] ); ?>"><?php
- $this->msgWiki( $inputItem['msg'] ) ?></label><?php
- } else {
- ?><td><?php
- }
- ?></td>
- <td class="mw-input">
- <input type="<?php echo htmlspecialchars( $inputItem['type'] ) ?>" name="<?php
- echo htmlspecialchars( $inputItem['name'] ); ?>"
- tabindex="<?php echo $tabIndex++; ?>"
- value="<?php
- if ( $inputItem['type'] != 'checkbox' ) {
- echo htmlspecialchars( $inputItem['value'] );
- } else {
- echo '1';
- }
- ?>" id="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"
- <?php
- if ( $inputItem['type'] == 'checkbox' && !empty( $inputItem['value'] ) )
- echo 'checked="checked"';
- ?> /> <?php
- if ( $inputItem['type'] == 'checkbox' && !empty( $inputItem['msg'] ) ) {
+ <?php
+ $tabIndex = 9;
+ if ( isset( $this->data['extraInput'] ) && is_array( $this->data['extraInput'] ) ) {
+ foreach ( $this->data['extraInput'] as $inputItem ) { ?>
+ <div>
+ <?php
+ // If it's a checkbox, output the whole thing (assume it has a msg).
+ if ( $inputItem['type'] == 'checkbox' ) {
+ ?>
+ <label class="mw-ui-checkbox-label">
+ <input
+ name="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"
+ id="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"
+ type="checkbox" value="1"
+ tabindex="<?php echo $tabIndex++; ?>"
+ <?php if ( !empty( $inputItem['value'] ) ) {
+ echo 'checked="checked"';
+ } ?>
+ >
+ <?php $this->msg( $inputItem['msg'] ); ?>
+ </label>
+ <?php
+ } else {
+ // Not a checkbox.
+ // TODO (bug 31909) support other input types, e.g. select boxes.
?>
- <label for="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"><?php
- $this->msgHtml( $inputItem['msg'] ) ?></label><?php
- }
- if( $inputItem['helptext'] !== false ) {
+ <?php if ( !empty( $inputItem['msg'] ) ) { ?>
+ <label for="<?php echo htmlspecialchars( $inputItem['name'] ); ?>">
+ <?php $this->msgWiki( $inputItem['msg'] ); ?>
+ </label>
+ <?php } ?>
+ <input
+ type="<?php echo htmlspecialchars( $inputItem['type'] ); ?>"
+ class="mw-input"
+ name="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"
+ tabindex="<?php echo $tabIndex++; ?>"
+ value="<?php echo htmlspecialchars( $inputItem['value'] ); ?>"
+ id="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"
+ />
+ <?php } ?>
+ <?php if ( $inputItem['helptext'] !== false ) { ?>
+ <div class="prefsectiontip">
+ <?php $this->msgWiki( $inputItem['helptext'] ); ?>
+ </div>
+ <?php } ?>
+ </div>
+ <?php
+ }
+ }
+
+ // JS attempts to move the image CAPTCHA below this part of the form,
+ // so skip one index.
+ $tabIndex++;
+ ?>
+ <div class="mw-submit">
+ <?php
+ echo Html::input(
+ 'wpCreateaccount',
+ $this->getMsg( $this->data['loggedin'] ? 'createacct-another-submit' : 'createacct-submit' ),
+ 'submit',
+ array(
+ 'class' => "mw-ui-button mw-ui-big mw-ui-block mw-ui-primary",
+ 'id' => 'wpCreateaccount',
+ 'tabindex' => $tabIndex++
+ )
+ );
?>
- <div class="prefsectiontip">
- <?php $this->msgWiki( $inputItem['helptext'] ); ?>
+ </div>
+ <?php if ( $this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
+ <?php if ( $this->haveData( 'token' ) ) { ?><input type="hidden" name="wpCreateaccountToken" value="<?php $this->text( 'token' ); ?>" /><?php } ?>
+ </form>
+ <?php if ( !wfMessage( 'signupend' )->isDisabled() ) { ?>
+ <div id="signupend"><?php $this->html( 'signupend' ); ?></div>
+ <?php } ?>
+ </div>
+ <div class="mw-createacct-benefits-container">
+ <h2><?php $this->msg( 'createacct-benefit-heading' ); ?></h2>
+ <div class="mw-createacct-benefits-list">
+ <?php
+ for ( $benefitIdx = 1; $benefitIdx <= $this->data['benefitCount']; $benefitIdx++ ) {
+ // Pass each benefit's head text (by default a number) as a parameter to the body's message for PLURAL handling.
+ $headUnescaped = $this->getMsg( "createacct-benefit-head$benefitIdx" )->text();
+ ?>
+ <div class="mw-number-text <?php $this->msg( "createacct-benefit-icon$benefitIdx" ); ?>">
+ <h3><?php $this->msg( "createacct-benefit-head$benefitIdx" ); ?></h3>
+ <p><?php echo $this->getMsg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped(); ?></p>
</div>
- <?php } ?>
- </td>
- </tr>
-<?php
-
- }
- }
-?>
- <tr>
- <td></td>
- <td class="mw-submit">
- <input type='submit' name="wpCreateaccount" id="wpCreateaccount"
- tabindex="<?php echo $tabIndex++; ?>"
- value="<?php $this->msg('createaccount') ?>" />
- <?php if( $this->data['createemail'] ) { ?>
- <input type='submit' name="wpCreateaccountMail" id="wpCreateaccountMail"
- tabindex="<?php echo $tabIndex++; ?>"
- value="<?php $this->msg('createaccountmail') ?>" />
- <?php } ?>
- </td>
- </tr>
- </table>
-<?php if( $this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
-<?php if( $this->haveData( 'token' ) ) { ?><input type="hidden" name="wpCreateaccountToken" value="<?php $this->text( 'token' ); ?>" /><?php } ?>
-</form>
+ <?php } ?>
+ </div>
+ </div>
</div>
-<div id="signupend"><?php $this->html( 'signupend' ); ?></div>
<?php
}
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index a3f6a38b..5eb60948 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -1,6 +1,6 @@
<?php
/**
- * Html form for user login.
+ * Html form for user login (since 1.22 with VForm appearance).
*
* 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
@@ -21,158 +21,160 @@
* @ingroup Templates
*/
-/**
- * @defgroup Templates Templates
- */
-
-if( !defined( 'MEDIAWIKI' ) ) die( -1 );
+class UserloginTemplate extends BaseTemplate {
-/**
- * HTML template for Special:Userlogin form
- * @ingroup Templates
- */
-class UserloginTemplate extends QuickTemplate {
function execute() {
- if( $this->data['message'] ) {
+ global $wgCookieExpiration;
+ $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
?>
- <div class="<?php $this->text('messagetype') ?>box">
- <?php if ( $this->data['messagetype'] == 'error' ) { ?>
- <strong><?php $this->msg( 'loginerror' )?></strong><br />
- <?php } ?>
- <?php $this->html('message') ?>
- </div>
- <div class="visualClear"></div>
-<?php } ?>
+<div class="mw-ui-container">
+ <?php if ( $this->haveData( 'languages' ) ) { ?>
+ <div id="languagelinks">
+ <p><?php $this->html( 'languages' ); ?></p>
+ </div>
+ <?php } ?>
+ <div id="userloginForm">
+ <form name="userlogin" class="mw-ui-vform" method="post" action="<?php $this->text( 'action' ); ?>">
+ <?php if ( $this->data['loggedin'] ) { ?>
+ <div class="warningbox">
+ <?php echo $this->getMsg( 'userlogin-loggedin' )->params( $this->data['loggedinuser'] )->parse(); ?>
+ </div>
+ <?php } ?>
+ <section class="mw-form-header">
+ <?php $this->html( 'header' ); /* extensions such as ConfirmEdit add form HTML here */ ?>
+ </section>
-<div id="loginstart"><?php $this->msgWiki( 'loginstart' ); ?></div>
-<div id="userloginForm">
-<form name="userlogin" method="post" action="<?php $this->text('action') ?>">
- <h2><?php $this->msg('login') ?></h2>
- <p id="userloginlink"><?php $this->html('link') ?></p>
- <?php $this->html('header'); /* pre-table point for form plugins... */ ?>
- <div id="userloginprompt"><?php $this->msgWiki('loginprompt') ?></div>
- <?php if( $this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
- <table>
- <tr>
- <td class="mw-label"><label for='wpName1'><?php $this->msg('yourname') ?></label></td>
- <td class="mw-input">
- <?php
- echo Html::input( 'wpName', $this->data['name'], 'text', array(
- 'class' => 'loginText',
- 'id' => 'wpName1',
- 'tabindex' => '1',
- 'size' => '20',
- 'required'
- # Can't do + array( 'autofocus' ) because + for arrays in PHP
- # only works right for associative arrays! Thanks, PHP.
- ) + ( $this->data['name'] ? array() : array( 'autofocus' => '' ) ) ); ?>
+ <?php if ( $this->data['message'] ) { ?>
+ <div class="<?php $this->text( 'messagetype' ); ?>box">
+ <?php if ( $this->data['messagetype'] == 'error' ) { ?>
+ <strong><?php $this->msg( 'loginerror' ); ?></strong>
+ <br />
+ <?php } ?>
+ <?php $this->html( 'message' ); ?>
+ </div>
+ <?php } ?>
+
+ <div>
+ <label for='wpName1'>
+ <?php
+ $this->msg( 'userlogin-yourname' );
- </td>
- </tr>
- <tr>
- <td class="mw-label"><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td>
- <td class="mw-input">
+ if ( $this->data['secureLoginUrl'] ) {
+ echo Html::element( 'a', array(
+ 'href' => $this->data['secureLoginUrl'],
+ 'class' => 'mw-ui-flush-right mw-secure',
+ ), $this->getMsg( 'userlogin-signwithsecure' )->text() );
+ }
+ ?>
+ </label>
<?php
- echo Html::input( 'wpPassword', null, 'password', array(
- 'class' => 'loginPassword',
- 'id' => 'wpPassword1',
- 'tabindex' => '2',
- 'size' => '20'
- ) + ( $this->data['name'] ? array( 'autofocus' ) : array() ) ); ?>
+ $extraAttrs = array();
+ echo Html::input( 'wpName', $this->data['name'], 'text', array(
+ 'class' => 'loginText',
+ 'id' => 'wpName1',
+ 'tabindex' => '1',
+ 'size' => '20',
+ // 'required' is blacklisted for now in Html.php due to browser issues.
+ // Keeping here in case that changes.
+ 'required' => true,
+ // Set focus to this field if it's blank.
+ 'autofocus' => !$this->data['name'],
+ 'placeholder' => $this->getMsg( 'userlogin-yourname-ph' )->text()
+ ) );
+ ?>
+ </div>
- </td>
- </tr>
- <?php if( isset( $this->data['usedomain'] ) && $this->data['usedomain'] ) {
- $doms = "";
- foreach( $this->data['domainnames'] as $dom ) {
- $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
- }
- ?>
- <tr id="mw-user-domain-section">
- <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td>
- <td class="mw-input">
- <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
- tabindex="3">
- <?php echo $doms ?>
- </select>
- </td>
- </tr>
- <?php }
+ <div>
+ <label for='wpPassword1'>
+ <?php
+ $this->msg( 'userlogin-yourpassword' );
- if( $this->haveData( 'extrafields' ) ) {
- echo $this->data['extrafields'];
- }
-
- if( $this->data['canremember'] ) { ?>
- <tr>
- <td></td>
- <td class="mw-input">
+ if ( $this->data['useemail'] && $this->data['canreset'] && $this->data['resetlink'] === true ) {
+ echo ' ' . Linker::link(
+ SpecialPage::getTitleFor( 'PasswordReset' ),
+ $this->getMsg( 'userlogin-resetpassword-link' )->parse(),
+ array( 'class' => 'mw-ui-flush-right' )
+ );
+ }
+ ?>
+ </label>
<?php
- global $wgCookieExpiration;
- $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
- echo Xml::checkLabel(
- wfMessage( 'remembermypassword' )->numParams( $expirationDays )->text(),
- 'wpRemember',
- 'wpRemember',
- $this->data['remember'],
- array( 'tabindex' => '8' )
- )
+ echo Html::input( 'wpPassword', null, 'password', array(
+ 'class' => 'loginPassword',
+ 'id' => 'wpPassword1',
+ 'tabindex' => '2',
+ 'size' => '20',
+ // Set focus to this field if username is filled in.
+ 'autofocus' => (bool)$this->data['name'],
+ 'placeholder' => $this->getMsg( 'userlogin-yourpassword-ph' )->text()
+ ) );
?>
- </td>
- </tr>
-<?php } ?>
-<?php if( $this->data['cansecurelogin'] ) { ?>
- <tr>
- <td></td>
- <td class="mw-input">
+ </div>
+
<?php
- echo Xml::checkLabel(
- wfMessage( 'securelogin-stick-https' )->text(),
- 'wpStickHTTPS',
- 'wpStickHTTPS',
- $this->data['stickHTTPS'],
- array( 'tabindex' => '9' )
- );
- ?>
- </td>
- </tr>
-<?php } ?>
- <tr>
- <td></td>
- <td class="mw-submit">
- <?php
- echo Html::input( 'wpLoginAttempt', wfMessage( 'login' )->text(), 'submit', array(
- 'id' => 'wpLoginAttempt',
- 'tabindex' => '9'
- ) );
- if ( $this->data['useemail'] && $this->data['canreset'] ) {
- if( $this->data['resetlink'] === true ){
- echo '&#160;';
- echo Linker::link(
- SpecialPage::getTitleFor( 'PasswordReset' ),
- wfMessage( 'userlogin-resetlink' )
- );
- } elseif( $this->data['resetlink'] === null ) {
- echo '&#160;';
- echo Html::input(
- 'wpMailmypassword',
- wfMessage( 'mailmypassword' )->text(),
- 'submit', array(
- 'id' => 'wpMailmypassword',
- 'tabindex' => '10'
- )
- );
+ if ( isset( $this->data['usedomain'] ) && $this->data['usedomain'] ) {
+ $doms = "";
+ foreach ( $this->data['domainnames'] as $dom ) {
+ $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
+ }
+ ?>
+ <div id="mw-user-domain-section">
+ <label for='wpDomain'><?php $this->msg( 'yourdomainname' ); ?></label>
+ <select name="wpDomain" value="<?php $this->text( 'domain' ); ?>" tabindex="3">
+ <?php echo $doms; ?>
+ </select>
+ </div>
+ <?php } ?>
+
+ <?php
+ if ( $this->haveData( 'extrafields' ) ) {
+ echo $this->data['extrafields'];
}
- } ?>
+ ?>
- </td>
- </tr>
- </table>
-<?php if( $this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
-<?php if( $this->haveData( 'token' ) ) { ?><input type="hidden" name="wpLoginToken" value="<?php $this->text( 'token' ); ?>" /><?php } ?>
-</form>
+ <div>
+ <?php if ( $this->data['canremember'] ) { ?>
+ <label class="mw-ui-checkbox-label">
+ <input name="wpRemember" type="checkbox" value="1" id="wpRemember" tabindex="4"
+ <?php if ( $this->data['remember'] ) {
+ echo 'checked="checked"';
+ } ?>
+ >
+ <?php echo $this->getMsg( 'userlogin-remembermypassword' )->numParams( $expirationDays )->escaped(); ?>
+ </label>
+ <?php } ?>
+ </div>
+
+ <div>
+ <?php
+ echo Html::input( 'wpLoginAttempt', $this->getMsg( 'login' )->text(), 'submit', array(
+ 'id' => 'wpLoginAttempt',
+ 'tabindex' => '6',
+ 'class' => 'mw-ui-button mw-ui-big mw-ui-block mw-ui-primary'
+ ) );
+ ?>
+ </div>
+
+ <div id="mw-userlogin-help">
+ <?php echo $this->getMsg( 'userlogin-helplink' )->parse(); ?>
+ </div>
+ <?php if ( $this->haveData( 'createOrLoginHref' ) ) { ?>
+ <?php if ( $this->data['loggedin'] ) { ?>
+ <div id="mw-createaccount-another">
+ <h3 id="mw-userloginlink"><a href="<?php $this->text( 'createOrLoginHref' ); ?>" id="mw-createaccount-join" tabindex="7" class="mw-ui-button"><?php $this->msg( 'userlogin-createanother' ); ?></a></h3>
+ </div>
+ <?php } else { ?>
+ <div id="mw-createaccount-cta">
+ <h3 id="mw-userloginlink"><?php $this->msg( 'userlogin-noaccount' ); ?><a href="<?php $this->text( 'createOrLoginHref' ); ?>" id="mw-createaccount-join" tabindex="7" class="mw-ui-button mw-ui-constructive"><?php $this->msg( 'userlogin-joinproject' ); ?></a></h3>
+ </div>
+ <?php } ?>
+ <?php } ?>
+ <?php if ( $this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
+ <?php if ( $this->haveData( 'token' ) ) { ?><input type="hidden" name="wpLoginToken" value="<?php $this->text( 'token' ); ?>" /><?php } ?>
+ <?php if ( $this->data['cansecurelogin'] ) {?><input type="hidden" name="wpForceHttps" value="<?php $this->text( 'stickhttps' ); ?>" /><?php } ?>
+ </form>
+ </div>
</div>
-<div id="loginend"><?php $this->html( 'loginend' ); ?></div>
<?php
}
diff --git a/includes/tidy.conf b/includes/tidy.conf
index aa333fcb..4c4daed5 100644
--- a/includes/tidy.conf
+++ b/includes/tidy.conf
@@ -16,4 +16,7 @@ quiet: yes
quote-nbsp: yes
fix-backslash: no
fix-uri: no
-new-inline-tags: video,audio,source,track,bdi
+# Don't strip html5 elements we support
+# html-{meta,link} is a hack we use to prevent Tidy from stripping <meta> and <link> used in the body for Microdata
+new-empty-tags: html-meta, html-link, wbr
+new-inline-tags: video, audio, source, track, bdi, data, time, mark
diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php
index 0848780f..916ad6c1 100644
--- a/includes/upload/UploadBase.php
+++ b/includes/upload/UploadBase.php
@@ -65,24 +65,27 @@ abstract class UploadBase {
const WINDOWS_NONASCII_FILENAME = 13;
const FILENAME_TOO_LONG = 14;
+ const SESSION_STATUS_KEY = 'wsUploadStatusData';
+
/**
* @param $error int
* @return string
*/
public function getVerificationErrorCode( $error ) {
- $code_to_status = array(self::EMPTY_FILE => 'empty-file',
- self::FILE_TOO_LARGE => 'file-too-large',
- self::FILETYPE_MISSING => 'filetype-missing',
- self::FILETYPE_BADTYPE => 'filetype-banned',
- self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
- self::ILLEGAL_FILENAME => 'illegal-filename',
- self::OVERWRITE_EXISTING_FILE => 'overwrite',
- self::VERIFICATION_ERROR => 'verification-error',
- self::HOOK_ABORTED => 'hookaborted',
- self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
- self::FILENAME_TOO_LONG => 'filename-toolong',
+ $code_to_status = array(
+ self::EMPTY_FILE => 'empty-file',
+ self::FILE_TOO_LARGE => 'file-too-large',
+ self::FILETYPE_MISSING => 'filetype-missing',
+ self::FILETYPE_BADTYPE => 'filetype-banned',
+ self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
+ self::ILLEGAL_FILENAME => 'illegal-filename',
+ self::OVERWRITE_EXISTING_FILE => 'overwrite',
+ self::VERIFICATION_ERROR => 'verification-error',
+ self::HOOK_ABORTED => 'hookaborted',
+ self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
+ self::FILENAME_TOO_LONG => 'filename-toolong',
);
- if( isset( $code_to_status[$error] ) ) {
+ if ( isset( $code_to_status[$error] ) ) {
return $code_to_status[$error];
}
@@ -108,7 +111,7 @@ abstract class UploadBase {
/**
* Returns true if the user can use this upload module or else a string
* identifying the missing permission.
- * Can be overriden by subclasses.
+ * Can be overridden by subclasses.
*
* @param $user User
* @return bool
@@ -135,7 +138,7 @@ abstract class UploadBase {
public static function createFromRequest( &$request, $type = null ) {
$type = $type ? $type : $request->getVal( 'wpSourceType', 'File' );
- if( !$type ) {
+ if ( !$type ) {
return null;
}
@@ -148,18 +151,18 @@ abstract class UploadBase {
if ( is_null( $className ) ) {
$className = 'UploadFrom' . $type;
wfDebug( __METHOD__ . ": class name: $className\n" );
- if( !in_array( $type, self::$uploadHandlers ) ) {
+ if ( !in_array( $type, self::$uploadHandlers ) ) {
return null;
}
}
// Check whether this upload class is enabled
- if( !call_user_func( array( $className, 'isEnabled' ) ) ) {
+ if ( !call_user_func( array( $className, 'isEnabled' ) ) ) {
return null;
}
// Check whether the request is valid
- if( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) {
+ if ( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) {
return null;
}
@@ -186,14 +189,16 @@ abstract class UploadBase {
* @since 1.18
* @return string
*/
- public function getSourceType() { return null; }
+ public function getSourceType() {
+ return null;
+ }
/**
* Initialize the path information
- * @param $name string the desired destination name
- * @param $tempPath string the temporary path
- * @param $fileSize int the file size
- * @param $removeTempFile bool (false) remove the temporary file?
+ * @param string $name the desired destination name
+ * @param string $tempPath the temporary path
+ * @param int $fileSize the file size
+ * @param bool $removeTempFile (false) remove the temporary file?
* @throws MWException
*/
public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
@@ -209,7 +214,7 @@ abstract class UploadBase {
/**
* Initialize from a WebRequest. Override this in a subclass.
*/
- public abstract function initializeFromRequest( &$request );
+ abstract public function initializeFromRequest( &$request );
/**
* Fetch the file. Usually a no-op
@@ -236,22 +241,33 @@ abstract class UploadBase {
}
/**
- * @param $srcPath String: the source path
- * @return string the real path if it was a virtual URL
+ * Get the base 36 SHA1 of the file
+ * @return string
+ */
+ public function getTempFileSha1Base36() {
+ return FSFile::getSha1Base36FromPath( $this->mTempPath );
+ }
+
+ /**
+ * @param string $srcPath the source path
+ * @return string|bool the real path if it was a virtual URL Returns false on failure
*/
function getRealPath( $srcPath ) {
wfProfileIn( __METHOD__ );
$repo = RepoGroup::singleton()->getLocalRepo();
if ( $repo->isVirtualUrl( $srcPath ) ) {
- // @TODO: just make uploads work with storage paths
- // UploadFromStash loads files via virtuals URLs
+ // @todo just make uploads work with storage paths
+ // UploadFromStash loads files via virtual URLs
$tmpFile = $repo->getLocalCopy( $srcPath );
- $tmpFile->bind( $this ); // keep alive with $thumb
- wfProfileOut( __METHOD__ );
- return $tmpFile->getPath();
+ if ( $tmpFile ) {
+ $tmpFile->bind( $this ); // keep alive with $this
+ }
+ $path = $tmpFile ? $tmpFile->getPath() : false;
+ } else {
+ $path = $srcPath;
}
wfProfileOut( __METHOD__ );
- return $srcPath;
+ return $path;
}
/**
@@ -264,7 +280,7 @@ abstract class UploadBase {
/**
* If there was no filename or a zero size given, give up quick.
*/
- if( $this->isEmptyFile() ) {
+ if ( $this->isEmptyFile() ) {
wfProfileOut( __METHOD__ );
return array( 'status' => self::EMPTY_FILE );
}
@@ -273,7 +289,7 @@ abstract class UploadBase {
* Honor $wgMaxUploadSize
*/
$maxSize = self::getMaxUploadSize( $this->getSourceType() );
- if( $this->mFileSize > $maxSize ) {
+ if ( $this->mFileSize > $maxSize ) {
wfProfileOut( __METHOD__ );
return array(
'status' => self::FILE_TOO_LARGE,
@@ -287,7 +303,7 @@ abstract class UploadBase {
* probably not accept it.
*/
$verification = $this->verifyFile();
- if( $verification !== true ) {
+ if ( $verification !== true ) {
wfProfileOut( __METHOD__ );
return array(
'status' => self::VERIFICATION_ERROR,
@@ -299,13 +315,13 @@ abstract class UploadBase {
* Make sure this file can be created
*/
$result = $this->validateName();
- if( $result !== true ) {
+ if ( $result !== true ) {
wfProfileOut( __METHOD__ );
return $result;
}
$error = '';
- if( !wfRunHooks( 'UploadVerification',
+ if ( !wfRunHooks( 'UploadVerification',
array( $this->mDestName, $this->mTempPath, &$error ) ) )
{
wfProfileOut( __METHOD__ );
@@ -322,11 +338,11 @@ abstract class UploadBase {
* @return mixed true if valid, otherwise and array with 'status'
* and other keys
**/
- protected function validateName() {
+ public function validateName() {
$nt = $this->getTitle();
- if( is_null( $nt ) ) {
+ if ( is_null( $nt ) ) {
$result = array( 'status' => $this->mTitleError );
- if( $this->mTitleError == self::ILLEGAL_FILENAME ) {
+ if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
$result['filtered'] = $this->mFilteredName;
}
if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
@@ -343,18 +359,18 @@ abstract class UploadBase {
}
/**
- * Verify the mime type
+ * Verify the mime type.
*
* @note Only checks that it is not an evil mime. The does it have
* correct extension given its mime type check is in verifyFile.
- * @param $mime string representing the mime
+ * @param string $mime representing the mime
* @return mixed true if the file is verified, an array otherwise
*/
protected function verifyMimeType( $mime ) {
global $wgVerifyMimeType;
wfProfileIn( __METHOD__ );
if ( $wgVerifyMimeType ) {
- wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n");
+ wfDebug( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n" );
global $wgMimeTypeBlacklist;
if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
wfProfileOut( __METHOD__ );
@@ -381,6 +397,7 @@ abstract class UploadBase {
return true;
}
+
/**
* Verifies that it's ok to include the uploaded file
*
@@ -396,10 +413,10 @@ abstract class UploadBase {
return $status;
}
- if ( $wgVerifyMimeType ) {
- $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
- $mime = $this->mFileProps['file-mime'];
+ $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
+ $mime = $this->mFileProps['file-mime'];
+ if ( $wgVerifyMimeType ) {
# XXX: Missing extension will be caught by validateName() via getTitle()
if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) {
wfProfileOut( __METHOD__ );
@@ -407,6 +424,7 @@ abstract class UploadBase {
}
}
+
$handler = MediaHandler::getHandler( $mime );
if ( $handler ) {
$handlerStatus = $handler->verifyUpload( $this->mTempPath );
@@ -440,14 +458,13 @@ abstract class UploadBase {
global $wgAllowJavaUploads, $wgDisableUploadScriptChecks;
wfProfileIn( __METHOD__ );
- # get the title, even though we are doing nothing with it, because
- # we need to populate mFinalExtension
+ # getTitle() sets some internal parameters like $this->mFinalExtension
$this->getTitle();
$this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
# check mime type, if desired
- $mime = $this->mFileProps[ 'file-mime' ];
+ $mime = $this->mFileProps['file-mime'];
$status = $this->verifyMimeType( $mime );
if ( $status !== true ) {
wfProfileOut( __METHOD__ );
@@ -456,14 +473,15 @@ abstract class UploadBase {
# check for htmlish code and javascript
if ( !$wgDisableUploadScriptChecks ) {
- if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
+ if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
wfProfileOut( __METHOD__ );
return array( 'uploadscripted' );
}
- if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
- if( $this->detectScriptInSvg( $this->mTempPath ) ) {
+ if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
+ $svgStatus = $this->detectScriptInSvg( $this->mTempPath );
+ if ( $svgStatus !== false ) {
wfProfileOut( __METHOD__ );
- return array( 'uploadscripted' );
+ return $svgStatus;
}
}
}
@@ -550,7 +568,7 @@ abstract class UploadBase {
* to modify it by uploading a new revision.
*/
$nt = $this->getTitle();
- if( is_null( $nt ) ) {
+ if ( is_null( $nt ) ) {
return true;
}
$permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
@@ -560,7 +578,7 @@ abstract class UploadBase {
} else {
$permErrorsCreate = array();
}
- if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
+ if ( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
$permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
$permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
return $permErrors;
@@ -575,7 +593,9 @@ abstract class UploadBase {
}
/**
- * Check for non fatal problems with the file
+ * Check for non fatal problems with the file.
+ *
+ * This should not assume that mTempPath is set.
*
* @return Array of warnings
*/
@@ -595,22 +615,23 @@ abstract class UploadBase {
$comparableName = str_replace( ' ', '_', $this->mDesiredDestName );
$comparableName = Title::capitalize( $comparableName, NS_FILE );
- if( $this->mDesiredDestName != $filename && $comparableName != $filename ) {
+ if ( $this->mDesiredDestName != $filename && $comparableName != $filename ) {
$warnings['badfilename'] = $filename;
}
// Check whether the file extension is on the unwanted list
global $wgCheckFileExtensions, $wgFileExtensions;
if ( $wgCheckFileExtensions ) {
- if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) {
+ $extensions = array_unique( $wgFileExtensions );
+ if ( !$this->checkFileExtension( $this->mFinalExtension, $extensions ) ) {
$warnings['filetype-unwanted-type'] = array( $this->mFinalExtension,
- $wgLang->commaList( $wgFileExtensions ), count( $wgFileExtensions ) );
+ $wgLang->commaList( $extensions ), count( $extensions ) );
}
}
global $wgUploadSizeWarning;
if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
- $warnings['large-file'] = $wgUploadSizeWarning;
+ $warnings['large-file'] = array( $wgUploadSizeWarning, $this->mFileSize );
}
if ( $this->mFileSize == 0 ) {
@@ -618,21 +639,21 @@ abstract class UploadBase {
}
$exists = self::getExistsWarning( $localFile );
- if( $exists !== false ) {
+ if ( $exists !== false ) {
$warnings['exists'] = $exists;
}
// Check dupes against existing files
- $hash = FSFile::getSha1Base36FromPath( $this->mTempPath );
+ $hash = $this->getTempFileSha1Base36();
$dupes = RepoGroup::singleton()->findBySha1( $hash );
$title = $this->getTitle();
// Remove all matches against self
foreach ( $dupes as $key => $dupe ) {
- if( $title->equals( $dupe->getTitle() ) ) {
+ if ( $title->equals( $dupe->getTitle() ) ) {
unset( $dupes[$key] );
}
}
- if( $dupes ) {
+ if ( $dupes ) {
$warnings['duplicate'] = $dupes;
}
@@ -670,9 +691,9 @@ abstract class UploadBase {
$user
);
- if( $status->isGood() ) {
+ if ( $status->isGood() ) {
if ( $watch ) {
- $user->addWatch( $this->getLocalFile()->getTitle() );
+ WatchAction::doWatch( $this->getLocalFile()->getTitle(), $user, WatchedItem::IGNORE_USER_RIGHTS );
}
wfRunHooks( 'UploadComplete', array( &$this ) );
}
@@ -691,7 +712,6 @@ abstract class UploadBase {
if ( $this->mTitle !== false ) {
return $this->mTitle;
}
-
/* Assume that if a user specified File:Something.jpg, this is an error
* and that the namespace prefix needs to be stripped of.
*/
@@ -717,21 +737,19 @@ abstract class UploadBase {
$this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
/* Normalize to title form before we do any further processing */
$nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
- if( is_null( $nt ) ) {
+ if ( is_null( $nt ) ) {
$this->mTitleError = self::ILLEGAL_FILENAME;
return $this->mTitle = null;
}
$this->mFilteredName = $nt->getDBkey();
-
-
/**
* We'll want to blacklist against *any* 'extension', and use
* only the final one for the whitelist.
*/
list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
- if( count( $ext ) ) {
+ if ( count( $ext ) ) {
$this->mFinalExtension = trim( $ext[count( $ext ) - 1] );
} else {
$this->mFinalExtension = '';
@@ -752,7 +770,6 @@ abstract class UploadBase {
$ext = array( $this->mFinalExtension );
}
}
-
}
/* Don't allow users to override the blacklist (check file extension) */
@@ -780,14 +797,14 @@ abstract class UploadBase {
# 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++ ) {
+ if ( count( $ext ) > 1 ) {
+ for ( $i = 0; $i < count( $ext ) - 1; $i++ ) {
$partname .= '.' . $ext[$i];
}
}
- if( strlen( $partname ) < 1 ) {
- $this->mTitleError = self::MIN_LENGTH_PARTNAME;
+ if ( strlen( $partname ) < 1 ) {
+ $this->mTitleError = self::MIN_LENGTH_PARTNAME;
return $this->mTitle = null;
}
@@ -800,7 +817,7 @@ abstract class UploadBase {
* @return LocalFile|null
*/
public function getLocalFile() {
- if( is_null( $this->mLocalFile ) ) {
+ if ( is_null( $this->mLocalFile ) ) {
$nt = $this->getTitle();
$this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
}
@@ -816,13 +833,14 @@ abstract class UploadBase {
* This method returns the file object, which also has a 'fileKey' property which can be passed through a form or
* API request to find this stashed file again.
*
+ * @param $user User
* @return UploadStashFile stashed file
*/
- public function stashFile() {
+ public function stashFile( User $user = null ) {
// was stashSessionFile
wfProfileIn( __METHOD__ );
- $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
+ $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
$file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
$this->mLocalFile = $file;
@@ -905,30 +923,36 @@ abstract class UploadBase {
/**
* Checks if the mime type of the uploaded file matches the file extension.
*
- * @param $mime String: the mime type of the uploaded file
- * @param $extension String: the filename extension that the file is to be served with
+ * @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 Boolean
*/
public static function verifyExtension( $mime, $extension ) {
$magic = MimeMagic::singleton();
- if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
+ 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; ".
+ 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 ) {
+ if ( $magic->getTypesForExtension( $extension ) !== null ) {
+ wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension\n" );
+ return false;
+ } else {
+ 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!
@@ -946,9 +970,9 @@ abstract class UploadBase {
* potentially harmful. The present implementation will produce false
* positives in some situations.
*
- * @param $file String: pathname to the temporary upload file
- * @param $mime String: the mime type of the file
- * @param $extension String: the extension of the file
+ * @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 Boolean: true if the file contains something looking like embedded scripts
*/
public static function detectScript( $file, $mime, $extension ) {
@@ -958,7 +982,7 @@ abstract class UploadBase {
# ugly hack: for text files, always look at the entire file.
# For binary field, just check the first K.
- if( strpos( $mime,'text/' ) === 0 ) {
+ if ( strpos( $mime, 'text/' ) === 0 ) {
$chunk = file_get_contents( $file );
} else {
$fp = fopen( $file, 'rb' );
@@ -968,27 +992,27 @@ abstract class UploadBase {
$chunk = strtolower( $chunk );
- if( !$chunk ) {
+ if ( !$chunk ) {
wfProfileOut( __METHOD__ );
return false;
}
# decode from UTF-16 if needed (could be used for obfuscation).
- if( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
+ if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
$enc = 'UTF-16BE';
- } elseif( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
+ } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
$enc = 'UTF-16LE';
} else {
$enc = null;
}
- if( $enc ) {
+ if ( $enc ) {
$chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
}
$chunk = trim( $chunk );
- # @todo FIXME: Convert from UTF-16 if necessarry!
+ # @todo FIXME: Convert from UTF-16 if necessary!
wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" );
# check for HTML doctype
@@ -1032,12 +1056,12 @@ abstract class UploadBase {
'<table'
);
- if( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
+ if ( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
$tags[] = '<title';
}
- foreach( $tags as $tag ) {
- if( false !== strpos( $chunk, $tag ) ) {
+ foreach ( $tags as $tag ) {
+ if ( false !== strpos( $chunk, $tag ) ) {
wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" );
wfProfileOut( __METHOD__ );
return true;
@@ -1052,21 +1076,21 @@ abstract class UploadBase {
$chunk = Sanitizer::decodeCharReferences( $chunk );
# look for script-types
- if( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
+ if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
wfDebug( __METHOD__ . ": found script types\n" );
wfProfileOut( __METHOD__ );
return true;
}
# look for html-style script-urls
- if( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
+ if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
wfDebug( __METHOD__ . ": found html-style script urls\n" );
wfProfileOut( __METHOD__ );
return true;
}
# look for css-style script-urls
- if( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
+ if ( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
wfDebug( __METHOD__ . ": found css-style script urls\n" );
wfProfileOut( __METHOD__ );
return true;
@@ -1102,7 +1126,7 @@ abstract class UploadBase {
// bytes. There shouldn't be a legitimate reason for this to happen.
wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" );
return true;
- } elseif ( substr( $contents, 0, 4) == "\x4C\x6F\xA7\x94" ) {
+ } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
// EBCDIC encoded XML
wfDebug( __METHOD__ . ": EBCDIC Encoded XML\n" );
return true;
@@ -1138,8 +1162,33 @@ abstract class UploadBase {
* @return bool
*/
protected function detectScriptInSvg( $filename ) {
- $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) );
- return $check->filterMatch;
+ $check = new XmlTypeCheck(
+ $filename,
+ array( $this, 'checkSvgScriptCallback' ),
+ true,
+ array( 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback' )
+ );
+ if ( $check->wellFormed !== true ) {
+ // Invalid xml (bug 58553)
+ return array( 'uploadinvalidxml' );
+ } elseif ( $check->filterMatch ) {
+ return array( 'uploadscripted' );
+ }
+ return false;
+ }
+
+ /**
+ * Callback to filter SVG Processing Instructions.
+ * @param $target string processing instruction name
+ * @param $data string processing instruction attribute and value
+ * @return bool (true if the filter identified something bad)
+ */
+ public static function checkSvgPICallback( $target, $data ) {
+ // Don't allow external stylesheets (bug 57550)
+ if ( preg_match( '/xml-stylesheet/i', $target) ) {
+ return true;
+ }
+ return false;
}
/**
@@ -1154,80 +1203,79 @@ abstract class UploadBase {
/*
* check for elements that can contain javascript
*/
- if( $strippedElement == 'script' ) {
+ if ( $strippedElement == 'script' ) {
wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" );
return true;
}
# e.g., <svg xmlns="http://www.w3.org/2000/svg"> <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
- if( $strippedElement == 'handler' ) {
+ if ( $strippedElement == 'handler' ) {
wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" );
return true;
}
# SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
- if( $strippedElement == 'stylesheet' ) {
+ if ( $strippedElement == 'stylesheet' ) {
wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" );
return true;
}
- foreach( $attribs as $attrib => $value ) {
+ foreach ( $attribs as $attrib => $value ) {
$stripped = $this->stripXmlNamespace( $attrib );
- $value = strtolower($value);
+ $value = strtolower( $value );
- if( substr( $stripped, 0, 2 ) == 'on' ) {
+ if ( substr( $stripped, 0, 2 ) == 'on' ) {
wfDebug( __METHOD__ . ": Found event-handler attribute '$attrib'='$value' in uploaded file.\n" );
return true;
}
# href with javascript target
- if( $stripped == 'href' && strpos( strtolower( $value ), 'javascript:' ) !== false ) {
+ if ( $stripped == 'href' && strpos( strtolower( $value ), 'javascript:' ) !== false ) {
wfDebug( __METHOD__ . ": Found script in href attribute '$attrib'='$value' in uploaded file.\n" );
return true;
}
- # href with embeded svg as target
- if( $stripped == 'href' && preg_match( '!data:[^,]*image/svg[^,]*,!sim', $value ) ) {
+ # href with embedded svg as target
+ if ( $stripped == 'href' && preg_match( '!data:[^,]*image/svg[^,]*,!sim', $value ) ) {
wfDebug( __METHOD__ . ": Found href to embedded svg \"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
return true;
}
- # href with embeded (text/xml) svg as target
- if( $stripped == 'href' && preg_match( '!data:[^,]*text/xml[^,]*,!sim', $value ) ) {
+ # href with embedded (text/xml) svg as target
+ if ( $stripped == 'href' && preg_match( '!data:[^,]*text/xml[^,]*,!sim', $value ) ) {
wfDebug( __METHOD__ . ": Found href to embedded svg \"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
return true;
}
# use set/animate to add event-handler attribute to parent
- if( ( $strippedElement == 'set' || $strippedElement == 'animate' ) && $stripped == 'attributename' && substr( $value, 0, 2 ) == 'on' ) {
+ if ( ( $strippedElement == 'set' || $strippedElement == 'animate' ) && $stripped == 'attributename' && substr( $value, 0, 2 ) == 'on' ) {
wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with \"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
return true;
}
# use set to add href attribute to parent element
- if( $strippedElement == 'set' && $stripped == 'attributename' && strpos( $value, 'href' ) !== false ) {
- wfDebug( __METHOD__ . ": Found svg setting href attibute '$value' in uploaded file.\n" );
+ if ( $strippedElement == 'set' && $stripped == 'attributename' && strpos( $value, 'href' ) !== false ) {
+ wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file.\n" );
return true;
}
# use set to add a remote / data / script target to an element
- if( $strippedElement == 'set' && $stripped == 'to' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
- wfDebug( __METHOD__ . ": Found svg setting attibute to '$value' in uploaded file.\n" );
+ if ( $strippedElement == 'set' && $stripped == 'to' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
+ wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file.\n" );
return true;
}
-
# use handler attribute with remote / data / script
- if( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
+ if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script '$attrib'='$value' in uploaded file.\n" );
return true;
}
# use CSS styles to bring in remote code
# catch url("http:..., url('http:..., url(http:..., but not url("#..., url('#..., url(#....
- if( $stripped == 'style' && preg_match_all( '!((?:font|clip-path|fill|filter|marker|marker-end|marker-mid|marker-start|mask|stroke)\s*:\s*url\s*\(\s*["\']?\s*[^#]+.*?\))!sim', $value, $matches ) ) {
- foreach ($matches[1] as $match) {
- if (!preg_match( '!(?:font|clip-path|fill|filter|marker|marker-end|marker-mid|marker-start|mask|stroke)\s*:\s*url\s*\(\s*(#|\'#|"#)!sim', $match ) ) {
+ if ( $stripped == 'style' && preg_match_all( '!((?:font|clip-path|fill|filter|marker|marker-end|marker-mid|marker-start|mask|stroke)\s*:\s*url\s*\(\s*["\']?\s*[^#]+.*?\))!sim', $value, $matches ) ) {
+ foreach ( $matches[1] as $match ) {
+ if ( !preg_match( '!(?:font|clip-path|fill|filter|marker|marker-end|marker-mid|marker-start|mask|stroke)\s*:\s*url\s*\(\s*(#|\'#|"#)!sim', $match ) ) {
wfDebug( __METHOD__ . ": Found svg setting a style with remote url '$attrib'='$value' in uploaded file.\n" );
return true;
}
@@ -1235,7 +1283,7 @@ abstract class UploadBase {
}
# image filters can pull in url, which could be svg that executes scripts
- if( $strippedElement == 'image' && $stripped == 'filter' && preg_match( '!url\s*\(!sim', $value ) ) {
+ if ( $strippedElement == 'image' && $stripped == 'filter' && preg_match( '!url\s*\(!sim', $value ) ) {
wfDebug( __METHOD__ . ": Found image filter with url: \"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
return true;
}
@@ -1260,7 +1308,7 @@ abstract class UploadBase {
* This relies on the $wgAntivirus and $wgAntivirusSetup variables.
* $wgAntivirusRequired may be used to deny upload if the scan fails.
*
- * @param $file String: pathname to the temporary upload file
+ * @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.
@@ -1305,7 +1353,7 @@ abstract class UploadBase {
# NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
# that does not seem to be worth the pain.
# Ask me (Duesentrieb) about it if it's ever needed.
- $output = wfShellExec( "$command 2>&1", $exitCode );
+ $output = wfShellExecWithStderr( $command, $exitCode );
# map exit code to AV_xxx constants.
$mappedCode = $exitCode;
@@ -1317,27 +1365,22 @@ abstract class UploadBase {
}
}
+ /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
+ * so we need the strict equalities === and thus can't use a switch here
+ */
if ( $mappedCode === AV_SCAN_FAILED ) {
# scan failed (code was mapped to false by $exitCodeMap)
wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" );
- if ( $wgAntivirusRequired ) {
- wfProfileOut( __METHOD__ );
- return wfMessage( 'virus-scanfailed', array( $exitCode ) )->text();
- } else {
- wfProfileOut( __METHOD__ );
- return null;
- }
+ $output = $wgAntivirusRequired ? wfMessage( 'virus-scanfailed', array( $exitCode ) )->text() : null;
} elseif ( $mappedCode === AV_SCAN_ABORTED ) {
# scan failed because filetype is unknown (probably imune)
wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" );
- wfProfileOut( __METHOD__ );
- return null;
+ $output = null;
} elseif ( $mappedCode === AV_NO_VIRUS ) {
# no virus found
wfDebug( __METHOD__ . ": file passed virus scan.\n" );
- wfProfileOut( __METHOD__ );
- return false;
+ $output = false;
} else {
$output = trim( $output );
@@ -1353,9 +1396,10 @@ abstract class UploadBase {
}
wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" );
- wfProfileOut( __METHOD__ );
- return $output;
}
+
+ wfProfileOut( __METHOD__ );
+ return $output;
}
/**
@@ -1369,8 +1413,8 @@ abstract class UploadBase {
private function checkOverwrite( $user ) {
// First check whether the local file can be overwritten
$file = $this->getLocalFile();
- if( $file->exists() ) {
- if( !self::userCanReUpload( $user, $file ) ) {
+ if ( $file->exists() ) {
+ if ( !self::userCanReUpload( $user, $file ) ) {
return array( 'fileexists-forbidden', $file->getName() );
} else {
return true;
@@ -1392,17 +1436,17 @@ abstract class UploadBase {
* Check if a user is the last uploader
*
* @param $user User object
- * @param $img String: image name
+ * @param string $img image name
* @return Boolean
*/
public static function userCanReUpload( User $user, $img ) {
- if( $user->isAllowed( 'reupload' ) ) {
+ if ( $user->isAllowed( 'reupload' ) ) {
return true; // non-conditional
}
- if( !$user->isAllowed( 'reupload-own' ) ) {
+ if ( !$user->isAllowed( 'reupload-own' ) ) {
return false;
}
- if( is_string( $img ) ) {
+ if ( is_string( $img ) ) {
$img = wfLocalFile( $img );
}
if ( !( $img instanceof LocalFile ) ) {
@@ -1424,11 +1468,11 @@ abstract class UploadBase {
* @return mixed False if the file does not exists, else an array
*/
public static function getExistsWarning( $file ) {
- if( $file->exists() ) {
+ if ( $file->exists() ) {
return array( 'warning' => 'exists', 'file' => $file );
}
- if( $file->getTitle()->getArticleID() ) {
+ if ( $file->getTitle()->getArticleID() ) {
return array( 'warning' => 'page-exists', 'file' => $file );
}
@@ -1436,7 +1480,7 @@ abstract class UploadBase {
return array( 'warning' => 'was-deleted', 'file' => $file );
}
- if( strpos( $file->getName(), '.' ) == false ) {
+ if ( strpos( $file->getName(), '.' ) == false ) {
$partname = $file->getName();
$extension = '';
} else {
@@ -1455,7 +1499,7 @@ abstract class UploadBase {
$nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
$file_lc = wfLocalFile( $nt_lc );
- if( $file_lc->exists() ) {
+ if ( $file_lc->exists() ) {
return array(
'warning' => 'exists-normalized',
'file' => $file,
@@ -1464,11 +1508,22 @@ abstract class UploadBase {
}
}
+ // Check for files with the same name but a different extension
+ $similarFiles = RepoGroup::singleton()->getLocalRepo()->findFilesByPrefix(
+ "{$partname}.", 1 );
+ if ( count( $similarFiles ) ) {
+ return array(
+ 'warning' => 'exists-normalized',
+ 'file' => $file,
+ 'normalizedFile' => $similarFiles[0],
+ );
+ }
+
if ( self::isThumbName( $file->getName() ) ) {
# Check for filenames like 50px- or 180px-, these are mostly thumbnails
- $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $extension, NS_FILE );
+ $nt_thb = Title::newFromText( substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension, NS_FILE );
$file_thb = wfLocalFile( $nt_thb );
- if( $file_thb->exists() ) {
+ if ( $file_thb->exists() ) {
return array(
'warning' => 'thumb',
'file' => $file,
@@ -1484,8 +1539,7 @@ abstract class UploadBase {
}
}
-
- foreach( self::getFilenamePrefixBlacklist() as $prefix ) {
+ foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
return array(
'warning' => 'bad-prefix',
@@ -1507,10 +1561,10 @@ abstract class UploadBase {
$n = strrpos( $filename, '.' );
$partname = $n ? substr( $filename, 0, $n ) : $filename;
return (
- substr( $partname , 3, 3 ) == 'px-' ||
- substr( $partname , 2, 3 ) == 'px-'
+ substr( $partname, 3, 3 ) == 'px-' ||
+ substr( $partname, 2, 3 ) == 'px-'
) &&
- preg_match( "/[0-9]{2}/" , substr( $partname , 0, 2 ) );
+ preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
}
/**
@@ -1521,9 +1575,9 @@ abstract class UploadBase {
public static function getFilenamePrefixBlacklist() {
$blacklist = array();
$message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
- if( !$message->isDisabled() ) {
+ if ( !$message->isDisabled() ) {
$lines = explode( "\n", $message->plain() );
- foreach( $lines as $line ) {
+ foreach ( $lines as $line ) {
// Remove comment lines
$comment = substr( trim( $line ), 0, 1 );
if ( $comment == '#' || $comment == '' ) {
@@ -1532,7 +1586,7 @@ abstract class UploadBase {
// Remove additional comments after a prefix
$comment = strpos( $line, '#' );
if ( $comment > 0 ) {
- $line = substr( $line, 0, $comment-1 );
+ $line = substr( $line, 0, $comment - 1 );
}
$blacklist[] = trim( $line );
}
@@ -1590,6 +1644,32 @@ abstract class UploadBase {
} else {
return intval( $wgMaxUploadSize );
}
+ }
+ /**
+ * Get the current status of a chunked upload (used for polling).
+ * The status will be read from the *current* user session.
+ * @param $statusKey string
+ * @return Array|bool
+ */
+ public static function getSessionStatus( $statusKey ) {
+ return isset( $_SESSION[self::SESSION_STATUS_KEY][$statusKey] )
+ ? $_SESSION[self::SESSION_STATUS_KEY][$statusKey]
+ : false;
+ }
+
+ /**
+ * Set the current status of a chunked upload (used for polling).
+ * The status will be stored in the *current* user session.
+ * @param $statusKey string
+ * @param $value array|false
+ * @return void
+ */
+ public static function setSessionStatus( $statusKey, $value ) {
+ if ( $value === false ) {
+ unset( $_SESSION[self::SESSION_STATUS_KEY][$statusKey] );
+ } else {
+ $_SESSION[self::SESSION_STATUS_KEY][$statusKey] = $value;
+ }
}
}
diff --git a/includes/upload/UploadFromChunks.php b/includes/upload/UploadFromChunks.php
index 531f7be4..2e0b9444 100644
--- a/includes/upload/UploadFromChunks.php
+++ b/includes/upload/UploadFromChunks.php
@@ -31,26 +31,26 @@ class UploadFromChunks extends UploadFromFile {
protected $mOffset, $mChunkIndex, $mFileKey, $mVirtualTempPath;
/**
- * Setup local pointers to stash, repo and user ( similar to UploadFromStash )
+ * Setup local pointers to stash, repo and user (similar to UploadFromStash)
*
* @param $user User
* @param $stash UploadStash
* @param $repo FileRepo
*/
- public function __construct( $user = false, $stash = false, $repo = false ) {
+ public function __construct( $user = null, $stash = false, $repo = false ) {
// user object. sometimes this won't exist, as when running from cron.
$this->user = $user;
- if( $repo ) {
+ if ( $repo ) {
$this->repo = $repo;
} else {
$this->repo = RepoGroup::singleton()->getLocalRepo();
}
- if( $stash ) {
+ if ( $stash ) {
$this->stash = $stash;
} else {
- if( $user ) {
+ if ( $user ) {
wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" );
} else {
wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" );
@@ -60,12 +60,13 @@ class UploadFromChunks extends UploadFromFile {
return true;
}
+
/**
* Calls the parent stashFile and updates the uploadsession table to handle "chunks"
*
* @return UploadStashFile stashed file
*/
- public function stashFile() {
+ public function stashFile( User $user = null ) {
// Stash file is the called on creating a new chunk session:
$this->mChunkIndex = 0;
$this->mOffset = 0;
@@ -73,12 +74,12 @@ class UploadFromChunks extends UploadFromFile {
$this->verifyChunk();
// Create a local stash target
$this->mLocalFile = parent::stashFile();
- // Update the initial file offset ( based on file size )
+ // Update the initial file offset (based on file size)
$this->mOffset = $this->mLocalFile->getSize();
$this->mFileKey = $this->mLocalFile->getFileKey();
// Output a copy of this first to chunk 0 location:
- $status = $this->outputChunk( $this->mLocalFile->getPath() );
+ $this->outputChunk( $this->mLocalFile->getPath() );
// Update db table to reflect initial "chunk" state
$this->updateChunkStatus();
@@ -113,7 +114,7 @@ class UploadFromChunks extends UploadFromFile {
// Concatenate all the chunks to mVirtualTempPath
$fileList = Array();
// The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1"
- for( $i = 0; $i <= $this->getChunkIndex(); $i++ ){
+ for ( $i = 0; $i <= $this->getChunkIndex(); $i++ ) {
$fileList[] = $this->getVirtualChunkLocation( $i );
}
@@ -122,13 +123,16 @@ class UploadFromChunks extends UploadFromFile {
// Get a 0-byte temp file to perform the concatenation at
$tmpFile = TempFSFile::factory( 'chunkedupload_', $ext );
$tmpPath = $tmpFile
- ? $tmpFile->getPath()
+ ? $tmpFile->bind( $this )->getPath() // keep alive with $this
: false; // fail in concatenate()
// Concatenate the chunks at the temp file
+ $tStart = microtime( true );
$status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
- if( !$status->isOk() ){
+ $tAmount = microtime( true ) - $tStart;
+ if ( !$status->isOk() ) {
return $status;
}
+ wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds.\n" );
$this->mTempPath = $tmpPath; // file system path
$this->mFileSize = filesize( $this->mTempPath ); //Since this was set for the last chunk previously
@@ -140,8 +144,12 @@ class UploadFromChunks extends UploadFromFile {
}
// Update the mTempPath and mLocalFile
- // ( for FileUpload or normal Stash to take over )
- $this->mLocalFile = parent::stashFile();
+ // (for FileUpload or normal Stash to take over)
+ $tStart = microtime( true );
+ $this->mLocalFile = parent::stashFile( $this->user );
+ $tAmount = microtime( true ) - $tStart;
+ $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
+ wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds.\n" );
return $status;
}
@@ -164,7 +172,7 @@ class UploadFromChunks extends UploadFromFile {
* @param $index
* @return string
*/
- function getVirtualChunkLocation( $index ){
+ function getVirtualChunkLocation( $index ) {
return $this->repo->getVirtualUrl( 'temp' ) .
'/' .
$this->repo->getHashPath(
@@ -176,16 +184,16 @@ class UploadFromChunks extends UploadFromFile {
/**
* Add a chunk to the temporary directory
*
- * @param $chunkPath string path to temporary chunk file
- * @param $chunkSize int size of the current chunk
- * @param $offset int offset of current chunk ( mutch match database chunk offset )
+ * @param string $chunkPath path to temporary chunk file
+ * @param int $chunkSize size of the current chunk
+ * @param int $offset offset of current chunk ( mutch match database chunk offset )
* @return Status
*/
public function addChunk( $chunkPath, $chunkSize, $offset ) {
// Get the offset before we add the chunk to the file system
$preAppendOffset = $this->getOffset();
- if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize()) {
+ if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) {
$status = Status::newFatal( 'file-too-large' );
} else {
// Make sure the client is uploading the correct chunk with a matching offset.
@@ -202,7 +210,7 @@ class UploadFromChunks extends UploadFromFile {
return Status::newFatal( $e->getMessage() );
}
$status = $this->outputChunk( $chunkPath );
- if( $status->isGood() ){
+ if ( $status->isGood() ) {
// Update local offset:
$this->mOffset = $preAppendOffset + $chunkSize;
// Update chunk table status db
@@ -218,11 +226,14 @@ class UploadFromChunks extends UploadFromFile {
/**
* Update the chunk db table with the current status:
*/
- private function updateChunkStatus(){
+ private function updateChunkStatus() {
wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
$this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
$dbw = $this->repo->getMasterDb();
+ // Use a quick transaction since we will upload the full temp file into shared
+ // storage, which takes time for large files. We don't want to hold locks then.
+ $dbw->begin( __METHOD__ );
$dbw->update(
'uploadstash',
array(
@@ -233,12 +244,13 @@ class UploadFromChunks extends UploadFromFile {
array( 'us_key' => $this->mFileKey ),
__METHOD__
);
+ $dbw->commit( __METHOD__ );
}
/**
* Get the chunk db state and populate update relevant local values
*/
- private function getChunkStatus(){
+ private function getChunkStatus() {
// get Master db to avoid race conditions.
// Otherwise, if chunk upload time < replag there will be spurious errors
$dbw = $this->repo->getMasterDb();
@@ -264,8 +276,8 @@ class UploadFromChunks extends UploadFromFile {
* Get the current Chunk index
* @return Integer index of the current chunk
*/
- private function getChunkIndex(){
- if( $this->mChunkIndex !== null ){
+ private function getChunkIndex() {
+ if ( $this->mChunkIndex !== null ) {
return $this->mChunkIndex;
}
return 0;
@@ -275,8 +287,8 @@ class UploadFromChunks extends UploadFromFile {
* Gets the current offset in fromt the stashedupload table
* @return Integer current byte offset of the chunk file set
*/
- private function getOffset(){
- if ( $this->mOffset !== null ){
+ private function getOffset() {
+ if ( $this->mOffset !== null ) {
return $this->mOffset;
}
return 0;
@@ -289,7 +301,7 @@ class UploadFromChunks extends UploadFromFile {
* @throws UploadChunkFileException
* @return FileRepoStatus
*/
- private function outputChunk( $chunkPath ){
+ private function outputChunk( $chunkPath ) {
// Key is fileKey + chunk index
$fileKey = $this->getChunkFileKey();
@@ -314,11 +326,11 @@ class UploadFromChunks extends UploadFromFile {
return $storeStatus;
}
- private function getChunkFileKey( $index = null ){
- if( $index === null ){
+ private function getChunkFileKey( $index = null ) {
+ if ( $index === null ) {
$index = $this->getChunkIndex();
}
- return $this->mFileKey . '.' . $index ;
+ return $this->mFileKey . '.' . $index;
}
/**
@@ -334,7 +346,7 @@ class UploadFromChunks extends UploadFromFile {
$res = $this->verifyPartialFile();
$this->mDesiredDestName = $oldDesiredDestName;
$this->mTitle = false;
- if( is_array( $res ) ) {
+ if ( is_array( $res ) ) {
throw new UploadChunkVerificationException( $res[0] );
}
}
diff --git a/includes/upload/UploadFromFile.php b/includes/upload/UploadFromFile.php
index aa0cc77b..a00ed327 100644
--- a/includes/upload/UploadFromFile.php
+++ b/includes/upload/UploadFromFile.php
@@ -40,7 +40,7 @@ class UploadFromFile extends UploadBase {
function initializeFromRequest( &$request ) {
$upload = $request->getUpload( 'wpUploadFile' );
$desiredDestName = $request->getText( 'wpDestFile' );
- if( !$desiredDestName ) {
+ if ( !$desiredDestName ) {
$desiredDestName = $upload->getName();
}
@@ -79,21 +79,21 @@ class UploadFromFile extends UploadBase {
* @return array
*/
public function verifyUpload() {
- # Check for a post_max_size or upload_max_size overflow, so that a
+ # Check for a post_max_size or upload_max_size overflow, so that a
# proper error can be shown to the user
if ( is_null( $this->mTempPath ) || $this->isEmptyFile() ) {
if ( $this->mUpload->isIniSizeOverflow() ) {
- return array(
+ return array(
'status' => UploadBase::FILE_TOO_LARGE,
- 'max' => min(
- self::getMaxUploadSize( $this->getSourceType() ),
- wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ),
+ 'max' => min(
+ self::getMaxUploadSize( $this->getSourceType() ),
+ wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ),
wfShorthandToInteger( ini_get( 'post_max_size' ) )
),
);
}
}
-
+
return parent::verifyUpload();
}
}
diff --git a/includes/upload/UploadFromStash.php b/includes/upload/UploadFromStash.php
index d79641ce..cb85fc63 100644
--- a/includes/upload/UploadFromStash.php
+++ b/includes/upload/UploadFromStash.php
@@ -45,16 +45,16 @@ class UploadFromStash extends UploadBase {
// user object. sometimes this won't exist, as when running from cron.
$this->user = $user;
- if( $repo ) {
+ if ( $repo ) {
$this->repo = $repo;
} else {
$this->repo = RepoGroup::singleton()->getLocalRepo();
}
- if( $stash ) {
+ if ( $stash ) {
$this->stash = $stash;
} else {
- if( $user ) {
+ if ( $user ) {
wfDebug( __METHOD__ . " creating new UploadStash instance for " . $user->getId() . "\n" );
} else {
wfDebug( __METHOD__ . " creating new UploadStash instance with no user\n" );
@@ -89,7 +89,7 @@ class UploadFromStash extends UploadBase {
* @param $key string
* @param $name string
*/
- public function initialize( $key, $name = 'upload_file' ) {
+ public function initialize( $key, $name = 'upload_file', $initTempFile = true ) {
/**
* Confirming a temporarily stashed upload.
* We don't want path names to be forged, so we keep
@@ -98,7 +98,7 @@ class UploadFromStash extends UploadBase {
*/
$metadata = $this->stash->getMetadata( $key );
$this->initializePathInfo( $name,
- $this->getRealPath ( $metadata['us_path'] ),
+ $initTempFile ? $this->getRealPath( $metadata['us_path'] ) : false,
$metadata['us_size'],
false
);
@@ -129,6 +129,14 @@ class UploadFromStash extends UploadBase {
return $this->mSourceType;
}
+ /**
+ * Get the base 36 SHA1 of the file
+ * @return string
+ */
+ public function getTempFileSha1Base36() {
+ return $this->mFileProps['sha1'];
+ }
+
/*
* protected function verifyFile() inherited
*/
@@ -136,12 +144,13 @@ class UploadFromStash extends UploadBase {
/**
* Stash the file.
*
+ * @param $user User
* @return UploadStashFile
*/
- public function stashFile() {
+ public function stashFile( User $user = null ) {
// replace mLocalFile with an instance of UploadStashFile, which adds some methods
// that are useful for stashed files.
- $this->mLocalFile = parent::stashFile();
+ $this->mLocalFile = parent::stashFile( $user );
return $this->mLocalFile;
}
diff --git a/includes/upload/UploadFromUrl.php b/includes/upload/UploadFromUrl.php
index 927c3cd9..0201d5f4 100644
--- a/includes/upload/UploadFromUrl.php
+++ b/includes/upload/UploadFromUrl.php
@@ -34,6 +34,8 @@ class UploadFromUrl extends UploadBase {
protected $mTempPath, $mTmpHandle;
+ protected static $allowedUrls = array();
+
/**
* Checks if the user is allowed to use the upload-by-URL feature. If the
* user is not allowed, return the name of the user right as a string. If
@@ -61,6 +63,8 @@ class UploadFromUrl extends UploadBase {
/**
* Checks whether the URL is for an allowed host
+ * The domains in the whitelist can include wildcard characters (*) in place
+ * of any of the domain levels, e.g. '*.flickr.com' or 'upload.*.gov.uk'.
*
* @param $url string
* @return bool
@@ -75,16 +79,49 @@ class UploadFromUrl extends UploadBase {
return false;
}
$valid = false;
- foreach( $wgCopyUploadsDomains as $domain ) {
+ foreach ( $wgCopyUploadsDomains as $domain ) {
+ // See if the domain for the upload matches this whitelisted domain
+ $whitelistedDomainPieces = explode( '.', $domain );
+ $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
+ if ( count( $whitelistedDomainPieces ) === count( $uploadDomainPieces ) ) {
+ $valid = true;
+ // See if all the pieces match or not (excluding wildcards)
+ foreach ( $whitelistedDomainPieces as $index => $piece ) {
+ if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
+ $valid = false;
+ }
+ }
+ if ( $valid ) {
+ // We found a match, so quit comparing against the list
+ break;
+ }
+ }
+ /* Non-wildcard test
if ( $parsedUrl['host'] === $domain ) {
$valid = true;
break;
}
+ */
}
return $valid;
}
/**
+ * Checks whether the URL is not allowed.
+ *
+ * @param $url string
+ * @return bool
+ */
+ public static function isAllowedUrl( $url ) {
+ if ( !isset( self::$allowedUrls[$url] ) ) {
+ $allowed = true;
+ wfRunHooks( 'IsUploadAllowedFromUrl', array( $url, &$allowed ) );
+ self::$allowedUrls[$url] = $allowed;
+ }
+ return self::$allowedUrls[$url];
+ }
+
+ /**
* Entry point for API upload
*
* @param $name string
@@ -140,21 +177,30 @@ class UploadFromUrl extends UploadBase {
/**
* @return string
*/
- public function getSourceType() { return 'url'; }
+ public function getSourceType() {
+ return 'url';
+ }
/**
+ * Download the file (if not async)
+ *
+ * @param Array $httpOptions Array of options for MWHttpRequest. Ignored if async.
+ * This could be used to override the timeout on the http request.
* @return Status
*/
- public function fetchFile() {
+ public function fetchFile( $httpOptions = array() ) {
if ( !Http::isValidURI( $this->mUrl ) ) {
return Status::newFatal( 'http-invalid-url' );
}
- if( !self::isAllowedHost( $this->mUrl ) ) {
+ if ( !self::isAllowedHost( $this->mUrl ) ) {
return Status::newFatal( 'upload-copy-upload-invalid-domain' );
}
+ if ( !self::isAllowedUrl( $this->mUrl ) ) {
+ return Status::newFatal( 'upload-copy-upload-invalid-url' );
+ }
if ( !$this->mAsync ) {
- return $this->reallyFetchFile();
+ return $this->reallyFetchFile( $httpOptions );
}
return Status::newGood();
}
@@ -191,9 +237,12 @@ class UploadFromUrl extends UploadBase {
/**
* Download the file, save it to the temporary file and update the file
* size and set $mRemoveTempFile to true.
+ *
+ * @param Array $httpOptions Array of options for MWHttpRequest
* @return Status
*/
- protected function reallyFetchFile() {
+ protected function reallyFetchFile( $httpOptions = array() ) {
+ global $wgCopyUploadProxy, $wgCopyUploadTimeout;
if ( $this->mTempPath === false ) {
return Status::newFatal( 'tmp-create-error' );
}
@@ -207,13 +256,15 @@ class UploadFromUrl extends UploadBase {
$this->mRemoveTempFile = true;
$this->mFileSize = 0;
- $options = array(
- 'followRedirects' => true
+ $options = $httpOptions + array(
+ 'followRedirects' => true,
);
- global $wgCopyUploadProxy;
if ( $wgCopyUploadProxy !== false ) {
$options['proxy'] = $wgCopyUploadProxy;
}
+ if ( $wgCopyUploadTimeout && !isset( $options['timeout'] ) ) {
+ $options['timeout'] = $wgCopyUploadTimeout;
+ }
$req = MWHttpRequest::factory( $this->mUrl, $options );
$req->setCallback( array( $this, 'saveTempFileChunk' ) );
$status = $req->execute();
@@ -312,7 +363,7 @@ class UploadFromUrl extends UploadBase {
'sessionKey' => $sessionKey,
) );
$job->initializeSessionData();
- $job->insert();
+ JobQueueGroup::singleton()->push( $job );
return $sessionKey;
}
diff --git a/includes/upload/UploadStash.php b/includes/upload/UploadStash.php
index 53a90582..7db6c64b 100644
--- a/includes/upload/UploadStash.php
+++ b/includes/upload/UploadStash.php
@@ -97,7 +97,7 @@ class UploadStash {
* Get a file and its metadata from the stash.
* The noAuth param is a bit janky but is required for automated scripts which clean out the stash.
*
- * @param $key String: key under which file information is stored
+ * @param string $key key under which file information is stored
* @param $noAuth Boolean (optional) Don't check authentication. Used by maintenance scripts.
* @throws UploadStashFileNotFoundException
* @throws UploadStashNotLoggedInException
@@ -106,15 +106,13 @@ class UploadStash {
* @return UploadStashFile
*/
public function getFile( $key, $noAuth = false ) {
-
if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
}
- if ( !$noAuth ) {
- if ( !$this->isLoggedIn ) {
- throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
- }
+ if ( !$noAuth && !$this->isLoggedIn ) {
+ throw new UploadStashNotLoggedInException( __METHOD__ .
+ ' No user is logged in, files must belong to users' );
}
if ( !isset( $this->fileMetadata[$key] ) ) {
@@ -131,8 +129,13 @@ class UploadStash {
$this->initFile( $key );
// fetch fileprops
- $path = $this->fileMetadata[$key]['us_path'];
- $this->fileProps[$key] = $this->repo->getFileProps( $path );
+ if ( strlen( $this->fileMetadata[$key]['us_props'] ) ) {
+ $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
+ } else { // b/c for rows with no us_props
+ wfDebug( __METHOD__ . " fetched props for $key from file\n" );
+ $path = $this->fileMetadata[$key]['us_path'];
+ $this->fileProps[$key] = $this->repo->getFileProps( $path );
+ }
}
if ( ! $this->files[$key]->exists() ) {
@@ -152,10 +155,10 @@ class UploadStash {
/**
* Getter for file metadata.
*
- * @param key String: key under which file information is stored
+ * @param string $key key under which file information is stored
* @return Array
*/
- public function getMetadata ( $key ) {
+ public function getMetadata( $key ) {
$this->getFile( $key );
return $this->fileMetadata[$key];
}
@@ -163,10 +166,10 @@ class UploadStash {
/**
* Getter for fileProps
*
- * @param key String: key under which file information is stored
+ * @param string $key key under which file information is stored
* @return Array
*/
- public function getFileProps ( $key ) {
+ public function getFileProps( $key ) {
$this->getFile( $key );
return $this->fileProps[$key];
}
@@ -174,15 +177,15 @@ class UploadStash {
/**
* Stash a file in a temp directory and record that we did this in the database, along with other metadata.
*
- * @param $path String: path to file you want stashed
- * @param $sourceType String: the type of upload that generated this file (currently, I believe, 'file' or null)
+ * @param string $path path to file you want stashed
+ * @param string $sourceType the type of upload that generated this file (currently, I believe, 'file' or null)
* @throws UploadStashBadPathException
* @throws UploadStashFileException
* @throws UploadStashNotLoggedInException
* @return UploadStashFile: file, or null on failure
*/
public function stashFile( $path, $sourceType = null ) {
- if ( ! file_exists( $path ) ) {
+ if ( !is_file( $path ) ) {
wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
throw new UploadStashBadPathException( "path doesn't exist" );
}
@@ -192,12 +195,10 @@ class UploadStash {
// we will be initializing from some tmpnam files that don't have extensions.
// most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
$extension = self::getExtensionForPath( $path );
- if ( ! preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
+ if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
$pathWithGoodExtension = "$path.$extension";
- if ( ! rename( $path, $pathWithGoodExtension ) ) {
- throw new UploadStashFileException( "couldn't rename $path to have a better extension at $pathWithGoodExtension" );
- }
- $path = $pathWithGoodExtension;
+ } else {
+ $pathWithGoodExtension = $path;
}
// If no key was supplied, make one. a mysql insertid would be totally reasonable here, except
@@ -205,10 +206,10 @@ class UploadStash {
//
// some things that when combined will make a suitably unique key.
// see: http://www.jwz.org/doc/mid.html
- list ($usec, $sec) = explode( ' ', microtime() );
- $usec = substr($usec, 2);
+ list( $usec, $sec ) = explode( ' ', microtime() );
+ $usec = substr( $usec, 2 );
$key = wfBaseConvert( $sec . $usec, 10, 36 ) . '.' .
- wfBaseConvert( mt_rand(), 10, 36 ) . '.'.
+ wfBaseConvert( mt_rand(), 10, 36 ) . '.' .
$this->userId . '.' .
$extension;
@@ -221,7 +222,7 @@ class UploadStash {
wfDebug( __METHOD__ . " key for '$path': $key\n" );
// if not already in a temporary area, put it there
- $storeStatus = $this->repo->storeTemp( basename( $path ), $path );
+ $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
if ( ! $storeStatus->isOK() ) {
// It is a convention in MediaWiki to only return one error per API exception, even if multiple errors
@@ -244,9 +245,6 @@ class UploadStash {
}
$stashPath = $storeStatus->value;
- // we have renamed the file so we have to cleanup once done
- unlink($path);
-
// fetch the current user ID
if ( !$this->isLoggedIn ) {
throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
@@ -262,6 +260,7 @@ class UploadStash {
'us_key' => $key,
'us_orig_path' => $path,
'us_path' => $stashPath, // virtual URL
+ 'us_props' => serialize( $fileProps ),
'us_size' => $fileProps['size'],
'us_sha1' => $fileProps['sha1'],
'us_mime' => $fileProps['mime'],
@@ -319,8 +318,8 @@ class UploadStash {
/**
* Remove a particular file from the stash. Also removes it from the repo.
*
- * @throws UploadStashNotLoggedInException
- * @throws UploadStashWrongOwnerException
+ * @param $key
+ * @throws UploadStashNoSuchKeyException|UploadStashNotLoggedInException|UploadStashWrongOwnerException
* @return boolean: success
*/
public function removeFile( $key ) {
@@ -339,7 +338,7 @@ class UploadStash {
__METHOD__
);
- if( !$row ) {
+ if ( !$row ) {
throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
}
@@ -350,7 +349,6 @@ class UploadStash {
return $this->removeFileNoAuth( $key );
}
-
/**
* Remove a file (see removeFile), but doesn't check ownership first.
*
@@ -359,16 +357,16 @@ class UploadStash {
public function removeFileNoAuth( $key ) {
wfDebug( __METHOD__ . " clearing row $key\n" );
+ // Ensure we have the UploadStashFile loaded for this key
+ $this->getFile( $key, true );
+
$dbw = $this->repo->getMasterDb();
- // this gets its own transaction since it's called serially by the cleanupUploadStash maintenance script
- $dbw->begin( __METHOD__ );
$dbw->delete(
'uploadstash',
array( 'us_key' => $key ),
__METHOD__
);
- $dbw->commit( __METHOD__ );
// TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed)
// for now, ignore.
@@ -419,6 +417,8 @@ class UploadStash {
* with an extension.
* XXX this is somewhat redundant with the checks that ApiUpload.php does with incoming
* uploads versus the desired filename. Maybe we can get that passed to us...
+ * @param $path
+ * @throws UploadStashFileException
* @return string
*/
public static function getExtensionForPath( $path ) {
@@ -456,14 +456,14 @@ class UploadStash {
/**
* Helper function: do the actual database query to fetch file metadata.
*
- * @param $key String: key
+ * @param string $key key
* @param $readFromDB: constant (default: DB_SLAVE)
* @return boolean
*/
protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
// populate $fileMetadata[$key]
$dbr = null;
- if( $readFromDB === DB_MASTER ) {
+ if ( $readFromDB === DB_MASTER ) {
// sometimes reading from the master is necessary, if there's replication lag.
$dbr = $this->repo->getMasterDb();
} else {
@@ -490,7 +490,7 @@ class UploadStash {
/**
* Helper function: Initialize the UploadStashFile for a given file.
*
- * @param $key String: key under which to store the object
+ * @param string $key key under which to store the object
* @throws UploadStashZeroLengthFileException
* @return bool
*/
@@ -514,8 +514,8 @@ class UploadStashFile extends UnregisteredLocalFile {
* Arguably UnregisteredLocalFile should be handling its own file repo but that class is a bit retarded currently
*
* @param $repo FileRepo: repository where we should find the path
- * @param $path String: path to file
- * @param $key String: key to store the path and any stashed data under
+ * @param string $path path to file
+ * @param string $key key to store the path and any stashed data under
* @throws UploadStashBadPathException
* @throws UploadStashFileNotFoundException
*/
@@ -564,7 +564,7 @@ class UploadStashFile extends UnregisteredLocalFile {
* The actual argument is the result of thumbName although we seem to have
* buggy code elsewhere that expects a boolean 'suffix'
*
- * @param $thumbName String: name of thumbnail (e.g. "120px-123456.jpg" ), or false to just get the path
+ * @param string $thumbName name of thumbnail (e.g. "120px-123456.jpg" ), or false to just get the path
* @return String: path thumbnail should take on filesystem, or containing directory if thumbname is false
*/
public function getThumbPath( $thumbName = false ) {
@@ -580,7 +580,7 @@ class UploadStashFile extends UnregisteredLocalFile {
* We override this because we want to use the pretty url name instead of the
* ugly file name.
*
- * @param $params Array: handler-specific parameters
+ * @param array $params handler-specific parameters
* @param $flags integer Bitfield that supports THUMB_* constants
* @return String: base name for URL, like '120px-12345.jpg', or null if there is no handler
*/
@@ -603,7 +603,7 @@ class UploadStashFile extends UnregisteredLocalFile {
* the thumbnail urls be predictable. However, in our model the URL is not based on the filename
* (that's hidden in the db)
*
- * @param $thumbName String: basename of thumbnail file -- however, we don't want to use the file exactly
+ * @param string $thumbName basename of thumbnail file -- however, we don't want to use the file exactly
* @return String: URL to access thumbnail, or URL with partial path
*/
public function getThumbUrl( $thumbName = false ) {
@@ -675,11 +675,12 @@ class UploadStashFile extends UnregisteredLocalFile {
}
-class UploadStashNotAvailableException extends MWException {};
-class UploadStashFileNotFoundException extends MWException {};
-class UploadStashBadPathException extends MWException {};
-class UploadStashFileException extends MWException {};
-class UploadStashZeroLengthFileException extends MWException {};
-class UploadStashNotLoggedInException extends MWException {};
-class UploadStashWrongOwnerException extends MWException {};
-class UploadStashNoSuchKeyException extends MWException {};
+class UploadStashException extends MWException {};
+class UploadStashNotAvailableException extends UploadStashException {};
+class UploadStashFileNotFoundException extends UploadStashException {};
+class UploadStashBadPathException extends UploadStashException {};
+class UploadStashFileException extends UploadStashException {};
+class UploadStashZeroLengthFileException extends UploadStashException {};
+class UploadStashNotLoggedInException extends UploadStashException {};
+class UploadStashWrongOwnerException extends UploadStashException {};
+class UploadStashNoSuchKeyException extends UploadStashException {};
diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile
deleted file mode 100644
index 5dd88d38..00000000
--- a/includes/zhtable/Makefile
+++ /dev/null
@@ -1,336 +0,0 @@
-#
-# Creating the file ZhConversion.php used for Simplified/Traditional
-# Chinese conversion. It gets the basic conversion table from the Unihan
-# database, and construct the phrase tables using phrase libraries in
-# the SCIM packages and the libtabe package. There are also special
-# tables used to for adjustment.
-#
-
-GREP = LANG=zh_CN.UTF8 grep
-SED = LANG=zh_CN.UTF8 sed
-DIFF = LANG=zh_CN.UTF8 diff
-CC ?= gcc
-
-SF_MIRROR = easynews
-SCIM_TABLES_VER = 0.5.9
-SCIM_PINYIN_VER = 0.5.91
-LIBTABE_VER = 0.2.3
-
-# Installation directory
-INSTDIR = /usr/local/share/zhdaemons/
-
-all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toHans.dict toHant.dict toCN.dict toTW.dict toHK.dict toSG.dict
-
-# Download Unihan database and Traditional Chinese / Simplified Chinese phrases files
-Unihan.zip:
- wget -nc http://www.unicode.org/Public/UNIDATA/Unihan.zip
-
-scim-tables-$(SCIM_TABLES_VER).tar.gz:
- wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-tables-$(SCIM_TABLES_VER).tar.gz
-
-scim-pinyin-$(SCIM_PINYIN_VER).tar.gz:
- wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-pinyin-$(SCIM_PINYIN_VER).tar.gz
-
-libtabe-$(LIBTABE_VER).tgz:
- wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/libtabe/libtabe-$(LIBTABE_VER).tgz
-
-# Extract the file from a comressed files
-Unihan.txt: Unihan.zip
- unzip -oq Unihan.zip
-
-EZ.txt.in: scim-tables-$(SCIM_TABLES_VER).tar.gz
- tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/EZ-Big.txt.in > EZ.txt.in
-
-Wubi.txt.in: scim-tables-$(SCIM_TABLES_VER).tar.gz
- tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/Wubi.txt.in > Wubi.txt.in
-
-Ziranma.txt.in: scim-tables-$(SCIM_TABLES_VER).tar.gz
- tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/Ziranma.txt.in > Ziranma.txt.in
-
-
-phrase_lib.txt: scim-pinyin-$(SCIM_PINYIN_VER).tar.gz
- tar -xzf scim-pinyin-$(SCIM_PINYIN_VER).tar.gz -O scim-pinyin-$(SCIM_PINYIN_VER)/data/phrase_lib.txt > phrase_lib.txt
-
-tsi.src: libtabe-$(LIBTABE_VER).tgz
- tar -xzf libtabe-$(LIBTABE_VER).tgz -O libtabe/tsi-src/tsi.src > tsi.src
-
-# Make a word list
-wordlist: phrase_lib.txt EZ.txt.in tsi.src
- iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/# //g' | $(SED) 's/[ ][0-9].*//' > wordlist
- $(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >> wordlist
- $(SED) '1,/BEGIN_TABLE/d' EZ.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" >> wordlist
- sort wordlist | uniq | $(SED) 's/ //g' > t
- mv t wordlist
-
-printutf8: printutf8.c
- $(CC) -o printutf8 printutf8.c
-
-unihan.t2s.t: Unihan.txt printutf8
- $(GREP) kSimplifiedVariant Unihan.txt | $(SED) '/#/d' | $(SED) 's/kSimplifiedVariant//' | ./printutf8 > unihan.t2s.t
-
-trad2simp.t: trad2simp.manual unihan.t2s.t
- cp unihan.t2s.t tmp1
- for I in `colrm 11 < trad2simp.manual` ; do $(SED) "/^$$I/d" tmp1 > tmp2; mv tmp2 tmp1; done
- cat trad2simp.manual tmp1 > trad2simp.t
-
-unihan.s2t.t: Unihan.txt printutf8
- $(GREP) kTraditionalVariant Unihan.txt | $(SED) '/#/d' | $(SED) 's/kTraditionalVariant//' | ./printutf8 > unihan.s2t.t
-
-simp2trad.t: unihan.s2t.t simp2trad.manual
- cp unihan.s2t.t tmp1
- for I in `colrm 11 < simp2trad.manual` ; do $(SED) "/^$$I/d" tmp1 > tmp2; mv tmp2 tmp1; done
- cat simp2trad.manual tmp1 > simp2trad.t
-
-t2s_1tomany.t: trad2simp.t
- $(GREP) -s ".\{19,\}" trad2simp.t | $(SED) 's/U+...../"/' | $(SED) 's/|U+...../"=>"/' | $(SED) 's/|U+.....//g' | $(SED) 's/|/",/' > t2s_1tomany.t
-
-t2s_1to1.t: trad2simp.t s2t_1tomany.t
- $(SED) "/.*|.*|.*|.*/d" trad2simp.t | $(SED) 's/U+[0-9a-z][0-9a-z]*/"/' | $(SED) 's/|U+[0-9a-z][0-9a-z]*/"=>"/' | $(SED) 's/|/",/' > t2s_1to1.t
- $(GREP) '"."=>"..",' s2t_1tomany.t | $(SED) 's/\("."\)=>".\(.\)",/"\2"=>\1,/' >> t2s_1to1.t
- $(GREP) '"."=>"...",' s2t_1tomany.t | $(SED) 's/\("."\)=>".\(.\).",/"\2"=>\1,/' >> t2s_1to1.t
- $(GREP) '"."=>"...",' s2t_1tomany.t | $(SED) 's/\("."\)=>"..\(.\)",/"\2"=>\1,/' >> t2s_1to1.t
- $(GREP) '"."=>"....",' s2t_1tomany.t | $(SED) 's/\("."\)=>".\(.\)..",/"\2"=>\1,/' >> t2s_1to1.t
- $(GREP) '"."=>"....",' s2t_1tomany.t | $(SED) 's/\("."\)=>"..\(.\).",/"\2"=>\1,/' >> t2s_1to1.t
- $(GREP) '"."=>"....",' s2t_1tomany.t | $(SED) 's/\("."\)=>"...\(.\)",/"\2"=>\1,/' >> t2s_1to1.t
- sort t2s_1to1.t | uniq > t
- mv t t2s_1to1.t
-
-
-s2t_1tomany.t: simp2trad.t
- $(GREP) -s ".\{19,\}" simp2trad.t | $(SED) 's/U+...../"/' | $(SED) 's/|U+...../"=>"/' | $(SED) 's/|U+.....//g' | $(SED) 's/|/",/' > s2t_1tomany.t
-
-s2t_1to1.t: simp2trad.t t2s_1tomany.t
- $(SED) "/.*|.*|.*|.*/d" simp2trad.t | $(SED) 's/U+[0-9a-z][0-9a-z]*/"/' | $(SED) 's/|U+[0-9a-z][0-9a-z]*/"=>"/' | $(SED) 's/|/",/' > s2t_1to1.t
- $(GREP) '"."=>"..",' t2s_1tomany.t | $(SED) 's/\("."\)=>".\(.\)",/"\2"=>\1,/' >> s2t_1to1.t
- $(GREP) '"."=>"...",' t2s_1tomany.t | $(SED) 's/\("."\)=>".\(.\).",/"\2"=>\1,/' >> s2t_1to1.t
- $(GREP) '"."=>"...",' t2s_1tomany.t | $(SED) 's/\("."\)=>"..\(.\)",/"\2"=>\1,/' >> s2t_1to1.t
- $(GREP) '"."=>"....",' t2s_1tomany.t | $(SED) 's/\("."\)=>".\(.\)..",/"\2"=>\1,/' >> s2t_1to1.t
- $(GREP) '"."=>"....",' t2s_1tomany.t | $(SED) 's/\("."\)=>"..\(.\).",/"\2"=>\1,/' >> s2t_1to1.t
- $(GREP) '"."=>"....",' t2s_1tomany.t | $(SED) 's/\("."\)=>"...\(.\)",/"\2"=>\1,/' >> s2t_1to1.t
- sort s2t_1to1.t | uniq > t
- mv t s2t_1to1.t
-
-tphrase.t: EZ.txt.in tsi.src
- colrm 1 8 < EZ.txt.in | $(SED) 's/\t//g' | $(GREP) "^.\{2,4\}[0-9]" | $(SED) 's/[0-9]//g' > t
- iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/ [0-9].*//g' | $(SED) 's/[# ]//g'| $(GREP) "^.\{2,4\}" >> t
- sort t | uniq > tphrase.t
-
-alltradphrases.t: tphrase.t s2t_1tomany.t tradphrases_exclude.manual
- for i in `cat s2t_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' |$(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' |sort | uniq`; do $(GREP) -s $$i tphrase.t ; done > alltradphrases.t || true
- cat alltradphrases.t | $(GREP) -vf tradphrases_exclude.manual > alltradphrases.tt ; mv alltradphrases.tt alltradphrases.t
-
-
-tradphrases_2.t: alltradphrases.t
- cat alltradphrases.t | $(GREP) "^..$$" | sort | uniq > tradphrases_2.t
-
-tradphrases_3.t: alltradphrases.t
- cat alltradphrases.t | $(GREP) "^...$$" | sort | uniq > tradphrases_3.t
- for i in `cat tradphrases_2.t`; do $(GREP) $$i tradphrases_3.t ; done | sort | uniq > t3 || true
- $(DIFF) t3 tradphrases_3.t | $(GREP) ">" | $(SED) 's/> //' > t
- mv t tradphrases_3.t
-
-
-tradphrases_4.t: alltradphrases.t
- cat alltradphrases.t | $(GREP) "^....$$" | sort | uniq > tradphrases_4.t
- for i in `cat tradphrases_2.t`; do $(GREP) $$i tradphrases_4.t ; done | sort | uniq > t3 || true
- $(DIFF) t3 tradphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
- mv t tradphrases_4.t
- for i in `cat tradphrases_3.t`; do $(GREP) $$i tradphrases_4.t ; done | sort | uniq > t3 || true
- $(DIFF) t3 tradphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
- mv t tradphrases_4.t
-
-tradphrases.t: tradphrases.manual tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany.t
- cat tradphrases.manual tradphrases_2.t tradphrases_3.t tradphrases_4.t |sort | uniq > tradphrases.t
- for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i tradphrases.t ; done | $(DIFF) tradphrases.t - | $(GREP) '<' | $(SED) 's/< //' > t
- for i in `$(SED) 's/"\(..\)..*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i tradphrases.t ; done | $(DIFF) tradphrases.t - | $(GREP) '<' | $(SED) 's/< //' >> t
- mv t tradphrases.t
- cat tradphrases.t | sort | uniq > t
- mv t tradphrases.t
-
-tradphrases.notsure: tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany.t
- cat tradphrases_2.t tradphrases_3.t tradphrases_4.t |sort | uniq > t
- for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i t; done | $(DIFF) t - | $(GREP) '>' | $(SED) 's/> //' > tradphrases.notsure
-
-
-ph.t: phrase_lib.txt
- $(SED) 's/[\t0-9a-zA-Z]//g' phrase_lib.txt | $(GREP) "^.\{2,4\}$$" > ph.t
-
-Wubi.t: Wubi.txt.in
- $(SED) '1,/BEGIN_TABLE/d' Wubi.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" > Wubi.t
-
-Ziranma.t: Ziranma.txt.in
- $(SED) '1,/BEGIN_TABLE/d' Ziranma.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" > Ziranma.t
-
-
-allsimpphrases.t: t2s_1tomany.t ph.t Wubi.t Ziranma.t simpphrases_exclude.manual
- rm -f allsimpphrases.t
- for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i Wubi.t >> allsimpphrases.t; done
- for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i Ziranma.t >> allsimpphrases.t; done
- for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i ph.t >> allsimpphrases.t; done
- cat allsimpphrases.t | $(GREP) -vf simpphrases_exclude.manual > allsimpphrases.tt ; mv allsimpphrases.tt allsimpphrases.t
-
-simpphrases_2.t: allsimpphrases.t
- cat allsimpphrases.t | $(GREP) "^..$$" | sort | uniq > simpphrases_2.t
-
-simpphrases_3.t: allsimpphrases.t
- cat allsimpphrases.t | $(GREP) "^...$$" | sort | uniq > simpphrases_3.t
- for i in `cat simpphrases_2.t`; do $(GREP) $$i simpphrases_3.t ; done | sort | uniq > t3 || true
- $(DIFF) t3 simpphrases_3.t | $(GREP) ">" | $(SED) 's/> //' > t
- mv t simpphrases_3.t
-
-simpphrases_4.t: allsimpphrases.t
- cat allsimpphrases.t | $(GREP) "^....$$" | sort | uniq > simpphrases_4.t
- rm -f t
- for i in `cat simpphrases_2.t`; do $(GREP) $$i simpphrases_4.t >> t; done || true
- sort t | uniq > t3
- $(DIFF) t3 simpphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
- mv t simpphrases_4.t
- for i in `cat simpphrases_3.t`; do $(GREP) $$i simpphrases_4.t; done | sort | uniq > t3 || true
- $(DIFF) t3 simpphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
- mv t simpphrases_4.t
-
-simpphrases.t: simpphrases.manual simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
- cat simpphrases.manual simpphrases_2.t simpphrases_3.t simpphrases_4.t > simpphrases.t
- for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i simpphrases.t ; done | $(DIFF) simpphrases.t - | $(GREP) '<' | $(SED) 's/< //' > t
- for i in `$(SED) 's/"\(..\)..*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i simpphrases.t ; done | $(DIFF) simpphrases.t - | $(GREP) '<' | $(SED) 's/< //' >> t
- mv t simpphrases.t
- cat simpphrases.t | sort | uniq > t
- mv t simpphrases.t
-
-simpphrases.notsure: simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
- cat simpphrases_2.t simpphrases_3.t simpphrases_4.t > t
- for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i t ; done | $(DIFF) t - | $(GREP) '>' | $(SED) 's/> //' > simpphrases.notsure
-
-trad2simp1to1.t: t2s_1tomany.t t2s_1to1.t trad2simp_noconvert.manual
- $(SED) 's/\(.......\).*/\1",/' t2s_1tomany.t > tt
- colrm 1 7 < trad2simp.manual | colrm 3 > trad2simpcharsrc.t
- colrm 1 17 < trad2simp.manual | colrm 3 > trad2simpchardest.t
- cat trad2simpcharsrc.t | $(GREP) -f trad2simpchardest.t > trad2simprepeatedchar.t
- cat tt | $(GREP) -vf trad2simprepeatedchar.t > trad2simp1to1.t
- cat t2s_1to1.t >> trad2simp1to1.t
- cat trad2simp1to1.t | $(GREP) -vf trad2simp_noconvert.manual > tt
- mv tt trad2simp1to1.t
-
-simp2trad1to1.t: s2t_1tomany.t s2t_1to1.t simp2trad.manual simp2trad_noconvert.manual
- $(SED) 's/\(.......\).*/\1",/' s2t_1tomany.t > tt
- colrm 1 7 < simp2trad.manual | colrm 3 > simp2tradcharsrc.t
- colrm 1 17 < simp2trad.manual | colrm 3 > simp2tradchardest.t
- cat simp2tradcharsrc.t | $(GREP) -f simp2tradchardest.t > simp2tradrepeatedchar.t
- cat tt | $(GREP) -vf simp2tradrepeatedchar.t > simp2trad1to1.t
- cat s2t_1to1.t >> simp2trad1to1.t
- cat simp2trad1to1.t | $(GREP) -vf simp2trad_noconvert.manual > tt
- mv tt simp2trad1to1.t
-
-trad2simp.php: trad2simp1to1.t tradphrases.t trad2simp_supp_unset.manual trad2simp_supp_set.manual
- printf '<?php\n$$trad2simp=array(' > trad2simp.php
- cat trad2simp1to1.t >> trad2simp.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' trad2simp_supp_set.manual >> trad2simp.php
- printf ');\n$$str=\n"' >> trad2simp.php
- cat tradphrases.t >> trad2simp.php
- printf '";\n$$t=strtr($$str, $$trad2simp);\necho $$t;\n?>' >> trad2simp.php
- cat trad2simp1to1.t | $(GREP) -vf trad2simp_supp_unset.manual > tt
- mv tt trad2simp1to1.t
-
-simp2trad.php: simp2trad1to1.t simpphrases.t simp2trad_supp_set.manual
- printf '<?php\n$$simp2trad=array(' > simp2trad.php
- cat simp2trad1to1.t >> simp2trad.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' simp2trad_supp_set.manual >> simp2trad.php
- printf ');\n$$str=\n"' >> simp2trad.php
- cat simpphrases.t >> simp2trad.php
- printf '";\n$$t=strtr($$str, $$simp2trad);\necho $$t;\n?>' >> simp2trad.php
-
-simp2trad.phrases.t: trad2simp.php tradphrases.t simp2trad_supp_set.manual
- php -f trad2simp.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
- cat tradphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
- paste tmp1 tmp2 > simp2trad.phrases.t
- colrm 3 < simp2trad_supp_set.manual > simp2trad_supp_noconvert.t
- cat trad2simp.php | $(GREP) -vf simp2trad_supp_noconvert.t > trad2simp.tt
- mv trad2simp.tt trad2simp.php
-
-trad2simp.phrases.t: simp2trad.php simpphrases.t trad2simp_supp_set.manual
- php -f simp2trad.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
- cat simpphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
- paste tmp1 tmp2 > trad2simp.phrases.t
- colrm 3 < trad2simp_supp_set.manual > trad2simp_supp_noconvert.t
- cat simp2trad.php | $(GREP) -vf trad2simp_supp_noconvert.t > simp2trad.tt
- mv simp2trad.tt simp2trad.php
-
-toHans.dict: trad2simp1to1.t trad2simp.phrases.t toSimp.manual
- cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHans.dict
- cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHans.dict
- cat toSimp.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' >> toHans.dict
-
-toHant.dict: simp2trad1to1.t simp2trad.phrases.t toTrad.manual
- cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHant.dict
- cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHant.dict
- cat toTrad.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' >> toHant.dict
-
-toTW.dict: toTW.manual
- cat toTW.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toTW.dict
-
-toHK.dict: toHK.manual
- cat toHK.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toHK.dict
-
-toCN.dict: toCN.manual
- cat toCN.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toCN.dict
-
-toSG.dict: toSG.manual
- cat toSG.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toSG.dict
-
-ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toSimp.manual toTrad.manual toCN.manual toHK.manual toSG.manual toTW.manual
- printf '<?php\n/**\n * Simplified / Traditional Chinese conversion tables\n' > ZhConversion.php
- printf ' *\n * Automatically generated using code and data in includes/zhtable/\n' >> ZhConversion.php
- printf ' * Do not modify directly!\n */\n\n' >> ZhConversion.php
- printf '$$zh2Hant = array(\n' >> ZhConversion.php
- cat simp2trad1to1.t >> ZhConversion.php
- echo >> ZhConversion.php
- cat simp2trad.phrases.t >> ZhConversion.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toTrad.manual >> ZhConversion.php
- echo ');' >> ZhConversion.php
- echo >> ZhConversion.php
- printf '$$zh2Hans = array(\n' >> ZhConversion.php
- cat trad2simp1to1.t >> ZhConversion.php
- echo >> ZhConversion.php
- cat trad2simp.phrases.t >> ZhConversion.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toSimp.manual >> ZhConversion.php
- echo ');' >> ZhConversion.php
- echo >> ZhConversion.php
- printf '$$zh2TW = array(\n' >> ZhConversion.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toTW.manual >> ZhConversion.php
- echo ');' >> ZhConversion.php
- echo >> ZhConversion.php
- printf '$$zh2HK = array(\n' >> ZhConversion.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toHK.manual >> ZhConversion.php
- echo ');' >> ZhConversion.php
- echo >> ZhConversion.php
- printf '$$zh2CN = array(\n' >> ZhConversion.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toCN.manual >> ZhConversion.php
- echo ');' >> ZhConversion.php
- echo >> ZhConversion.php
- printf '$$zh2SG = array(\n' >> ZhConversion.php
- $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toSG.manual >> ZhConversion.php
- echo >> ZhConversion.php
- printf ');' >> ZhConversion.php
-
-clean: cleantmp cleandl
-
-cleantmp:
- # Stuff unpacked from the files fetched by wget
- rm -f \
- Unihan.txt \
- EZ.txt.in \
- Wubi.txt.in \
- Ziranma.txt.in \
- phrase_lib.txt \
- tsi.src
- # Temporary files and other trash
- rm -f ZhConversion.php tmp1 tmp2 tmp3 t3 *.t trad2simp.php simp2trad.php *.dict printutf8 *~ \
- simpphrases.notsure tradphrases.notsure wordlist
-
-cleandl:
- rm -f \
- Unihan.zip \
- scim-tables-$(SCIM_TABLES_VER).tar.gz \
- scim-pinyin-$(SCIM_PINYIN_VER).tar.gz \
- libtabe-$(LIBTABE_VER).tgz
-
diff --git a/includes/zhtable/Makefile.py b/includes/zhtable/Makefile.py
deleted file mode 100644
index fd603ce4..00000000
--- a/includes/zhtable/Makefile.py
+++ /dev/null
@@ -1,391 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# @author Philip
-import tarfile as tf
-import zipfile as zf
-import os, re, shutil, sys, platform
-
-pyversion = platform.python_version()
-islinux = platform.system().lower() == 'linux'
-
-if pyversion[:3] in ['2.6', '2.7']:
- import urllib as urllib_request
- import codecs
- open = codecs.open
- _unichr = unichr
- if sys.maxunicode < 0x10000:
- def unichr(i):
- if i < 0x10000:
- return _unichr(i)
- else:
- return _unichr( 0xD7C0 + ( i>>10 ) ) + _unichr( 0xDC00 + ( i & 0x3FF ) )
-elif pyversion[:2] == '3.':
- import urllib.request as urllib_request
- unichr = chr
-
-def unichr2( *args ):
- return [unichr( int( i.split('<')[0][2:], 16 ) ) for i in args]
-
-def unichr3( *args ):
- return [unichr( int( i[2:7], 16 ) ) for i in args if i[2:7]]
-
-# DEFINE
-UNIHAN_VER = '5.2.0'
-SF_MIRROR = 'dfn'
-SCIM_TABLES_VER = '0.5.10'
-SCIM_PINYIN_VER = '0.5.91'
-LIBTABE_VER = '0.2.3'
-# END OF DEFINE
-
-def download( url, dest ):
- if os.path.isfile( dest ):
- print( 'File %s is up to date.' % dest )
- return
- global islinux
- if islinux:
- # we use wget instead urlretrieve under Linux,
- # because wget could display details like download progress
- os.system( 'wget %s -O %s' % ( url, dest ) )
- else:
- print( 'Downloading from [%s] ...' % url )
- urllib_request.urlretrieve( url, dest )
- print( 'Download complete.\n' )
- return
-
-def uncompress( fp, member, encoding = 'U8' ):
- name = member.rsplit( '/', 1 )[-1]
- print( 'Extracting %s ...' % name )
- fp.extract( member )
- shutil.move( member, name )
- if '/' in member:
- shutil.rmtree( member.split( '/', 1 )[0] )
- return open( name, 'rb', encoding, 'ignore' )
-
-unzip = lambda path, member, encoding = 'U8': \
- uncompress( zf.ZipFile( path ), member, encoding )
-
-untargz = lambda path, member, encoding = 'U8': \
- uncompress( tf.open( path, 'r:gz' ), member, encoding )
-
-def parserCore( fp, pos, beginmark = None, endmark = None ):
- if beginmark and endmark:
- start = False
- else: start = True
- mlist = set()
- for line in fp:
- if beginmark and line.startswith( beginmark ):
- start = True
- continue
- elif endmark and line.startswith( endmark ):
- break
- if start and not line.startswith( '#' ):
- elems = line.split()
- if len( elems ) < 2:
- continue
- elif len( elems[0] ) > 1 and \
- len( elems[pos] ) > 1: # words only
- mlist.add( elems[pos] )
- return mlist
-
-def tablesParser( path, name ):
- """ Read file from scim-tables and parse it. """
- global SCIM_TABLES_VER
- src = 'scim-tables-%s/tables/zh/%s' % ( SCIM_TABLES_VER, name )
- fp = untargz( path, src, 'U8' )
- return parserCore( fp, 1, 'BEGIN_TABLE', 'END_TABLE' )
-
-ezbigParser = lambda path: tablesParser( path, 'EZ-Big.txt.in' )
-wubiParser = lambda path: tablesParser( path, 'Wubi.txt.in' )
-zrmParser = lambda path: tablesParser( path, 'Ziranma.txt.in' )
-
-def phraseParser( path ):
- """ Read phrase_lib.txt and parse it. """
- global SCIM_PINYIN_VER
- src = 'scim-pinyin-%s/data/phrase_lib.txt' % SCIM_PINYIN_VER
- dst = 'phrase_lib.txt'
- fp = untargz( path, src, 'U8' )
- return parserCore( fp, 0 )
-
-def tsiParser( path ):
- """ Read tsi.src and parse it. """
- src = 'libtabe/tsi-src/tsi.src'
- dst = 'tsi.src'
- fp = untargz( path, src, 'big5hkscs' )
- return parserCore( fp, 0 )
-
-def unihanParser( path ):
- """ Read Unihan_Variants.txt and parse it. """
- fp = unzip( path, 'Unihan_Variants.txt', 'U8' )
- t2s = dict()
- s2t = dict()
- for line in fp:
- if line.startswith( '#' ):
- continue
- else:
- elems = line.split()
- if len( elems ) < 3:
- continue
- type = elems.pop( 1 )
- elems = unichr2( *elems )
- if type == 'kTraditionalVariant':
- s2t[elems[0]] = elems[1:]
- elif type == 'kSimplifiedVariant':
- t2s[elems[0]] = elems[1:]
- fp.close()
- return ( t2s, s2t )
-
-def applyExcludes( mlist, path ):
- """ Apply exclude rules from path to mlist. """
- excludes = open( path, 'rb', 'U8' ).read().split()
- excludes = [word.split( '#' )[0].strip() for word in excludes]
- excludes = '|'.join( excludes )
- excptn = re.compile( '.*(?:%s).*' % excludes )
- diff = [mword for mword in mlist if excptn.search( mword )]
- mlist.difference_update( diff )
- return mlist
-
-def charManualTable( path ):
- fp = open( path, 'rb', 'U8' )
- ret = {}
- for line in fp:
- elems = line.split( '#' )[0].split( '|' )
- elems = unichr3( *elems )
- if len( elems ) > 1:
- ret[elems[0]] = elems[1:]
- return ret
-
-def toManyRules( src_table ):
- tomany = set()
- for ( f, t ) in src_table.iteritems():
- for i in range( 1, len( t ) ):
- tomany.add( t[i] )
- return tomany
-
-def removeRules( path, table ):
- fp = open( path, 'rb', 'U8' )
- texc = list()
- for line in fp:
- elems = line.split( '=>' )
- f = t = elems[0].strip()
- if len( elems ) == 2:
- t = elems[1].strip()
- f = f.strip('"').strip("'")
- t = t.strip('"').strip("'")
- if f:
- try:
- table.pop( f )
- except:
- pass
- if t:
- texc.append( t )
- texcptn = re.compile( '^(?:%s)$' % '|'.join( texc ) )
- for (tmp_f, tmp_t) in table.copy().iteritems():
- if texcptn.match( tmp_t ):
- table.pop( tmp_f )
- return table
-
-def customRules( path ):
- fp = open( path, 'rb', 'U8' )
- ret = dict()
- for line in fp:
- elems = line.split( '#' )[0].split()
- if len( elems ) > 1:
- ret[elems[0]] = elems[1]
- return ret
-
-def dictToSortedList( src_table, pos ):
- return sorted( src_table.items(), key = lambda m: m[pos] )
-
-def translate( text, conv_table ):
- i = 0
- while i < len( text ):
- for j in range( len( text ) - i, 0, -1 ):
- f = text[i:][:j]
- t = conv_table.get( f )
- if t:
- text = text[:i] + t + text[i:][j:]
- i += len(t) - 1
- break
- i += 1
- return text
-
-def manualWordsTable( path, conv_table, reconv_table ):
- fp = open( path, 'rb', 'U8' )
- reconv_table = {}
- wordlist = [line.split( '#' )[0].strip() for line in fp]
- wordlist = list( set( wordlist ) )
- wordlist.sort( key = len, reverse = True )
- while wordlist:
- word = wordlist.pop()
- new_word = translate( word, conv_table )
- rcv_word = translate( word, reconv_table )
- if word != rcv_word:
- reconv_table[word] = word
- reconv_table[new_word] = word
- return reconv_table
-
-def defaultWordsTable( src_wordlist, src_tomany, char_conv_table, char_reconv_table ):
- wordlist = list( src_wordlist )
- wordlist.sort( key = len, reverse = True )
- word_conv_table = {}
- word_reconv_table = {}
- conv_table = char_conv_table.copy()
- reconv_table = char_reconv_table.copy()
- tomanyptn = re.compile( '(?:%s)' % '|'.join( src_tomany ) )
- while wordlist:
- conv_table.update( word_conv_table )
- reconv_table.update( word_reconv_table )
- word = wordlist.pop()
- new_word_len = word_len = len( word )
- while new_word_len == word_len:
- add = False
- test_word = translate( word, reconv_table )
- new_word = translate( word, conv_table )
- if not reconv_table.get( new_word ) \
- and ( test_word != word \
- or ( tomanyptn.search( word ) \
- and word != translate( new_word, reconv_table ) ) ):
- word_conv_table[word] = new_word
- word_reconv_table[new_word] = word
- try:
- word = wordlist.pop()
- except IndexError:
- break
- new_word_len = len(word)
- return word_reconv_table
-
-def PHPArray( table ):
- lines = ['\'%s\' => \'%s\',' % (f, t) for (f, t) in table if f and t]
- return '\n'.join(lines)
-
-def main():
- #Get Unihan.zip:
- url = 'http://www.unicode.org/Public/%s/ucd/Unihan.zip' % UNIHAN_VER
- han_dest = 'Unihan.zip'
- download( url, han_dest )
-
- # Get scim-tables-$(SCIM_TABLES_VER).tar.gz:
- url = 'http://%s.dl.sourceforge.net/sourceforge/scim/scim-tables-%s.tar.gz' % ( SF_MIRROR, SCIM_TABLES_VER )
- tbe_dest = 'scim-tables-%s.tar.gz' % SCIM_TABLES_VER
- download( url, tbe_dest )
-
- # Get scim-pinyin-$(SCIM_PINYIN_VER).tar.gz:
- url = 'http://%s.dl.sourceforge.net/sourceforge/scim/scim-pinyin-%s.tar.gz' % ( SF_MIRROR, SCIM_PINYIN_VER )
- pyn_dest = 'scim-pinyin-%s.tar.gz' % SCIM_PINYIN_VER
- download( url, pyn_dest )
-
- # Get libtabe-$(LIBTABE_VER).tgz:
- url = 'http://%s.dl.sourceforge.net/sourceforge/libtabe/libtabe-%s.tgz' % ( SF_MIRROR, LIBTABE_VER )
- lbt_dest = 'libtabe-%s.tgz' % LIBTABE_VER
- download( url, lbt_dest )
-
- # Unihan.txt
- ( t2s_1tomany, s2t_1tomany ) = unihanParser( han_dest )
-
- t2s_1tomany.update( charManualTable( 'trad2simp.manual' ) )
- s2t_1tomany.update( charManualTable( 'simp2trad.manual' ) )
-
- t2s_1to1 = dict( [( f, t[0] ) for ( f, t ) in t2s_1tomany.iteritems()] )
- s2t_1to1 = dict( [( f, t[0] ) for ( f, t ) in s2t_1tomany.iteritems()] )
-
- s_tomany = toManyRules( t2s_1tomany )
- t_tomany = toManyRules( s2t_1tomany )
-
- # noconvert rules
- t2s_1to1 = removeRules( 'trad2simp_noconvert.manual', t2s_1to1 )
- s2t_1to1 = removeRules( 'simp2trad_noconvert.manual', s2t_1to1 )
-
- # the supper set for word to word conversion
- t2s_1to1_supp = t2s_1to1.copy()
- s2t_1to1_supp = s2t_1to1.copy()
- t2s_1to1_supp.update( customRules( 'trad2simp_supp_set.manual' ) )
- s2t_1to1_supp.update( customRules( 'simp2trad_supp_set.manual' ) )
-
- # word to word manual rules
- t2s_word2word_manual = manualWordsTable( 'simpphrases.manual', s2t_1to1_supp, t2s_1to1_supp )
- t2s_word2word_manual.update( customRules( 'toSimp.manual' ) )
- s2t_word2word_manual = manualWordsTable( 'tradphrases.manual', t2s_1to1_supp, s2t_1to1_supp )
- s2t_word2word_manual.update( customRules( 'toTrad.manual' ) )
-
- # word to word rules from input methods
- t_wordlist = set()
- s_wordlist = set()
- t_wordlist.update( ezbigParser( tbe_dest ),
- tsiParser( lbt_dest ) )
- s_wordlist.update( wubiParser( tbe_dest ),
- zrmParser( tbe_dest ),
- phraseParser( pyn_dest ) )
-
- # exclude
- s_wordlist = applyExcludes( s_wordlist, 'simpphrases_exclude.manual' )
- t_wordlist = applyExcludes( t_wordlist, 'tradphrases_exclude.manual' )
-
- s2t_supp = s2t_1to1_supp.copy()
- s2t_supp.update( s2t_word2word_manual )
- t2s_supp = t2s_1to1_supp.copy()
- t2s_supp.update( t2s_word2word_manual )
-
- # parse list to dict
- t2s_word2word = defaultWordsTable( s_wordlist, s_tomany, s2t_1to1_supp, t2s_supp )
- t2s_word2word.update( t2s_word2word_manual )
- s2t_word2word = defaultWordsTable( t_wordlist, t_tomany, t2s_1to1_supp, s2t_supp )
- s2t_word2word.update( s2t_word2word_manual )
-
- # Final tables
- # sorted list toHans
- t2s_1to1 = dict( [( f, t ) for ( f, t ) in t2s_1to1.iteritems() if f != t] )
- toHans = dictToSortedList( t2s_1to1, 0 ) + dictToSortedList( t2s_word2word, 1 )
- # sorted list toHant
- s2t_1to1 = dict( [( f, t ) for ( f, t ) in s2t_1to1.iteritems() if f != t] )
- toHant = dictToSortedList( s2t_1to1, 0 ) + dictToSortedList( s2t_word2word, 1 )
- # sorted list toCN
- toCN = dictToSortedList( customRules( 'toCN.manual' ), 1 )
- # sorted list toHK
- toHK = dictToSortedList( customRules( 'toHK.manual' ), 1 )
- # sorted list toSG
- toSG = dictToSortedList( customRules( 'toSG.manual' ), 1 )
- # sorted list toTW
- toTW = dictToSortedList( customRules( 'toTW.manual' ), 1 )
-
- # Get PHP Array
- php = '''<?php
-/**
- * Simplified / Traditional Chinese conversion tables
- *
- * Automatically generated using code and data in includes/zhtable/
- * Do not modify directly!
- *
- * @file
- */
-
-$zh2Hant = array(\n'''
- php += PHPArray( toHant ) \
- + '\n);\n\n$zh2Hans = array(\n' \
- + PHPArray( toHans ) \
- + '\n);\n\n$zh2TW = array(\n' \
- + PHPArray( toTW ) \
- + '\n);\n\n$zh2HK = array(\n' \
- + PHPArray( toHK ) \
- + '\n);\n\n$zh2CN = array(\n' \
- + PHPArray( toCN ) \
- + '\n);\n\n$zh2SG = array(\n' \
- + PHPArray( toSG ) \
- + '\n);\n'
-
- f = open( os.path.join( '..', 'ZhConversion.php' ), 'wb', encoding = 'utf8' )
- print ('Writing ZhConversion.php ... ')
- f.write( php )
- f.close()
-
- # Remove temporary files
- print ('Deleting temporary files ... ')
- os.remove('EZ-Big.txt.in')
- os.remove('phrase_lib.txt')
- os.remove('tsi.src')
- os.remove('Unihan_Variants.txt')
- os.remove('Wubi.txt.in')
- os.remove('Ziranma.txt.in')
-
-
-if __name__ == '__main__':
- main()
diff --git a/includes/zhtable/README b/includes/zhtable/README
deleted file mode 100644
index 7e3f87e2..00000000
--- a/includes/zhtable/README
+++ /dev/null
@@ -1,33 +0,0 @@
-The various .manual files contains special mappings not included in the
-unihan database, and phrases not included in the SCIM package.
-
-- simp2trad.manual: Simplified to Traditional character mapping. Most
- data adapted from
-
- 冯寿忠,“非对称繁简字”对照表, 《语文建设通讯》1997-9第53期.
- /http://www.yywzw.com/jt/feng/fengb01.htm
-
-- trad2simp.manual: Traditional to Simplified character mapping.
-
-- simp2trad_noconvert.manual: Do not convert the chars as inapporiate.
-
-- trad2simp_noconvert.manual: Do not convert the chars as inapporiate.
-
-- tradphrases.manual: Phrases in Traditional Chinese. A portition is obtained
- from the TongWen package (http://tongwen.mozdev.org/)
-
-- simpphrases.manual: Phrases in Simplified Chinese.
-
-- tradphrases_exclude.manual: Excluding several phrases from
- the SCIM phrases as inappoiated.
-
-- simpphrases_exclude.manual: Excluding several phrases from
- the SCIM phrases as inapporated.
-
-- toTrad.manual, toSimp.manual: Special phrase mappings that
- tradphrases.manual or simphrases.manual cannot be handled.
-
-- toTW.manual, toCN.manual, toSG.manual and toHK.manual: Special phrase
- mappings.
-
-zhengzhu at gmail dot com & shinjiman at gmail dot com
diff --git a/includes/zhtable/printutf8.c b/includes/zhtable/printutf8.c
deleted file mode 100644
index b6ccf17c..00000000
--- a/includes/zhtable/printutf8.c
+++ /dev/null
@@ -1,99 +0,0 @@
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
-/*
- Unicode UTF8
-0x00000000 - 0x0000007F: 0xxxxxxx
-0x00000080 - 0x000007FF: 110xxx xx 10xx xxxx
-0x00000800 - 0x0000FFFF: 1110xxxx 10xxxx xx 10xx xxxx
-0x00010000 - 0x001FFFFF: 11110x xx 10xx xxxx 10xxxx xx 10xx xxxx
-0x00200000 - 0x03FFFFFF: 111110xx 10xxxx xx 10xx xxxx 10xxxx xx 10xx xxxx
-0x04000000 - 0x7FFFFFFF: 1111110x 10xx xxxx 10xxxx xx 10xx xxxx 10xxxx xx 10xx xxxx
-
-0000 0 1001 9
-0001 1 1010 A
-0010 2 1011 B
-0011 3 1100 C
-0100 4 1101 D
-0101 5 1110 E
-0110 6 1111 F
-0111 7
-1000 8
-*/
-void printUTF8(long long u) {
- long long m;
- if(u<0x80) {
- printf("%c", (unsigned char)u);
- }
- else if(u<0x800) {
- m = ((u&0x7c0)>>6) | 0xc0;
- printf("%c", (unsigned char)m);
- m = (u&0x3f) | 0x80;
- printf("%c", (unsigned char)m);
- }
- else if(u<0x10000) {
- m = ((u&0xf000)>>12) | 0xe0;
- printf("%c",(unsigned char)m);
- m = ((u&0xfc0)>>6) | 0x80;
- printf("%c",(unsigned char)m);
- m = (u & 0x3f) | 0x80;
- printf("%c",(unsigned char)m);
- }
- else if(u<0x200000) {
- m = ((u&0x1c0000)>>18) | 0xf0;
- printf("%c", (unsigned char)m);
- m = ((u& 0x3f000)>>12) | 0x80;
- printf("%c", (unsigned char)m);
- m = ((u& 0xfc0)>>6) | 0x80;
- printf("%c", (unsigned char)m);
- m = (u&0x3f) | 0x80;
- printf("%c", (unsigned char)m);
- }
- else if(u<0x4000000){
- m = ((u&0x3000000)>>24) | 0xf8;
- printf("%c", (unsigned char)m);
- m = ((u&0xfc0000)>>18) | 0x80;
- printf("%c", (unsigned char)m);
- m = ((u&0x3f000)>>12) | 0x80;
- printf("%c", (unsigned char)m);
- m = ((u&0xfc00)>>6) | 0x80;
- printf("%c", (unsigned char)m);
- m = (u&0x3f) | 0x80;
- printf("%c", (unsigned char)m);
- }
- else {
- m = ((u&0x40000000)>>30) | 0xfc;
- printf("%c", (unsigned char)m);
- m = ((u&0x3f000000)>>24) | 0x80;
- printf("%c", (unsigned char)m);
- m = ((u&0xfc0000)>>18) | 0x80;
- printf("%c", (unsigned char)m);
- m = ((u&0x3f000)>>12) | 0x80;
- printf("%c", (unsigned char)m);
- m = ((u&0xfc0)>>6) | 0x80;
- printf("%c", (unsigned char)m);
- m = (u&0x3f)| 0x80;
- printf("%c", (unsigned char)m);
- }
-}
-
-int main() {
- int i,j;
- long long n1, n2;
- unsigned char b1[15], b2[15];
- unsigned char buf[1024];
- i=0;
- while(fgets(buf, 1024, stdin)) {
- // printf("read %s\n", buf);
- for(i=0;i<strlen(buf); i++)
- if(buf[i]=='U') {
- if(buf[i+1] == '+') {
- n1 = strtoll(buf+i+2,0,16);
- printf("U+%05x", n1);
- printUTF8(n1);printf("|");
- }
- }
- printf("\n");
- }
-}
-
diff --git a/includes/zhtable/simp2trad.manual b/includes/zhtable/simp2trad.manual
deleted file mode 100644
index 1b84f8e7..00000000
--- a/includes/zhtable/simp2trad.manual
+++ /dev/null
@@ -1,372 +0,0 @@
-U+03CE0㳠|U+06FBE澾|
-U+0447D䑽|U+26A99𦪙|
-U+0497A䥺|U+091FE釾|
-U+0497D䥽|U+093FA鏺|
-U+04983䦃|U+0942F鐯|
-U+04985䦅|U+09425鐥|
-U+04B6A䭪|U+297AF𩞯|
-U+04C9F䲟|U+09BA3鮣|
-U+04CA0䲠|U+09C06鰆|
-U+04CA1䲡|U+09C0C鰌|
-U+04CA2䲢|U+09C27鰧|
-U+04CA3䲣|U+04C77䱷|
-U+04DAE䶮|U+09F91龑|
-U+04E07万|U+0842C萬|U+04E07万|
-U+04E0E与|U+08207與|U+04E0E与|
-U+04E11丑|U+04E11丑|U+0919C醜|
-U+04E2A个|U+0500B個|U+07B87箇|
-U+04E30丰|U+08C50豐|U+04E30丰|
-U+04E3A为|U+070BA為|U+07232爲|
-U+04E48么|U+04E48么|U+09EBD麽|U+05E7A幺|U+09EBC麼|
-U+04E86了|U+04E86了|U+077AD瞭|
-U+04E8E于|U+065BC於|U+04E8E于|
-U+04E91云|U+096F2雲|U+04E91云|
-U+04EA7产|U+07522產|U+07523産|
-U+04EC6仆|U+04EC6仆|U+050D5僕|
-U+04EC7仇|U+04EC7仇|U+08B8E讎|
-U+04ED1仑|U+04F96侖|U+05D19崙|
-U+04EF7价|U+050F9價|U+04EF7价|
-U+04F17众|U+0773E眾|U+08846衆|
-U+04F19伙|U+04F19伙|U+05925夥|
-U+04F2A伪|U+0507D偽|U+050DE僞|
-U+04F53体|U+09AD4體|U+04F53体|
-U+04F59余|U+04F59余|U+09918餘|
-U+04F63佣|U+04F63佣|U+050AD傭|
-U+0501F借|U+0501F借|U+085C9藉|
-U+0513F儿|U+05152兒|U+0513F儿|
-U+0514B克|U+0514B克|U+0524B剋|
-U+0515A党|U+09EE8黨|U+0515A党|
-U+051AC冬|U+051AC冬|U+09F15鼕|
-U+051B2冲|U+06C96沖|U+0885D衝|
-U+051C6准|U+051C6准|U+06E96準|
-U+051E0几|U+05E7E幾|U+051E0几|
-U+051EB凫|U+09CE7鳧|U+09CEC鳬|
-U+051FA出|U+051FA出|U+09F63齣|
-U+05212划|U+05283劃|U+05212划|
-U+0522B别|U+05225別|U+05F46彆|
-U+0522E刮|U+0522E刮|U+098B3颳|
-U+05236制|U+05236制|U+088FD製|
-U+05343千|U+05343千|U+097C6韆|
-U+05347升|U+05347升|U+06607昇|U+0965E陞|
-U+0535C卜|U+0535C卜|U+08514蔔|
-U+05360占|U+05360占|U+04F54佔|
-U+05364卤|U+09E75鹵|U+06EF7滷|
-U+05377卷|U+05377卷|U+06372捲|
-U+0537A卺|U+05DF9巹|
-U+05382厂|U+05EE0廠|U+05382厂|
-U+05386历|U+06B77歷|U+066C6曆|U+053A4厤|
-U+05395厕|U+05EC1廁|U+053A0厠|
-U+05398厘|U+05398厘|U+091D0釐|
-U+053D1发|U+0767C發|U+09AEE髮|
-U+053EA只|U+053EA只|U+096BB隻|
-U+053F0台|U+053F0台|U+081FA臺|U+06AAF檯|U+098B1颱|
-U+053F6叶|U+08449葉|U+053F6叶|
-U+05401吁|U+05401吁|U+07C72籲|
-U+05408合|U+05408合|U+095A4閤|
-U+0540A吊|U+0540A吊|U+05F14弔|
-U+0540C同|U+0540C同|U+08855衕|
-U+0540E后|U+05F8C後|U+0540E后|
-U+05411向|U+05411向|U+056AE嚮|U+066CF曏|
-U+0542F启|U+0555F啟|U+05553啓|
-U+05446呆|U+05446呆|U+07343獃|
-U+054B8咸|U+054B8咸|U+09E79鹹|
-U+054C4哄|U+054C4哄|U+09B28鬨|
-U+05582喂|U+05582喂|U+09935餵|
-U+056DE回|U+056DE回|U+08FF4迴|
-U+056E2团|U+05718團|U+07CF0糰|
-U+056F0困|U+056F0困|U+0774F睏|
-U+05742坂|U+05742坂|U+0962A阪|
-U+0574F坏|U+058DE壞|U+0574F坏|
-U+0575B坛|U+058C7壇|U+07F48罈|
-U+057FC埼|U+057FC埼|U+07895碕|
-U+05899墙|U+07246牆|U+058BB墻|
-U+058F3壳|U+06BBC殼|U+06BBB殻|
-U+0590D复|U+05FA9復|U+08907複|
-U+05956奖|U+0734E獎|U+0596C奬|
-U+05978奸|U+05978奸|U+059E6姦|
-U+059AB妫|U+05AAF媯|U+05B00嬀|
-U+059DC姜|U+059DC姜|U+08591薑|
-U+05B81宁|U+05BE7寧|U+05B81宁|
-U+05BB6家|U+05BB6家|U+050A2傢|
-U+05C3D尽|U+076E1盡|U+05118儘|
-U+05CB3岳|U+05CB3岳|U+05DBD嶽|
-U+05E03布|U+05E03布|U+04F48佈|
-U+05E18帘|U+07C3E簾|U+05E18帘|
-U+05E5E幞|U+08946襆|
-U+05E72干|U+05E72干|U+04E7E乾|U+05E79幹|U+069A6榦|
-U+05E76并|U+04E26並|U+04F75併|
-U+05E78幸|U+05E78幸|U+05016倖|
-U+05E7F广|U+05EE3廣|U+05E7F广|
-U+05E84庄|U+05E84庄|U+0838A莊|
-U+05EB5庵|U+05EB5庵|U+083F4菴|
-U+05F25弥|U+05F4C彌|U+07030瀰|
-U+05F53当|U+07576當|U+05679噹|
-U+05F55录|U+09304錄|U+09332録|
-U+05F69彩|U+05F69彩|U+07DB5綵|
-U+05F81征|U+05F81征|U+05FB5徵|
-U+05FA1御|U+05FA1御|U+079A6禦|
-U+05FD7志|U+05FD7志|U+08A8C誌|
-U+06076恶|U+060E1惡|U+05641噁|
-U+060AB悫|U+06128愨|U+06164慤|
-U+0613F愿|U+09858願|U+0613F愿|
-U+0621A戚|U+0621A戚|U+0617C慼|U+093DA鏚|
-U+0624D才|U+0624D才|U+07E94纔|
-U+0624E扎|U+0624E扎|U+07D2E紮|
-U+06258托|U+06258托|U+08A17託|
-U+06298折|U+06298折|U+0647A摺|
-U+062C5担|U+064D4擔|U+062C5担|
-U+062FC拼|U+062FC拼|U+062DA拚|
-U+06328挨|U+06328挨|U+06371捱|
-U+0633D挽|U+0633D挽|U+08F13輓|
-U+0636E据|U+064DA據|U+0636E据|
-U+06597斗|U+06597斗|U+09B25鬥|
-U+065CB旋|U+065CB旋|U+0955F镟|
-U+065D7旗|U+065D7旗|U+065C2旂|
-U+066F2曲|U+066F2曲|U+09EAF麯|U+09EB4麯|
-U+0672F术|U+08853術|U+0672E朮|
-U+06731朱|U+06731朱|U+07843硃|
-U+06734朴|U+06734朴|U+06A38樸|
-U+0676F杯|U+0676F杯|U+076C3盃|
-U+0677E松|U+0677E松|U+09B06鬆|
-U+0677F板|U+0677F板|U+095C6闆|
-U+06781极|U+06975極|U+06781极|
-U+067DC柜|U+06AC3櫃|U+067DC柜|
-U+06817栗|U+06817栗|U+06144慄|
-U+06881梁|U+06881梁|U+06A11樑|
-U+068F1棱|U+068F1棱|U+07A1C稜|
-U+06B32欲|U+06B32欲|U+0617E慾|
-U+06C47汇|U+0532F匯|U+06ED9滙|U+05F59彙|
-U+06C84沄|U+06C84沄|U+06F90澐|
-U+06C88沈|U+06C88沈|U+0700B瀋|
-U+06CA9沩|U+06E88溈|U+06F59潙|
-U+06CE8注|U+06CE8注|U+08A3B註|
-U+06D82涂|U+05857塗|U+06D82涂|
-U+06D8C涌|U+06D8C涌|U+06E67湧|
-U+06DC0淀|U+06DC0淀|U+06FB1澱|
-U+06E38游|U+06E38游|U+0904A遊|
-U+06EAF溯|U+06EAF溯|U+06CDD泝|
-U+06F13漓|U+06F13漓|U+07055灕|
-U+070BC炼|U+07149煉|U+0934A鍊|
-U+0753B画|U+0756B畫|U+07575畵|
-U+075C7症|U+075C7症|U+07665癥|
-U+07618瘘|U+0763A瘺|U+0763B瘻|
-U+0786E确|U+078BA確|U+0786E确|
-U+07877硷|U+07906礆|U+09E7C鹼|
-U+079CB秋|U+079CB秋|U+097A6鞦|
-U+079CD种|U+07A2E種|U+079CD种|
-U+07A57穗|U+07A57穗|U+07E50繐|
-U+07AD6竖|U+08C4E豎|U+07AEA竪|
-U+07B51筑|U+07BC9築|U+07B51筑|
-U+07B7E签|U+07C3D簽|U+07C64籤|
-U+07CFB系|U+07CFB系|U+07E6B繫|U+04FC2係|
-U+07D2F累|U+07D2F累|U+07E8D纍|
-U+07EA4纤|U+07E96纖|U+07E34縴|
-U+07EBF线|U+07DDA線|U+07DAB綫|
-U+07EDD绝|U+07D55絕|U+07D76絶|
-U+07EE3绣|U+07D89綉|U+07E61繡|
-U+07EE6绦|U+07D5B絛|U+07E27縧|
-U+07EF1绱|U+07DD4緔|U+0979D鞝|
-U+07EF7绷|U+07DB3綳|U+07E43繃|
-U+07EFF绿|U+07DA0綠|U+07DD1緑|
-U+07F30缰|U+097C1韁|U+07E6E繮|
-U+07FA1羡|U+07FA8羨|
-U+080DC胜|U+052DD勝|U+080DC胜|
-U+080E1胡|U+080E1胡|U+09B0D鬍|U+0885A衚|
-U+0810F脏|U+081DF臟|U+09AD2髒|
-U+0814A腊|U+081D8臘|U+0814A腊|
-U+081F4致|U+081F4致|U+07DFB緻|
-U+0820D舍|U+0820D舍|U+06368捨|
-U+082B8芸|U+082B8芸|U+08553蕓|
-U+082CE苎|U+082E7苧|
-U+082CF苏|U+08607蘇|U+056CC囌|U+07C64甦|
-U+082E7苧|U+085B4薴|
-U+082F9苹|U+0860B蘋|U+082F9苹|
-U+08303范|U+08303范|U+07BC4範|
-U+0836F药|U+0846F葯|U+085E5藥|
-U+083B7获|U+07372獲|U+07A6B穫|
-U+083BC莼|U+08493蒓|U+084F4蓴|
-U+08499蒙|U+08499蒙|U+077C7矇|U+06FDB濛|U+061DE懞|
-U+084D1蓑|U+084D1蓑|U+07C11簑|
-U+08511蔑|U+08511蔑|U+0884A衊|
-U+08574蕴|U+0860A蘊|U+085F4藴|
-U+0866B虫|U+087F2蟲|U+0866B虫|
-U+08721蜡|U+0881F蠟|U+08721蜡|
-U+0874E蝎|U+0880D蠍|
-U+08868表|U+08868表|U+09336錶|
-U+08BF4说|U+08AAA說|U+08AAC説|
-U+08C23谣|U+08B20謠|U+08B21謡|
-U+08C2B谫|U+08B7E譾|U+08B2D謭|
-U+08C37谷|U+08C37谷|U+07A40穀|
-U+08D43赃|U+08D13贓|U+08D1C贜|
-U+08D4D赍|U+09F4E齎|U+08CEB賫|
-U+08D5D赝|U+08D17贗|U+08D0B贋|
-U+08D5E赞|U+08D0A贊|U+08B9A讚|
-U+08F9F辟|U+08F9F辟|U+095E2闢|
-U+09002适|U+09069適|U+09002适|
-U+090C1郁|U+090C1郁|U+09B31鬱|
-U+0915D酝|U+0919E醞|U+09196醖|
-U+09170酰|U+09170酰|U+091AF醯|
-U+09178酸|U+09178酸|U+075E0痠|
-U+091C7采|U+091C7采|U+063A1採|U+057F0埰|
-U+091CC里|U+091CC里|U+088E1裡|U+088CF裏|
-U+093AD鎭|U+093AE鎮|
-U+0949F钟|U+0937E鍾|U+09418鐘|
-U+094A9钩|U+09264鉤|U+0920E鈎|
-U+094B5钵|U+07F3D缽|U+09262鉢|
-U+094F2铲|U+093DF鏟|U+05277剷|
-U+09508锈|U+092B9銹|U+093FD鏽|
-U+09510锐|U+092B3銳|U+092ED鋭|
-U+09528锨|U+06774杴|U+09341鍁|
-U+0954C镌|U+0942B鐫|U+093B8鎸|
-U+09562镢|U+09481钁|U+0941D鐝|
-U+09605阅|U+095B1閱|U+095B2閲|
-U+096C7雇|U+096C7雇|U+050F1僱|
-U+096D5雕|U+096D5雕|U+09D70鵰|
-U+09709霉|U+09709霉|U+09EF4黴|
-U+09762面|U+09762面|U+09EB5麵|U+09EAA麪|U+09EAB麫|
-U+097B2鞲|U+097DD韝|
-U+0987B须|U+09808須|U+09B1A鬚|
-U+09893颓|U+09839頹|U+0983D頽|
-U+0989C颜|U+0984F顏|U+09854顔|
-U+09965饥|U+098E2飢|U+09951饑|
-U+09980馀|U+09918餘|
-U+09986馆|U+09928館|U+08218舘|
-U+09A82骂|U+07F75罵|U+099E1駡|
-U+09C87鲇|U+09BF0鯰|U+09B8E鮎|
-U+09C9E鲞|U+09BD7鯗|U+09B9D鮝|
-U+09CC4鳄|U+09C77鱷|U+09C10鰐|
-U+09E21鸡|U+096DE雞|U+09DC4鷄|
-U+09E5A鹚|U+09DBF鶿|U+09DC0鷀|
-U+09E6E鹮|U+04D09䴉|
-U+09F44齄|U+09F47齇|
-U+20BB6𠮶|U+055F0嗰|
-U+26216𦈖|U+04308䌈|
-U+28C3E𨰾|U+093B7鎷|
-U+28C3F𨰿|U+091F3釳|
-U+28C40𨱀|U+2895B𨥛|
-U+28C41𨱁|U+09220鈠|
-U+28C42𨱂|U+0920B鈋|
-U+28C43𨱃|U+09232鈲|
-U+28C44𨱄|U+0922F鈯|
-U+28C45𨱅|U+09241鉁|
-U+28C47𨱇|U+092B6銶|
-U+28C48𨱈|U+092C9鋉|
-U+28C49𨱉|U+09344鍄|
-U+28C4A𨱊|U+289F1𨧱|
-U+28C4B𨱋|U+09302錂|
-U+28C4C𨱌|U+093C6鏆|
-U+28C4D𨱍|U+093AF鎯|
-U+28C4E𨱎|U+0936E鍮|
-U+28C4F𨱏|U+0939D鎝|
-U+28C50𨱐|U+28AD2𨫒|
-U+28C52𨱒|U+093C9鏉|
-U+28C53𨱓|U+0940E鐎|
-U+28C54𨱔|U+0940F鐏|
-U+28C55𨱕|U+28B82𨮂|
-U+28E02𨸂|U+0958D閍|
-U+28E03𨸃|U+09590閐|
-U+293FC𩏼|U+04A8F䪏|
-U+293FD𩏽|U+293EA𩏪|
-U+293FE𩏾|U+293A2𩎢|
-U+293FF𩏿|U+04A98䪘|
-U+29400𩐀|U+04A97䪗|
-U+29595𩖕|U+294E3𩓣|
-U+29596𩖖|U+09843顃|
-U+29597𩖗|U+04AF4䫴|
-U+29665𩙥|U+098B0颰|
-U+29666𩙦|U+295C0𩗀|
-U+29667𩙧|U+295E1𩗡|
-U+29668𩙨|U+29639𩘹|
-U+29669𩙩|U+29600𩘀|
-U+2966A𩙪|U+098B7颷|
-U+2966B𩙫|U+098BE颾|
-U+2966C𩙬|U+2963A𩘺|
-U+2966D𩙭|U+2961D𩘝|
-U+2966E𩙮|U+04B18䬘|
-U+2966F𩙯|U+04B1D䬝|
-U+29670𩙰|U+29648𩙈|
-U+29805𩠅|U+297D0𩟐|
-U+29806𩠆|U+29726𩜦|
-U+29807𩠇|U+04B40䭀|
-U+29808𩠈|U+04B43䭃|
-U+2980B𩠋|U+29754𩝔|
-U+2980C𩠌|U+09938餸|
-U+299E6𩧦|U+2987A𩡺|
-U+299E8𩧨|U+099CE駎|
-U+299E9𩧩|U+2990A𩤊|
-U+299EA𩧪|U+04BBE䮾|
-U+299EB𩧫|U+099DA駚|
-U+299EC𩧬|U+298A1𩢡|
-U+299ED𩧭|U+04B7F䭿|
-U+299EE𩧮|U+298BE𩢾|
-U+299EF𩧯|U+09A4B驋|
-U+299F0𩧰|U+04B9D䮝|
-U+299F1𩧱|U+29949𩥉|
-U+299F2𩧲|U+099E7駧|
-U+299F3𩧳|U+298B8𩢸|
-U+299F4𩧴|U+099E9駩|
-U+299F5𩧵|U+298B4𩢴|
-U+299F6𩧶|U+298CF𩣏|
-U+299FA𩧺|U+099F6駶|
-U+299FB𩧻|U+298F5𩣵|
-U+299FC𩧼|U+298FA𩣺|
-U+299FF𩧿|U+04BA0䮠|
-U+29A00𩨀|U+09A14騔|
-U+29A01𩨁|U+04B9E䮞|
-U+29A03𩨃|U+09A1D騝|
-U+29A04𩨄|U+09A2A騪|
-U+29A05𩨅|U+29938𩤸|
-U+29A06𩨆|U+29919𩤙|
-U+29A08𩨈|U+09A1F騟|
-U+29A09𩨉|U+29932𩤲|
-U+29A0A𩨊|U+09A1A騚|
-U+29A0B𩨋|U+29944𩥄|
-U+29A0C𩨌|U+29951𩥑|
-U+29A0D𩨍|U+29947𩥇|
-U+29A0F𩨏|U+04BB3䮳|
-U+29A10𩨐|U+299C6𩧆|
-U+29F79𩽹|U+09B65魥|
-U+29F7A𩽺|U+29D69𩵩|
-U+29F7B𩽻|U+29D79𩵹|
-U+29F7C𩽼|U+09BF6鯶|
-U+29F7D𩽽|U+29DB1𩶱|
-U+29F7E𩽾|U+09B9F鮟|
-U+29F7F𩽿|U+29DB0𩶰|
-U+29F80𩾀|U+09B95鮕|
-U+29F81𩾁|U+09BC4鯄|
-U+29F83𩾃|U+09BB8鮸|
-U+29F84𩾄|U+29DF0𩷰|
-U+29F85𩾅|U+29E03𩸃|
-U+29F86𩾆|U+29E26𩸦|
-U+29F87𩾇|U+09BF1鯱|
-U+29F88𩾈|U+04C59䱙|
-U+29F8A𩾊|U+04C6C䱬|
-U+29F8B𩾋|U+04C70䱰|
-U+29F8C𩾌|U+09C47鱇|
-U+29F8C𩾌|U+09C47鱇|
-U+29F8E𩾎|U+29F47𩽇|
-U+2A242𪉂|U+04CB0䲰|
-U+2A243𪉃|U+09CFC鳼|
-U+2A244𪉄|U+29FEA𩿪|
-U+2A245𪉅|U+2A026𪀦|
-U+2A246𪉆|U+09D32鴲|
-U+2A248𪉈|U+09D1C鴜|
-U+2A249𪉉|U+2A048𪁈|
-U+2A24A𪉊|U+09DE8鷨|
-U+2A24B𪉋|U+2A03E𪀾|
-U+2A24C𪉌|U+2A056𪁖|
-U+2A24D𪉍|U+09D5A鵚|
-U+2A24E𪉎|U+2A086𪂆|
-U+2A24F𪉏|U+2A0CF𪃏|
-U+2A250𪉐|U+2A0CD𪃍|
-U+2A251𪉑|U+09DD4鷔|
-U+2A252𪉒|U+2A115𪄕|
-U+2A254𪉔|U+2A106𪄆|
-U+2A255𪉕|U+2A1F3𪇳|
-U+2A388𪎈|U+04D2C䴬|
-U+2A389𪎉|U+09EB2麲|
-U+2A38A𪎊|U+09EA8麨|
-U+2A38B𪎋|U+04D34䴴|
-U+2A38C𪎌|U+09EB3麳|
-U+2A68F𪚏|U+2A600𪘀|
-U+2A690𪚐|U+2A62F𪘯|
diff --git a/includes/zhtable/simp2trad_noconvert.manual b/includes/zhtable/simp2trad_noconvert.manual
deleted file mode 100644
index a46560a7..00000000
--- a/includes/zhtable/simp2trad_noconvert.manual
+++ /dev/null
@@ -1,139 +0,0 @@
-著
-竈
-彞
-=>"余"
-=>"𫗭"
-=>"𪨧"
-=>"𫚭"
-=>"𫔀"
-=>"𫊻"
-=>"𫋌"
-=>"蚃"
-=>"𩾂"
-=>"𫚜"
-=>"𫚢"
-=>"𧉰"
-=>"䙌"
-=>"𫊮"
-=>"𫋇"
-=>"𫉄"
-=>"𫘛"
-=>"𫘜"
-=>"𫘝"
-=>"𫘟"
-=>"𩧨"
-=>"𩧫"
-=>"𫘞"
-=>"𫘠"
-=>"𩧲"
-=>"𩧴"
-=>"𫘡"
-=>"𩧺"
-=>"𫘣"
-=>"𫘤"
-=>"𫘧"
-=>"𫘥"
-=>"𫘦"
-=>"𩨀"
-=>"𩨊"
-=>"𫘩"
-=>"𩨃"
-=>"𫘪"
-=>"𫘪"
-=>"𫘫"
-=>"𫘬"
-=>"𩨈"
-=>"𫘨"
-=>"𩨄"
-=>"𫘭"
-=>"𩧯"
-=>"𫘯"
-=>"𫘰"
-=>"𫘱"
-=>"𫘽"
-=>"𫚉"
-=>"𩽹"
-=>"𫚌"
-=>"𫚍"
-=>"𫚒"
-=>"𫚑"
-=>"𫚖"
-=>"𩽾"
-=>"䲟"
-=>"𫚓"
-=>"𫚗"
-=>"𫚔"
-=>"𫚛"
-=>"𩾃"
-=>"𫚚"
-=>"𩾁"
-=>"𫚙"
-=>"𫚡"
-=>"𫚞"
-=>"𩾇"
-=>"𩽼"
-=>"𫚣"
-=>"䲠"
-=>"䲡"
-=>"𫚊"
-=>"𫚥"
-=>"𫚕"
-=>"𫚤"
-=>"䲢"
-=>"𫚦"
-=>"𫚧"
-=>"𫚋"
-=>"𩾌"
-=>"𫚪"
-=>"𫚫"
-=>"𫚈"
-=>"𫚭"
-=>"𫛛"
-=>"𪉃"
-=>"𫛚"
-=>"𫛜"
-=>"𫛞"
-=>"𫛝"
-=>"𫛤"
-=>"𫛡"
-=>"𫁡"
-=>"𪉈"
-=>"𫛣"
-=>"𫛦"
-=>"𪉆"
-=>"𫛩"
-=>"𫛪"
-=>"𫛥"
-=>"𪉍"
-=>"𫛭"
-=>"𫛨"
-=>"𫛳"
-=>"𫛱"
-=>"𫛲"
-=>"𫛵"
-=>"𫛶"
-=>"𫛸"
-=>"𫛷"
-=>"𫛯"
-=>"𫛫"
-=>"𫛽"
-=>"𫜀"
-=>"𪉑"
-=>"𫜃"
-=>"𫛴"
-=>"𪉊"
-=>"𫜁"
-=>"𫜄"
-=>"𫛢"
-=>"𫛟"
-=>"𪎊"
-=>"𤿲"
-=>"𪎉"
-=>"𪎌"
-=>"𫜑"
-=>"𫜩"
-=>"𫜪"
-=>"𫜭"
-=>"𫜬"
-=>"𫜮"
-=>"𫜰"
diff --git a/includes/zhtable/simp2trad_supp_set.manual b/includes/zhtable/simp2trad_supp_set.manual
deleted file mode 100644
index a5038a5d..00000000
--- a/includes/zhtable/simp2trad_supp_set.manual
+++ /dev/null
@@ -1,2 +0,0 @@
-余 餘
-着 著 \ No newline at end of file
diff --git a/includes/zhtable/simpphrases.manual b/includes/zhtable/simpphrases.manual
deleted file mode 100644
index d8602fec..00000000
--- a/includes/zhtable/simpphrases.manual
+++ /dev/null
@@ -1,2239 +0,0 @@
-乾上乾下
-乾为天
-乾为阳
-乾九
-乾乾
-乾亨
-乾仪
-乾位
-乾健
-乾元
-乾光
-乾兴
-乾冈
-乾刘
-乾刚
-乾化
-乾卦
-乾县
-乾台
-乾吉
-乾启
-乾命
-乾和
-乾嘉
-乾图
-乾坤
-乾城
-乾基
-乾始
-乾姓
-乾宁
-乾宅
-乾宇
-乾安
-乾定
-乾封
-乾居
-乾岗
-乾巛
-乾州
-乾式
-乾录
-乾律
-乾德
-乾心
-乾文
-乾断
-乾方
-乾施
-乾旦
-乾明
-乾昧
-乾晖
-乾景
-乾晷
-乾曜
-乾构
-乾枢
-乾栋
-乾步
-乾氏
-乾泉
-乾清宫
-乾渥
-乾灵
-乾男
-乾皋
-乾盛世
-乾矢
-乾祐
-乾穹
-乾窦
-乾竺
-乾笃
-乾符
-乾策
-乾精
-乾红
-乾纲
-乾纽
-乾络
-乾统
-乾维
-乾罗
-乾花
-乾荫
-乾行
-乾衡
-乾覆
-乾象
-乾象历
-乾贞
-乾贶
-乾车
-乾轴
-乾造
-乾道
-乾鉴
-乾钧
-乾闼
-乾陀
-乾陵
-乾隆
-乾音
-乾顾
-乾风
-乾首
-乾马
-乾鹄
-乾鹊
-乾龙
-乾,健也
-乾,天也
-乾健也
-乾天也
-坤乾
-天道为乾
-尼乾陀
-康乾
-张法乾
-旋乾转坤
-易·乾
-《易乾
-周易乾
-易经·乾
-易经乾
-李乾德
-萧乾
-郭子乾
-雍乾
-乾务
-乾沓和
-乾沓婆
-乾通
-乾忠
-乾淳
-李乾顺
-黄润乾
-男性为乾
-男为乾
-阳为乾
-乾一组
-乾一坛
-陈乾生
-陈公乾生
-字乾生
-不着痕迹
-不着边际
-与着
-与著书
-与著作
-与著名
-与著录
-与著称
-与著者
-与著述
-丑着
-丑著书
-丑著作
-丑著名
-丑著录
-丑著称
-丑著者
-丑著述
-临着
-临著书
-临著作
-临著名
-临著录
-临著称
-临著者
-临著述
-丽着
-丽著书
-丽著作
-丽著名
-丽著录
-丽著称
-丽著者
-丽著述
-乐着
-乐著书
-乐著作
-乐著名
-乐著录
-乐著称
-乐著者
-乐著述
-乘着
-乘著书
-乘著作
-乘著名
-乘著录
-乘著称
-乘著者
-乘著述
-争着
-争著书
-争著作
-争著名
-争著录
-争著称
-争著者
-争著述
-亮着
-亮著书
-亮著作
-亮著名
-亮著录
-亮著称
-亮著者
-亮著述
-仗着
-仗著书
-仗著作
-仗著名
-仗著录
-仗著称
-仗著者
-仗著述
-代表着
-代表著书
-代表著作
-代表著名
-代表著录
-代表著称
-代表著者
-代表著述
-伴着
-伴著书
-伴著作
-伴著名
-伴著录
-伴著称
-伴著者
-伴著述
-低着
-低著书
-低著作
-低著名
-低著录
-低著称
-低著者
-低著述
-住着
-住著书
-住著作
-住著名
-住著录
-住著称
-住著者
-住著述
-侧着
-侧著书
-侧著作
-侧著名
-侧著录
-侧著称
-侧著者
-侧著述
-保障着
-保障著书
-保障著作
-保障著名
-保障著录
-保障著称
-保障著者
-保障著述
-信着
-信著书
-信著作
-信著名
-信著录
-信著称
-信著者
-信著述
-候着
-候著书
-候著作
-候著名
-候著录
-候著称
-候著者
-候著述
-借着
-借著书
-借著作
-借著名
-借著录
-借著称
-借著者
-借著述
-做着
-做著书
-做著作
-做著名
-做著录
-做著称
-做著者
-做著述
-偷着
-偷著书
-偷著作
-偷著名
-偷著录
-偷著称
-偷著者
-偷著述
-光着
-光著书
-光著作
-光著名
-光著录
-光著称
-光著者
-光著述
-关着
-关著书
-关著作
-关著名
-关著录
-关著称
-关著者
-关著述
-冀着
-冀著书
-冀著作
-冀著名
-冀著录
-冀著称
-冀著者
-冀著述
-冒着
-冒著书
-冒著作
-冒著名
-冒著录
-冒著称
-冒著者
-冒著述
-写着
-写著书
-写著作
-写著名
-写著录
-写著称
-写著者
-写著述
-凉着
-凉著书
-凉著作
-凉著名
-凉著录
-凉著称
-凉著者
-凉著述
-制着
-制著书
-制著作
-制著名
-制著录
-制著称
-制著者
-制著述
-刻着
-刻著书
-刻著作
-刻著名
-刻著录
-刻著称
-刻著者
-刻著述
-办着
-办著书
-办著作
-办著名
-办著录
-办著称
-办著者
-办著述
-动着
-动著书
-动著作
-动著名
-动著录
-动著称
-动著者
-动著述
-努力着
-努力著书
-努力著作
-努力著名
-努力著录
-努力著称
-努力著者
-努力著述
-努着
-努著书
-努著作
-努著名
-努著录
-努著称
-努著者
-努著述
-印着
-印著书
-印著作
-印著名
-印著录
-印著称
-印著者
-印著述
-压着
-压著书
-压著作
-压著名
-压著录
-压著称
-压著者
-压著述
-去着
-去著书
-去著作
-去著名
-去著录
-去著称
-去著者
-去著述
-受着
-受著书
-受著作
-受著名
-受著录
-受著称
-受著者
-受著述
-变着
-变著书
-变著作
-变著名
-变著录
-变著称
-变著者
-变著述
-叫着
-叫著书
-叫著作
-叫著名
-叫著录
-叫著称
-叫著者
-叫著述
-向着
-向著书
-向著作
-向著名
-向著录
-向著称
-向著者
-向著述
-含着
-含著书
-含著作
-含著名
-含著录
-含著称
-含著者
-含著述
-听得着
-听不着
-听着
-听著书
-听著作
-听著名
-听著录
-听著称
-听著者
-听著述
-吹着
-吹著书
-吹著作
-吹著名
-吹著录
-吹著称
-吹著者
-吹著述
-味着
-味著书
-味著作
-味著名
-味著录
-味著称
-味著者
-味著述
-响着
-响著书
-响著作
-响著名
-响著录
-响著称
-响著者
-响著述
-哭着
-哭著书
-哭著作
-哭著名
-哭著录
-哭著称
-哭著者
-哭著述
-唱着
-唱著书
-唱著作
-唱著名
-唱著录
-唱著称
-唱著者
-唱著述
-喝着
-喝著书
-喝著作
-喝著名
-喝著录
-喝著称
-喝著者
-喝著述
-嚷着
-嚷著书
-嚷著作
-嚷著名
-嚷著录
-嚷著称
-嚷著者
-嚷著述
-因着
-因著书
-因著作
-因著名
-因著录
-因著称
-因著者
-因著述
-困着
-困著书
-困著作
-困著名
-困著录
-困著称
-困著者
-困著述
-围着
-围著书
-围著作
-围著名
-围著录
-围著称
-围著者
-围著述
-在着
-在著书
-在著作
-在著名
-在著录
-在著称
-在著者
-在著述
-坐着
-坐著书
-坐著作
-坐著名
-坐著录
-坐著称
-坐著者
-坐著述
-备着
-备著书
-备著作
-备著名
-备著录
-备著称
-备著者
-备著述
-夹着
-夹著书
-夹著作
-夹著名
-夹著录
-夹著称
-夹著者
-夹著述
-孤着
-孤著书
-孤著作
-孤著名
-孤著录
-孤著称
-孤著者
-孤著述
-学着
-学著书
-学著作
-学著名
-学著录
-学著称
-学著者
-学著述
-守着
-守著书
-守著作
-守著名
-守著录
-守著称
-守著者
-守著述
-定着
-定著书
-定著作
-定著名
-定著录
-定著称
-定著者
-定著述
-对着
-对著书
-对著作
-对著名
-对著录
-对著称
-对著者
-对著述
-寻着
-寻著书
-寻著作
-寻著名
-寻著录
-寻著称
-寻著者
-寻著述
-展着
-展著书
-展著作
-展著名
-展著录
-展著称
-展著者
-展著述
-带着
-带著书
-带著作
-带著名
-带著录
-带著称
-带著者
-带著述
-帮着
-帮著书
-帮著作
-帮著名
-帮著录
-帮著称
-帮著者
-帮著述
-应着
-应著书
-应著作
-应著名
-应著录
-应著称
-应著者
-应著述
-康着
-康著书
-康著作
-康著名
-康著录
-康著称
-康著者
-康著述
-开着
-开著书
-开著作
-开著名
-开著录
-开著称
-开著者
-开著述
-当着
-当著书
-当著作
-当著名
-当著录
-当著称
-当著者
-当著述
-待着
-待著书
-待著作
-待著名
-待著录
-待著称
-待著者
-待著述
-得着
-得著书
-得著作
-得著名
-得著录
-得著称
-得著者
-得著述
-循着
-循著书
-循著作
-循著名
-循著录
-循著称
-循著者
-循著述
-心着
-心著书
-心著作
-心著名
-心著录
-心著称
-心著者
-心著述
-忍着
-忍著书
-忍著作
-忍著名
-忍著录
-忍著称
-忍著者
-忍著述
-志着
-志著书
-志著作
-志著名
-志著录
-志著称
-志著者
-志著述
-忙着
-忙著书
-忙著作
-忙著名
-忙著录
-忙著称
-忙著者
-忙著述
-怀着
-怀著书
-怀著作
-怀著名
-怀著录
-怀著称
-怀著者
-怀著述
-急着
-急著书
-急著作
-急著名
-急著录
-急著称
-急著者
-急著述
-性着
-性著书
-性著作
-性著名
-性著录
-性著称
-性著者
-性著述
-恋着
-恋著书
-恋著作
-恋著名
-恋著录
-恋著称
-恋著者
-恋著述
-悠着
-悠著书
-悠著作
-悠著名
-悠著录
-悠著称
-悠著者
-悠著述
-惯着
-惯著书
-惯著作
-惯著名
-惯著录
-惯著称
-惯著者
-惯著述
-想着
-想著书
-想著作
-想著名
-想著录
-想著称
-想著者
-想著述
-战着
-战著书
-战著作
-战著名
-战著录
-战著称
-战著者
-战著述
-戴着
-戴著书
-戴著作
-戴著名
-戴著录
-戴著称
-戴著者
-戴著述
-扎着
-扎著书
-扎著作
-扎著名
-扎著录
-扎著称
-扎著者
-扎著述
-打着
-打著书
-打著作
-打著名
-打著录
-打著称
-打著者
-打著述
-扛着
-扛著书
-扛著作
-扛著名
-扛著录
-扛著称
-扛著者
-扛著述
-找得着
-找不着
-抓着
-抓著作
-抓著名
-抓著录
-抓著称
-抓著者
-抓著述
-披着
-披著书
-披著作
-披著名
-披著录
-披著称
-披著者
-披著述
-抬着
-抬著作
-抬著名
-抬著录
-抬著称
-抬著者
-抬著述
-抱着
-抱著作
-抱著名
-抱著录
-抱著称
-抱著者
-抱著述
-拉着
-拉著书
-拉著作
-拉著名
-拉著录
-拉著称
-拉著者
-拉著述
-拎着
-拎著作
-拎著名
-拎著录
-拎著称
-拎著者
-拎著述
-拖着
-拖著作
-拖著名
-拖著录
-拖著称
-拖著者
-拖著述
-拼着
-拼著作
-拼著名
-拼著录
-拼著称
-拼著者
-拼著述
-拿着
-拿著作
-拿著名
-拿著录
-拿著称
-拿著者
-拿著述
-持着
-持著作
-持著名
-持著录
-持著称
-持著者
-持著述
-挑着
-挑著作
-挑著名
-挑著录
-挑著称
-挑著者
-挑著述
-挡着
-挡著作
-挡著名
-挡著录
-挡著称
-挡著者
-挡著述
-挣着
-挣著书
-挣著作
-挣著名
-挣著录
-挣著称
-挣著者
-挣著述
-挥着
-挥著作
-挥著名
-挥著录
-挥著称
-挥著者
-挥著述
-挨着
-挨著作
-挨著名
-挨著录
-挨著称
-挨著者
-挨著述
-捆着
-捆著作
-捆著名
-捆著录
-捆著称
-捆著者
-捆著述
-据着
-据著书
-据著作
-据著名
-据著录
-据著称
-据著者
-据著述
-掖着
-掖著作
-掖著名
-掖著录
-掖著称
-掖著者
-掖著述
-接着
-接著作
-接著名
-接著录
-接著称
-接著者
-接著述
-揉着
-揉著书
-揉著作
-揉著名
-揉著录
-揉著称
-揉著者
-揉著述
-提着
-提著作
-提著名
-提著录
-提著称
-提著者
-提著述
-搂着
-搂著作
-搂著名
-搂著录
-搂著称
-搂著者
-搂著述
-摆着
-摆著作
-摆著名
-摆著录
-摆著称
-摆著者
-摆著述
-撼着
-撼著书
-撼著作
-撼著名
-撼著录
-撼著称
-撼著者
-撼著述
-敞着
-敞著作
-敞著名
-敞著录
-敞著称
-敞著者
-敞著述
-数着
-数著作
-数著名
-数著录
-数著称
-数著者
-数著述
-斗着
-斗著书
-斗著作
-斗著名
-斗著录
-斗著称
-斗著者
-斗著述
-斥着
-斥著书
-斥著作
-斥著名
-斥著录
-斥著称
-斥著者
-斥著述
-昂着
-昂著书
-昂著作
-昂著名
-昂著录
-昂著称
-昂著者
-昂著述
-映着
-映著书
-映著作
-映著名
-映著录
-映著称
-映著者
-映著述
-晃着
-晃著作
-晃著名
-晃著录
-晃著称
-晃著者
-晃著述
-暗着
-暗著书
-暗著作
-暗著名
-暗著录
-暗著称
-暗著者
-暗著述
-有着
-有著书
-有著作
-有著名
-有著录
-有著称
-有著者
-有著述
-望着
-望著作
-望著名
-望著录
-望著称
-望著者
-望著述
-朝着
-朝著作
-朝著名
-朝著录
-朝著称
-朝著者
-朝著述
-本着
-本著书
-本著作
-本著名
-本著录
-本著称
-本著者
-本著述
-杀着
-杀著书
-杀著作
-杀著名
-杀著录
-杀著称
-杀著者
-杀著述
-杂着
-杂著书
-杂著作
-杂著名
-杂著录
-杂著称
-杂著者
-杂著述
-来着
-来著书
-来著作
-来著名
-来著录
-来著称
-来著者
-来著述
-枕着
-枕著作
-枕著名
-枕著录
-枕著称
-枕著者
-枕著述
-梦着
-梦著书
-梦著作
-梦著名
-梦著录
-梦著称
-梦著者
-梦著述
-梳着
-梳著作
-梳著名
-梳著录
-梳著称
-梳著者
-梳著述
-求着
-求著书
-求著作
-求著名
-求著录
-求著称
-求著者
-求著述
-沉着
-沉著书
-沉著作
-沉著名
-沉著录
-沉著称
-沉著者
-沉著述
-沿着
-沿著书
-沿著作
-沿著名
-沿著录
-沿著称
-沿著者
-沿著述
-活着
-活著书
-活著作
-活著名
-活著录
-活著称
-活著者
-活著述
-流着
-流著书
-流著作
-流著名
-流著录
-流著称
-流著者
-流著述
-浮着
-浮著书
-浮著作
-浮著名
-浮著录
-浮著称
-浮著者
-浮著述
-润着
-润著书
-润著作
-润著名
-润著录
-润著称
-润著者
-润著述
-涵着
-涵著书
-涵著作
-涵著名
-涵著录
-涵著称
-涵著者
-涵著述
-渴着
-渴著书
-渴著作
-渴著名
-渴著录
-渴著称
-渴著者
-渴著述
-溢着
-溢著书
-溢著作
-溢著名
-溢著录
-溢著称
-溢著者
-溢著述
-演着
-演著书
-演著作
-演著名
-演著录
-演著称
-演著者
-演著述
-漫着
-漫著书
-漫著作
-漫著名
-漫著录
-漫著称
-漫著者
-漫著述
-点着
-点著作
-点著名
-点著录
-点著称
-点著者
-点著述
-烧着
-烧著作
-烧著名
-烧著录
-烧著称
-烧著者
-烧著述
-照着
-照著书
-照著作
-照著名
-照著录
-照著称
-照著者
-照著述
-爱着
-爱著书
-爱著作
-爱著名
-爱著录
-爱著称
-爱著者
-爱著述
-牵着
-牵著书
-牵著作
-牵著名
-牵著录
-牵著称
-牵著者
-牵著述
-犯得着
-犯不着
-独着
-独著书
-独著作
-独著名
-独著录
-独著称
-独著者
-独著述
-猜着
-猜着书
-猜著作
-猜著名
-猜著录
-猜著称
-猜著者
-猜著述
-甜着
-甜著书
-甜著作
-甜著名
-甜著录
-甜著称
-甜著者
-甜著述
-用得着
-用不着
-用着
-用著书
-用著作
-用著名
-用著录
-用著称
-用著者
-用著述
-留着
-留着书
-留著作
-留著名
-留著录
-留著称
-留著者
-留著述
-疑着
-疑著书
-疑著作
-疑著名
-疑著录
-疑著称
-疑著者
-疑著述
-皱着
-皱著书
-皱著作
-皱著名
-皱著录
-皱著称
-皱著者
-皱著述
-盛着
-盛著书
-盛著作
-盛著名
-盛著录
-盛著称
-盛著者
-盛著述
-盯着
-盯着书
-盯著作
-盯著名
-盯著录
-盯著称
-盯著者
-盯著述
-盾着
-盾著书
-盾著作
-盾著名
-盾著录
-盾著称
-盾著者
-盾著述
-看得着
-看不着
-看着
-看着书
-看著作
-看著名
-看著录
-看著称
-看著者
-看著述
-瞧着
-瞧着书
-瞧著作
-瞧著名
-瞧著录
-瞧著称
-瞧著者
-瞧著述
-着业
-着丝
-着么
-着人
-着什么急
-着他
-着令
-着位
-着体
-着你
-着便
-着凉
-着力
-着劲
-着号
-着呢
-着哩
-着地
-着墨
-着声
-着处
-着她
-着妳
-着姓
-着它
-着定
-着实
-着己
-着帐
-着床
-着庸
-着式
-着录
-着心
-着志
-着忙
-着急
-着恼
-着惊
-着想
-着意
-着慌
-着我
-着手
-着抹
-着摸
-着撰
-着数
-着明
-着末
-着极
-着格
-着棋
-着槁
-着气
-着法
-着浅
-着火
-着然
-着甚
-着生
-着疑
-着白
-着相
-着眼
-着着
-着祂
-着积
-着稿
-着笔
-着籍
-着紧
-着緑
-着绊
-着绩
-着绯
-着绿
-着肉
-着脚
-着舰
-着色
-着节
-着花
-着莫
-着落
-着藁
-着衣
-着装
-着要
-着警
-着趣
-着边
-着迷
-着迹
-着重
-着録
-着闻
-着陆
-着雝
-着鞭
-着题
-着魔
-睡得着
-睡不着
-睡着
-睡著书
-睡著作
-睡著名
-睡著录
-睡著称
-睡著者
-睡著述
-瞒着
-瞒著书
-瞒著作
-瞒著名
-瞒著录
-瞒著称
-瞒著者
-瞒著述
-瞪着
-瞪著书
-瞪著作
-瞪著名
-瞪著录
-瞪著称
-瞪著者
-瞪著述
-福着
-福著书
-福著作
-福著名
-福著录
-福著称
-福著者
-福著述
-空着
-空著书
-空著作
-空著名
-空著录
-空著称
-空著者
-空著述
-穿着
-穿著书
-穿著作
-穿著名
-穿著录
-穿著称
-穿著者
-穿著述
-竖着
-竖著书
-竖著作
-竖著名
-竖著录
-竖著称
-竖著者
-竖著述
-站着
-站著书
-站著作
-站著名
-站著录
-站著称
-站著者
-站著述
-笑着
-笑著书
-笑著作
-笑著名
-笑著录
-笑著称
-笑著者
-笑著述
-管着
-管著书
-管著作
-管著名
-管著录
-管著称
-管著者
-管著述
-绑着
-绑著书
-绑著作
-绑著名
-绑著录
-绑著称
-绑著者
-绑著述
-绕着
-绕著书
-绕著作
-绕著名
-绕著录
-绕著称
-绕著者
-绕著述
-缠着
-缠著书
-缠著作
-缠著名
-缠著录
-缠著称
-缠著者
-缠著述
-罩着
-罩著书
-罩著作
-罩著名
-罩著录
-罩著称
-罩著者
-罩著述
-美着
-美著书
-美著作
-美著名
-美著录
-美著称
-美著者
-美著述
-耀着
-耀著书
-耀著作
-耀著名
-耀著录
-耀著称
-耀著者
-耀著述
-考着
-考著书
-考著作
-考著名
-考著录
-考著称
-考著者
-考著述
-背着
-背著书
-背著作
-背著名
-背著录
-背著称
-背著者
-背著述
-胶着
-胶著书
-胶著作
-胶著名
-胶著录
-胶著称
-胶著者
-胶著述
-艺着
-艺著书
-艺著作
-艺著名
-艺著录
-艺著称
-艺著者
-艺著述
-苦着
-苦著书
-苦著作
-苦著名
-苦著录
-苦著称
-苦著者
-苦著述
-获着
-获著书
-获著作
-获著名
-获著录
-获著称
-获著者
-获著述
-落着
-落著书
-落著作
-落著名
-落著录
-落著称
-落著者
-落著述
-蒙着
-蒙著书
-蒙著作
-蒙著名
-蒙著录
-蒙著称
-蒙著者
-蒙著述
-藏着
-藏著书
-藏著作
-藏著名
-藏著录
-藏著称
-藏著者
-藏著述
-蘸着
-蘸著书
-蘸著作
-蘸著名
-蘸著录
-蘸著称
-蘸著者
-蘸著述
-行着
-行著书
-行著作
-行著名
-行著录
-行著称
-行著者
-行著述
-衣着
-衣著书
-衣著作
-衣著名
-衣著录
-衣著称
-衣著者
-衣著述
-装着
-装著书
-装著作
-装著名
-装著录
-装著称
-装著者
-装著述
-裹着
-裹著书
-裹著作
-裹著名
-裹著录
-裹著称
-裹著者
-裹著述
-见着
-见著书
-见著作
-见著名
-见著录
-见著称
-见著者
-见著述
-记着
-记著书
-记著作
-记著名
-记著录
-记著称
-记著者
-记著述
-试着
-试著书
-试著作
-试著名
-试著录
-试著称
-试著者
-试著述
-语着
-语著书
-语著作
-语著名
-语著录
-语著称
-语著者
-语著述
-豫着
-豫著书
-豫著作
-豫著名
-豫著录
-豫著称
-豫著者
-豫著述
-贞着
-贞著书
-贞著作
-贞著名
-贞著录
-贞著称
-贞著者
-贞著述
-走着
-走著书
-走著作
-走著名
-走著录
-走著称
-走著者
-走著述
-赶着
-赶著书
-赶著作
-赶著名
-赶著录
-赶著称
-赶著者
-赶著述
-趴着
-趴著书
-趴著作
-趴著名
-趴著录
-趴著称
-趴著者
-趴著述
-跃着
-跃著书
-跃著作
-跃著名
-跃著录
-跃著称
-跃著者
-跃著述
-跑着
-跑著书
-跑著作
-跑著名
-跑著录
-跑著称
-跑著者
-跑著述
-跟着
-跟著书
-跟著作
-跟著名
-跟著录
-跟著称
-跟著者
-跟著述
-跪着
-跪著书
-跪著作
-跪著名
-跪著录
-跪著称
-跪著者
-跪著述
-跳着
-跳著书
-跳著作
-跳著名
-跳著录
-跳著称
-跳著者
-跳著述
-踏着
-踏著书
-踏著作
-踏著名
-踏著录
-踏著称
-踏著者
-踏著述
-踩着
-踩著书
-踩著作
-踩著名
-踩著录
-踩著称
-踩著者
-踩著述
-身着
-身著书
-身著作
-身著名
-身著录
-身著称
-身著者
-身著述
-躺着
-躺著书
-躺著作
-躺著名
-躺著录
-躺著称
-躺著者
-躺著述
-转着
-转著书
-转著作
-转著名
-转著录
-转著称
-转著者
-转著述
-载着
-载著书
-载著作
-载著名
-载著录
-载著称
-载著者
-载著述
-达着
-达著书
-达著作
-达著名
-达著录
-达著称
-达著者
-达著述
-远着
-远著书
-远著作
-远著名
-远著录
-远著称
-远著者
-远著述
-连着
-连著书
-连著作
-连著名
-连著录
-连著称
-连著者
-连著述
-追着
-追著书
-追著作
-追著名
-追著录
-追著称
-追著者
-追著述
-逆着
-逆著书
-逆著作
-逆著名
-逆著录
-逆著称
-逆著者
-逆著述
-逼着
-逼著书
-逼著作
-逼著名
-逼著录
-逼著称
-逼著者
-逼著述
-遇着
-遇著书
-遇著作
-遇著名
-遇著录
-遇著称
-遇著者
-遇著述
-配着
-配著书
-配著作
-配著名
-配著录
-配著称
-配著者
-配著述
-酿着
-酿著书
-酿著作
-酿著名
-酿著录
-酿著称
-酿著者
-酿著述
-铺着
-铺著书
-铺著作
-铺著名
-铺著录
-铺著称
-铺著者
-铺著述
-闭着
-闭著书
-闭著作
-闭著名
-闭著录
-闭著称
-闭著者
-闭著述
-闲着
-闲著书
-闲著作
-闲著名
-闲著录
-闲著称
-闲著者
-闲著述
-附着
-附著书
-附著作
-附著名
-附著录
-附著称
-附著者
-附著述
-陋着
-陋著书
-陋著作
-陋著名
-陋著录
-陋著称
-陋著者
-陋著述
-陪着
-陪著书
-陪著作
-陪著名
-陪著录
-陪著称
-陪著者
-陪著述
-随着
-随著书
-随著作
-随著名
-随著录
-随著称
-随著者
-随著述
-隔着
-隔著书
-隔著作
-隔著名
-隔著录
-隔著称
-隔著者
-隔著述
-雅着
-雅著书
-雅著作
-雅著名
-雅著录
-雅著称
-雅著者
-雅著述
-顶着
-顶著书
-顶著作
-顶著名
-顶著录
-顶著称
-顶著者
-顶著述
-顺着
-顺著书
-顺著作
-顺著名
-顺著录
-顺著称
-顺著者
-顺著述
-领着
-领著书
-领著作
-领著名
-领著录
-领著称
-领著者
-领著述
-飘着
-飘著书
-飘著作
-飘著名
-飘著录
-飘著称
-飘著者
-飘著述
-驾着
-驾著书
-驾著作
-驾著名
-驾著录
-驾著称
-驾著者
-驾著述
-骂着
-骂著书
-骂著作
-骂著名
-骂著录
-骂著称
-骂著者
-骂著述
-骑着
-骑著书
-骑著作
-骑著名
-骑著录
-骑著称
-骑著者
-骑著述
-骗着
-骗著书
-骗著作
-骗著名
-骗著录
-骗著称
-骗著者
-骗著述
-高着
-高著书
-高著作
-高著名
-高著录
-高著称
-高著者
-高著述
-髭着
-髭著书
-髭著作
-髭著名
-髭著录
-髭著称
-髭著者
-髭著述
-黏着
-黏著书
-黏著作
-黏著名
-黏著录
-黏著称
-黏著者
-黏著述
-新著龙虎门
-护着
-护著书
-护著作
-护著名
-护著录
-护著称
-护著者
-护著述
-保护着
-爱护着
-庇护着
-传着
-传著书
-传著作
-传著名
-传著录
-传著称
-传著者
-传著述
-标志着
-流露着
-靠着
-靠著作
-靠著名
-靠著录
-靠著称
-靠著者
-靠著述
-玩着
-迫着
-吃得着
-吃不着
-吃着
-闻得着
-闻不着
-闻着
-嗅得着
-嗅不着
-嗅着
-警戒着
-於乎
-於戏
-魏徵
-柳诒徵
-於姓
-於氏
-於夫罗
-於梨华
-卷舌
-樊於期
-於菟
-於潜县
-石碁镇
-因著《
-因著〈
-李泽钜
-於祥玉
-於崇文
-於世成
-於乙宇同
-於宇同
-朴於宇同
-於哲
-於除鞬
-於志贺
-覆蓋
-五箇山
-麽麽
-幺厮
-幺半群
-幺元
-幺爹
-幺叔
-幺舅
-幺爸
-幺妈
-幺姨
-幺娘
-幺妹
-幺小
-幺姓
-姓幺
-幺氏
-麽氏
-幺蛾子
-幺麽
-幺麽小丑
-幺凤
-幺二三
-幺篇
-幺谦
-麴义
-麴英
-麯崇裕
-阿部正瞭
-醯酱
-醯鸡
-醯醋
-醯醢
-醯壶
-苧烯
-近角聪信
-米泽瑠美
-峯岸南
-僧伽吒
-王道乾
-後姓
diff --git a/includes/zhtable/simpphrases_exclude.manual b/includes/zhtable/simpphrases_exclude.manual
deleted file mode 100644
index 3e9d3ecc..00000000
--- a/includes/zhtable/simpphrases_exclude.manual
+++ /dev/null
@@ -1,21 +0,0 @@
-整飭
-後
-谘
-彷佛
-三番四复
-三复
-藉
-关於
-对於
-属於
-至於
-夥计
-薹
-嚇
-醣
-捱
-簑
-樑
-摺叠
-餗
-安甯 \ No newline at end of file
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
deleted file mode 100644
index 243f61b0..00000000
--- a/includes/zhtable/toCN.manual
+++ /dev/null
@@ -1,275 +0,0 @@
-」 ”
-「 “
-『 ‘
-』 ’
-記憶體 内存
-預設 默认
-串列 串行
-串列加速器 串列加速器
-乙太網 以太网
-點陣圖 位图
-常式 例程
-光碟 光盘
-光碟機 光驱
-全形 全角
-載入 加载
-半形 半角
-變數 变量
-雜訊 噪声
-因數 因子
-功能變數名稱 域名
-音效卡 声卡
-字型大小 字号
-字型檔 字库
-欄位 字段
-字元 字符
-字元济 字元济
-字元濟 字元济
-字元会 字元会
-字元會 字元会
-存檔 存盘
-定址 寻址
-章節附註 尾注
-非同步 异步
-匯流排 总线
-括弧 括号
-介面 接口
-控制項 控件
-許可權 权限
-碟片 盘片
-矽片 硅片
-矽谷 硅谷
-硬碟 硬盘
-磁碟 磁盘
-磁軌 磁道
-程式控制 程控
-遠程控制 远程控制
-远程控制 远程控制
-運算元 算子
-演算法 算法
-晶片 芯片
-晶元 芯片
-片語 词组
-軟碟機 软驱
-快閃記憶體 快闪存储器
-滑鼠 鼠标
-滑鼠蛇 滑鼠蛇
-二進位 二进制
-滿二進位 满二进位
-六進位 六进制
-滿六進位 满六进位
-滿十六進位 满十六进位
-八進位 八进制
-滿八進位 满八进位
-十進位 十进制
-滿十進位 满十进位
-16進位 16进位
-滿16進位 满16进位
-二進位制 二进位制
-六進位制 六进位制
-八進位制 八进位制
-十進位制 十进位制
-16進位制 16进位制
-互動式 交互式
-優先順序 优先级
-感測 传感
-攜帶型 便携式
-資訊理論 信息论
-迴圈 循环
-防寫 写保护
-解析度 分辨率
-伺服器 服务器
-等於 等于
-區域網 局域网
-巨集 宏
-掃瞄器 扫瞄仪
-寬頻 宽带
-資料庫 数据库
-萬曆 万历
-永曆 永历
-辭彙 词汇
-母音 元音
-字母 字母
-頭槌 头球
-進球 入球
-顆進球 粒入球
-射門 打门
-蓋火鍋 火锅盖帽
-印表機 打印机
-打印機 打印机
-位元組 字节
-字節 字节
-列印 打印
-打印 打印
-硬體 硬件
-二極體 二极管
-二極管 二极管
-三極體 三极管
-三極管 三极管
-軟體 软件
-軟件 软件
-網路 网络
-網絡 网络
-人工智慧 人工智能
-太空梭 航天飞机
-穿梭機 航天飞机
-網際網路 互联网
-互聯網 互联网
-機械人 机器人
-機器人 机器人
-行動電話 移动电话
-流動電話 移动电话
-調制解調器 调制解调器
-數據機 调制解调器
-短訊 短信
-簡訊 短信
-烏茲別克 乌兹别克斯坦
-葉門 也门
-伯利茲 伯利兹
-貝里斯 伯利兹
-維德角 佛得角
-克羅埃西亞 克罗地亚
-甘比亞 冈比亚
-幾內亞比索 几内亚比绍
-列支敦斯登 列支敦士登
-賴比瑞亞 利比里亚
-迦納 加纳
-加彭 加蓬
-波札那 博茨瓦纳
-盧安達 卢旺达
-瓜地馬拉 危地马拉
-厄瓜多爾 厄瓜多尔
-厄瓜多尔 厄瓜多尔
-厄瓜多 厄瓜多尔
-厄利垂亞 厄立特里亚
-吉布地 吉布提
-哈薩克 哈萨克斯坦
-哥斯大黎加 哥斯达黎加
-吐瓦魯 图瓦卢
-土庫曼 土库曼斯坦
-聖露西亞 圣卢西亚
-聖吉斯納域斯 圣基茨和尼维斯
-聖克里斯多福及尼維斯 圣基茨和尼维斯
-聖文森及格瑞那丁 圣文森特和格林纳丁斯
-聖馬利諾 圣马力诺
-蓋亞那 圭亚那
-坦尚尼亞 坦桑尼亚
-衣索匹亞 埃塞俄比亚
-衣索比亞 埃塞俄比亚
-吉里巴斯 基里巴斯
-塔吉克 塔吉克斯坦
-塞拉利昂 塞拉利昂
-塞普勒斯 塞浦路斯
-塞席爾 塞舌尔
-多米尼克 多米尼加国
-安地卡及巴布達 安提瓜和巴布达
-尼日利亞 尼日利亚
-尼日利亚 尼日利亚
-奈及利亞 尼日利亚
-尼日爾 尼日尔
-尼日尔 尼日尔
-巴貝多 巴巴多斯
-巴布亞紐幾內亞 巴布亚新几内亚
-布基納法索 布基纳法索
-布吉納法索 布基纳法索
-蒲隆地 布隆迪
-帛琉 帕劳
-義大利 意大利
-索羅門群島 所罗门群岛
-汶萊 文莱
-史瓦濟蘭 斯威士兰
-斯洛維尼亞 斯洛文尼亚
-紐西蘭 新西兰
-格瑞那達 格林纳达
-茅利塔尼亞 毛里塔尼亚
-毛里裘斯 毛里求斯
-模里西斯 毛里求斯
-沙地阿拉伯 沙特阿拉伯
-沙烏地阿拉伯 沙特阿拉伯
-波士尼亞赫塞哥維納 波斯尼亚和黑塞哥维那
-辛巴威 津巴布韦
-宏都拉斯 洪都拉斯
-千里達托貝哥 特立尼达和托巴哥
-諾魯 瑙鲁
-萬那杜 瓦努阿图
-溫納圖 瓦努阿图
-葛摩 科摩罗
-象牙海岸 科特迪瓦
-突尼西亞 突尼斯
-索馬利亞 索马里
-寮國 老挝
-肯雅 肯尼亚
-肯亞 肯尼亚
-蘇利南 苏里南
-莫三比克 莫桑比克
-賴索托 莱索托
-貝南 贝宁
-尚比亞 赞比亚
-亞塞拜然 阿塞拜疆
-阿拉伯聯合大公國 阿拉伯联合酋长国
-南韓 韩国
-馬爾地夫 马尔代夫
-馬爾他 马耳他
-馬利共和國 马里共和国
-即食麵 方便面
-快速面 方便面
-速食麵 方便面
-泡麵 方便面
-笨豬跳 蹦极跳
-绑紧跳 蹦极跳
-冷盤 凉菜
-冷菜 凉菜
-散钱 零钱
-谐星 笑星
-夜学 夜校
-华乐 民乐
-中樂 民乐
-軍中樂園 军中乐园
-华乐街 华乐街
-屋价 房价
-計程車 出租车
-單車 自行车
-節慶 节日
-芝士 乾酪
-狗隻 犬只
-士多啤梨 草莓
-忌廉 奶油
-桌球 台球
-撞球 台球
-衞生 卫生
-衛生 卫生
-賓士 奔驰
-平治 奔驰
-平治之亂 平治之乱
-平治之乱 平治之乱
-積架 捷豹
-福斯 大众
-福士 大众
-萬事得 马自达
-寶獅 标志
-拿破崙 拿破仑
-布殊 布什
-布希 布什
-布希亞 布希亚
-布希亚 布希亚
-柯林頓 克林顿
-海珊 侯赛因
-梵谷 凡高
-大衛碧咸 大卫·贝克汉姆
-米高奧雲 迈克尔·欧文
-卡佩雅蒂 珍妮弗·卡普里亚蒂
-沙芬 马拉特·萨芬
-舒麥加 迈克尔·舒马赫
-希特拉 希特勒
-黛安娜 戴安娜
-榴槤 榴莲
-榴梿 榴莲
-矽 硅
-矽肺 矽肺
-矽塵 矽尘
-矽尘 矽尘
-矽鋼 矽钢
-矽钢 矽钢
-侏儸紀 侏罗纪
-甚麽 什么
-甚麼 什么
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
deleted file mode 100644
index 1f7fe7d0..00000000
--- a/includes/zhtable/toHK.manual
+++ /dev/null
@@ -1,2300 +0,0 @@
-” 」
-“ 「
-‘ 『
-’ 』
-鉤 鈎
-衛 衞
-凶殺 兇殺
-凶殘 兇殘
-緝凶 緝兇
-買凶 買兇
-印表機 打印機
-字节 位元組
-字節 位元組
-列印 打印
-硬件 硬件
-硬體 硬件
-二極體 二極管
-三極體 三極管
-軟體 軟件
-網路 網絡
-人工智慧 人工智能
-航天飞机 穿梭機
-太空梭 穿梭機
-因特网 互聯網
-網際網路 互聯網
-机器人 機械人
-機器人 機械人
-移动电话 流動電話
-行動電話 流動電話
-數據機 調制解調器
-短信 短訊
-簡訊 短訊
-查德 乍得
-葉門 也門
-貝里斯 伯利茲
-維德角 佛得角
-克羅埃西亞 克羅地亞
-甘比亞 岡比亞
-幾內亞比索 幾內亞比紹
-列支敦斯登 列支敦士登
-賴比瑞亞 利比里亞
-迦納 加納
-加彭 加蓬
-波札那 博茨瓦納
-盧安達 盧旺達
-瓜地馬拉 危地馬拉
-厄瓜多尔 厄瓜多爾
-厄瓜多爾 厄瓜多爾
-厄瓜多 厄瓜多爾
-厄利垂亞 厄立特里亞
-吉布地 吉布堤
-哥斯大黎加 哥斯達黎加
-吐瓦魯 圖瓦盧
-聖露西亞 聖盧西亞
-圣基茨和尼维斯 聖吉斯納域斯
-聖克里斯多福及尼維斯 聖吉斯納域斯
-聖文森及格瑞那丁 聖文森特和格林納丁斯
-聖馬利諾 聖馬力諾
-蓋亞那 圭亞那
-坦尚尼亞 坦桑尼亞
-衣索匹亞 埃塞俄比亞
-衣索比亞 埃塞俄比亞
-吉里巴斯 基里巴斯
-塞普勒斯 塞浦路斯
-塞席爾 塞舌爾
-安地卡及巴布達 安提瓜和巴布達
-尼日利亚 尼日利亞
-尼日利亞 尼日利亞
-奈及利亞 尼日利亞
-尼日尔 尼日爾
-尼日爾 尼日爾
-尼日 尼日爾
-巴貝多 巴巴多斯
-巴布亞紐幾內亞 巴布亞新畿內亞
-布吉納法索 布基納法索
-蒲隆地 布隆迪
-帕劳 帛琉
-義大利 意大利
-索羅門群島 所羅門群島
-文莱 汶萊
-史瓦濟蘭 斯威士蘭
-斯洛維尼亞 斯洛文尼亞
-紐西蘭 新西蘭
-格瑞那達 格林納達
-茅利塔尼亞 毛里塔尼亞
-毛里求斯 毛里裘斯
-模里西斯 毛里裘斯
-沙地阿拉伯 沙特阿拉伯
-沙烏地阿拉伯 沙特阿拉伯
-波士尼亞赫塞哥維納 波斯尼亞黑塞哥維那
-辛巴威 津巴布韋
-宏都拉斯 洪都拉斯
-千里達托貝哥 特立尼達和多巴哥
-諾魯 瑙魯
-萬那杜 瓦努阿圖
-葛摩 科摩羅
-索馬利亞 索馬里
-寮國 老撾
-肯尼亚 肯雅
-肯亞 肯雅
-莫三比克 莫桑比克
-賴索托 萊索托
-貝南 貝寧
-尚比亞 贊比亞
-亞塞拜然 阿塞拜疆
-阿拉伯聯合大公國 阿拉伯聯合酋長國
-馬爾地夫 馬爾代夫
-馬利共和國 馬里共和國
-方便面 即食麵
-快速面 即食麵
-速食麵 即食麵
-泡麵 即食麵
-土豆 馬鈴薯
-土豆网 土豆網
-土豆網 土豆網
-华乐 中樂
-民乐 中樂
-計程車 的士
-出租车 的士
-公車 巴士
-公車上書 公車上書
-自行车 單車
-犬只 狗隻
-台球 桌球
-撞球 桌球
-冰淇淋 雪糕
-賓士 平治
-捷豹 積架
-福斯 福士
-雪铁龙 先進
-雪鐵龍 先進
-沃尓沃 富豪
-马自达 萬事得
-馬自達 萬事得
-寶獅 標致
-布什 布殊
-布希 布殊
-布希亞 布希亞
-布希亚 布希亞
-柯林頓 克林頓
-萨达姆 薩達姆
-海珊 侯賽因
-大卫·贝克汉姆 大衛碧咸
-迈克尔·欧文 米高奧雲
-珍妮弗·卡普里亚蒂 卡佩雅蒂
-马拉特·萨芬 沙芬
-迈克尔·舒马赫 舒麥加
-希特勒 希特拉
-狄安娜 戴安娜
-黛安娜 戴安娜
-颁布 頒佈
-頒布 頒佈
-挨著 挨着
-愛著 愛着
-暗著 暗着
-昂著 昂着
-擺著 擺着
-伴著 伴着
-辦著 辦着
-幫著 幫着
-綁著 綁着
-抱著 抱着
-背著 背着
-備著 備着
-本著 本着
-逼著 逼着
-閉著 閉着
-變著 變着
-猜著 猜着
-踩著 踩着
-藏著 藏着
-側著 側着
-纏著 纏着
-敞著 敞着
-唱著 唱着
-朝著 朝着
-沉著 沉着
-乘著 乘着
-持著 持着
-斥著 斥着
-醜著 醜着
-穿著 穿着
-吹著 吹着
-達著 達着
-打著 打着
-待著 待着
-帶著 帶着
-戴著 戴着
-當著 當着
-擋著 擋着
-得著 得着
-瞪著 瞪着
-低著 低着
-點著 點着
-盯著 盯着
-頂著 頂着
-定著 定着
-動著 動着
-鬥著 鬥着
-獨著 獨着
-對著 對着
-盾著 盾着
-犯得著 犯得着
-犯不著 犯不着
-福著 福着
-趕著 趕着
-高著 高着
-隔著 隔着
-跟著 跟着
-孤著 孤着
-關著 關着
-管著 管着
-慣著 慣着
-光著 光着
-跪著 跪着
-裹著 裹着
-撼著 撼着
-喝著 喝着
-候著 候着
-懷著 懷着
-晃著 晃着
-揮著 揮着
-活著 活着
-獲著 獲着
-獲著 獲着
-急著 急着
-記著 記着
-冀著 冀着
-夾著 夾着
-駕著 駕着
-見著 見着
-閑著 閑着
-叫著 叫着
-接著 接着
-借著 借着
-借著 借着
-據著 據着
-開著 開着
-看得著 看得着
-看不著 看不着
-看著 看着
-康著 康着
-扛著 扛着
-考著 考着
-渴著 渴着
-刻著 刻着
-空著 空着
-哭著 哭着
-苦著 苦着
-捆著 捆着
-困著 困着
-拉著 拉着
-來著 來着
-樂著 樂着
-努力著 努力着
-麗著 麗着
-連著 連着
-戀著 戀着
-涼著 涼着
-亮著 亮着
-臨著 臨着
-拎著 拎着
-領著 領着
-流著 流着
-留著 留着
-摟著 摟着
-陋著 陋着
-落著 落着
-罵著 罵着
-瞞著 瞞着
-漫著 漫着
-忙著 忙着
-冒著 冒着
-美著 美着
-夢著 夢着
-蒙著 蒙着
-拿著 拿着
-逆著 逆着
-釀著 釀着
-努著 努着
-趴著 趴着
-跑著 跑着
-陪著 陪着
-配著 配着
-披著 披着
-騙著 騙着
-飄著 飄着
-拼著 拼着
-鋪著 鋪着
-騎著 騎着
-牽著 牽着
-求著 求着
-去著 去着
-嚷著 嚷着
-繞著 繞着
-忍著 忍着
-揉著 揉着
-潤著 潤着
-燒著 燒着
-身著 身着
-沉著 沉着
-盛著 盛着
-試著 試着
-守著 守着
-受著 受着
-梳著 梳着
-豎著 豎着
-數著 數着
-睡得著 睡得着
-睡不著 睡不着
-睡著 睡着
-順著 順着
-隨著 隨着
-踏著 踏着
-抬著 抬着
-躺著 躺着
-提著 提着
-甜著 甜着
-挑著 挑着
-跳著 跳着
-聽得著 聽得着
-聽不著 聽不着
-聽著 聽着
-偷著 偷着
-拖著 拖着
-望著 望着
-圍著 圍着
-味著 味着
-想著 想着
-響著 響着
-向著 向着
-笑著 笑着
-心著 心着
-信著 信着
-行著 行着
-性著 性着
-學著 學着
-尋著 尋着
-循著 循着
-壓著 壓着
-雅著 雅着
-沿著 沿着
-耀著 耀着
-掖著 掖着
-衣著 衣着
-疑著 疑着
-溢著 溢着
-藝著 藝着
-因著 因着
-印著 印着
-應著 應着
-映著 映着
-用得著 用得着
-用不著 用不着
-用著 用着
-悠著 悠着
-有著 有着
-與著 與着
-語著 語着
-豫著 豫着
-遠著 遠着
-躍著 躍着
-雜著 雜着
-載著 載着
-在著 在着
-紮著 紮着
-展著 展着
-站著 站着
-戰著 戰着
-蘸著 蘸着
-仗著 仗着
-找得著 找得着
-找不著 找不着
-照著 照着
-罩著 罩着
-貞著 貞着
-枕著 枕着
-爭著 爭着
-掙著 掙着
-制著 制着
-志著 志着
-皺著 皺着
-住著 住着
-抓著 抓着
-轉著 轉着
-裝著 裝着
-追著 追着
-髭著 髭着
-走著 走着
-坐著 坐着
-做著 做着
-含著 含着
-涵著 涵着
-演著 演着
-保障著 保障着
-黏著 黏着
-膠著 膠着
-附著 附着
-代表著 代表着
-浮著 浮着
-寫著 寫着
-遇著 遇着
-殺著 殺着
-著筆 着筆
-著鞭 着鞭
-著法 着法
-著火 着火
-著急 着急
-著艦 着艦
-著腳 着腳
-著她 着她
-著緊 着緊
-著力 着力
-著涼 着涼
-著陸 着陸
-著錄 着錄
-著落 着落
-著忙 着忙
-著迷 着迷
-著墨 着墨
-著妳 着妳
-著你 着你
-著色 着色
-著什麼急 着什麼急
-著實 着實
-著手 着手
-著數 着數
-著絲 着絲
-著他 着他
-著它 着它
-著祂 着祂
-著我 着我
-著想 着想
-著眼 着眼
-著衣 着衣
-著意 着意
-著重 着重
-著重 着重
-著裝 着裝
-著地 着地
-不著邊際 不着邊際
-不著痕跡 不着痕跡
-挨著作 挨著作
-挨著者 挨著者
-挨著名 挨著名
-挨著述 挨著述
-挨著稱 挨著稱
-挨著錄 挨著錄
-愛著作 愛著作
-愛著者 愛著者
-愛著名 愛著名
-愛著述 愛著述
-愛著稱 愛著稱
-愛著錄 愛著錄
-愛著書 愛著書
-暗著作 暗著作
-暗著者 暗著者
-暗著名 暗著名
-暗著述 暗著述
-暗著稱 暗著稱
-暗著錄 暗著錄
-暗著書 暗著書
-昂著作 昂著作
-昂著者 昂著者
-昂著名 昂著名
-昂著述 昂著述
-昂著稱 昂著稱
-昂著錄 昂著錄
-昂著書 昂著書
-擺著作 擺著作
-擺著者 擺著者
-擺著名 擺著名
-擺著述 擺著述
-擺著稱 擺著稱
-擺著錄 擺著錄
-伴著作 伴著作
-伴著者 伴著者
-伴著名 伴著名
-伴著述 伴著述
-伴著稱 伴著稱
-伴著錄 伴著錄
-伴著書 伴著書
-辦著作 辦著作
-辦著者 辦著者
-辦著名 辦著名
-辦著述 辦著述
-辦著稱 辦著稱
-辦著錄 辦著錄
-辦著書 辦著書
-幫著作 幫著作
-幫著者 幫著者
-幫著名 幫著名
-幫著述 幫著述
-幫著稱 幫著稱
-幫著錄 幫著錄
-幫著書 幫著書
-綁著作 綁著作
-綁著者 綁著者
-綁著名 綁著名
-綁著述 綁著述
-綁著稱 綁著稱
-綁著錄 綁著錄
-綁著書 綁著書
-抱著作 抱著作
-抱著者 抱著者
-抱著名 抱著名
-抱著述 抱著述
-抱著稱 抱著稱
-抱著錄 抱著錄
-背著作 背著作
-背著者 背著者
-背著名 背著名
-背著述 背著述
-背著稱 背著稱
-背著錄 背著錄
-背著書 背著書
-備著作 備著作
-備著者 備著者
-備著名 備著名
-備著述 備著述
-備著稱 備著稱
-備著錄 備著錄
-備著書 備著書
-本著作 本著作
-本著者 本著者
-本著名 本著名
-本著述 本著述
-本著稱 本著稱
-本著錄 本著錄
-本著書 本著書
-逼著作 逼著作
-逼著者 逼著者
-逼著名 逼著名
-逼著述 逼著述
-逼著稱 逼著稱
-逼著錄 逼著錄
-逼著書 逼著書
-閉著作 閉著作
-閉著者 閉著者
-閉著名 閉著名
-閉著述 閉著述
-閉著稱 閉著稱
-閉著錄 閉著錄
-閉著書 閉著書
-變著作 變著作
-變著者 變著者
-變著名 變著名
-變著述 變著述
-變著稱 變著稱
-變著錄 變著錄
-變著書 變著書
-猜著作 猜著作
-猜著者 猜著者
-猜著名 猜著名
-猜著述 猜著述
-猜著稱 猜著稱
-猜著錄 猜著錄
-猜著書 猜著書
-踩著作 踩著作
-踩著者 踩著者
-踩著名 踩著名
-踩著述 踩著述
-踩著稱 踩著稱
-踩著錄 踩著錄
-踩著書 踩著書
-藏著作 藏著作
-藏著者 藏著者
-藏著名 藏著名
-藏著述 藏著述
-藏著稱 藏著稱
-藏著錄 藏著錄
-藏著書 藏著書
-側著作 側著作
-側著者 側著者
-側著名 側著名
-側著述 側著述
-側著稱 側著稱
-側著錄 側著錄
-側著書 側著書
-纏著作 纏著作
-纏著者 纏著者
-纏著名 纏著名
-纏著述 纏著述
-纏著稱 纏著稱
-纏著錄 纏著錄
-纏著書 纏著書
-敞著作 敞著作
-敞著者 敞著者
-敞著名 敞著名
-敞著述 敞著述
-敞著稱 敞著稱
-敞著錄 敞著錄
-唱著作 唱著作
-唱著者 唱著者
-唱著名 唱著名
-唱著述 唱著述
-唱著稱 唱著稱
-唱著錄 唱著錄
-唱著書 唱著書
-朝著作 朝著作
-朝著者 朝著者
-朝著名 朝著名
-朝著述 朝著述
-朝著稱 朝著稱
-朝著錄 朝著錄
-沉著作 沉著作
-沉著者 沉著者
-沉著名 沉著名
-沉著述 沉著述
-沉著稱 沉著稱
-沉著錄 沉著錄
-沉著書 沉著書
-乘著作 乘著作
-乘著者 乘著者
-乘著名 乘著名
-乘著述 乘著述
-乘著稱 乘著稱
-乘著錄 乘著錄
-乘著書 乘著書
-持著作 持著作
-持著者 持著者
-持著名 持著名
-持著述 持著述
-持著稱 持著稱
-持著錄 持著錄
-斥著作 斥著作
-斥著者 斥著者
-斥著名 斥著名
-斥著述 斥著述
-斥著稱 斥著稱
-斥著錄 斥著錄
-斥著書 斥著書
-醜著作 醜著作
-醜著者 醜著者
-醜著名 醜著名
-醜著述 醜著述
-醜著稱 醜著稱
-醜著錄 醜著錄
-醜著書 醜著書
-穿著作 穿著作
-穿著者 穿著者
-穿著名 穿著名
-穿著述 穿著述
-穿著稱 穿著稱
-穿著錄 穿著錄
-穿著書 穿著書
-吹著作 吹著作
-吹著者 吹著者
-吹著名 吹著名
-吹著述 吹著述
-吹著稱 吹著稱
-吹著錄 吹著錄
-吹著書 吹著書
-達著作 達著作
-達著者 達著者
-達著名 達著名
-達著述 達著述
-達著稱 達著稱
-達著錄 達著錄
-達著書 達著書
-打著作 打著作
-打著者 打著者
-打著名 打著名
-打著述 打著述
-打著稱 打著稱
-打著錄 打著錄
-打著書 打著書
-待著作 待著作
-待著者 待著者
-待著名 待著名
-待著述 待著述
-待著稱 待著稱
-待著錄 待著錄
-待著書 待著書
-帶著作 帶著作
-帶著者 帶著者
-帶著名 帶著名
-帶著述 帶著述
-帶著稱 帶著稱
-帶著錄 帶著錄
-帶著書 帶著書
-戴著作 戴著作
-戴著者 戴著者
-戴著名 戴著名
-戴著述 戴著述
-戴著稱 戴著稱
-戴著錄 戴著錄
-戴著書 戴著書
-當著作 當著作
-當著者 當著者
-當著名 當著名
-當著述 當著述
-當著稱 當著稱
-當著錄 當著錄
-當著書 當著書
-擋著作 擋著作
-擋著者 擋著者
-擋著名 擋著名
-擋著述 擋著述
-擋著稱 擋著稱
-擋著錄 擋著錄
-得著作 得著作
-得著者 得著者
-得著名 得著名
-得著述 得著述
-得著稱 得著稱
-得著錄 得著錄
-得著書 得著書
-瞪著作 瞪著作
-瞪著者 瞪著者
-瞪著名 瞪著名
-瞪著述 瞪著述
-瞪著稱 瞪著稱
-瞪著錄 瞪著錄
-瞪著書 瞪著書
-低著作 低著作
-低著者 低著者
-低著名 低著名
-低著述 低著述
-低著稱 低著稱
-低著錄 低著錄
-低著書 低著書
-點著作 點著作
-點著者 點著者
-點著名 點著名
-點著述 點著述
-點著稱 點著稱
-點著錄 點著錄
-點著書 點著書
-盯著作 盯著作
-盯著者 盯著者
-盯著名 盯著名
-盯著述 盯著述
-盯著稱 盯著稱
-盯著錄 盯著錄
-盯著書 盯著書
-頂著作 頂著作
-頂著者 頂著者
-頂著名 頂著名
-頂著述 頂著述
-頂著稱 頂著稱
-頂著錄 頂著錄
-頂著書 頂著書
-定著作 定著作
-定著者 定著者
-定著名 定著名
-定著述 定著述
-定著稱 定著稱
-定著錄 定著錄
-定著書 定著書
-動著作 動著作
-動著者 動著者
-動著名 動著名
-動著述 動著述
-動著稱 動著稱
-動著錄 動著錄
-動著書 動著書
-鬥著作 鬥著作
-鬥著者 鬥著者
-鬥著名 鬥著名
-鬥著述 鬥著述
-鬥著稱 鬥著稱
-鬥著錄 鬥著錄
-鬥著書 鬥著書
-獨著作 獨著作
-獨著者 獨著者
-獨著名 獨著名
-獨著述 獨著述
-獨著稱 獨著稱
-獨著錄 獨著錄
-獨著書 獨著書
-對著作 對著作
-對著者 對著者
-對著名 對著名
-對著述 對著述
-對著稱 對著稱
-對著錄 對著錄
-對著書 對著書
-盾著作 盾著作
-盾著者 盾著者
-盾著名 盾著名
-盾著述 盾著述
-盾著稱 盾著稱
-盾著錄 盾著錄
-盾著書 盾著書
-犯不著作 犯不著作
-犯不著者 犯不著者
-犯不著名 犯不著名
-犯不著述 犯不著述
-犯不著稱 犯不著稱
-犯不著錄 犯不著錄
-犯不著書 犯不著書
-福著作 福著作
-福著者 福著者
-福著名 福著名
-福著述 福著述
-福著稱 福著稱
-福著錄 福著錄
-福著書 福著書
-趕著作 趕著作
-趕著者 趕著者
-趕著名 趕著名
-趕著述 趕著述
-趕著稱 趕著稱
-趕著錄 趕著錄
-趕著書 趕著書
-高著作 高著作
-高著者 高著者
-高著名 高著名
-高著述 高著述
-高著稱 高著稱
-高著錄 高著錄
-高著書 高著書
-隔著作 隔著作
-隔著者 隔著者
-隔著名 隔著名
-隔著述 隔著述
-隔著稱 隔著稱
-隔著錄 隔著錄
-隔著書 隔著書
-跟著作 跟著作
-跟著者 跟著者
-跟著名 跟著名
-跟著述 跟著述
-跟著稱 跟著稱
-跟著錄 跟著錄
-跟著書 跟著書
-孤著作 孤著作
-孤著者 孤著者
-孤著名 孤著名
-孤著述 孤著述
-孤著稱 孤著稱
-孤著錄 孤著錄
-孤著書 孤著書
-關著作 關著作
-關著者 關著者
-關著名 關著名
-關著述 關著述
-關著稱 關著稱
-關著錄 關著錄
-關著書 關著書
-管著作 管著作
-管著者 管著者
-管著名 管著名
-管著述 管著述
-管著稱 管著稱
-管著錄 管著錄
-管著書 管著書
-慣著作 慣著作
-慣著者 慣著者
-慣著名 慣著名
-慣著述 慣著述
-慣著稱 慣著稱
-慣著錄 慣著錄
-慣著書 慣著書
-光著作 光著作
-光著者 光著者
-光著名 光著名
-光著述 光著述
-光著稱 光著稱
-光著錄 光著錄
-光著書 光著書
-跪著作 跪著作
-跪著者 跪著者
-跪著名 跪著名
-跪著述 跪著述
-跪著稱 跪著稱
-跪著錄 跪著錄
-跪著書 跪著書
-裹著作 裹著作
-裹著者 裹著者
-裹著名 裹著名
-裹著述 裹著述
-裹著稱 裹著稱
-裹著錄 裹著錄
-裹著書 裹著書
-撼著作 撼著作
-撼著者 撼著者
-撼著名 撼著名
-撼著述 撼著述
-撼著稱 撼著稱
-撼著錄 撼著錄
-撼著書 撼著書
-喝著作 喝著作
-喝著者 喝著者
-喝著名 喝著名
-喝著述 喝著述
-喝著稱 喝著稱
-喝著錄 喝著錄
-喝著書 喝著書
-候著作 候著作
-候著者 候著者
-候著名 候著名
-候著述 候著述
-候著稱 候著稱
-候著錄 候著錄
-候著書 候著書
-懷著作 懷著作
-懷著者 懷著者
-懷著名 懷著名
-懷著述 懷著述
-懷著稱 懷著稱
-懷著錄 懷著錄
-懷著書 懷著書
-晃著作 晃著作
-晃著者 晃著者
-晃著名 晃著名
-晃著述 晃著述
-晃著稱 晃著稱
-晃著錄 晃著錄
-揮著作 揮著作
-揮著者 揮著者
-揮著名 揮著名
-揮著述 揮著述
-揮著稱 揮著稱
-揮著錄 揮著錄
-活著作 活著作
-活著者 活著者
-活著名 活著名
-活著述 活著述
-活著稱 活著稱
-活著錄 活著錄
-活著書 活著書
-獲著作 獲著作
-獲著者 獲著者
-獲著名 獲著名
-獲著述 獲著述
-獲著稱 獲著稱
-獲著錄 獲著錄
-獲著書 獲著書
-獲著作 獲著作
-獲著者 獲著者
-獲著名 獲著名
-獲著述 獲著述
-獲著稱 獲著稱
-獲著錄 獲著錄
-獲著書 獲著書
-急著作 急著作
-急著者 急著者
-急著名 急著名
-急著述 急著述
-急著稱 急著稱
-急著錄 急著錄
-急著書 急著書
-記著作 記著作
-記著者 記著者
-記著名 記著名
-記著述 記著述
-記著稱 記著稱
-記著錄 記著錄
-記著書 記著書
-冀著作 冀著作
-冀著者 冀著者
-冀著名 冀著名
-冀著述 冀著述
-冀著稱 冀著稱
-冀著錄 冀著錄
-冀著書 冀著書
-夾著作 夾著作
-夾著者 夾著者
-夾著名 夾著名
-夾著述 夾著述
-夾著稱 夾著稱
-夾著錄 夾著錄
-夾著書 夾著書
-駕著作 駕著作
-駕著者 駕著者
-駕著名 駕著名
-駕著述 駕著述
-駕著稱 駕著稱
-駕著錄 駕著錄
-駕著書 駕著書
-見著作 見著作
-見著者 見著者
-見著名 見著名
-見著述 見著述
-見著稱 見著稱
-見著錄 見著錄
-見著書 見著書
-閑著作 閑著作
-閑著者 閑著者
-閑著名 閑著名
-閑著述 閑著述
-閑著稱 閑著稱
-閑著錄 閑著錄
-閑著書 閑著書
-叫著作 叫著作
-叫著者 叫著者
-叫著名 叫著名
-叫著述 叫著述
-叫著稱 叫著稱
-叫著錄 叫著錄
-叫著書 叫著書
-接著作 接著作
-接著者 接著者
-接著名 接著名
-接著述 接著述
-接著稱 接著稱
-接著錄 接著錄
-借著作 借著作
-借著者 借著者
-借著名 借著名
-借著述 借著述
-借著稱 借著稱
-借著錄 借著錄
-借著書 借著書
-借著作 借著作
-借著者 借著者
-借著名 借著名
-借著述 借著述
-借著稱 借著稱
-借著錄 借著錄
-借著書 借著書
-據著作 據著作
-據著者 據著者
-據著名 據著名
-據著述 據著述
-據著稱 據著稱
-據著錄 據著錄
-據著書 據著書
-開著作 開著作
-開著者 開著者
-開著名 開著名
-開著述 開著述
-開著稱 開著稱
-開著錄 開著錄
-開著書 開著書
-看著作 看著作
-看著者 看著者
-看著名 看著名
-看著述 看著述
-看著稱 看著稱
-看著錄 看著錄
-看著書 看著書
-康著作 康著作
-康著者 康著者
-康著名 康著名
-康著述 康著述
-康著稱 康著稱
-康著錄 康著錄
-康著書 康著書
-扛著作 扛著作
-扛著者 扛著者
-扛著名 扛著名
-扛著述 扛著述
-扛著稱 扛著稱
-扛著錄 扛著錄
-扛著書 扛著書
-考著作 考著作
-考著者 考著者
-考著名 考著名
-考著述 考著述
-考著稱 考著稱
-考著錄 考著錄
-考著書 考著書
-渴著作 渴著作
-渴著者 渴著者
-渴著名 渴著名
-渴著述 渴著述
-渴著稱 渴著稱
-渴著錄 渴著錄
-渴著書 渴著書
-刻著作 刻著作
-刻著者 刻著者
-刻著名 刻著名
-刻著述 刻著述
-刻著稱 刻著稱
-刻著錄 刻著錄
-刻著書 刻著書
-空著作 空著作
-空著者 空著者
-空著名 空著名
-空著述 空著述
-空著稱 空著稱
-空著錄 空著錄
-空著書 空著書
-哭著作 哭著作
-哭著者 哭著者
-哭著名 哭著名
-哭著述 哭著述
-哭著稱 哭著稱
-哭著錄 哭著錄
-哭著書 哭著書
-苦著作 苦著作
-苦著者 苦著者
-苦著名 苦著名
-苦著述 苦著述
-苦著稱 苦著稱
-苦著錄 苦著錄
-苦著書 苦著書
-捆著作 捆著作
-捆著者 捆著者
-捆著名 捆著名
-捆著述 捆著述
-捆著稱 捆著稱
-捆著錄 捆著錄
-困著作 困著作
-困著者 困著者
-困著名 困著名
-困著述 困著述
-困著稱 困著稱
-困著錄 困著錄
-困著書 困著書
-拉著作 拉著作
-拉著者 拉著者
-拉著名 拉著名
-拉著述 拉著述
-拉著稱 拉著稱
-拉著錄 拉著錄
-拉著書 拉著書
-來著作 來著作
-來著者 來著者
-來著名 來著名
-來著述 來著述
-來著稱 來著稱
-來著錄 來著錄
-來著書 來著書
-樂著作 樂著作
-樂著者 樂著者
-樂著名 樂著名
-樂著述 樂著述
-樂著稱 樂著稱
-樂著錄 樂著錄
-樂著書 樂著書
-努力著作 努力著作
-努力著者 努力著者
-努力著名 努力著名
-努力著述 努力著述
-努力著稱 努力著稱
-努力著錄 努力著錄
-努力著書 努力著書
-麗著作 麗著作
-麗著者 麗著者
-麗著名 麗著名
-麗著述 麗著述
-麗著稱 麗著稱
-麗著錄 麗著錄
-麗著書 麗著書
-連著作 連著作
-連著者 連著者
-連著名 連著名
-連著述 連著述
-連著稱 連著稱
-連著錄 連著錄
-連著書 連著書
-戀著作 戀著作
-戀著者 戀著者
-戀著名 戀著名
-戀著述 戀著述
-戀著稱 戀著稱
-戀著錄 戀著錄
-戀著書 戀著書
-涼著作 涼著作
-涼著者 涼著者
-涼著名 涼著名
-涼著述 涼著述
-涼著稱 涼著稱
-涼著錄 涼著錄
-涼著書 涼著書
-亮著作 亮著作
-亮著者 亮著者
-亮著名 亮著名
-亮著述 亮著述
-亮著稱 亮著稱
-亮著錄 亮著錄
-亮著書 亮著書
-臨著作 臨著作
-臨著者 臨著者
-臨著名 臨著名
-臨著述 臨著述
-臨著稱 臨著稱
-臨著錄 臨著錄
-臨著書 臨著書
-拎著作 拎著作
-拎著者 拎著者
-拎著名 拎著名
-拎著述 拎著述
-拎著稱 拎著稱
-拎著錄 拎著錄
-領著作 領著作
-領著者 領著者
-領著名 領著名
-領著述 領著述
-領著稱 領著稱
-領著錄 領著錄
-領著書 領著書
-流著作 流著作
-流著者 流著者
-流著名 流著名
-流著述 流著述
-流著稱 流著稱
-流著錄 流著錄
-流著書 流著書
-留著作 留著作
-留著者 留著者
-留著名 留著名
-留著述 留著述
-留著稱 留著稱
-留著錄 留著錄
-留著書 留著書
-摟著作 摟著作
-摟著者 摟著者
-摟著名 摟著名
-摟著述 摟著述
-摟著稱 摟著稱
-摟著錄 摟著錄
-陋著作 陋著作
-陋著者 陋著者
-陋著名 陋著名
-陋著述 陋著述
-陋著稱 陋著稱
-陋著錄 陋著錄
-陋著書 陋著書
-落著作 落著作
-落著者 落著者
-落著名 落著名
-落著述 落著述
-落著稱 落著稱
-落著錄 落著錄
-落著書 落著書
-罵著作 罵著作
-罵著者 罵著者
-罵著名 罵著名
-罵著述 罵著述
-罵著稱 罵著稱
-罵著錄 罵著錄
-罵著書 罵著書
-瞞著作 瞞著作
-瞞著者 瞞著者
-瞞著名 瞞著名
-瞞著述 瞞著述
-瞞著稱 瞞著稱
-瞞著錄 瞞著錄
-瞞著書 瞞著書
-漫著作 漫著作
-漫著者 漫著者
-漫著名 漫著名
-漫著述 漫著述
-漫著稱 漫著稱
-漫著錄 漫著錄
-漫著書 漫著書
-忙著作 忙著作
-忙著者 忙著者
-忙著名 忙著名
-忙著述 忙著述
-忙著稱 忙著稱
-忙著錄 忙著錄
-忙著書 忙著書
-冒著作 冒著作
-冒著者 冒著者
-冒著名 冒著名
-冒著述 冒著述
-冒著稱 冒著稱
-冒著錄 冒著錄
-冒著書 冒著書
-美著作 美著作
-美著者 美著者
-美著名 美著名
-美著述 美著述
-美著稱 美著稱
-美著錄 美著錄
-美著書 美著書
-夢著作 夢著作
-夢著者 夢著者
-夢著名 夢著名
-夢著述 夢著述
-夢著稱 夢著稱
-夢著錄 夢著錄
-夢著書 夢著書
-蒙著作 蒙著作
-蒙著者 蒙著者
-蒙著名 蒙著名
-蒙著述 蒙著述
-蒙著稱 蒙著稱
-蒙著錄 蒙著錄
-蒙著書 蒙著書
-拿著作 拿著作
-拿著者 拿著者
-拿著名 拿著名
-拿著述 拿著述
-拿著稱 拿著稱
-拿著錄 拿著錄
-逆著作 逆著作
-逆著者 逆著者
-逆著名 逆著名
-逆著述 逆著述
-逆著稱 逆著稱
-逆著錄 逆著錄
-逆著書 逆著書
-釀著作 釀著作
-釀著者 釀著者
-釀著名 釀著名
-釀著述 釀著述
-釀著稱 釀著稱
-釀著錄 釀著錄
-釀著書 釀著書
-努著作 努著作
-努著者 努著者
-努著名 努著名
-努著述 努著述
-努著稱 努著稱
-努著錄 努著錄
-努著書 努著書
-趴著作 趴著作
-趴著者 趴著者
-趴著名 趴著名
-趴著述 趴著述
-趴著稱 趴著稱
-趴著錄 趴著錄
-趴著書 趴著書
-跑著作 跑著作
-跑著者 跑著者
-跑著名 跑著名
-跑著述 跑著述
-跑著稱 跑著稱
-跑著錄 跑著錄
-跑著書 跑著書
-陪著作 陪著作
-陪著者 陪著者
-陪著名 陪著名
-陪著述 陪著述
-陪著稱 陪著稱
-陪著錄 陪著錄
-陪著書 陪著書
-配著作 配著作
-配著者 配著者
-配著名 配著名
-配著述 配著述
-配著稱 配著稱
-配著錄 配著錄
-配著書 配著書
-披著作 披著作
-披著者 披著者
-披著名 披著名
-披著述 披著述
-披著稱 披著稱
-披著錄 披著錄
-披著書 披著書
-騙著作 騙著作
-騙著者 騙著者
-騙著名 騙著名
-騙著述 騙著述
-騙著稱 騙著稱
-騙著錄 騙著錄
-騙著書 騙著書
-飄著作 飄著作
-飄著者 飄著者
-飄著名 飄著名
-飄著述 飄著述
-飄著稱 飄著稱
-飄著錄 飄著錄
-飄著書 飄著書
-拼著作 拼著作
-拼著者 拼著者
-拼著名 拼著名
-拼著述 拼著述
-拼著稱 拼著稱
-拼著錄 拼著錄
-鋪著作 鋪著作
-鋪著者 鋪著者
-鋪著名 鋪著名
-鋪著述 鋪著述
-鋪著稱 鋪著稱
-鋪著錄 鋪著錄
-鋪著書 鋪著書
-騎著作 騎著作
-騎著者 騎著者
-騎著名 騎著名
-騎著述 騎著述
-騎著稱 騎著稱
-騎著錄 騎著錄
-騎著書 騎著書
-牽著作 牽著作
-牽著者 牽著者
-牽著名 牽著名
-牽著述 牽著述
-牽著稱 牽著稱
-牽著錄 牽著錄
-牽著書 牽著書
-求著作 求著作
-求著者 求著者
-求著名 求著名
-求著述 求著述
-求著稱 求著稱
-求著錄 求著錄
-求著書 求著書
-去著作 去著作
-去著者 去著者
-去著名 去著名
-去著述 去著述
-去著稱 去著稱
-去著錄 去著錄
-去著書 去著書
-嚷著作 嚷著作
-嚷著者 嚷著者
-嚷著名 嚷著名
-嚷著述 嚷著述
-嚷著稱 嚷著稱
-嚷著錄 嚷著錄
-嚷著書 嚷著書
-繞著作 繞著作
-繞著者 繞著者
-繞著名 繞著名
-繞著述 繞著述
-繞著稱 繞著稱
-繞著錄 繞著錄
-繞著書 繞著書
-忍著作 忍著作
-忍著者 忍著者
-忍著名 忍著名
-忍著述 忍著述
-忍著稱 忍著稱
-忍著錄 忍著錄
-忍著書 忍著書
-揉著作 揉著作
-揉著者 揉著者
-揉著名 揉著名
-揉著述 揉著述
-揉著稱 揉著稱
-揉著錄 揉著錄
-揉著書 揉著書
-潤著作 潤著作
-潤著者 潤著者
-潤著名 潤著名
-潤著述 潤著述
-潤著稱 潤著稱
-潤著錄 潤著錄
-潤著書 潤著書
-燒著作 燒著作
-燒著者 燒著者
-燒著名 燒著名
-燒著述 燒著述
-燒著稱 燒著稱
-燒著錄 燒著錄
-燒著書 燒著書
-身著作 身著作
-身著者 身著者
-身著名 身著名
-身著述 身著述
-身著稱 身著稱
-身著錄 身著錄
-身著書 身著書
-沉著作 沉著作
-沉著者 沉著者
-沉著名 沉著名
-沉著述 沉著述
-沉著稱 沉著稱
-沉著錄 沉著錄
-沉著書 沉著書
-盛著作 盛著作
-盛著者 盛著者
-盛著名 盛著名
-盛著述 盛著述
-盛著稱 盛著稱
-盛著錄 盛著錄
-盛著書 盛著書
-試著作 試著作
-試著者 試著者
-試著名 試著名
-試著述 試著述
-試著稱 試著稱
-試著錄 試著錄
-試著書 試著書
-守著作 守著作
-守著者 守著者
-守著名 守著名
-守著述 守著述
-守著稱 守著稱
-守著錄 守著錄
-守著書 守著書
-受著作 受著作
-受著者 受著者
-受著名 受著名
-受著述 受著述
-受著稱 受著稱
-受著錄 受著錄
-受著書 受著書
-梳著作 梳著作
-梳著者 梳著者
-梳著名 梳著名
-梳著述 梳著述
-梳著稱 梳著稱
-梳著錄 梳著錄
-豎著作 豎著作
-豎著者 豎著者
-豎著名 豎著名
-豎著述 豎著述
-豎著稱 豎著稱
-豎著錄 豎著錄
-豎著書 豎著書
-數著作 數著作
-數著者 數著者
-數著名 數著名
-數著述 數著述
-數著稱 數著稱
-數著錄 數著錄
-睡著作 睡著作
-睡著者 睡著者
-睡著名 睡著名
-睡著述 睡著述
-睡著稱 睡著稱
-睡著錄 睡著錄
-睡著書 睡著書
-順著作 順著作
-順著者 順著者
-順著名 順著名
-順著述 順著述
-順著稱 順著稱
-順著錄 順著錄
-順著書 順著書
-隨著作 隨著作
-隨著者 隨著者
-隨著名 隨著名
-隨著述 隨著述
-隨著稱 隨著稱
-隨著錄 隨著錄
-隨著書 隨著書
-踏著作 踏著作
-踏著者 踏著者
-踏著名 踏著名
-踏著述 踏著述
-踏著稱 踏著稱
-踏著錄 踏著錄
-抬著作 抬著作
-抬著者 抬著者
-抬著名 抬著名
-抬著述 抬著述
-抬著稱 抬著稱
-抬著錄 抬著錄
-躺著作 躺著作
-躺著者 躺著者
-躺著名 躺著名
-躺著述 躺著述
-躺著稱 躺著稱
-躺著錄 躺著錄
-躺著書 躺著書
-提著作 提著作
-提著者 提著者
-提著名 提著名
-提著述 提著述
-提著稱 提著稱
-提著錄 提著錄
-甜著作 甜著作
-甜著者 甜著者
-甜著名 甜著名
-甜著述 甜著述
-甜著稱 甜著稱
-甜著錄 甜著錄
-甜著書 甜著書
-挑著作 挑著作
-挑著者 挑著者
-挑著名 挑著名
-挑著述 挑著述
-挑著稱 挑著稱
-挑著錄 挑著錄
-跳著作 跳著作
-跳著者 跳著者
-跳著名 跳著名
-跳著述 跳著述
-跳著稱 跳著稱
-跳著錄 跳著錄
-跳著書 跳著書
-聽著作 聽著作
-聽著者 聽著者
-聽著名 聽著名
-聽著述 聽著述
-聽著稱 聽著稱
-聽著錄 聽著錄
-聽著書 聽著書
-偷著作 偷著作
-偷著者 偷著者
-偷著名 偷著名
-偷著述 偷著述
-偷著稱 偷著稱
-偷著錄 偷著錄
-偷著書 偷著書
-拖著作 拖著作
-拖著者 拖著者
-拖著名 拖著名
-拖著述 拖著述
-拖著稱 拖著稱
-拖著錄 拖著錄
-望著作 望著作
-望著者 望著者
-望著名 望著名
-望著述 望著述
-望著稱 望著稱
-望著錄 望著錄
-望著書 望著書
-圍著作 圍著作
-圍著者 圍著者
-圍著名 圍著名
-圍著述 圍著述
-圍著稱 圍著稱
-圍著錄 圍著錄
-圍著書 圍著書
-味著作 味著作
-味著者 味著者
-味著名 味著名
-味著述 味著述
-味著稱 味著稱
-味著錄 味著錄
-味著書 味著書
-想著作 想著作
-想著者 想著者
-想著名 想著名
-想著述 想著述
-想著稱 想著稱
-想著錄 想著錄
-想著書 想著書
-響著作 響著作
-響著者 響著者
-響著名 響著名
-響著述 響著述
-響著稱 響著稱
-響著錄 響著錄
-響著書 響著書
-向著作 向著作
-向著者 向著者
-向著名 向著名
-向著述 向著述
-向著稱 向著稱
-向著錄 向著錄
-向著書 向著書
-笑著作 笑著作
-笑著者 笑著者
-笑著名 笑著名
-笑著述 笑著述
-笑著稱 笑著稱
-笑著錄 笑著錄
-笑著書 笑著書
-心著作 心著作
-心著者 心著者
-心著名 心著名
-心著述 心著述
-心著稱 心著稱
-心著錄 心著錄
-心著書 心著書
-信著作 信著作
-信著者 信著者
-信著名 信著名
-信著述 信著述
-信著稱 信著稱
-信著錄 信著錄
-信著書 信著書
-行著作 行著作
-行著者 行著者
-行著名 行著名
-行著述 行著述
-行著稱 行著稱
-行著錄 行著錄
-行著書 行著書
-性著作 性著作
-性著者 性著者
-性著名 性著名
-性著述 性著述
-性著稱 性著稱
-性著錄 性著錄
-性著書 性著書
-學著作 學著作
-學著者 學著者
-學著名 學著名
-學著述 學著述
-學著稱 學著稱
-學著錄 學著錄
-學著書 學著書
-尋著作 尋著作
-尋著者 尋著者
-尋著名 尋著名
-尋著述 尋著述
-尋著稱 尋著稱
-尋著錄 尋著錄
-尋著書 尋著書
-循著作 循著作
-循著者 循著者
-循著名 循著名
-循著述 循著述
-循著稱 循著稱
-循著錄 循著錄
-循著書 循著書
-壓著作 壓著作
-壓著者 壓著者
-壓著名 壓著名
-壓著述 壓著述
-壓著稱 壓著稱
-壓著錄 壓著錄
-壓著書 壓著書
-雅著作 雅著作
-雅著者 雅著者
-雅著名 雅著名
-雅著述 雅著述
-雅著稱 雅著稱
-雅著錄 雅著錄
-雅著書 雅著書
-沿著作 沿著作
-沿著者 沿著者
-沿著名 沿著名
-沿著述 沿著述
-沿著稱 沿著稱
-沿著錄 沿著錄
-沿著書 沿著書
-耀著作 耀著作
-耀著者 耀著者
-耀著名 耀著名
-耀著述 耀著述
-耀著稱 耀著稱
-耀著錄 耀著錄
-耀著書 耀著書
-掖著作 掖著作
-掖著者 掖著者
-掖著名 掖著名
-掖著述 掖著述
-掖著稱 掖著稱
-掖著錄 掖著錄
-衣著作 衣著作
-衣著者 衣著者
-衣著名 衣著名
-衣著述 衣著述
-衣著稱 衣著稱
-衣著錄 衣著錄
-衣著書 衣著書
-疑著作 疑著作
-疑著者 疑著者
-疑著名 疑著名
-疑著述 疑著述
-疑著稱 疑著稱
-疑著錄 疑著錄
-疑著書 疑著書
-溢著作 溢著作
-溢著者 溢著者
-溢著名 溢著名
-溢著述 溢著述
-溢著稱 溢著稱
-溢著錄 溢著錄
-溢著書 溢著書
-藝著作 藝著作
-藝著者 藝著者
-藝著名 藝著名
-藝著述 藝著述
-藝著稱 藝著稱
-藝著錄 藝著錄
-藝著書 藝著書
-因著作 因著作
-因著者 因著者
-因著名 因著名
-因著述 因著述
-因著稱 因著稱
-因著錄 因著錄
-因著書 因著書
-印著作 印著作
-印著者 印著者
-印著名 印著名
-印著述 印著述
-印著稱 印著稱
-印著錄 印著錄
-印著書 印著書
-應著作 應著作
-應著者 應著者
-應著名 應著名
-應著述 應著述
-應著稱 應著稱
-應著錄 應著錄
-應著書 應著書
-映著作 映著作
-映著者 映著者
-映著名 映著名
-映著述 映著述
-映著稱 映著稱
-映著錄 映著錄
-映著書 映著書
-用著作 用著作
-用著者 用著者
-用著名 用著名
-用著述 用著述
-用著稱 用著稱
-用著錄 用著錄
-用著書 用著書
-悠著作 悠著作
-悠著者 悠著者
-悠著名 悠著名
-悠著述 悠著述
-悠著稱 悠著稱
-悠著錄 悠著錄
-悠著書 悠著書
-有著作 有著作
-有著者 有著者
-有著名 有著名
-有著述 有著述
-有著稱 有著稱
-有著錄 有著錄
-有著書 有著書
-與著作 與著作
-與著者 與著者
-與著名 與著名
-與著述 與著述
-與著稱 與著稱
-與著錄 與著錄
-與著書 與著書
-語著作 語著作
-語著者 語著者
-語著名 語著名
-語著述 語著述
-語著稱 語著稱
-語著錄 語著錄
-語著書 語著書
-豫著作 豫著作
-豫著者 豫著者
-豫著名 豫著名
-豫著述 豫著述
-豫著稱 豫著稱
-豫著錄 豫著錄
-豫著書 豫著書
-遠著作 遠著作
-遠著者 遠著者
-遠著名 遠著名
-遠著述 遠著述
-遠著稱 遠著稱
-遠著錄 遠著錄
-遠著書 遠著書
-躍著作 躍著作
-躍著者 躍著者
-躍著名 躍著名
-躍著述 躍著述
-躍著稱 躍著稱
-躍著錄 躍著錄
-躍著書 躍著書
-雜著作 雜著作
-雜著者 雜著者
-雜著名 雜著名
-雜著述 雜著述
-雜著稱 雜著稱
-雜著錄 雜著錄
-雜著書 雜著書
-載著作 載著作
-載著者 載著者
-載著名 載著名
-載著述 載著述
-載著稱 載著稱
-載著錄 載著錄
-載著書 載著書
-在著作 在著作
-在著者 在著者
-在著名 在著名
-在著述 在著述
-在著稱 在著稱
-在著錄 在著錄
-在著書 在著書
-紮著作 紮著作
-紮著者 紮著者
-紮著名 紮著名
-紮著述 紮著述
-紮著稱 紮著稱
-紮著錄 紮著錄
-紮著書 紮著書
-展著作 展著作
-展著者 展著者
-展著名 展著名
-展著述 展著述
-展著稱 展著稱
-展著錄 展著錄
-展著書 展著書
-站著作 站著作
-站著者 站著者
-站著名 站著名
-站著述 站著述
-站著稱 站著稱
-站著錄 站著錄
-站著書 站著書
-戰著作 戰著作
-戰著者 戰著者
-戰著名 戰著名
-戰著述 戰著述
-戰著稱 戰著稱
-戰著錄 戰著錄
-戰著書 戰著書
-蘸著作 蘸著作
-蘸著者 蘸著者
-蘸著名 蘸著名
-蘸著述 蘸著述
-蘸著稱 蘸著稱
-蘸著錄 蘸著錄
-蘸著書 蘸著書
-仗著作 仗著作
-仗著者 仗著者
-仗著名 仗著名
-仗著述 仗著述
-仗著稱 仗著稱
-仗著錄 仗著錄
-仗著書 仗著書
-照著作 照著作
-照著者 照著者
-照著名 照著名
-照著述 照著述
-照著稱 照著稱
-照著錄 照著錄
-照著書 照著書
-罩著作 罩著作
-罩著者 罩著者
-罩著名 罩著名
-罩著述 罩著述
-罩著稱 罩著稱
-罩著錄 罩著錄
-罩著書 罩著書
-貞著作 貞著作
-貞著者 貞著者
-貞著名 貞著名
-貞著述 貞著述
-貞著稱 貞著稱
-貞著錄 貞著錄
-貞著書 貞著書
-枕著作 枕著作
-枕著者 枕著者
-枕著名 枕著名
-枕著述 枕著述
-枕著稱 枕著稱
-枕著錄 枕著錄
-爭著作 爭著作
-爭著者 爭著者
-爭著名 爭著名
-爭著述 爭著述
-爭著稱 爭著稱
-爭著錄 爭著錄
-爭著書 爭著書
-掙著作 掙著作
-掙著者 掙著者
-掙著名 掙著名
-掙著述 掙著述
-掙著稱 掙著稱
-掙著錄 掙著錄
-掙著書 掙著書
-制著作 制著作
-制著者 制著者
-制著名 制著名
-制著述 制著述
-制著稱 制著稱
-制著錄 制著錄
-制著書 制著書
-志著作 志著作
-志著者 志著者
-志著名 志著名
-志著述 志著述
-志著稱 志著稱
-志著錄 志著錄
-志著書 志著書
-皺著作 皺著作
-皺著者 皺著者
-皺著名 皺著名
-皺著述 皺著述
-皺著稱 皺著稱
-皺著錄 皺著錄
-皺著書 皺著書
-住著作 住著作
-住著者 住著者
-住著名 住著名
-住著述 住著述
-住著稱 住著稱
-住著錄 住著錄
-住著書 住著書
-抓著作 抓著作
-抓著者 抓著者
-抓著名 抓著名
-抓著述 抓著述
-抓著稱 抓著稱
-抓著錄 抓著錄
-轉著作 轉著作
-轉著者 轉著者
-轉著名 轉著名
-轉著述 轉著述
-轉著稱 轉著稱
-轉著錄 轉著錄
-轉著書 轉著書
-裝著作 裝著作
-裝著者 裝著者
-裝著名 裝著名
-裝著述 裝著述
-裝著稱 裝著稱
-裝著錄 裝著錄
-裝著書 裝著書
-追著作 追著作
-追著者 追著者
-追著名 追著名
-追著述 追著述
-追著稱 追著稱
-追著錄 追著錄
-追著書 追著書
-髭著作 髭著作
-髭著者 髭著者
-髭著名 髭著名
-髭著述 髭著述
-髭著稱 髭著稱
-髭著錄 髭著錄
-髭著書 髭著書
-走著作 走著作
-走著者 走著者
-走著名 走著名
-走著述 走著述
-走著稱 走著稱
-走著錄 走著錄
-走著書 走著書
-坐著作 坐著作
-坐著者 坐著者
-坐著名 坐著名
-坐著述 坐著述
-坐著稱 坐著稱
-坐著錄 坐著錄
-坐著書 坐著書
-做著作 做著作
-做著者 做著者
-做著名 做著名
-做著述 做著述
-做著稱 做著稱
-做著錄 做著錄
-做著書 做著書
-含著作 含著作
-含著者 含著者
-含著名 含著名
-含著述 含著述
-含著稱 含著稱
-含著錄 含著錄
-含著書 含著書
-涵著作 涵著作
-涵著者 涵著者
-涵著名 涵著名
-涵著述 涵著述
-涵著稱 涵著稱
-涵著錄 涵著錄
-涵著書 涵著書
-演著作 演著作
-演著者 演著者
-演著名 演著名
-演著述 演著述
-演著稱 演著稱
-演著錄 演著錄
-演著書 演著書
-保障著作 保障著作
-保障著者 保障著者
-保障著名 保障著名
-保障著述 保障著述
-保障著稱 保障著稱
-保障著錄 保障著錄
-保障著書 保障著書
-黏著作 黏著作
-黏著者 黏著者
-黏著名 黏著名
-黏著述 黏著述
-黏著稱 黏著稱
-黏著錄 黏著錄
-黏著書 黏著書
-膠著作 膠著作
-膠著者 膠著者
-膠著名 膠著名
-膠著述 膠著述
-膠著稱 膠著稱
-膠著錄 膠著錄
-膠著書 膠著書
-附著作 附著作
-附著者 附著者
-附著名 附著名
-附著述 附著述
-附著稱 附著稱
-附著錄 附著錄
-附著書 附著書
-代表著作 代表著作
-代表著者 代表著者
-代表著名 代表著名
-代表著述 代表著述
-代表著稱 代表著稱
-代表著錄 代表著錄
-代表著書 代表著書
-浮著作 浮著作
-浮著者 浮著者
-浮著名 浮著名
-浮著述 浮著述
-浮著稱 浮著稱
-浮著錄 浮著錄
-浮著書 浮著書
-寫著作 寫著作
-寫著者 寫著者
-寫著名 寫著名
-寫著述 寫著述
-寫著稱 寫著稱
-寫著錄 寫著錄
-寫著書 寫著書
-遇著作 遇著作
-遇著者 遇著者
-遇著名 遇著名
-遇著述 遇著述
-遇著稱 遇著稱
-遇著錄 遇著錄
-遇著書 遇著書
-殺著作 殺著作
-殺著者 殺著者
-殺著名 殺著名
-殺著述 殺著述
-殺著稱 殺著稱
-殺著錄 殺著錄
-殺著書 殺著書
-標誌著 標誌着
-幹著 幹着
-干着 幹着
-干着急 干着急
-流露著 流露着
-靠著 靠着
-靠著作 靠著作
-靠著名 靠著名
-靠著錄 靠著錄
-靠著录 靠著錄
-靠著稱 靠著稱
-靠著称 靠著稱
-靠著者 靠著者
-靠著述 靠著述
-新著龍虎門 新著龍虎門
-迫著 迫着
-心繫著 心繫着
-藉著 藉着
-吃得著 吃得着
-吃不著 吃不着
-吃著 吃着
-聞得著 闻得着
-聞不著 闻不着
-聞著 闻着
-嗅得著 嗅得着
-嗅不著 嗅不着
-嗅著 嗅着
-警戒著 警戒着
-榴莲 榴槤
-榴蓮 榴槤
-发布 發佈
-發布 發佈
-掛鉤 掛鈎
-鉤心鬥角 鈎心鬥角
-咤 咤
-叱吒 叱咤
-叱咤 叱咤
-醯 酰
-醯醬 醯醬
-醯雞 醯雞
-醯酱 醯醬
-醯鸡 醯雞
-醯醋 醯醋
-醯醢 醯醢
-醯壶 醯壺
-醯壺 醯壺
-菸 煙
-雪裡紅 雪裏紅
-雪裡蕻 雪裏蕻
-雪里蕻 雪裏蕻
-雪里红 雪裏紅
-森林裡 森林裏
-森林里 森林裏
-日子裡 日子裏
-日子里 日子裏
-故事裡 故事裏
-故事里 故事裏
-領域裡 領域裏
-领域里 領域裏
-時間裡 時間裏
-时间里 時間裏
-深淵裡 深淵裏
-深渊里 深渊裏
-醫院裡 醫院裏
-医院里 医院裏
-春假裡 春假裏
-春假里 春假裏
-暑假裡 暑假裏
-暑假里 暑假裏
-秋假裡 秋假裏
-秋假里 秋假裏
-寒假裡 寒假裏
-寒假里 寒假裏
-春天裡 春天裏
-春天里 春天裏
-夏天裡 夏天裏
-夏天里 夏天裏
-秋天裡 秋天裏
-秋天里 秋天裏
-冬天裡 冬天裏
-冬天里 冬天裏
-春日裡 春日裏
-夏日裡 夏日裏
-秋日裡 秋日裏
-冬日裡 冬日裏
-春日里 春日裏
-夏日里 夏日裏
-秋日里 秋日裏
-冬日里 冬日裏
-嘴裡 嘴裏
-嘴里 嘴裏
-心裡 心裏
-心里 心裏
-皮裡陽秋 皮裏陽秋
-皮里阳秋 皮裏陽秋
-肚裡 肚裏
-肚里 肚裏
-苦裡 苦裏
-苦里 苦裏
-裡勾外連 裏勾外連
-里勾外连 裏勾外連
-裡面 裏面
-里面 裏面
-這裡 這裏
-這里 這裏
-點裡 點裏
-点里 點裏
-中文裡 中文裏
-中文里 中文裏
-山洞里 山洞裏
-山洞裡 山洞裏
-近角聪信 近角聰信
-近角聰信 近角聰信
-世界里 世界裏
-世界裡 世界裏
-眼睛里 眼睛裏
-眼睛裡 眼睛裏
-百科裡 百科裏
-百科里 百科裏
-歷史裡 歷史裏
-历史里 歷史裏
-戲裡 戲裏
-戏里 戲裏
-作品裡 作品裏
-作品里 作品裏
-專輯裡 專輯裏
-专辑里 專輯裏
-年代裡 年代裏
-年代里 年代裏
-棺材裡 棺材裏
-棺材里 棺材裏
-學裡 學裏
-学里 學裏
-獄裡 獄裏
-狱里 獄裏
-館裡 館裏
-馆里 館裏
-系列裡 系列裏
-系列里 系列裏
-村子裡 村子裏
-村子里 村子裏
-分布 分佈
-分布于 分佈於
-分布於 分佈於
-想象 想像
-無線電視 無綫電視
-无线电视 無綫電視
-無線收費 無綫收費
-无线收费 無綫收費
-無線節目 無綫節目
-无线节目 無綫節目
-無線劇集 無綫劇集
-无线剧集 無綫劇集
-東鐵線 東鐵綫
-东铁线 東鐵綫
-觀塘線 觀塘綫
-观塘线 觀塘綫
-荃灣線 荃灣綫
-荃湾线 荃灣綫
-港島線 港島綫
-港岛线 港島綫
-東涌線 東涌綫
-东涌线 東涌綫
-將軍澳線 將軍澳綫
-将军澳线 將軍澳綫
-西鐵線 西鐵綫
-西铁线 西鐵綫
-馬鞍山線 馬鞍山綫
-马鞍山线 馬鞍山綫
-迪士尼線 迪士尼綫
-迪士尼线 迪士尼綫
-沙田至中環線 沙田至中環綫
-沙田至中环线 沙田至中環綫
-沙中線 沙中綫
-沙中线 沙中綫
-北環線 北環綫
-北环线 北環綫
-機場快線 機場快綫
-机场快线 機場快綫
-505線 505綫
-505线 505綫
-507線 507綫
-507线 507綫
-610線 610綫
-610线 610綫
-614線 614綫
-614线 614綫
-614P線 614P綫
-614P线 614P綫
-615線 615綫
-615线 615綫
-615P線 615P綫
-615P线 615P綫
-705線 705綫
-705线 705綫
-706線 706綫
-706线 706綫
-751線 751綫
-751线 751綫
-751P線 751P綫
-751P线 751P綫
-761P線 761P綫
-761P线 761P綫
diff --git a/includes/zhtable/toSG.manual b/includes/zhtable/toSG.manual
deleted file mode 100644
index 2d39aa35..00000000
--- a/includes/zhtable/toSG.manual
+++ /dev/null
@@ -1,21 +0,0 @@
-」 ”
-「 “
-『 ‘
-』 ’
-方便面 快速面
-速食麵 快速面
-即食麵 快速面
-泡麵 快速面
-蹦极跳 绑紧跳
-笨豬跳 绑紧跳
-凉菜 冷菜
-冷盤 冷菜
-零钱 散钱
-散紙 散钱
-笑星 谐星
-夜校 夜学
-民乐 华乐
-住房 住屋
-房价 屋价
-榴莲 榴梿
-榴蓮 榴梿 \ No newline at end of file
diff --git a/includes/zhtable/toSimp.manual b/includes/zhtable/toSimp.manual
deleted file mode 100644
index 739d04c3..00000000
--- a/includes/zhtable/toSimp.manual
+++ /dev/null
@@ -1,165 +0,0 @@
-乾县 乾县
-萧乾 萧乾
-乾断 乾断
-乾图 乾图
-乾纲 乾纲
-乾红 乾红
-乾清宫 乾清宫
-乾仪 乾仪
-乾兴 乾兴
-乾冈 乾冈
-乾刘 乾刘
-乾刚 乾刚
-乾启 乾启
-乾宁 乾宁
-乾岗 乾岗
-乾录 乾录
-乾晖 乾晖
-乾构 乾构
-乾枢 乾枢
-乾栋 乾栋
-乾灵 乾灵
-乾窦 乾窦
-乾笃 乾笃
-乾纽 乾纽
-乾络 乾络
-乾统 乾统
-乾维 乾维
-乾罗 乾罗
-乾荫 乾荫
-乾象历 乾象历
-乾贞 乾贞
-乾贶 乾贶
-乾车 乾车
-乾轴 乾轴
-乾鉴 乾鉴
-乾钧 乾钧
-乾闼 乾闼
-乾顾 乾顾
-乾风 乾风
-乾马 乾马
-乾鹄 乾鹄
-乾鹊 乾鹊
-乾龙 乾龙
-张法乾 张法乾
-旋乾转坤 旋乾转坤
-天道为乾 天道为乾
-易经·乾 易经·乾
-易经乾 易经乾
-乾务 乾务
-黄润乾 黄润乾
-男性为乾 男性为乾
-男为乾 男为乾
-阳为乾 阳为乾
-男性为乾 男性为乾
-男性爲乾 男性为乾
-男为乾 男为乾
-男爲乾 男为乾
-阳为乾 阳为乾
-陽爲乾 阳为乾
-乾一组 乾一组
-乾一坛 乾一坛
-陈乾生 陈乾生
-陈公乾生 陈公乾生
-柳诒徵 柳诒徵
-於夫罗 於夫罗
-於梨华 於梨华
-於潜县 於潜县
-於志贺 於志贺
-憑藉 凭借
-藉端 借端
-藉故 借故
-藉口 借口
-藉助 借助
-藉手 借手
-藉詞 借词
-藉機 借机
-藉此 借此
-藉由 借由
-藉著 借着
-藉着 借着
-沈積 沉积
-沈船 沉船
-沈默 沉默
-沈沒 沉没
-彷彿 仿佛
-項鍊 项链
-肘手鍊足 肘手链足
-鍊子 链子
-鍊條 链条
-拉鍊 拉链
-鉸鍊 铰链
-鍊鎖 链锁
-鎖鍊 锁链
-鐵鍊 铁链
-金鍊 金链
-銀鍊 银链
-鍊錘 链锤
-洗鍊 洗练
-石碁镇 石碁镇
-反覆 反复
-回覆 回复
-答覆 答复
-反反覆覆 反反复复
-重覆 重复
-覆核 复核
-覆查 复查
-鬱姓 鬱姓
-鬱氏 鬱氏
-侏儸紀 侏罗纪
-夥計 伙计
-吳其濬 吴其濬
-吴其濬 吴其濬
-乾泉水 干泉水
-么半群 幺半群
-么元 幺元
-么爹 幺爹
-么叔 幺叔
-么舅 幺舅
-么爸 幺爸
-么媽 幺妈
-么姨 幺姨
-么娘 幺娘
-么孃 幺娘
-幺孃 幺娘
-么妹 幺妹
-么小 幺小
-么姓 幺姓
-么氏 幺氏
-么蛾子 幺蛾子
-幺厮 幺厮
-睪丸 睾丸
-附睪 附睾
-隱睪 隱睾
-麼麼 麽麽
-么麼 幺麽
-么麼小丑 幺麽小丑
-么鳳 幺凤
-么二三 幺二三
-么篇 幺篇
-么謙 幺谦
-麴义 麴义
-乾乾淨淨 干干净净
-乾乾脆脆 干干脆脆
-肉乾乾 肉干干
-魚乾乾 鱼干干
-於于同 於于同
-於乙于同 於乙于同
-閻懷禮 闫怀礼
-醯酱 醯酱
-醯鸡 醯鸡
-醯壶 醯壶
-苧烯 苧烯
-李乾顺 李乾顺
-幹著 干着
-氾濫 泛滥
-显著 显著
-顯著 显著
-標誌著 标志着
-近角聪信 近角聪信
-修鍊 修炼
-米泽瑠美 米泽瑠美
-太閤 太阁
-候覆 候复
-待覆 待复
-批覆 批复
diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual
deleted file mode 100644
index 1a14e99a..00000000
--- a/includes/zhtable/toTW.manual
+++ /dev/null
@@ -1,411 +0,0 @@
-” 」
-“ 「
-‘ 『
-’ 』
-着 著
-鈎 鉤
-钩 鉤
-衞 衛
-元凶 元凶
-元兇 元凶
-凶器 凶器
-兇器 凶器
-凶徒 凶徒
-兇徒 凶徒
-凶手 凶手
-兇手 凶手
-凶案 凶案
-兇案 凶案
-凶残 凶殘
-凶殘 凶殘
-兇殘 凶殘
-凶杀 凶殺
-凶殺 凶殺
-兇殺 凶殺
-疑凶 疑凶
-疑兇 疑凶
-真凶 真凶
-真兇 真凶
-缉凶 緝凶
-緝凶 緝凶
-緝兇 緝凶
-行凶 行凶
-行兇 行凶
-行凶后 行凶後
-行凶後 行凶後
-行兇後 行凶後
-买凶 買凶
-買凶 買凶
-買兇 買凶
-追凶 追凶
-追兇 追凶
-逞凶斗狠 逞凶鬥狠
-逞凶鬥狠 逞凶鬥狠
-逞兇鬥狠 逞凶鬥狠
-复苏 復甦
-復蘇 復甦
-缺省 預設
-串行 串列
-串列加速器 串列加速器
-以太网 乙太網
-位图 點陣圖
-例程 常式
-光标 游標
-光盘 光碟
-光驱 光碟機
-全角 全形
-加载 載入
-半角 半形
-变量 變數
-噪声 雜訊
-脱机 離線
-声卡 音效卡
-老字号 老字號
-连字号 連字號
-字号 字型大小
-字库 字型檔
-字段 欄位
-字符 字元
-字符集 字符集
-存盘 存檔
-寻址 定址
-尾注 章節附註
-异步 非同步
-总线 匯流排
-括号 括弧
-接口 介面
-控件 控制項
-权限 許可權
-盘片 碟片
-硅片 矽片
-硅谷 矽谷
-硬盘 硬碟
-磁盘 磁碟
-磁道 磁軌
-程控 程式控制
-远程控制 遠程控制
-遠程控制 遠程控制
-行程控制 行程控制
-流程控制 流程控制
-端口 埠
-算子 運算元
-算法 演算法
-芯片 晶片
-芯片 晶元
-词组 片語
-译码 解碼
-软驱 軟碟機
-快闪存储器 快閃記憶體
-闪存 快閃記憶體
-鼠标 滑鼠
-进制 進位
-交互式 互動式
-仿真 模擬
-优先级 優先順序
-传感 感測
-便携式 攜帶型
-信息论 資訊理論
-写保护 防寫
-分辨率 解析度
-服务器 伺服器
-等于 等於
-局域网 區域網
-扫瞄仪 掃瞄器
-宽带 寬頻
-数据库 資料庫
-奶酪 乳酪
-手电 手電筒
-手电筒 手電筒
-万历 萬曆
-永历 永曆
-词汇 辭彙
-习用 慣用
-元音 母音
-新纪元 新紀元
-新紀元 新紀元
-宋元 宋元
-头球 頭槌
-入球 進球
-粒入球 顆進球
-打门 射門
-火锅盖帽 蓋火鍋
-打印机 印表機
-打印機 印表機
-字节 位元組
-字節 位元組
-打印 列印
-打印 列印
-硬件 硬體
-硬件 硬體
-二极管 二極體
-二極管 二極體
-三极管 三極體
-三極管 三極體
-软件 軟體
-軟件 軟體
-网络 網路
-網絡 網路
-人工智能 人工智慧
-航天飞机 太空梭
-航天大学 航天大學
-穿梭機 太空梭
-因特网 網際網路
-互聯網 網際網路
-机器人 機器人
-機械人 機器人
-移动电话 行動電話
-流動電話 行動電話
-调制解调器 數據機
-調制解調器 數據機
-短信 簡訊
-短訊 簡訊
-乌兹别克斯坦 烏茲別克
-乍得 查德
-乍得 查德
-也门 葉門
-也門 葉門
-伯利兹 貝里斯
-伯利茲 貝里斯
-佛得角 維德角
-克罗地亚 克羅埃西亞
-克羅地亞 克羅埃西亞
-冈比亚 甘比亞
-岡比亞 甘比亞
-几内亚比绍 幾內亞比索
-幾內亞比紹 幾內亞比索
-列支敦士登 列支敦斯登
-列支敦士登 列支敦斯登
-利比里亚 賴比瑞亞
-利比里亞 賴比瑞亞
-加纳 迦納
-加納 迦納
-加蓬 加彭
-加蓬 加彭
-博茨瓦纳 波札那
-博茨瓦納 波札那
-卡塔尔 卡達
-卡塔爾 卡達
-卢旺达 盧安達
-盧旺達 盧安達
-危地马拉 瓜地馬拉
-危地馬拉 瓜地馬拉
-厄瓜多尔 厄瓜多
-厄瓜多爾 厄瓜多
-厄立特里亚 厄利垂亞
-厄立特里亞 厄利垂亞
-吉布提 吉布地
-吉布堤 吉布地
-哈萨克斯坦 哈薩克
-哥斯达黎加 哥斯大黎加
-哥斯達黎加 哥斯大黎加
-图瓦卢 吐瓦魯
-圖瓦盧 吐瓦魯
-土库曼斯坦 土庫曼
-圣卢西亚 聖露西亞
-聖盧西亞 聖露西亞
-圣基茨和尼维斯 聖克里斯多福及尼維斯
-聖吉斯納域斯 聖克里斯多福及尼維斯
-圣文森特和格林纳丁斯 聖文森及格瑞那丁
-聖文森特和格林納丁斯 聖文森及格瑞那丁
-圣马力诺 聖馬利諾
-聖馬力諾 聖馬利諾
-圭亚那 蓋亞那
-圭亞那 蓋亞那
-坦桑尼亚 坦尚尼亞
-坦桑尼亞 坦尚尼亞
-埃塞俄比亚 衣索比亞
-埃塞俄比亞 衣索比亞
-基里巴斯 吉里巴斯
-基里巴斯 吉里巴斯
-塔吉克斯坦 塔吉克
-塞拉利昂 獅子山
-塞拉利昂 獅子山
-塞浦路斯 塞普勒斯
-塞浦路斯 塞普勒斯
-塞舌尔 塞席爾
-塞舌爾 塞席爾
-多米尼加共和国 多明尼加
-多米尼加共和國 多明尼加
-多明尼加共和國 多明尼加
-多米尼加国 多米尼克
-多明尼加國 多米尼克
-安提瓜和巴布达 安地卡及巴布達
-安提瓜和巴布達 安地卡及巴布達
-尼日利亚 奈及利亞
-尼日利亞 奈及利亞
-尼日尔 尼日
-尼日爾 尼日
-巴巴多斯 巴貝多
-巴布亚新几内亚 巴布亞紐幾內亞
-巴布亞新畿內亞 巴布亞紐幾內亞
-布基纳法索 布吉納法索
-布基納法索 布吉納法索
-布隆迪 蒲隆地
-布隆迪 蒲隆地
-帕劳 帛琉
-意大利 義大利
-所罗门群岛 索羅門群島
-所羅門群島 索羅門群島
-文莱 汶萊
-斯威士兰 史瓦濟蘭
-斯威士蘭 史瓦濟蘭
-斯洛文尼亚 斯洛維尼亞
-斯洛文尼亞 斯洛維尼亞
-新西兰 紐西蘭
-新西蘭 紐西蘭
-格林纳达 格瑞那達
-格林納達 格瑞那達
-格鲁吉亚 喬治亞
-格魯吉亞 喬治亞
-佐治亚 喬治亞
-佐治亞 喬治亞
-毛里塔尼亚 茅利塔尼亞
-毛里塔尼亞 茅利塔尼亞
-毛里求斯 模里西斯
-毛里裘斯 模里西斯
-沙特阿拉伯 沙烏地阿拉伯
-沙地阿拉伯 沙烏地阿拉伯
-波斯尼亚和黑塞哥维那 波士尼亞赫塞哥維納
-波斯尼亞黑塞哥維那 波士尼亞赫塞哥維納
-津巴布韦 辛巴威
-津巴布韋 辛巴威
-洪都拉斯 宏都拉斯
-洪都拉斯 宏都拉斯
-特立尼达和托巴哥 千里達托貝哥
-特立尼達和多巴哥 千里達托貝哥
-瑙鲁 諾魯
-瑙魯 諾魯
-瓦努阿图 萬那杜
-瓦努阿圖 萬那杜
-溫納圖萬 那杜
-科摩罗 葛摩
-科摩羅 葛摩
-科特迪瓦 象牙海岸
-突尼斯 突尼西亞
-索马里 索馬利亞
-索馬里 索馬利亞
-老挝 寮國
-老撾 寮國
-肯尼亚 肯亞
-肯雅 肯亞
-苏里南 蘇利南
-莫桑比克 莫三比克
-莱索托 賴索托
-萊索托 賴索托
-贝宁 貝南
-貝寧 貝南
-赞比亚 尚比亞
-贊比亞 尚比亞
-阿塞拜疆 亞塞拜然
-阿拉伯联合酋长国 阿拉伯聯合大公國
-阿拉伯聯合酋長國 阿拉伯聯合大公國
-马尔代夫 馬爾地夫
-馬爾代夫 馬爾地夫
-马耳他 馬爾他
-马里共和国 馬利共和國
-馬里共和國 馬利共和國
-方便面 速食麵
-快速面 速食麵
-即食麵 速食麵
-薯仔 土豆
-土豆网 土豆網
-土豆網 土豆網
-蹦极跳 笨豬跳
-绑紧跳 笨豬跳
-冷菜 冷盤
-凉菜 冷盤
-出租车 計程車
-台球 撞球
-桌球 撞球
-卫生 衛生
-衞生 衛生
-平治之亂 平治之亂
-平治之乱 平治之亂
-平治 賓士
-奔驰 賓士
-積架 捷豹
-雪铁龙 雪鐵龍
-萬事得 馬自達
-拿破仑 拿破崙
-拿破侖 拿破崙
-布什 布希
-布殊 布希
-克林顿 柯林頓
-克林頓 柯林頓
-侯赛因 海珊
-侯賽因 海珊
-凡高 梵谷
-狄安娜 黛安娜
-戴安娜 黛安娜
-颁布 頒布
-頒佈 頒布
-彩带 彩帶
-彩排 彩排
-彩楼 彩樓
-彩牌楼 彩牌樓
-彩球 綵球
-彩绸 綵綢
-彩线 綵線
-彩船 綵船
-彩衣 綵衣
-结彩 結綵
-戏彩娱亲 戲綵娛親
-剪彩 剪綵
-榴莲 榴槤
-榴蓮 榴槤
-掛鈎 掛鉤
-挂钩 掛鉤
-鈎心鬥角 鉤心鬥角
-钩心斗角 鉤心鬥角
-酰 醯
-雪裏紅 雪裡紅
-雪裏蕻 雪裡蕻
-森林裏 森林裡
-日子裏 日子裡
-故事裏 故事裡
-領域裏 領域裡
-時間裏 時間裡
-深淵裏 深淵裡
-醫院裏 醫院裡
-春假裏 春假裡
-暑假裏 暑假裡
-秋假裏 秋假裡
-寒假裏 寒假裡
-春天裏 春天裡
-夏天裏 夏天裡
-秋天裏 秋天裡
-冬天裏 冬天裡
-春日裏 春日裡
-夏日裏 夏日裡
-秋日裏 秋日裡
-冬日裏 冬日裡
-百科裏 百科裡
-歷史裏 歷史裡
-戲裏 戲裡
-作品裏 作品裡
-專輯裏 專輯裡
-年代裏 年代裡
-棺材裏 棺材裡
-嘴裏 嘴裡
-心裏 心裡
-皮裏陽秋 皮裡陽秋
-肚裏 肚裡
-苦裏 苦裡
-裏勾外連 裡勾外連
-裏面 裡面
-這裏 這裡
-點裏 點裡
-中文裏 中文裡
-山洞裏 山洞裡
-世界裏 世界裡
-眼睛裏 眼睛裡
-學裏 學裡
-獄裏 獄裡
-館裏 館裡
-系列裏 系列裡
-村子裏 村子裡
-青霉素 青黴素
-想象 想像
-锎 鉲
-信道 信道
-綫 線
diff --git a/includes/zhtable/toTrad.manual b/includes/zhtable/toTrad.manual
deleted file mode 100644
index b0efd28e..00000000
--- a/includes/zhtable/toTrad.manual
+++ /dev/null
@@ -1,186 +0,0 @@
-手塚治虫 手塚治虫
-校仇 校讎
-仇校 讎校
-仇夷 讎夷
-仇問 讎問
-無言不仇 無言不讎
-視如寇仇 視如寇讎
-往日無仇 往日無讎
-近日無仇 近日無讎
-李連杰 李連杰
-周杰倫 周杰倫
-寶曆 寶曆
-涂謹申 涂謹申
-涂鴻欽 涂鴻欽
-涂壯勳 涂壯勳
-於姓 於姓
-於氏 於氏
-於夫羅 於夫羅
-於梨華 於梨華
-鄭凱云 鄭凱云
-筑陽 筑陽
-筑後 筑後
-采石磯 采石磯
-采石之戰 采石之戰
-張三丰 張三丰
-丰韻 丰韻
-丰儀 丰儀
-丰標不凡 丰標不凡
-干細胞 幹細胞
-干熱 乾熱
-二里頭 二里頭
-水里鄉 水里鄉
-蒙胧 朦朧
-酒曲 酒麴
-呆里呆气 呆裡呆氣
-拜托 拜託
-委托书 委託書
-委托 委託
-挽詞 輓詞
-挽聯 輓聯
-挽詩 輓詩
-於夫罗 於夫羅
-府干預 府干預
-府干擾 府干擾
-分布圖 分布圖
-頁面 頁面
-面條目 面條目
-黃鈺筑 黃鈺筑
-仿佛 彷彿
-凶殘 兇殘
-凶殺 兇殺
-緝凶 緝兇
-行凶後 行兇後
-買凶 買兇
-逞凶鬥狠 逞兇鬥狠
-合著者 合著者
-答复 答覆
-反复 反覆
-索馬里 索馬里
-洗练 洗鍊
-朝乾夕惕 朝乾夕惕
-乾象曆 乾象曆
-乾象历 乾象曆
-不好干預 不好干預
-不干預 不干預
-不干擾 不干擾
-不干牠 不干牠
-矽谷 矽谷
-范文瀾 范文瀾
-發表 發表
-機械系 機械系
-頂多 頂多
-馬占山 馬占山
-叱咤樂壇 叱咤樂壇
-闫怀礼 閆懷禮
-变髒 變髒
-薴烯 薴烯
-后豐 后豐
-于謙 于謙
-詩云 詩云
-鄭凱云 鄭凱云
-云為 云為
-古書云 古書云
-古語云 古語云
-經有云 經有云
-語有云 語有云
-显著标志 顯著標志
-占領 佔領
-采納 採納
-風采 風采
-于樂 于樂
-于軍 于軍
-于堅 于堅
-于帥 于帥
-于濤 于濤
-于贈 于贈
-于會泳 于會泳
-于偉國 于偉國
-于光遠 于光遠
-于鳳至 于鳳至
-于台煙 于台煙
-于國楨 于國楨
-于大寶 于大寶
-于學忠 于學忠
-于小偉 于小偉
-于山國 于山國
-于幼軍 于幼軍
-于廣洲 于廣洲
-于從濂 于從濂
-于志寧 于志寧
-于成龍 于成龍
-于明濤 于明濤
-于根偉 于根偉
-于樹潔 于樹潔
-于正昇 于正昇
-于漢超 于漢超
-于洪區 于洪區
-于湘蘭 于湘蘭
-于蔭霖 于蔭霖
-于遠偉 于遠偉
-于都縣 于都縣
-于震寰 于震寰
-于震環 于震環
-于非闇 于非闇
-于風政 于風政
-于鳳桐 于鳳桐
-于默奧 于默奧
-于爾岑 于爾岑
-于默奧 于默奧
-于貝爾 于貝爾
-于爾根 于爾根
-于雙戈 于雙戈
-于澤爾 于澤爾
-于斯達爾 于斯達爾
-于爾里克 于爾里克
-于奇庫杜克 于奇庫杜克
-于韋斯屈萊 于韋斯屈萊
-于克-蘭多縣 于克-蘭多縣
-于斯納爾斯貝里 于斯納爾斯貝里
-夏于喬 夏于喬
-涂澤民 涂澤民
-涂長望 涂長望
-涂敏恆 涂敏恆
-台历 枱曆
-艷后 艷后
-廢后 廢后
-后髮座 后髮座
-后髮星系團 后髮星系團
-后髮FK型星 后髮FK型星
-后海灣 后海灣
-賈后 賈后
-賢后 賢后
-呂后 呂后
-蟻后 蟻后
-馬格里布 馬格里布
-佳里鎮 佳里鎮
-埔裡社撫墾局 埔裏社撫墾局
-埔裏社撫墾局 埔裏社撫墾局
-有只採 有只採
-任何表達 任何表達
-會干擾 會干擾
-党項 党項
-余三勝 余三勝
-簡筑翎 簡筑翎
-楊雅筑 楊雅筑
-杰威爾音樂 杰威爾音樂
-尸羅精舍 尸羅精舍
-索馬里 索馬里
-騰格里 騰格里
-村里長 村里長
-進制 進制
-模范三軍 模范三軍
-黃詩杰 黃詩杰
-陳冲 陳冲
-劉佳怜 劉佳怜
-范賢惠 范賢惠
-于國治 于國治
-于楓 于楓
-黎吉雲 黎吉雲
-于飛島 于飛島
-鄉愿 鄉愿
-奇迹 奇蹟
-候复 候覆
-待复 待覆
-批复 批覆
-划槳 划槳
diff --git a/includes/zhtable/trad2simp.manual b/includes/zhtable/trad2simp.manual
deleted file mode 100644
index 7c3ce10d..00000000
--- a/includes/zhtable/trad2simp.manual
+++ /dev/null
@@ -1,150 +0,0 @@
-U+04E99亙|U+04E98亘|
-U+04F48佈|U+05E03布|
-U+04F48佈|U+05E03布|
-U+04F54佔|U+05360占|
-U+05016倖|U+05E78幸|
-U+050A2傢|U+05BB6家|
-U+050F1僱|U+096C7雇|
-U+05138儸|U+03469㑩|U+07F57罗|
-U+05147兇|U+051F6凶|
-U+05277剷|U+094F2铲|
-U+052F3勳|U+052CB勋|
-U+0537D卽|U+05373即|
-U+053A4厤|U+05386历|
-U+055AB喫|U+05403吃|
-U+05641噁|U+06076恶|
-U+05690嚐|U+05C1D尝|
-U+056A5嚥|U+054BD咽|
-U+056AE嚮|U+05411向|
-U+056CC囌|U+082CF苏|
-U+0585A塚|U+051A2冢|
-U+058B0墰|U+0575B坛|
-U+058DC壜|U+0575B坛|
-U+05925夥|U+04F19伙|
-U+05BC0寀|U+091C7采|
-U+05D11崑|U+06606昆|
-U+05D19崙|U+04ED1仑|
-U+05D57嵗|U+05C81岁|
-U+05DBD嶽|U+05CB3岳|
-U+05DD6巖|U+05CA9岩|
-U+05DF9巹|U+0537A卺|
-U+05F14弔|U+0540A吊|
-U+05F46彆|U+0522B别|
-U+0617C慼|U+0621A戚|
-U+0617E慾|U+06B32欲|
-U+061DE懞|U+08499蒙|
-U+062DA拚|U+062FC拼|
-U+06331挱|U+06332挲|
-U+06371捱|U+06328挨|
-U+06372捲|U+05377卷|
-U+0647A摺|U+06298折|
-U+065C2旂|U+065D7旗|
-U+065E3旣|U+065E2既|
-U+06607昇|U+05347升|
-U+0672E朮|U+0672F术|
-U+068CA棊|U+068CB棋|
-U+069A6榦|U+05E72干|
-U+069D3槓|U+06760杠|
-U+06A11樑|U+06881梁|
-U+06B05欅|U+06989榉|
-U+06B4E歎|U+053F9叹|
-U+06BAD殭|U+050F5僵|
-U+06C59汙|U+06C61污|
-U+06CDD泝|U+06EAF溯|
-U+06D29洩|U+06CC4泄|
-U+06DD2淒|U+051C4凄|
-U+06DE8淨|U+051C0净|
-U+06DE9淩|U+051CC凌|
-U+06E67湧|U+06D8C涌|
-U+06ED9滙|U+06C47汇|
-U+06F90澐|U+06C84沄|
-U+06FBE澾|U+03CE0㳠|
-U+06FDB濛|U+06FDB濛|U+08499蒙|
-U+07030瀰|U+05F25弥|
-U+071EC燬|U+06BC1毁|
-U+07343獃|U+05446呆|
-U+07515甕|U+074EE瓮|
-U+07526甦|U+082CF苏|
-U+0752F甯|U+05B81宁|
-U+0756B畫|U+0753B画|U+05212划|
-U+07575畵|U+0753B画|U+05212划|
-U+075E0痠|U+09178酸|
-U+07652癒|U+06108愈|
-U+07661癡|U+075F4痴|
-U+076C3盃|U+0676F杯|
-U+0771E眞|U+0771F真|
-U+077AD瞭|U+04E86了|
-U+077C7矇|U+08499蒙|
-U+07843硃|U+06731朱|
-U+07895碕|U+057FC埼|
-U+07958祘|U+07B97算|
-U+07A1C稜|U+068F1棱|
-U+07B87箇|U+04E2A个|
-U+07C11簑|U+084D1蓑|
-U+07C64籤|U+07B7E签|
-U+07C72籲|U+05401吁|
-U+07CF0糰|U+056E2团|
-U+07D2E紮|U+0624E扎|
-U+07DB5綵|U+05F69彩|U+0433D䌽|
-U+07E34縴|U+07EA4纤|
-U+07E50繐|U+07A57穗|
-U+07E94纔|U+0624D才|
-U+07F4E罎|U+0575B坛|
-U+07FA8羨|U+07FA1羡|
-U+08123脣|U+05507唇|
-U+081E5臥|U+05367卧|
-U+08218舘|U+09986馆|
-U+083F4菴|U+05EB5庵|
-U+08457著|U+08457著|U+07740着|
-U+08518蔘|U+053C2参|
-U+08591薑|U+059DC姜|
-U+085C9藉|U+085C9藉|U+0501F借|
-U+0880D蠍|U+0874E蝎|
-U+0884A衊|U+08511蔑|
-U+08946襆|U+05E5E幞|
-U+08986覆|U+08986覆|U+0590D复|
-U+08A17託|U+06258托|U+08BAC讬|
-U+08AEE諮|U+054A8咨|U+08C18谘|
-U+08B6D譭|U+06BC1毁|
-U+08B8E讎|U+04EC7仇|
-U+08B9A讚|U+08D5E赞|
-U+08C54豔|U+08273艳|
-U+08FF4迴|U+056DE回|
-U+09031週|U+05468周|
-U+0904A遊|U+06E38游|
-U+09061遡|U+06EAF溯|
-U+091A3醣|U+07CD6糖|
-U+091AF醯|U+09170酰|
-U+0934A鍊|U+070BC炼|U+094FE链|
-U+0938C鎌|U+09570镰|
-U+093AD鎭|U+093AE镇|
-U+093DA鏚|U+0621A戚|
-U+09451鑑|U+09274鉴|
-U+0955F镟|U+065CB旋|
-U+09592閒|U+095F2闲|
-U+095A4閤|U+05408合|
-U+095E2闢|U+08F9F辟|
-U+0962A阪|U+0962A阪|U+05742坂|
-U+0965E陞|U+05347升|
-U+097A6鞦|U+079CB秋|U+097A7鞧|
-U+097C6韆|U+05343千|
-U+097DD韝|U+097B2鞲|
-U+09858願|U+0613F愿|
-U+098F1飱|U+098E7飧|
-U+09918餘|U+04F59余|U+09980馀|
-U+09931餱|U+07CC7糇|
-U+09935餵|U+05582喂|
-U+09B28鬨|U+054C4哄|
-U+09D70鵰|U+096D5雕|U+05F6B彫|
-U+09E7C鹼|U+078B1碱|U+07877硷|
-U+09EAA麪|U+09762面|
-U+09EAB麫|U+09762面|
-U+09EAF麯|U+066F2曲|
-U+09EB4麴|U+066F2曲|U+09EB4麴|
-U+09EF4黴|U+09709霉|
-U+09F15鼕|U+051AC冬|
-U+09F47齇|U+09F44齄|
-U+09F63齣|U+051FA出|
-U+09F91龑|U+04DAE䶮|
-U+21ED5𡻕|U+05C81岁|
-U+298F5𩣵|U+299FB𩧻|
diff --git a/includes/zhtable/trad2simp_noconvert.manual b/includes/zhtable/trad2simp_noconvert.manual
deleted file mode 100644
index 052bab69..00000000
--- a/includes/zhtable/trad2simp_noconvert.manual
+++ /dev/null
@@ -1,5 +0,0 @@
-"余"=>
-碁
-藉
-=>"獃"
-𫚭
diff --git a/includes/zhtable/trad2simp_supp_set.manual b/includes/zhtable/trad2simp_supp_set.manual
deleted file mode 100644
index d1728f0a..00000000
--- a/includes/zhtable/trad2simp_supp_set.manual
+++ /dev/null
@@ -1,3 +0,0 @@
-著 着
-藉 借
-濛 蒙 \ No newline at end of file
diff --git a/includes/zhtable/trad2simp_supp_unset.manual b/includes/zhtable/trad2simp_supp_unset.manual
deleted file mode 100644
index e69de29b..00000000
--- a/includes/zhtable/trad2simp_supp_unset.manual
+++ /dev/null
diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual
deleted file mode 100644
index 9a9534f8..00000000
--- a/includes/zhtable/tradphrases.manual
+++ /dev/null
@@ -1,4310 +0,0 @@
-零隻
-〇隻
-一隻
-二隻
-兩隻
-三隻
-四隻
-五隻
-六隻
-七隻
-八隻
-九隻
-0隻
-1隻
-2隻
-3隻
-4隻
-5隻
-6隻
-7隻
-8隻
-9隻
-0隻
-1隻
-2隻
-3隻
-4隻
-5隻
-6隻
-7隻
-8隻
-9隻
-0只支援
-1只支援
-2只支援
-3只支援
-4只支援
-5只支援
-6只支援
-7只支援
-8只支援
-9只支援
-0只支持
-1只支持
-2只支持
-3只支持
-4只支持
-5只支持
-6只支持
-7只支持
-8只支持
-9只支持
-百隻
-千隻
-萬隻
-億隻
-最多
-至多
-頂多
-多隻
-0多隻
-0多隻
-零多隻
-十多隻
-百多隻
-千多隻
-萬多隻
-億多隻
-這只能
-這只可
-這只在
-這只是
-這只需
-這只會
-這只用
-那只能
-那只可
-那只在
-那只是
-那只需
-那只會
-那只用
-多只能
-多只可
-多只在
-多只有
-多只是
-多只需
-多只會
-多只用
-大只能
-大只可
-大只在
-大只有
-大只是
-大只需
-大只會
-小只能
-小只可
-小只在
-小只有
-小只是
-小只需
-小只會
-隻身
-形單影隻
-首隻
-數天後
-幾天後
-多天後
-零天後
-一天後
-二天後
-兩天後
-三天後
-四天後
-五天後
-六天後
-七天後
-八天後
-九天後
-十天後
-百天後
-千天後
-萬天後
-億天後
-0天後
-1天後
-2天後
-3天後
-4天後
-5天後
-6天後
-7天後
-8天後
-9天後
-0天後
-1天後
-2天後
-3天後
-4天後
-5天後
-6天後
-7天後
-8天後
-9天後
-天後來
-天後天
-天後半
-後印
-萬象
-並存著
-乾絲
-乾著急
-乾魚
-魚乾
-乾梅
-糕乾
-黃乾黑瘦
-馬乾
-香乾
-趲幹
-謀幹
-詞幹
-蟶乾
-薄幹
-腦幹
-營幹
-老乾
-老幹部
-管幹
-盲幹
-煨乾
-海乾
-乾漆
-淚乾
-沒幹
-沒乾沒淨
-枝不得大於榦
-杯乾
-打幹
-打乾噦
-徐幹
-府幹
-乾館
-乾顙
-幹革命
-乾霍亂
-乾雷
-乾阿奶
-乾量
-乾醋
-乾逼
-乾貨
-乾衣
-幹蠱
-乾虔
-乾落
-幹營生
-乾茶錢
-乾茨臘
-乾苔
-乾花
-乾肥
-乾耗
-幹缺
-乾繃
-乾結
-乾餱
-乾篾片
-乾稿
-乾禮
-乾瞪眼
-乾白兒
-乾疥
-乾生子
-乾生受
-幹父之蠱
-乾熬
-乾燈盞
-乾濕
-乾澀
-幹濟
-乾沒
-乾死
-乾村沙
-乾暖
-乾料
-乾敲梆子不賣油
-乾支支
-乾支剌
-乾擦
-乾撇下
-乾撂台
-乾折
-乾急
-幹當
-乾式
-乾屎橛
-幹家
-乾奴才
-幹頭
-乾塢
-乾圓潔淨
-乾回付
-乾啼
-乾哭
-乾噦
-乾咽
-乾和
-幹吏
-乾吊著下巴
-乾號
-乾颱
-乾卦
-乾剝剝
-乾刻版
-乾芻
-幹人
-乾產
-乾喬
-夯幹
-大目乾連
-國之楨榦
-唇乾
-單幹
-勾幹
-豆乾
-果乾
-如果幹
-乾麵
-乾柴
-枯乾
-晒乾
-顛乾倒坤
-強幹
-乾著
-乾眼
-幹的停當
-乾巴
-偎乾
-眼乾
-偷雞不著
-几絲
-划著
-划著走
-別著
-刮著
-千絲萬縷
-參合
-參考價值
-參與
-參與人員
-參與制
-參與感
-參與者
-參觀團
-參觀團體
-參閱
-吃著不盡
-合著
-吊帶褲
-吊掛著
-吊著
-吊褲
-吊褲帶
-向著
-嚴絲合縫
-回絲
-回著
-塗著
-壟斷價格
-壟斷資產
-壟斷集團
-姜絲
-帶團參加
-干著急
-幾絲
-彆著
-怎麼著
-憑藉著
-憑藉
-接著說
-擔著
-擔負著
-敘說著
-斗轉參橫
-旋繞著
-板著臉
-正當著
-沈著
-沖著
-派團參加
-涂著
-湊合著
-瀰漫著
-為著
-煙斗絲
-率團參加
-畫著
-當著
-發著
-直接參与
-睡著了
-秋褲
-積极參与
-積极參加
-簽著
-系著
-絕對參照
-絲來線去
-絲布
-絲板
-絲瓜布
-絲絨布
-絲線
-絲織廠
-絲蟲
-緊繃著
-繃著
-繃著臉
-繃著臉兒
-繫著
-罵著
-肉絲麵
-背向著
-菌絲體
-著兒
-著書立說
-著色軟體
-著重指出
-著錄
-著錄規則
-薑絲
-藉著
-蘊含著
-蘊涵著
-衝著
-被覆著
-覆著
-覆蓋著
-反覆
-訴說著
-說著
-請參閱
-謝絕參觀
-豎著
-豐濱
-豐濱鄉
-豐度
-象徵著
-這麼著
-那麼著
-配合著
-醞釀著
-錄著
-鍛鍊出
-關係著
-雞絲
-雞絲麵
-面朝著
-面臨著
-颳著
-髮絲
-斷髮
-不斷發
-判斷發
-評斷發
-買斷發
-賣斷發
-打斷發
-披頭散髮
-髮禁
-鬥著
-鬧著玩兒
-鯰魚
-世界盃
-其次辟地
-開闢
-闢地
-精闢
-別闢
-另闢
-闢佛
-闢田
-闢築
-闢謠
-闢辟
-透闢
-墾闢
-翕闢
-軒闢
-闢建
-闢室
-各闢
-增闢
-闢邪以律
-錶盤
-錶板
-錶帶
-錶針
-錶蒙子
-袋錶
-腕錶
-碼錶
-錶冠
-魔錶
-彆口氣
-彆強
-皺彆
-一彆頭
-并州
-併兼
-併產
-併骨
-併網
-併線
-併流
-逼併
-併名
-併當
-併火
-併肩子
-併除
-併疊
-忙併
-打併
-簡併
-並發表
-並發現
-並發展
-並發動
-並發布
-火並非
-舉手表
-揮手表
-併一不二
-連三併四
-相併
-撤併
-數罪併罰
-催併
-狂併潮
-薝蔔
-提摩太後書
-當家纔知柴米價
-剛纔一載
-裏海
-骨頭裡掙出來的錢纔做得肉
-恰纔
-遠縣纔至
-別日南鴻纔北去
-然身死纔數月耳
-纔得兩年
-纔則
-纔此
-你纔子發昏
-纔可容顏十五餘
-不採
-披榛採蘭
-謬採虛聲
-採樵人
-回採
-觀採
-開採
-揪採
-樵採
-採訪
-採辦
-採補
-採買
-採風問俗
-採納
-採獵
-採蓮
-採錄
-採購
-採光
-採礦
-採花
-採集
-採擷
-採掘
-採芹人
-採取
-採選
-採摭
-採摘
-採珠
-採種
-採茶
-採石
-採拾
-採收
-採生折割
-採樹種
-採擇
-採藥
-採薇
-採用
-盜採
-採信
-採行
-採證
-採菊
-博採
-採空採穗
-採挖
-採鐵
-採金
-採氣
-採油
-採煤
-採鹽
-採區
-採運
-採風
-官地為寀
-寮寀
-蔘綏
-個人# “個人參數”不是“個人蔘數”
-人蔘
-蕭蔘
-人參與
-人參選
-人參觀
-人參考
-人參展
-人參加
-人參議
-人參謀
-人參酌
-人參照
-人參政
-人參戰
-人參拜
-人參閱
-人參禪
-人參贊
-人參見
-人參透
-人參看
-東衝西突
-天克地衝
-六衝
-撞陣衝軍
-衝波
-衝風
-衝頭陣
-衝堅陷陣
-衝陷
-衝心
-衝州撞府
-衝殺
-衝然
-衝盹
-左衝右突
-虫部
-手塚治虫
-群醜
-百拙千醜
-大醜
-地醜德齊
-丟醜
-亮醜
-揭醜
-倛醜
-嫌好道醜
-醜巴怪
-醜末
-醜婦
-醜地
-醜頭怪臉
-醜女效顰
-醜剌剌
-醜話
-醜媳
-醜吒
-醜聲遠播
-醜夷
-弄醜
-露醜
-摧堅獲醜
-謷醜
-不嫌母醜
-一爭兩醜
-惡直醜正
-很醜
-醜男
-醜斃了
-醜奴兒
-醜言
-醜徒
-醜雜
-醜儕
-醜沮
-醜辭
-醜比
-醜辱
-醜逆
-醜史
-醜賊生
-醜婆子
-出乖弄醜
-出乖露醜
-獲匪其醜
-乙丑
-丁丑
-己丑
-辛丑
-癸丑
-丑時
-丑日
-丑月
-丑年
-文丑
-武丑
-女丑
-小丑
-大丑
-丑婆子
-丑旦
-丑角
-丑三
-丑表功
-公孫丑
-么麼小丑
-齣電影
-齣電視
-齣動畫
-齣節目
-齣卡通
-齣戲
-齣劇
-平平當當
-滿滿當當
-當當丁丁
-丁丁當當
-停停當當
-快快當當
-咯噹
-啷噹
-党參
-党進
-党太尉
-党項
-撲鼕
-洗髮
-牽一髮
-白發其事
-后髮座
-后髮星系團
-后髮FK型星
-波髮藻
-辮髮
-逋髮
-抿髮
-髮漂
-髮匪
-髮腳
-髮癬
-髮釵
-髮飾
-髮紗
-髮上指冠
-髮上沖冠
-髮乳
-髮引千鈞
-髮踴沖冠
-董氏封髮
-胎髮
-禿妃之髮
-捉髮
-綠髮
-括髮
-髡髮
-鵠髮
-截髮
-解髮佯狂
-淨髮
-秋髮
-噙齒戴髮
-青山一髮
-晞髮
-細不容髮
-心細如髮
-祝髮
-擢髮
-齒髮
-齒危髮秀
-沖冠髮怒
-甩髮
-絲髮
-絲恩髮怨
-蒜髮
-算髮
-有髮頭陀寺
-髮箋
-髮屋
-櫛髮工
-鬒髮
-模范棒棒堂
-模范三軍
-模范七棒
-模范14棒
-模范21棒
-顏範
-儀範
-典範
-坤範
-壼範
-容範
-懿範
-明範
-格範
-模範
-樣範
-母範
-洪範
-淑範
-遺範
-科範
-立範
-貽範
-道範
-閨範
-閫範
-雅範
-霽範
-鴻範
-沒樣範
-錢範
-銅範
-金範
-範金
-垂範
-範性形變
-範字
-有事之無範
-置言成範
-吾爲之範我馳驅
-天地為範
-範數
-丰采
-丰標不凡
-丰神
-丰茸
-丰儀
-丰度
-丰情
-丰韵
-子之丰兮
-艸木丰丰
-張三丰
-復始
-複分析
-複輔音
-複元音
-複平面
-複函數
-複流
-反複製
-複對數
-顛覆
-答覆
-覆沒
-覆亡
-覆水難收
-翻雲覆雨
-覆雨翻雲
-覆轍
-覆巢之下無完卵
-覆蓋
-覆命
-天翻地覆
-天覆地載
-撥穀
-扁擬穀盜蟲
-不穀
-辟穀
-米穀
-田穀
-脫穀機
-年穀
-礱穀機
-孤寡不穀
-穀米
-穀旦
-穀圭
-穀貴餓農
-穀食
-穀日
-館穀
-禾穀
-積穀
-嘉穀
-嚼穀
-九穀
-戩穀
-錢穀
-息穀
-殖穀
-川穀
-曬穀
-臧穀亡羊
-種穀
-颳雪
-刮風下雪倒便宜
-广部
-亂鬨不過來
-斗鬨
-亂鬨
-開鬨
-花鬨
-鬨動
-交鬨
-喧鬨
-起鬨
-內鬨
-於後
-猜三划五
-划龍舟
-南迴線
-南迴鐵路
-北迴線
-北迴鐵路
-文匯報
-河流匯集
-品彙
-博彙
-滙豐
-伙頭
-方几
-伏几
-高几
-雪窗螢几
-燕几
-隱几
-饑饉
-乾薑
-毛薑
-薑母
-薑湯
-薑桂
-薑是老的辣
-吃薑
-薑老辣
-野薑
-咬薑呷醋
-薑蓉
-薑黃
-狐藉虎威
-滑藉
-藉寇兵
-藉箸代籌
-藉手
-藉此
-龍捲
-捲舌
-夸父
-夸克
-夸特
-夸毗
-夸麗
-夸姣
-夸人
-夸容
-大言非夸
-言大而夸
-睏覺
-愛睏
-纍堆
-纍紲
-纍臣
-纍瓦結繩
-湘纍
-印纍綬若
-灕湘
-灕然
-澤滲灕而下降
-裏勾外連
-裏手
-水里鄉
-水里溪
-水里濁水溪
-二里頭
-年歷史
-西歷史
-國歷史
-國歷代
-國歷任
-國歷屆
-國歷經
-國歷來
-新歷史
-夏歷史
-百花曆
-寶曆
-穆罕默德曆
-大明曆
-大曆
-台曆
-太初曆
-通曆
-曆本
-曆命
-曆紀
-曆始
-曆室
-曆日
-曆尾
-曆元
-律曆志
-官曆
-回曆
-巧曆
-慶曆
-朱理安曆
-長曆
-藏曆
-四分曆
-三統曆
-額我略曆
-埃及曆
-伊斯蘭教曆
-合曆
-玉曆
-農民曆
-桌曆
-商曆
-周曆
-大衍曆
-皇極曆
-儒略改革曆
-希伯來曆
-格里曆
-格里高利曆
-共和曆
-掛曆
-曆獄
-天文曆表
-日心曆表
-地心曆表
-復活節曆表
-月球曆表
-伊爾汗曆表
-延曆
-共和歷史
-厤物之意
-爰定祥厤
-白黴
-黴黧
-黴黑
-麴黴
-蒙霧露
-懞懞懂懂
-懞直
-老懞
-放懞掙
-矇著
-矇聵
-矇瞍
-矇事
-矇頭轉
-矇松雨
-藏矇歌兒
-矇著鍋兒
-朦朧
-濛濛細雨
-濛汜
-冥濛
-溟濛
-淡濛濛
-凌濛初
-涳濛
-灰濛濛
-澒濛
-瀰山遍野
-瀰瀰
-冷麵
-撈麵
-煮麵
-炆麵
-煎麵
-泡麵
-食麵
-公仔麵
-方便麵
-白粉麵
-棒子麵
-麵缸
-麵坯兒
-麵碼兒
-麵坊
-麵湯
-麵疙瘩
-麵館
-麵漿
-甜水麵
-麵人兒
-麵塑
-捏麵人
-趕麵棍
-擀麵
-過水麵
-蕎麥麵
-巧婦做不得無麵餺飥
-削麵
-小米麵
-壯麵
-吃板刀麵
-吃辣麵
-扯麵
-搋麵
-重羅麵
-雜麵
-雜合麵兒
-溲麵
-索麵
-一鍋麵
-伊府麵
-藥麵兒
-意大利麵
-湯下麵
-茶麵
-麵糰
-冷面相
-糞穢衊面
-湟潦生苹
-食野之苹
-苹縈
-青苹
-青蘋果
-僕僕
-有僕
-冉有僕
-屢顧爾僕
-僕少
-僕雖罷駑
-僕夫
-僕僮
-僕吏
-僕姑
-僕固懷恩
-僕程
-僕使
-僕憎
-僕歐
-僕射
-太僕
-僮僕
-金僕姑
-僕婢
-樸實
-樸訥
-樸念仁
-白樸
-抱素懷樸
-抱朴而長吟兮
-樸鄙
-樸馬
-樸父
-樸陋
-樸魯
-樸厚
-樸學
-樸質
-樸拙
-樸重
-樸素
-樸樕
-樸野
-反樸
-古樸
-胡樸安
-返樸
-渾樸
-儉樸
-簡樸
-拙樸
-斫雕為樸
-斲雕為樸
-質樸
-誠樸
-純樸
-曾樸
-郁樸
-棫樸
-敦樸
-樸鈍
-樸直
-見素抱樸
-掣籤
-標籤
-書籤
-發籤
-粉籤子
-路籤
-更籤
-好籤
-火籤
-籤幐
-籤押
-照入籤
-制籤
-抽公籤
-瑤籤
-藥籤
-萬籤插架
-雲笈七籤
-上簽名
-上簽字
-上簽收
-上簽寫
-下簽名
-下簽字
-下簽收
-下簽寫
-犖确
-磽确
-确瘠
-言辯而确
-數與虜确
-關弓與我确
-拚捨
-廣捨
-齊王捨牛
-捨墮
-捨實
-棄捨
-捨安就危
-施舍之道
-瀋河
-瀋水
-瀋州
-瀋山線
-瀋吉線
-墨沈
-瀋海鐵路
-遼瀋
-胜肽
-胜鍵
-雙胜類
-兀朮
-白朮
-蒼朮
-赤朮
-朮赤
-髼鬆
-皮鬆
-濛鬆雨
-發鬆
-翻鬆
-浮鬆
-弄鬆
-精鬆
-懈鬆
-鬆蛋
-鬆寬
-鬆氣
-鬆一口氣
-鬆元音
-鬆喉
-囉囉囌囌
-囉囌
-骨罈
-罈騞
-餵驢
-剪牡丹喂牛
-鹹粥
-鹹食
-鹹潟
-鹹嘴淡舌
-鹽打怎麼鹹
-鹹派
-鹹批
-錦綉花園
-籲天
-勃鬱
-怫鬱
-氣鬱
-沉鬱
-神荼鬱壘
-躁鬱
-蒼鬱
-漚鬱
-伊鬱
-壹鬱
-悒鬱
-氤鬱
-湮鬱
-陰鬱
-泱鬱
-坱鬱
-滃鬱
-蓊鬱
-紆鬱
-鬱勃
-鬱陶
-鬱律
-鬱壘
-鬱火
-鬱積
-鬱金
-鬱江
-鬱血
-鬱蒸
-鬱症
-鬱沉沉
-鬱熱
-鬱塞
-鬱伊
-鬱邑
-鬱挹
-鬱堙不偶
-鬱泱
-鬱蓊
-鬱紆
-鬱燠
-肝鬱
-鬱卒
-鬱鬱不平
-鬱鬱不樂
-鬱鬱寡歡
-鬱鬱蔥蔥
-鬱鬱而終
-愿樸
-愿而恭
-許愿起經
-北嶽
-嶽麓
-但云
-胡云
-詩云
-注云
-鄭凱云
-云乎
-云然
-云為
-對摺
-網誌
-標標致致
-澄澹精致
-呆緻緻
-光緻緻
-工緻
-功緻
-縝緻
-堅緻
-种放
-种師道
-种師中
-後庄
-舊庄
-正官庄
-龜山庄
-寶山庄
-冬山庄
-員山庄
-松山庄
-厂部
-閤府
-佈道
-剪綵
-衝量
-衝車
-書獃子
-相干
-府干預
-府干涉
-府干政
-府干擾
-府干犯
-府干卿
-一干人
-未乾
-未干涉
-抹乾
-餅乾
-拭乾
-擦乾
-晾乾
-烘乾
-肉乾
-菜乾
-腐乾
-乾脆
-乾淨
-乾燥
-乾旱
-乾涸
-乾洗
-乾女
-乾等
-乾糧
-乾枯
-乾薪
-乾爹
-乾粉
-乾爽
-乾兒
-乾子
-乾渴
-乾股
-乾果
-乾草
-乾菜
-乾笑
-乾餾
-乾電
-乾飯
-乾冰
-乾嘔
-乾材
-乾媽
-乾季
-葡萄乾
-提子乾
-蘿蔔乾
-蘋果乾
-芒果乾
-菠蘿乾
-鳳梨乾
-豆腐乾
-果子乾
-龍眼乾
-乾乾淨淨
-乾柴烈火
-乾乾兒的
-桑乾
-撈乾
-搭乾鋪
-揩乾
-敢幹
-幹探
-幹事
-幹什麼
-幹細胞
-悶著頭兒幹
-配水幹管
-繐幃飄井幹
-站乾岸兒
-秋陰入井幹
-沒梢幹
-楨幹
-據榦而窺井底
-井榦摧敗
-杰特
-李連杰
-周杰倫
-杰倫
-姜文杰
-稜鏡
-稜角
-稜台
-稜錐
-觚稜
-稜子
-稜層
-稜柱
-盧稜伽
-波稜菜
-菠稜菜
-稜縫
-稜等登
-稜稜
-嶒稜
-蹭稜子
-稜體
-二不稜登
-有稜有角
-威稜
-負債纍纍
-傷痕纍纍
-儒略曆
-伊斯蘭曆
-酒麴
-昇平
-爾冬陞
-澹臺
-拜託
-委託
-輓曲
-敬輓
-万俟
-万旗
-鬚鯨
-鬚鯊
-兇手
-兇徒
-兇案
-兇器
-兇殺
-兇殘
-行兇
-緝兇
-追兇
-真兇
-疑兇
-買兇
-元兇
-叶韻
-叶音
-叶恭弘
-叶 恭弘
-叶 恭弘
-於1
-於2
-於3
-於4
-於5
-於6
-於7
-於8
-於9
-於0
-於1
-於2
-於3
-於4
-於5
-於6
-於7
-於8
-於9
-於0
-於一
-於二
-於三
-於四
-於五
-於六
-於七
-於八
-於九
-於十
-於半
-於夫羅
-於梨華
-置於
-佈於
-散於
-播於
-國於
-敗於
-於一役
-畢於
-畢業於
-寒於
-任於
-拘於
-插於
-中於
-於市
-於野
-敏於
-聽於
-短於
-成於
-樊於期
-淡於
-於陸
-於密
-於盡
-禍於
-格於
-猛於
-施於
-於牆
-於物
-於己
-於你
-於我
-於他
-於她
-於它
-於祂
-拒人於
-拒於
-潰於
-窮於
-相於
-形於
-半於
-於始
-於終
-詢於
-美於
-醜於
-好於
-坏於
-強於
-弱於
-差於
-劣於
-於美
-於醜
-於好
-於坏
-於強
-於弱
-於差
-於劣
-於垂
-染指於
-於火
-存十一於千百
-存於
-於勤
-隱於
-藏於
-嚴於
-寬於
-於幕
-給於
-於穆
-於呼哀哉
-於時
-於該
-危於
-於伏
-於何
-於家
-於國
-於潛縣
-於焉
-於徵
-離於
-於畢
-麗於
-下於
-亞於
-同於
-屑於
-絕於
-致於
-於行
-遜於
-任教於
-教於
-自於
-來於
-附於
-於人
-於世
-阻於
-於民
-於盲
-於色
-囿於
-直於
-建於
-都於
-於農
-於樂
-於前
-役於
-於心
-於法
-於事
-助於
-害於
-損於
-益於
-從於
-隨於
-順於
-汲於
-溺於
-迷於
-醉於
-行於
-泥於
-身於
-足於
-溢於
-於衷
-畏於
-視於
-衷於
-狃於
-疲於
-通於
-於途
-老於
-耿於
-於懷
-服於
-臻於
-匿於
-因於
-似於
-遷於
-怒於
-心於
-集於
-容於
-髒詞
-髒心
-新紮
-紙紮
-紮鐵
-紮寨
-一紮
-兩紮
-三紮
-四紮
-五紮
-六紮
-七紮
-八紮
-九紮
-十紮
-百紮
-千紮
-萬紮
-佔1
-佔2
-佔3
-佔4
-佔5
-佔6
-佔7
-佔8
-佔9
-佔0
-佔1
-佔2
-佔3
-佔4
-佔5
-佔6
-佔7
-佔8
-佔9
-佔0
-佔零
-佔〇
-佔一
-佔二
-佔兩
-佔三
-佔四
-佔五
-佔六
-佔七
-佔八
-佔九
-佔十
-佔百
-佔千
-佔万
-佔億
-佔超過
-佔不足
-佔至少
-佔少
-佔至多
-佔半
-佔多
-佔大
-佔小
-佔中
-佔東
-佔西
-佔南
-佔北
-佔平均
-佔總
-獨佔鰲頭
-所佔
-市佔
-佔率
-市佔率
-佔市場
-佔世界
-佔全
-佔國內
-佔美
-佔台
-佔香
-佔澳
-佔加
-佔新
-佔馬
-佔印
-佔英
-佔法
-佔德
-佔葡
-佔俄
-佔蘇
-佔缺
-佔A
-佔B
-佔C
-佔D
-佔E
-佔F
-佔G
-佔H
-佔I
-佔J
-佔K
-佔L
-佔M
-佔N
-佔O
-佔P
-佔Q
-佔R
-佔S
-佔T
-佔U
-佔V
-佔W
-佔X
-佔Y
-佔Z
-佔a
-佔b
-佔c
-佔d
-佔e
-佔f
-佔g
-佔h
-佔i
-佔j
-佔k
-佔l
-佔m
-佔n
-佔o
-佔p
-佔q
-佔r
-佔s
-佔t
-佔u
-佔v
-佔w
-佔x
-佔y
-佔z
-佔A
-佔B
-佔C
-佔D
-佔E
-佔F
-佔G
-佔H
-佔I
-佔J
-佔K
-佔L
-佔M
-佔N
-佔O
-佔P
-佔Q
-佔R
-佔S
-佔T
-佔U
-佔V
-佔W
-佔X
-佔Y
-佔Z
-佔a
-佔b
-佔c
-佔d
-佔e
-佔f
-佔g
-佔h
-佔i
-佔j
-佔k
-佔l
-佔m
-佔n
-佔o
-佔p
-佔q
-佔r
-佔s
-佔t
-佔u
-佔v
-佔w
-佔x
-佔y
-佔z
-佔不佔
-不佔
-佔了
-佔穩
-佔資源
-佔人便宜
-佔頭
-佔道
-佔屋
-佔網
-佔床
-佔座
-佔分
-佔飯
-佔個位
-佔後
-佔著
-佔山
-馬占山
-佔比
-佔停車
-佔哺乳
-佔下風
-少佔
-多佔
-費佔
-佔查
-佔壓
-佔優
-佔劣
-穩佔
-佔整體
-佔局部
-日佔
-美佔
-英佔
-德佔
-法佔
-俄佔
-葡佔
-西佔
-奧佔
-意佔
-義佔
-地佔
-佔場
-佔耕
-狂佔
-徵佔
-圈佔
-已佔
-佔囁
-佔主
-佔次
-寡佔
-佔去
-將佔
-將占卜
-要佔
-要占卜
-會佔
-會占卜
-占卜
-夢有五不占
-占有五不驗
-誌異
-筑前
-筑後
-筑紫
-筑波
-筑州
-筑肥
-筑西
-筑北
-肥筑方言
-筑邦
-筑陽
-南筑
-批准的
-核准的
-為準
-準直
-擺鐘
-編鐘
-碰鐘
-鳴鐘
-晨鐘
-鐘體
-飯後鐘
-盜鐘
-一天鐘
-撞鐘
-殿鐘自鳴
-天文鐘
-天文學鐘
-洛鐘東應
-亮鐘
-郘鐘
-歌鐘
-鐘不撞不鳴
-毀鐘為鐸
-洪鐘
-擊鐘
-警世鐘
-竊鐘掩耳
-琴鐘
-見鐘不打
-釁鐘
-朝鐘
-木鐘
-鐘不扣不鳴
-鐘鳴
-鐘塔
-鐘漏
-鐘琴
-鐘磬
-鐘形蟲
-鐘乳洞
-鐘乳石
-鐘在寺裡
-詩鐘
-懸鐘
-山崩鐘應
-坐鐘
-宗周鐘
-塞耳盜鐘
-二缶鐘惑
-口鐘
-鐘的
-的鐘
-這鐘
-叩鐘
-音聲如鐘
-應鐘
-原子鐘
-泳氣鐘
-電子鐘
-電子鐘錶
-石英鐘錶
-石英鐘
-鐘錶王
-鐘律
-看鐘
-看錶
-看表面
-鐵鐘
-看下鐘
-看下錶
-瞅下鐘
-瞅下錶
-拿下鐘
-拿下錶
-鐘不敲不響
-對準鐘
-對準鐘錶
-對準錶
-鐘錶快
-鐘快
-錶快
-鐘錶慢
-鐘慢
-錶慢
-響鐘
-鐘敲
-大本鐘敲
-大笨鐘敲
-世紀鐘錶
-世紀鐘
-錶王
-鐘王
-鐘錶
-古鐘
-古鐘錶
-鐘面
-鐘表面
-南京鐘
-南京鐘錶
-造鐘錶
-造鐘
-九龍表行
-鐘錶行
-鐘行
-錶行
-小型鐘表面
-小型鐘面
-小型鐘錶
-小型鐘
-中型鐘表面
-中型鐘面
-中型鐘錶
-中型鐘
-大型鐘表面
-大型鐘面
-大型鐘錶
-大型鐘
-鐘匠
-深山何處鐘
-下課鐘
-上課鐘
-老爺鐘
-萬年曆錶
-個鐘
-個鐘錶
-喜歡鐘
-喜歡鐘錶
-喜歡錶
-大鐘
-佛鐘
-鐘壁
-鐘腰
-鐘口
-鐘身
-鐘模
-鐘頂
-鐘紐
-鐘座
-他鐘
-寺鐘
-座鐘
-盜鐘
-大笨鐘
-大本鐘
-鐘錶歷史
-錶的歷史
-鐘錶的歷史
-點多鐘
-點半鐘
-分多鐘
-刻多鐘
-分半鐘
-刻半鐘
-教學鐘
-操作鐘
-南屏晚鐘
-敲鐘
-瞧著鐘
-瞧著鐘錶
-瞧著錶
-警報鐘
-猶如鐘
-猶如鐘錶
-猶如錶
-舊鐘錶
-繁鐘
-四面鐘
-更鐘
-警示鐘
-鐘差
-任何鐘錶
-任何鐘
-任何錶
-任何表示
-任何表達
-任何表演
-選手表現
-選手表達
-選手表示
-選手表明
-選手表決
-分子鐘
-飛行鐘
-鐘罩
-主鐘差
-花鐘
-磬鐘
-主鐘曲線
-鐘速
-紅鐘
-各類鐘
-打著鐘
-鐘意
-衛星鐘
-該鐘
-錶轉
-鐘調
-調鐘錶
-調錶
-原鐘
-鐘錶速
-件鐘
-鐘發音
-逆鐘
-拂鐘無聲
-鐘不空則啞
-看著鐘錶
-看著鐘
-看著錶
-晚鐘
-潛水鐘錶
-潛水鐘
-潛水錶
-樂器鐘
-鐘左右
-埋頭尋鐘錶
-埋頭尋鐘
-埋頭尋錶
-鐘陳列
-驚鐘
-望著鐘錶
-望著鐘
-望著錶
-鐘錶停
-鐘停
-銫鐘
-數字鐘錶
-數字鐘
-顯示鐘錶
-顯示鐘
-顯示錶
-坐如鐘
-錶停
-西周鐘
-東周鐘
-錶速
-機械鐘錶
-機械鐘
-機械錶
-之鐘
-鐘形
-架鐘
-順鐘向
-逆鐘向
-遺傳鐘
-鬧錶
-華嚴鐘
-懷鐘
-生物鐘
-鐘錶的
-錶的嘀嗒
-的鐘錶
-嘀嗒的錶
-鐘好
-鐘太
-鐘不
-鐘有
-鐘盤
-鐘錶盤
-鐘沒
-鐘被
-制鐘
-布穀鳥鐘
-咕咕鐘
-拉克施爾德鐘
-鐘上
-鐘下
-摸鐘
-舊鐘
-舊錶
-台鐘
-鐘響
-叩鐘
-計時錶
-防水錶
-射鵰
-神鵰
-神雕像
-采石磯
-采石之戰
-采石之役
-聊齋志異
-部落發
-角落發
-村落發
-蛇髮女妖
-畢生發展
-對華發動
-中美發表
-尸魂界
-樹樑
-屋樑
-樑柱
-柱樑
-下樑
-上梁山
-昇陽
-僥倖
-夏遊
-秋遊
-冬遊
-黑奴籲天錄
-林郁方
-讚歌
-編餘
-餘墨
-唾餘
-餘韻
-歸餘
-公餘
-寬餘
-餘糧
-餘慶
-餘殃
-餘燼
-劫餘
-結餘
-燼餘
-淨餘
-餕餘
-餘暉
-餘輝
-羨餘
-餘悸
-心餘
-刑餘
-緒餘
-血餘
-朱慶餘
-諸餘
-餘論
-茶餘
-廚餘
-餘裕
-餘氣
-詩餘
-詞餘
-餘僇
-餘辜
-餘責
-餘罪
-無餘
-耳餘
-餘烈
-餘思
-鹽餘
-嬴餘
-贏餘
-王餘魚
-紆餘
-餘波
-餘杯
-餘步
-餘妙
-餘音
-餘聲
-餘明
-餘風
-餘黨
-餘毒
-餘桃
-餘桶
-餘利
-餘瀝
-餘膏
-餘光
-餘杭
-餘竅
-餘缺
-餘暇
-餘閒
-餘羨
-餘響
-餘興
-餘蓄
-餘緒
-餘珍
-餘眾
-餘酲
-餘喘
-餘食
-餘熱
-餘刃
-餘閏
-餘存
-餘業
-餘姚
-餘蔭
-餘映
-餘外
-餘威
-餘味
-餘溫
-餘勇
-多餘
-剩餘
-餘生
-餘歡
-有餘
-一餘
-二餘
-兩餘
-三餘
-四餘
-五餘
-六餘
-七餘
-八餘
-九餘
-十餘
-百餘
-千餘
-萬餘
-億餘
-兆餘
-0餘
-1餘
-2餘
-3餘
-4餘
-5餘
-6餘
-7餘
-8餘
-9餘
-0餘
-1餘
-2餘
-3餘
-4餘
-5餘
-6餘
-7餘
-8餘
-9餘
-余姓
-余光生
-余光中
-余思敏
-余威德
-余子明
-余三勝
-崑山
-崑曲
-崑腔
-崑調
-崑劇
-崑蘇
-蘇崑
-分布圖
-一干家中
-星期後
-不准你
-不准我
-不准他
-不准她
-不准它
-不准誰
-不准許
-准不准你
-准不准我
-准不准他
-准不准她
-准不准它
-准不准誰
-准不准許
-依依不捨
-戀戀不捨
-窮追不捨
-緊追不捨
-鍥而不捨
-稜登
-前言不答後語
-繃扒弔拷
-不弔
-不通弔慶
-陪弔
-盆弔
-屁股大弔了心
-撇弔
-憑弔
-門弔兒
-伐罪弔民
-打出弔入
-搗鬼弔白
-弔膀子
-弔民
-弔民伐罪
-弔奠
-弔頭
-弔古
-弔古尋幽
-弔詭
-弔詭矜奇
-弔客
-弔拷
-弔拷繃扒
-弔扣
-弔賀迎送
-弔鶴
-弔喉
-弔謊
-弔祭
-弔腳兒事
-弔頸
-弔橋
-弔取
-弔孝
-弔紙
-弔者大悅
-弔場
-弔書
-弔詞
-弔死問孤
-弔死問疾
-弔撒
-弔喪
-弔喪問疾
-弔腰撒跨
-弔唁
-弔宴
-弔喭
-弔影
-弔慰
-弔文
-弔問
-頭巾弔在水裡
-提心弔膽
-弄鬼弔猴
-管人弔腳兒事
-開弔
-鶴弔
-昊天不弔
-花馬弔嘴
-會弔
-吉凶慶弔
-蟣蝨相弔
-祭弔
-祭弔文
-青蠅弔客
-慶弔
-形影相弔
-哀弔
-一弔
-唁弔
-於水
-安於
-迫於
-罷於
-蹪於
-於敝
-於過
-甚於
-等於
-定於
-利於
-對於
-推舟於陸
-退藏於密
-歸於
-難於
-移禍於
-生於
-立於
-多於
-勝於
-傳於
-流於
-過於
-關於
-毀於
-基於
-急於
-嫁禍於
-借聽於聾
-見於
-鑒於
-謹於心
-求道於盲
-始於
-於藍
-出於
-輕於
-行百里者半於九十
-幸於
-怠於
-詢於芻蕘
-止於
-至於
-拙於
-忠於
-終於
-重於
-垂於
-善於
-死於
-屬於
-浮於
-在於
-厝薪於火
-易於
-精於
-由於
-於此
-燕巢於幕
-於菟
-於乎
-於戲
-於邑
-補於
-位於
-於今
-於是
-於是乎
-於斯
-寓於
-月離於畢
-月麗於箕
-源於
-且於
-長於
-短於
-現於
-較於
-於之
-分布於
-分散於
-優於
-早於
-晚於
-感於
-鬼谷子
-于美人
-緊緻
-冗餘
-曰云
-若干
-徵婚
-鬥鬨
-事有鬥巧
-歹鬥
-鬥茶
-鬥鴨
-爭奇鬥妍
-誇能鬥智
-春香鬥學
-鬥引
-鬥彩
-鬥武
-鬥悶
-鬥牙拌齒
-鬥幌子
-鬥腳
-雞吵鵝鬥
-辯鬥
-廝鬥
-誇多鬥靡
-臨潼鬥寶
-鬥趣
-撩鬥
-傲霜鬥雪
-賭鬥
-搬鬥
-鬥爭鬥合
-鬥疊
-鬥文
-耍鬥
-鬥巧
-油鬥
-蚊動牛鬥
-卵與石鬥
-挑鬥
-爭奇鬥異
-鬥葉子
-鬥分子
-爭妍鬥奇
-不鬥
-鬥心眼
-鬥頭
-挌鬥
-好鬥
-鬥合
-拚鬥
-兩虎共鬥
-兩鼠鬥穴
-鬥犀臺
-鬥牙鬥齒
-惡鬥
-鬥勝
-鬥富
-鬥艦
-鬥葉兒
-鬥彆氣
-鬥話
-鬥牌
-鬥百草
-鬥打
-鬥犬
-鬥風
-鬥雪紅
-鬥暴
-鬥閑氣
-龍鬥虎傷
-殷師牛鬥
-二虎相鬥
-鬥力
-爭紅鬥紫
-鬥麗
-鬥狠
-鬥飣
-虎鬥
-引鬥
-爭妍鬥豔
-轉鬥千里
-鬥而鑄兵
-困鬥
-好勇鬥狠
-爭奇鬥豔
-使其鬥
-鬥地主
-石樑
-木樑
-藏歷史
-頁面
-方面
-表面
-面條目
-課餘
-節餘
-盈餘
-病餘
-餘地
-餘力
-餘子
-餘事
-扶餘國
-腐餘
-富餘
-之餘
-餘澤
-流風餘俗
-流風餘韻
-淋餘土
-餘一
-餘二
-餘三
-餘四
-餘五
-餘六
-餘七
-餘八
-餘九
-餘十
-零餘
-〇餘
-餘零
-餘〇
-餘1
-餘2
-餘3
-餘4
-餘5
-餘6
-餘7
-餘8
-餘9
-餘0
-餘1
-餘2
-餘3
-餘4
-餘5
-餘6
-餘7
-餘8
-餘9
-餘0
-餘數
-其餘
-尸居餘氣
-賸餘
-餘孽
-殘餘
-業餘
-餘割
-餘款
-餘角
-餘切
-餘霞
-餘下
-餘弦
-餘震
-餘貾
-餘額
-禹餘糧
-餘人
-編余
-病余
-餘俗
-餘倍
-同餘
-大讚
-唄讚
-褒讚
-謬讚
-誄讚
-祝讚
-詩讚
-賞讚
-讚唄
-飛紮
-紮裹
-紮腳
-紮詐
-紮囮
-住紮
-佔畢
-佔頭籌
-佔高枝兒
-隱佔
-憑摺
-沒摺至
-大摺兒
-大週摺
-火摺子
-裝摺
-變徵
-談徵
-納徵
-流徵
-柳詒徵
-固徵
-貴徵
-考徵
-咎徵
-杞宋無徵
-休徵
-徵辟
-徵名責實
-徵發
-徵風召雨
-徵答
-徵啟
-徵選
-徵招
-徵士
-徵庸
-之徵
-瑞徵
-三徵七辟
-額徵
-有徵
-有征服
-有征戰
-有征伐
-有征討
-無徵不信
-文徵明
-徵跡
-徵車
-徵效
-徵怪
-徵聖
-徵咎
-徵吏
-徵令
-本徵
-船鐘
-黃鈺筑
-齊莊
-鴻案相莊
-項莊
-韋莊
-鍋莊
-鄭莊公
-通莊
-蒙莊
-端莊
-票莊
-矜莊
-楚莊問鼎
-楚莊絕纓
-整莊
-打路莊板
-莊騷
-莊語
-莊舄越吟
-莊房
-莊客
-莊農
-平泉莊
-布莊
-香山庄
-寶莊
-坐莊
-周莊王
-發莊
-卞莊
-包莊
-剔莊貨
-劉克莊
-冷莊子
-石家莊
-卞莊子
-新莊市
-當準
-憑準
-沒準
-蜂準
-推情準理
-寇準
-合準
-準保
-準譜
-準分子
-準點
-一個準
-準擬
-準貨幣
-準式
-認準
-三準
-鵝準
-有準
-崑崙
-鎌倉
-請君入甕
-甕安
-痊癒
-治癒
-病癒
-大病初癒
-癒合
-槓桿
-宣洩
-圖鑑
-諮詢
-勳章
-張勳
-趙治勳
-殭屍
-有栖川
-兇惡
-兇狠
-兇猛
-兇橫
-兇悍
-兇險
-兇相
-兇犯
-嫌兇
-兇嫌
-兇疑
-兇刀
-兇槍
-很兇
-兇巴巴
-行兇前
-凝鍊
-鍊貧
-鍊度
-鍊形
-鍊師
-鍊石
-鍊字
-鍊冶
-細鍊
-陳鍊
-闖鍊
-鍊汞
-淬鍊
-鋼之鍊金術師
-索馬里
-范登堡
-世田谷
-製漿
-三統歷史
-伊斯蘭教歷史
-伊斯蘭歷史
-儒略改革歷史
-儒略歷史
-公歷史
-台歷史
-合歷史
-周歷史
-商歷史
-四分歷史
-回歷史
-埃及歷史
-大明歷史
-大歷史
-大衍歷史
-太初歷史
-官歷史
-寶歷史
-巧歷史
-希伯來歷史
-弘歷史
-慶歷史
-日歷史
-星歷史
-月歷史
-朱理安歷史
-桌歷史
-永歷史
-玉歷史
-百花歷史
-皇歷史
-皇極歷史
-穆罕默德歷史
-算歷史
-紀歷史
-舊歷史
-航海歷史
-萬歷史
-行事歷史
-農歷史
-農民歷史
-通歷史
-長歷史
-陰歷史
-陽歷史
-額我略歷史
-黃歷史
-天曆
-天歷史
-美醜
-獻醜
-出醜
-家醜
-遮醜
-醜八怪
-醜名
-醜詆
-醜態
-醜女
-醜類
-醜陋
-醜虜
-醜化
-醜劇
-醜媳婦
-醜小鴨
-醜行
-醜事
-醜聲
-醜人
-醜惡
-醜丫頭
-醜聞
-醜語
-母醜
-一齣子
-齣兒
-賣獃
-發獃
-大獃
-獃獃
-獃等
-獃頭
-獃腦
-獃根
-獃磕
-獃憨獃
-獃話
-獃氣
-獃想
-獃性
-獃滯
-獃著
-獃痴
-獃串了皮
-獃事
-獃人
-獃子
-好獃
-占便宜的是獃
-阿獃
-丰標
-丰姿
-丰韻
-鵰翎
-鵰心雁爪
-鵰鶚
-雙鵰
-撲鼕鼕
-普鼕鼕
-鼕鼕鼓
-令人髮指
-爆發指數
-開發
-剪其髮
-吐哺捉髮
-吐哺握髮
-含齒戴髮
-大金髮苔
-寸髮千金
-心長髮短
-戴髮含齒
-拔髮
-拔鬚
-揪髮
-揪鬚
-整髮用品
-斷髮文身
-滿頭洋髮
-燙一個髮
-燙一次髮
-燙個髮
-燙完髮
-燙次髮
-理一個髮
-理一次髮
-理個髮
-理完髮
-理次髮
-細如髮
-繫於一髮
-膚髮
-皮膚
-生華髮
-蒼髮
-被髮佯狂
-被髮入山
-被髮左衽
-被髮纓冠
-被髮陽狂
-身體髮膚
-髒髮
-髮光可鑑
-髮已霜白
-髮油
-髮為血之本
-髮網
-髮踊沖冠
-髮際
-黃髮
-齒落髮白
-剷頭
-剷刈
-口燥唇乾
-舌乾唇焦
-花菴詞選
-渾箇
-箇中原因
-箇中理由
-箇中高手
-箇中好手
-箇中強手
-箇中滋味
-箇中奧秘
-箇中奧妙
-箇中玄機
-箇中消息
-箇中資訊
-箇中訊息
-對表達
-對表現
-對表演
-對表揚
-對表中
-對表明
-不準確
-並不準確
-一伙頭
-一伙食
-一半只
-一干弟兄
-一干弟子
-一干部下
-一斗斗
-一面食
-萬一只
-上面糊
-不克自制
-不准沒
-不加自制
-不占凶吉
-不占卜
-不占吉凶
-不占算
-不好干涉
-不好干預
-不干預
-不干涉
-不干休
-不干犯
-不干擾
-不干你
-不干我
-不干他
-不干她
-不干它
-不干事
-不斗膽
-不每只
-不采聲
-專向往
-丰容
-之一只
-之二只
-之八九只
-也斗了膽
-事情干脆
-事都干脆
-二只得
-亦云
-人云
-以自制
-們斗了膽
-你斗了膽
-其一只
-其二只
-其八九只
-內面包
-內面包的
-准保護
-准保釋
-几上
-几淨窗明
-几凳
-几子
-几旁
-几椅
-几榻
-几面上
-出征收
-擊扑
-划一槳
-划了一會
-划到岸
-划到江心
-前面店
-千只可
-千只夠
-千只怕
-千只能
-千只足夠
-半只可
-半只夠
-占了卜
-口干冒
-口干政
-口干涉
-口干犯
-口干預
-古書云
-古語云
-只占卜
-只占吉
-只占神問卜
-只占算
-只身上已
-只身上無
-只身上有
-只身上沒
-只身上的
-只身世
-只身為
-只身份
-只身體
-只身前
-只身受
-只身後
-只身子
-只身形
-只身影
-只身心
-只身旁
-只身材
-只身段
-只身邊
-只身首
-只身高
-只采聲
-可自制
-台子女
-台子孫
-台布景
-台面前
-合府上
-後面店
-向往常
-向往日
-向往時
-向往來
-唯一只
-喂了一聲
-喜向往
-四出徵收
-四面包
-多半只
-好斗大
-好斗室
-好斗笠
-好斗篷
-好斗膽
-好斗蓬
-家具體
-家具備
-家具有
-小几
-尸利
-尸祿
-尸臣
-尸鳩
-已占卜
-已占算
-并迭
-所云
-所云云
-所占卜
-所占星
-所占算
-手表決
-手表態
-手表明
-手表演
-手表現
-手表示
-手表達
-手表露
-手表面
-才干休
-才干戈
-才干擾
-才干政
-才干涉
-才干預
-扎好底子
-扎好根
-扑撻
-打吨
-折向往
-拉面上
-拉面具
-拉面前
-拉面巾
-拉面無
-拉面皮
-拉面罩
-拉面色
-拉面部
-捉奸黨
-捉奸徒
-捉奸細
-捉奸賊
-敢情欲
-敢斗了膽
-敲扑
-方向往
-望了望
-桌几
-每每只
-法自制
-洒滌
-洒淅
-洒濯
-洒然
-灘涂
-特制住
-特制定
-特制止
-特制訂
-百只可
-百只夠
-百只怕
-百只足夠
-皮制服
-相克制
-相克服
-短几
-石几
-秒表明
-秒表示
-窗明几亮
-竹几
-精制伏
-精制住
-精制服
-經有云
-給我干脆
-編制法
-能干休
-能干戈
-能干擾
-能干政
-能干涉
-能干預
-能自制
-自制一下
-自制下來
-自制不
-自制之力
-自制之能
-自制他
-自制伏
-自制你
-自制地
-自制她
-自制情
-自制我
-自制服
-自制的能
-自制能力
-船只得
-船只有
-船只能
-草荐
-荐居
-荐臻
-荐饑
-要自制
-語有云
-跌扑
-轉向往
-酒帘
-裡面包
-金表態
-金表情
-金表揚
-金表明
-金表演
-金表現
-金表示
-金表達
-金表露
-金表面
-長几
-隆准許
-雄斗斗
-面包住
-面包辦
-面包廂
-面包含
-面包圍
-面包容
-面包庇
-面包紮
-面包抄
-面包括
-面包攬
-面包涵
-面包管
-面包羅
-面包著
-面包藏
-面包裝
-面包裹
-面包起
-面店舖
-面粉碎
-面粉紅
-面食麵
-面食飯
-顛顛仆仆
-高干擾
-高干預
-高度自制
-黃金表
-天后宮
-一吊錢
-不食乾腊
-傳位于四太子
-儉确之教
-党懷英
-八蜡
-憑几
-南宮适
-大蜡
-子云
-分子雲
-小价
-歲聿云暮
-崖广
-恕乏价催
-悲筑
-折子戲
-揮杆
-搤肮拊背
-文采郁郁
-木杆
-洪适
-球杆
-腊之以為餌
-腊毒
-蜡月
-蜡祭
-言云
-宜云
-貴价
-郁郁菲菲
-馬杆
-造麯
-麴生
-麴秀才
-麴塵
-麴櫱
-大麴
-黃麴毒素
-酒醴麴櫱
-麴道士
-麴錢
-麴車
-麴院
-鼠麴草
-不乾不淨
-生發生
-必須
-須根據
-·范
-、剋制
-,剋制
-。剋制
-!剋制
-?剋制
-;剋制
-:剋制
-不剋制
-也剋制
-了剋制
-他剋制
-們剋制
-剋制不了
-剋制不住
-力剋制
-力求剋制
-可以剋制
-和剋制
-在剋制
-地剋制
-夠剋制
-她剋制
-你剋制
-您剋制
-就剋制
-彼此剋制
-得剋制
-快剋制
-想剋制
-意剋制
-應剋制
-我剋制
-才剋制
-於剋制
-易剋制
-無法剋制
-的剋制
-盡量剋制
-而剋制
-能剋制
-與剋制
-著剋制
-要剋制
-軍隊剋制
-空投佈雷
-火箭佈雷
-海灣佈雷
-空中佈雷
-海上佈雷
-佈雷的
-佈雷,
-佈雷、
-佈雷。
-佈雷;
-佈雷艦
-佈雷艇
-佈雷速度
-佈雷封鎖
-滿拚自盡
-拚生盡死
-拚卻
-拚老命
-拚絕
-成於思
-單單於
-積澱
-澱積
-澱北片
-澱解物
-澱謂之滓
-淺澱
-堙澱
-茂都澱
-並曰入澱
-澱乃不耕之地
-藍澱
-皆可作澱
-澱山
-海淀山後
-澱澱
-掛鈎
-薴悴
-絡腮鬍
-落腮鬍
-山羊鬍
-幸運鬍
-刮鬍
-剃鬍
-吹鬍
-蓄鬍
-白鬍
-長鬍
-鬍髯
-髯鬍
-髭鬍
-鬚鬍
-范文瀾
-范文同
-范文正公
-范文程
-范文芳
-范文藤
-范文虎
-范文照
-發表
-乾重
-若干
-鈎心鬥角
-若干
-乾重
-全面包圍
-全面包裹
-機械系
-體系
-心理
-複分解
-鹰鵰
-叱咤903
-叱咤MY903
-叱咤My903
-叱咤樂壇
-叱咤咤
-叱咤叱咤叱咤咤
-叱咤叱叱咤
-正在叱咤
-空餘
-變髒
-天地志狼
-薴烯
-阿斯圖里亞斯
-雙折射
-心繫家
-心繫國
-心繫祖
-心繫北
-心繫京
-心繫南
-心繫西
-心繫東
-心繫四
-心繫川
-心繫浙
-心繫汶
-心繫廣
-心繫湖
-心繫山
-心繫台
-心繫江
-心繫昌
-心繫香
-心繫澳
-心繫港
-心繫泰
-心繫健
-心繫天
-心繫地
-心繫大
-心繫小
-心繫全
-心繫眾
-心繫奧
-心繫世
-心繫中
-心繫高
-心繫災
-心繫非
-心繫群
-心繫新
-心繫沈
-心繫唐
-心繫黃
-心繫乔
-心繫阮
-心繫父
-心繫母
-心繫病
-心繫故
-心繫哪
-心繫中
-心繫英
-心繫美
-心繫日
-心繫德
-心繫功
-心繫曉
-心繫神
-心繫萬
-心繫的
-心繫在
-心繫兩
-心繫社
-心繫曼
-心繫彼
-心繫風
-心繫募
-心繫一
-心繫何
-心繫困
-心繫輸
-心繫人
-心繫民
-心繫十
-心繫百
-心繫千
-心繫和
-心繫選
-心繫囑
-心繫我
-心繫你
-心繫您
-心繫他
-心繫她
-心繫它
-心繫伊
-心繫長
-心繫舞
-心繫蘭
-心繫五
-心繫生
-心繫婦
-心繫幼
-心繫茶
-心繫動
-心繫沙
-心繫林
-心繫摩
-心繫农
-心繫慈
-心繫麥
-心繫貧
-心繫富
-心繫遠
-心繫近
-心繫宣
-心繫傳
-心繫紅
-心繫老
-心繫重
-心繫震
-心繫妻
-心繫夫
-心繫女
-心繫子
-心繫著
-重回
-挑大樑
-扛大樑
-后豐
-製得
-限制
-控制
-製取
-第四出局
-心臟
-肝臟
-脾臟
-肺臟
-腎臟
-參與
-浮誇
-星巴克
-于謙
-于寘
-淳于
-于禁
-于敏中
-註:# 不作“注:”
-呆呆獸
-劃為# 不作“划為”
-併為一體
-併為一家
-一個# 避免“個裡”的錯誤
-兩個
-二個
-三個
-四個
-五個
-六個
-七個
-八個
-九個
-十個
-百個
-千個
-萬個
-億個
-兆個
-零個
-云:# 不作“雲:”
-電子表格
-雪裡紅
-雪裡蕻
-森林裡
-日子裡
-故事裡
-領域裡
-時間裡
-深淵裡
-醫院裡
-春假裡
-暑假裡
-秋假裡
-寒假裡
-春天裡
-夏天裡
-秋天裡
-冬天裡
-春日裡
-夏日裡
-秋日裡
-冬日裡
-嘴裡
-心裡
-皮裡陽秋
-肚裡
-苦裡
-裡勾外連
-裡面
-這裡
-中文裡
-山洞裡
-世界裡
-眼睛裡
-首發
-夸脫
-誰幹的
-鐘螺
-風采
-代碼表
-編碼表
-字碼表
-電碼表
-科斗
-佔領
-灕水
-點裡
-這只是
-這只不
-這只容
-這只允
-這只採
-這只用
-有只是
-有只不
-有只容
-有只允
-有只採
-有只用
-葉叶琹
-胡子昂
-包括
-特别致
-分别致
-會上簽訂
-會上簽署
-周一 # (及以下)避免“周一齣版”的錯誤
-周二
-周三
-周四
-周五
-周六
-韶山沖
-總裁制
-于丹
-于樂
-于冕
-于軍
-于吉
-于堅
-于姓
-于氏
-于娜
-于娟
-于山
-于帥
-于慧
-于振
-于敏
-于斌
-于晴
-于波
-于濤
-于衡
-于贈
-于越
-于靖
-于勒
-于格
-于仁泰
-于會泳
-于偉國
-于佳卉
-于光遠
-于克勒
-于凌奎
-于鳳至
-于化虎
-于占元
-于台煙
-于品海
-于國楨
-于大寶
-于天仁
-于子千
-于孔兼
-于學忠
-于家堡
-于小偉
-于小彤
-于山國
-于幼軍
-于廣洲
-于康震
-于式枚
-于從濂
-于德海
-于志寧
-于慎行
-于成龍
-于振武
-于明濤
-于是之
-于晨楠
-于根偉
-于樹潔
-于欣源
-于正昇
-于正昌
-于永波
-于漢超
-于江震
-于洪區
-于浩威
-于海洋
-于湘蘭
-于特森
-于玉立
-于秀敏
-于素秋
-于若木
-于蔭霖
-于西翰
-于遠偉
-于道泉
-于都縣
-于震寰
-于震環
-于非闇
-于風政
-于鳳桐
-于默奧
-于家堡
-于爾岑
-于默奧
-于貝爾
-于爾根
-于雙戈
-于里察
-于澤爾
-于斯塔德
-于斯達爾
-于爾里克
-于奇庫杜克
-于韋斯屈萊
-于克-蘭多縣
-于斯納爾斯貝里
-夏于喬
-涂姓
-涂坤
-涂天相
-涂序瑄
-涂澤民
-涂紹煃
-涂羽卿
-涂逢年
-涂長望
-涂謹申
-涂鴻欽
-涂壯勳
-涂醒哲
-涂善妮
-涂敏恆
-總裁制
-故云
-強制作用
-鬱南
-西米谷
-一出生
-二出生
-三出生
-四出生
-五出生
-六出生
-七出生
-八出生
-九出生
-十出生
-一出版
-二出版
-三出版
-四出版
-五出版
-六出版
-七出版
-八出版
-九出版
-十出版
-一出刊
-二出刊
-三出刊
-四出刊
-五出刊
-六出刊
-七出刊
-八出刊
-九出刊
-十出刊
-一出逃
-二出逃
-三出逃
-四出逃
-五出逃
-六出逃
-七出逃
-八出逃
-九出逃
-十出逃
-一出口
-二出口
-三出口
-四出口
-五出口
-六出口
-七出口
-八出口
-九出口
-十出口
-一出祁山
-二出祁山
-三出祁山
-四出祁山
-五出祁山
-六出祁山
-七出祁山
-八出祁山
-九出祁山
-十出祁山
-鬱林
-饑荒
-免徵
-亞美尼亞曆
-百科裡
-歷史裡
-戲裡
-作品裡
-專輯裡
-年代裡
-棺材裡
-注釋
-月面
-路面
-修杰楷
-修杰麟
-學裡
-獄裡
-館裡
-系列裡
-村子裡
-艷后
-廢后
-妖后
-后海灣
-仙后
-賈后
-賢后
-蜂后
-皇后
-王后
-王侯后
-母后
-武后
-歌后
-影后
-封后
-太后
-天后
-呂后
-后里
-后街
-后羿
-后稷
-后座
-后平路
-后安路
-后土
-后北街
-后冠
-望后石
-后角
-蟻后
-后妃
-大周后
-小周后
-染殿后
-准三后
-風后
-后母戊
-風後,
-人如風後入江雲
-中風後
-屏風後
-颱風後
-颳風後
-整風後
-打風後
-遇風後
-聞風後
-逆風後
-順風後
-大風後
-馬格里布
-伊里布
-劃入
-中庄子
-埔裏社撫墾局
-懸掛
-僱傭
-四捨六入
-宿舍
-會干擾
-代表
-高清愿
-瓷製
-竹製
-絲製
-莜麵
-劃入
-簡筑翎
-楊雅筑
-魔杰座
-杰威爾音樂
-彭于晏
-尸羅精舍
-索馬里 # (及以下)避免里海=>裏海的轉換
-西西里
-騰格里
-阿里
-村里長
-進制
-黃詩杰
-陳冲
-何杰
-劉佳怜
-于小惠
-于品海
-于耘婕
-于洋
-于澄
-于光新
-范賢惠
-于國治
-于楓
-于熙珍
-涂善妮
-邱于庭
-熊杰
-卜云吉
-黎吉雲
-于飛島
-代表
-水無怜奈
-傲遊 # 浏览器名
-夏于喬
-賭后
-后海灣
-立后綜
-甲后路
-劉芸后
-謝華后
-趙惠后
-趙威后
-聖后
-陳有后
-許虬
-網遊
-狄志杰
-伊適杰
-于冠華
-于台煙
-于雲鶴
-于忠肅集
-于友澤
-于和偉
-于來山
-于樂
-于天龍
-于謹
-于榮光
-電波鐘
-余三勝
-掛名
-啟發式
-舞后
-甄后
-郭后
-0年 # 協助分詞
-1年
-2年
-3年
-4年
-5年
-6年
-7年
-8年
-9年
-0年
-1年
-2年
-3年
-4年
-5年
-6年
-7年
-8年
-9年
-〇年
-零年
-一年
-兩年
-二年
-三年
-四年
-五年
-六年
-七年
-八年
-九年
-十年
-百年
-千年
-萬年
-億年
-周后
-0周後
-1周後
-2周後
-3周後
-4周後
-5周後
-6周後
-7周後
-8周後
-9周後
-0周後
-1周後
-2周後
-3周後
-4周後
-5周後
-6周後
-7周後
-8周後
-9周後
-零周後
-〇周後
-一周後
-二周後
-兩周後
-三周後
-四周後
-五周後
-六周後
-七周後
-八周後
-九周後
-十周後
-百周後
-千周後
-萬周後
-億周後
-幾周後
-多周後
-前往
-后瑞站
-帝后臺
-新井里美
-樗里子
-伊達里子
-濱田里佳子
-尊后
-叶志穗
-叶不二子
-于立成
-山谷道
-李志喜
-于欣
-于少保
-于海
-於海邊
-於海上
-于凌辰
-于魁智
-于鬯
-于仲文
-于再清
-于震
-於震前
-於震后
-於震中
-固定制
-毗婆尸佛
-尸棄佛
-划船
-划不來
-划拳
-划槳
-划動
-划艇
-划行
-划算
-總裁制
-恒生
-嚴云農
-手裏劍
-秦莊襄王
-伊東怜
-衛後莊公
-餘量
-並行
-郁郁青青
-協防
-對表格
-對表示
-對表達
-對表演
-對表明
-了然後
-戴表元
-張樂于張徐
-余力為
-葉叶琴
-万俟
-幾個
-澀谷區
-協調
-選手
-併發症
-併發重症
-併發模式
-併發型模式
-金色長髮
-紅色長髮
-一頭長髮
-的長髮
-黑色長髮
-前天
-昨天
-今天
-明天
-後天
-數學家
-科學家
-物理學家
-化學家
-生物學家
-天文學家
-游離
-子晳
-紅后假說
-書面
-不只
-高涌泉
-請求
-考試
-測試
-筆試
-口試
-冰冷
-王田里
-后姓
-台州
-田庄英雄
-計劃
-抑制劑
diff --git a/includes/zhtable/tradphrases_exclude.manual b/includes/zhtable/tradphrases_exclude.manual
deleted file mode 100644
index e6abb4e1..00000000
--- a/includes/zhtable/tradphrases_exclude.manual
+++ /dev/null
@@ -1,330 +0,0 @@
-三國誌
-聊齋誌異
-北迴
-南迴
-併排
-併進
-併在
-併成
-衝衝
-臺
-著
-佈
-纔
-采
-着
-借
-甦
-荐
-担
-可憐虫
-一齣
-上弔
-弔車
-弔橋
-弔嗓子
-弔床
-弔架
-弔桶
-弔桿
-弔橋
-弔燈
-弔環
-弔籃
-弔胃口
-弔臂
-弔銷
-形影相弔
-被髮
-散髮
-長髮
-髮毛
-髮端
-周而複始
-答複
-複興
-複舊
-顛複
-修複
-報複
-複活
-反複
-迴首
-彙總
-饑餓
-饑不擇食
-饑荒
-憑藉
-藉故
-藉口
-藉端
-藉詞
-藉酒
-蛋捲
-行李捲
-克裡
-纍纍
-華裡
-裡海
-瞭解
-明瞭
-發黴
-矇蔽
-矇住
-濛濛
-矇矇
-下麵
-白麵
-切麵
-和麵
-過水麵
-復甦
-複蘇
-甦醒
-体
-繫數
-遊擊
-馥鬱
-鬱鬱
-改製
-獃住
-獃氣
-獃子
-獃頭獃腦
-儘量
-希腊
-腊肉
-瞭如
-昇
-武鬆
-赤鬆
-黑鬆
-鬆林
-鬆科
-鬆濤
-鬆毛蟲
-鬆節油
-濕地鬆
-尼克鬆
-紮伊爾
-阿布紮比
-阿紮尼亞
-利比裡亞
-斯裡蘭卡
-烏蘇裡江
-加裡寧
-歐幾裡得
-格裡
-巴裡
-居裡
-卡裡
-墨索裡尼
-底裡
-裡人
-裡加
-裡裡
-馬裡
-裡拉
-阿裡
-裡斯
-鄰裡
-鄉裡
-百裡
-特裡
-海裡
-三元裡
-漏鬥
-春捲
-採邑
-嚮日
-佔城
-水錶
-名錶
-錶面
-彆腳
-併力
-併列
-併為
-豐富多採
-採採
-尼採
-小醜
-辛醜
-整齣
-嚴複
-枯幹
-干著急
-單於
-攻剋
-剋服
-闢邪
-釐米
-後樑
-石樑
-木樑
-舊莊
-介係詞
-介繫詞
-餘年
-大阪
-阪田
-豪杰
-七拚八湊
-一捲
-十捲
-上捲
-下捲
-加捲
-不捨
-不識檯舉
-稜登
-半弔子
-分布圖
-星鬥
-筋鬥
-斗鬨
-料鬥
-煙鬥
-熨鬥
-笆鬥
-箕鬥
-金鬥
-門鬥
-風鬥
-鬥子
-鬥笠
-老板娘
-剋制
-洋麵
-病癥
-製裁
-台製
-石家庄
-酒盃
-積极
-殭尸
-上梁不正
-項鍊
-鍊子
-鍊條
-拉鍊
-鉸鍊
-鍊鎖
-鐵鍊
-鍛鍊
-鍊乳
-鍊丹
-至于
-浮于
-附于
-次于
-于人
-助于
-行于
-于衷
-于事
-低于
-大于
-高于
-等于
-位于
-用于
-答覆
-複蓋
-反覆
-藉藉
-蘊藉
-蹈藉
-醞藉
-氆氌
-慰藉
-文藉
-枕藉
-狼藉
-別隻
-鼕鼕
-矇松雨
-佈雷
-丰度
-剪彩
-脣
-菴
-公裡
-箇中
-樑子
-樑書
-讚成
-讚同
-鐘表店
-精採
-鞭尸
-尸身
-尸首
-行尸走肉
-裹尸
-慼慼
-痠
-簑
-捱
-朝乾夕惕
-大曲酒
-神麴
-便于
-偏于
-勇于
-居于
-常見于
-強加于
-從事于
-忙于
-敢于
-服務于
-服從于
-樂于
-歸罪于
-歸諸于
-活動于
-瀕于
-苦于
-莫過于
-處于
-適于
-乾和
-鉤
-高陞
-大胆
-託福
-繫系
-酰
-醯
-大樑
-光採
-鍾錶
-複原
-參与
-浮夸
-剋日
-羡
-旅游
-穀風
-復讎
-避暑山庄
-遊牧
-烟草
-征
-占領
-入夥
-懸挂
-註釋
-浮遊
-冶鍊
-裡子
-裡外
-單隻
-聯係
-那裏
-殺虫藥
-好家伙
-姦污
-併發
-衚衕